傻傻分不清楚的kubernetes證書

sealyun發表於2020-04-05

傻傻分不清楚的kubernetes證書

kubeadm 生成的一坨證書是不是讓人很蒙逼,這些東西沒那麼神奇,來深入扒扒其內褲。

root@k8s-master:/etc/kubernetes/pki# tree
.
|-- apiserver.crt
|-- apiserver-etcd-client.crt
|-- apiserver-etcd-client.key
|-- apiserver.key
|-- apiserver-kubelet-client.crt
|-- apiserver-kubelet-client.key
|-- ca.crt
|-- ca.key
|-- etcd
|   |-- ca.crt
|   |-- ca.key
|   |-- healthcheck-client.crt
|   |-- healthcheck-client.key
|   |-- peer.crt
|   |-- peer.key
|   |-- server.crt
|   `-- server.key
|-- front-proxy-ca.crt
|-- front-proxy-ca.key
|-- front-proxy-client.crt
|-- front-proxy-client.key
|-- sa.key
`-- sa.pub

1 directory, 22 files複製程式碼

從RSA說起

要深入瞭解證書的作用,首先需要了解一些原理和具備一些基本知識,比如什麼是非對稱加密,什麼是公鑰,私鑰,數字簽名是啥等。先從RSA演算法說起。

非對稱加密會生成一個金鑰對,如上面的sa.key sa.pub就是金鑰對,一個用於加密一個用於解密。

明文 + 公鑰 => 密文

密文 + 私鑰 => 明文

那麼此時沒有私鑰,就很難把密文解密。

進一步再詳細看看其原理, 不想關注的可以跳過下面原理部分:

假設我們想加密一個單詞Caesar, 先把它變成一串數字,比如Ascii碼 X = 067097101115097114 這也就是我們需要加密的 明碼。 現在來對X進行加密。

  1. 找兩個很大的質數 P 和 Q 計算他們的乘積 N = P * Q 再令M = (P - 1)(Q - 1)
  2. 找到一個數E滿足E和M除了1以外沒有公約數
  3. 找到一個數D滿足E乘以D除以M餘1, E * D mod M = 1

現在 E就是公鑰,可以公開給任何人進行加密

D就是私鑰,用於解密,一定要自己儲存好

聯絡公鑰和私鑰的N是公開的, 為什麼這個可以公開,就是因為根據P Q算出N很簡單,但是把N分解成P Q兩個大質數非常的難,所以公開了現有的計算機算力也很難破解

現在來加密:

pow(X,E) mod N = Y Y就是密文,現在沒有D(私鑰) 神仙也沒法算出X(明文)

解密:

pow(Y,D) mod N = X X是明文,明文就出來了。

數學是不是很神奇,現在可認為 sa.key = D sa.pub = E

數字簽名

假設你寫一封信給老闆,內容是"老闆我崇拜你",然後讓同事把信送給老闆,怎麼確定這信就是你寫的,而且怎麼防止同事送信過程中把信改成 "老闆你是個SB"?

可以這樣做,首先你生成一個金鑰對,把公鑰給老闆,然後對信的內容做一個hash摘要,再用私鑰對摘要進行加密,結果就是簽名

這樣老闆拿到信之後用公鑰進行解密,發現得到的hash值與信的hash值是一致的,這樣確定了信就是你寫的

所以數字簽名是加密技術的一種運用,與完全加密資訊的區別是這裡資訊是公開的,你的同事可以看到你吹捧老闆。

數字證書

根證書與證書

通常我們配置https服務時需要到"權威機構"申請證書。

過程是這樣的:

  1. 網站建立一個金鑰對,提供公鑰和組織以及個人資訊給權威機構
  2. 權威機構頒發證書
  3. 瀏覽網頁的朋友利用權威機構的根證書公鑰解密簽名,對比摘要,確定合法性
  4. 客戶端驗證域名資訊有效時間等(瀏覽器基本都內建各大權威機構的CA公鑰)

這個證書包含如下內容:

  1. 申請者公鑰
  2. 申請者組織和個人資訊
  3. 簽發機構CA資訊,有效時間,序列號等
  4. 以上資訊的簽名

根證書又名自簽名證書,也就是自己給自己頒發的證書。CA(Certificate Authority)被稱為證書授權中心,k8s中的ca證書就是根證書。

kubernetes證書

有了以上基礎,下面我們們正式開始。。。

先分類:

金鑰對:sa.key sa.pub 根證書:ca.crt etcd/ca 私鑰 : ca.key 等 其它證書

首先其它證書都是由CA根證書頒發的,kubernetes與etcd使用了不同的CA, 很重要的一點是證書是用於客戶端校驗還是服務端校驗。 下面一個一個來看:

service Account金鑰對 sa.key sa.pub

提供給 kube-controller-manager 使用. kube-controller-manager 通過 sa.key 對 token 進行簽名, master 節點通過公鑰 sa.pub 進行簽名的驗證 如 kube-proxy 是以 pod 形式執行的, 在 pod 中, 直接使用 service account 與 kube-apiserver 進行認證, 此時就不需要再單獨為 kube-proxy 建立證書了, 會直接使用token校驗

根證書

pki/ca.crt
pki/ca.key複製程式碼

為k8s叢集證書籤發機構

apiserver 證書

pki/apiserver.crt
pki/apiserver.key複製程式碼

kubelet證書

pki/apiserver-kubelet-client.crt
pki/apiserver-kubelet-client.key複製程式碼

kubelet要主動訪問kube-apiserver, kube-apiserver也需要主動向kubelet發起請求, 所以雙方都需要有自己的根證書以及使用該根證書籤發的服務端證書和客戶端證書. 在kube-apiserver中, 一般明確指定用於https訪問的服務端證書和帶有CN使用者名稱資訊的客戶端證書. 而在kubelet的啟動配置中, 一般只指定了ca根證書, 而沒有明確指定用於https訪問的服務端證書,在生成服務端證書時, 一般會指定服務端地址或主機名, kube-apiserver相對變化不是很頻繁, 所以在建立叢集之初就可以預先分配好用作 kube-apiserver的IP 或主機名/域名, 但是由於部署在node節點上的kubelet會因為叢集規模的變化而頻繁變化, 而無法預知node的所有IP資訊, 所以kubelet上一般不會明確指定服務端證書, 而是隻指定ca根證書, 讓kubelet根據本地主機資訊自動生成服務端證書並儲存到配置的cert-dir資料夾中

Aggregation 證書

代理根證書:

pki/front-proxy-ca.crt
pki/front-proxy-ca.key複製程式碼

由代理根證書籤發的客戶端證書:

pki/front-proxy-client.crt
pki/front-proxy-client.key複製程式碼

比如使用kubectl proxy代理訪問時,kube-apiserver使用這個證書來驗證客戶端證書是否是自己簽發的證書。

etcd 根證書

pki/etcd/ca.crt
pki/etcd/ca.key複製程式碼

etcd節點間相互通訊 peer證書

由根證書籤發

pki/etcd/peer.crt
pki/etcd/peer.key複製程式碼

pod中Liveness探針客戶端證書

pki/etcd/healthcheck-client.crt
pki/etcd/healthcheck-client.key複製程式碼

可檢視yaml探活配置:

Liveness:       exec [/bin/sh -ec ETCDCTL_API=3 etcdctl \
  --endpoints=https://[127.0.0.1]:2379 \
  --cacert=/etc/kubernetes/pki/etcd/ca.crt \
  --cert=/etc/kubernetes/pki/etcd/healthcheck-client.crt \
  --key=/etc/kubernetes/pki/etcd/healthcheck-client.key get foo] \
  delay=15s timeout=15s period=10s #success=1 #failure=8複製程式碼

apiserver訪問etcd的證書

pki/apiserver-etcd-client.crt
pki/apiserver-etcd-client.key複製程式碼

這裡注意一下客戶端證書與服務端證書區別,服務端證書通常會校驗地址域名等。

程式碼實現

kubeadm把證書時間寫死成了1年(client-go就寫死了),這是個悲傷的故事,導致sealos不得不把證書生成的邏輯剝離出來以讓安裝支援任意過期時間。

下面根據原始碼來深入體驗下kubeadm的證書生成,直接看kubeadm程式碼可能有點累,sealos/cert目錄剝離出核心的程式碼更容易讀懂一些。

以下為了突出核心邏輯,程式碼中刪除一些錯誤處理細節,有興趣可閱讀github.com/fanux/sealos/cert原始碼

金鑰對生成

// create sa.key sa.pub for service Account
func GenerateServiceAccountKeyPaire(dir string) error {
    key, err := NewPrivateKey(x509.RSA)
    pub := key.Public()
    err = WriteKey(dir, "sa", key)
    return WritePublicKey(dir, "sa", pub)
}複製程式碼

生成私鑰, 這裡的keyType是x509.RSA

func NewPrivateKey(keyType x509.PublicKeyAlgorithm) (crypto.Signer, error) {
    if keyType == x509.ECDSA {
        return ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
    }
    return rsa.GenerateKey(rand.Reader, rsaKeySize)
}複製程式碼

生成CA證書

會返回ca.crt(自簽名證書) ca.key(私鑰)

func NewCaCertAndKey(cfg Config) (*x509.Certificate, crypto.Signer, error) {
    key, err := NewPrivateKey(x509.UnknownPublicKeyAlgorithm)
    cert, err := NewSelfSignedCACert(key, cfg.CommonName, cfg.Organization, cfg.Year)
    return cert, key, nil
}複製程式碼

根據私鑰生成自簽名證書, NotAfter就是證書過期時間,我們很友好的加了個變數而不是寫死:

// NewSelfSignedCACert creates a CA certificate
func NewSelfSignedCACert(key crypto.Signer, commonName string, organization []string, year time.Duration) (*x509.Certificate, error) {
    now := time.Now()
    tmpl := x509.Certificate{
        SerialNumber: new(big.Int).SetInt64(0),
        Subject: pkix.Name{
            CommonName:   commonName,
            Organization: organization,
        },
        NotBefore:             now.UTC(),
        NotAfter:              now.Add(duration365d * year).UTC(),
        KeyUsage:              x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
        BasicConstraintsValid: true,
        IsCA:                  true,
    }

    certDERBytes, err := x509.CreateCertificate(rand.Reader, &tmpl, &tmpl, key.Public(), key)
    return x509.ParseCertificate(certDERBytes)
}複製程式碼

非常要注意裡面的CommonName和Organization欄位,非常有用,比如我們建立一個k8s使用者指定該使用者屬於哪個使用者組,對應上面這兩個欄位。

比如證書中 fanux 屬於 sealyun這個組織,那麼生成一個kubeconfig, 就相當於有了fanux這個使用者,這樣k8s在做認證時只需要校驗簽名就行,而不需要去訪問 資料庫來做認證,這非常有利於apiserver的橫向擴充套件。

生成其它證書

金鑰對還是自己生成,然後簽證書時會把根證書資訊帶上

func NewCaCertAndKeyFromRoot(cfg Config, caCert *x509.Certificate, caKey crypto.Signer) (*x509.Certificate, crypto.Signer, error) {
    key, err := NewPrivateKey(x509.UnknownPublicKeyAlgorithm)
    cert, err := NewSignedCert(cfg, key, caCert, caKey)

    return cert, key, nil
}複製程式碼

此時就必須要求有CommonName了,Usages也得指定是服務端使用還是客戶端使用, 注意與上面SelfSign的區別

// NewSignedCert creates a signed certificate using the given CA certificate and key
func NewSignedCert(cfg Config, key crypto.Signer, caCert *x509.Certificate, caKey crypto.Signer) (*x509.Certificate, error) {
    serial, err := rand.Int(rand.Reader, new(big.Int).SetInt64(math.MaxInt64))
    if len(cfg.CommonName) == 0 {
        return nil, errors.New("must specify a CommonName")
    }
    if len(cfg.Usages) == 0 {
        return nil, errors.New("must specify at least one ExtKeyUsage")
    }

    certTmpl := x509.Certificate{
        Subject: pkix.Name{
            CommonName:   cfg.CommonName,
            Organization: cfg.Organization,
        },
        DNSNames:     cfg.AltNames.DNSNames,
        IPAddresses:  cfg.AltNames.IPs,
        SerialNumber: serial,
        NotBefore:    caCert.NotBefore,
        NotAfter:     time.Now().Add(duration365d * cfg.Year).UTC(),
        KeyUsage:     x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
        ExtKeyUsage:  cfg.Usages,
    }
    certDERBytes, err := x509.CreateCertificate(rand.Reader, &certTmpl, caCert, key.Public(), caKey)
    return x509.ParseCertificate(certDERBytes)
}複製程式碼

kubernetes中的所有證書

根證書列表

var caList = []Config{
    {
        Path:         BasePath,
        BaseName:     "ca",
        CommonName:   "kubernetes",
        Organization: nil,
        Year:         100,
        AltNames:     AltNames{},
        Usages:       nil,
    },
    {
        Path:         BasePath,
        BaseName:     "front-proxy-ca",
        CommonName:   "front-proxy-ca",
        Organization: nil,
        Year:         100,
        AltNames:     AltNames{},
        Usages:       nil,
    },
    {
        Path:         EtcdBasePath,
        BaseName:     "ca",
        CommonName:   "etcd-ca",
        Organization: nil,
        Year:         100,
        AltNames:     AltNames{},
        Usages:       nil,
    },
}複製程式碼

其它簽名證書列表

var certList = []Config{
    {
        Path:         BasePath,
        BaseName:     "apiserver",
        CAName:       "kubernetes",
        CommonName:   "kube-apiserver",
        Organization: nil,
        Year:         100,
        AltNames:     AltNames{// 實際安裝時還需要把伺服器IP使用者自定義域名加上
            DNSNames: []string{  
                "apiserver.cluster.local",
                "localhost",
                "master",
                "kubernetes",
                "kubernetes.default",
                "kubernetes.default.svc",
            },
            IPs: []net.IP{
                {127,0,0,1},
            },
        },
        Usages:       []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, // 用途是服務端校驗
    },
    {
        Path:         BasePath,
        BaseName:     "apiserver-kubelet-client",
        CAName:       "kubernetes",
        CommonName:   "kube-apiserver-kubelet-client",
        Organization: []string{"system:masters"},
        Year:         100,
        AltNames:     AltNames{},
        Usages:       []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
    },
    {
        Path:         BasePath,
        BaseName:     "front-proxy-client",
        CAName:       "front-proxy-ca",
        CommonName:   "front-proxy-client",
        Organization: nil,
        Year:         100,
        AltNames:     AltNames{},
        Usages:       []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
    },
    {
        Path:         BasePath,
        BaseName:     "apiserver-etcd-client",
        CAName:       "etcd-ca",
        CommonName:   "kube-apiserver-etcd-client",
        Organization: []string{"system:masters"},
        Year:         100,
        AltNames:     AltNames{},
        Usages:       []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
    },
    {
        Path:         EtcdBasePath,
        BaseName:     "server",
        CAName:       "etcd-ca",
        CommonName:   "etcd", // kubeadm etcd server證書common name使用節點名,這也是呼叫時需要改動的
        Organization: nil,
        Year:         100,
        AltNames:     AltNames{}, // 呼叫時需要把節點名,節點IP等加上
        Usages:       []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
    },
    {
        Path:         EtcdBasePath,
        BaseName:     "peer",
        CAName:       "etcd-ca",
        CommonName:   "etcd-peer", // 與etcd server同理
        Organization: nil,
        Year:         100,
        AltNames:     AltNames{}, // 與etcd server同理
        Usages:       []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
    },
    {
        Path:         EtcdBasePath,
        BaseName:     "healthcheck-client",
        CAName:       "etcd-ca",
        CommonName:   "kube-etcd-healthcheck-client",
        Organization: []string{"system:masters"},
        Year:         100,
        AltNames:     AltNames{},
        Usages:       []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
    },
}複製程式碼

上面非常要注意的是server端校驗的證書安裝時需要把IP 和域名加上,etcd的commonName也要設定成node name。

看最後生成的證書資訊:

apiserver:

[root@iZ2ze4ry74x8bh3cweeg69Z pki]# openssl x509 -in /etc/kubernetes/pki/apiserver.crt -text -noout
Certificate:
...
    Signature Algorithm: sha256WithRSAEncryption
        Issuer: CN=kubernetes
        Validity
            Not Before: Mar 31 09:18:06 2020 GMT
            Not After : Mar  8 09:18:06 2119 GMT
        Subject: CN=kube-apiserver
...
        X509v3 extensions:
            X509v3 Key Usage: critical
                Digital Signature, Key Encipherment
            X509v3 Extended Key Usage: 
                TLS Web Server Authentication
            X509v3 Subject Alternative Name: 
                DNS:iz2ze4ry74x8bh3cweeg69z, DNS:kubernetes, DNS:kubernetes.default, DNS:kubernetes.default.svc, DNS:kubernetes.default.svc.cluster.local, DNS:apiserver.cluster.local, DNS:apiserver.cluster.local, IP Address:10.96.0.1, IP Address:172.16.9.192, IP Address:127.0.0.1, IP Address:172.16.9.192, IP Address:172.16.9.193, IP Address:172.16.9.194, IP Address:10.103.97.2
    Signature Algorithm: sha256WithRSAEncryption複製程式碼

etcd server:

[root@iZ2ze4ry74x8bh3cweeg69Z pki]# openssl x509 -in /etc/kubernetes/pki/etcd/server.crt -text -noout
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number: 1930981199811083392 (0x1acc392ba2b27c80)
    Signature Algorithm: sha256WithRSAEncryption
        Issuer: CN=etcd-ca
        Validity
            Not Before: Mar 31 09:18:07 2020 GMT
            Not After : Mar  8 09:18:07 2119 GMT
        Subject: CN=iz2ze4ry74x8bh3cweeg69z
...
            X509v3 Extended Key Usage: 
                TLS Web Server Authentication, TLS Web Client Authentication
            X509v3 Subject Alternative Name: 
                DNS:iz2ze4ry74x8bh3cweeg69z, DNS:localhost, IP Address:172.16.9.192, IP Address:127.0.0.1, IP Address:0:0:0:0:0:0:0:1
    Signature Algorithm: sha256WithRSAEncryption複製程式碼

生成使用者證書和kubeconfig

現在有個實習生fanux來公司了,也想用用k8s,果斷不放心把admin 的kubeconfig交給他,那怎麼辦? 有了上面基礎,再進一步教你怎麼為fanux分配一個單獨的kubeconfig

  1. 從磁碟載入根證書,和私鑰

  2. 生成fanux這個使用者的證書, common name就是fanux

  3. 編碼成pem格式

  4. 寫kubeconfig, 寫磁碟

    func GenerateKubeconfig(conf Config) error{
     certs, err := cert.CertsFromFile(conf.CACrtFile)
     caCert := certs[0]
     cert := EncodeCertPEM(caCert)
     caKey,err := TryLoadKeyFromDisk(conf.CAKeyFile)
     // 這裡conf.User就是fanux, conf.Groups就是使用者組,可以是多個
     clientCert,clientKey,err := NewCertAndKey(caCert,caKey,conf.User,conf.Groups,conf.DNSNames,conf.IPAddresses)
     encodedClientKey,err := keyutil.MarshalPrivateKeyToPEM(clientKey)
     encodedClientCert := EncodeCertPEM(clientCert)
     // 構建kubeconfig的三元組資訊
     config := &api.Config{
         Clusters: map[string]*api.Cluster{
             conf.ClusterName: {
                 Server: conf.Apiserver, // 叢集地址 如 https://apiserver.cluster.local:6443
                 CertificateAuthorityData: cert, // pem格式的根證書,用於https
             },
         },
         Contexts: map[string]*api.Context{
             ctx: {            // 三元組資訊,使用者名稱 fanux, 上面的cluster名,以及namespace這裡沒寫
                 Cluster:  conf.ClusterName, 
                 AuthInfo: conf.User,
             },
         },
         AuthInfos:      map[string]*api.AuthInfo{  // 使用者資訊, 所以你直接改kubeconfig裡的user是沒用的,因為k8s只認證書裡的名字
             conf.User:&api.AuthInfo{
                 ClientCertificateData: encodedClientCert,  // pem格式的使用者證書
                 ClientKeyData:         encodedClientKey,   // pem格式的使用者私鑰
             },
         },
         CurrentContext: ctx,  // 當前上下文, kubeconfig可以很好支援多使用者和多叢集
     }
    
     err = clientcmd.WriteToFile(*config, conf.OutPut)
     return nil
    }複製程式碼

使用者證書和私鑰生成, 和上面簽名證書一樣,user就是fanux, group是使用者組:

func NewCertAndKey(caCert *x509.Certificate, caKey crypto.Signer, user string, groups []string, DNSNames []string,IPAddresses []net.IP) (*x509.Certificate, crypto.Signer, error) {
    key,err := rsa.GenerateKey(rand.Reader, 2048)
    serial, err := rand.Int(rand.Reader, new(big.Int).SetInt64(math.MaxInt64))

    certTmpl := x509.Certificate{
        Subject: pkix.Name{
            CommonName:   user,
            Organization: groups,
        },
        DNSNames:     DNSNames,
        IPAddresses:  IPAddresses,
        SerialNumber: serial,
        NotBefore:    caCert.NotBefore,
        NotAfter:     time.Now().Add(time.Hour * 24 * 365 * 99).UTC(),
        KeyUsage:     x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
        ExtKeyUsage:  []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
    }
    certDERBytes, err := x509.CreateCertificate(rand.Reader, &certTmpl, caCert, key.Public(), caKey)
    cert,err := x509.ParseCertificate(certDERBytes)
    return cert,key,nil
}複製程式碼

然後這位小夥伴的kubeconfig就生成了,此時沒有任何許可權:

kubectl --kubeconfig ./kube/config get pod
Error from server (Forbidden): pods is forbidden: User "fanux" cannot list resource "pods" in API group ...複製程式碼

最後發揮一下RBAC就可以了,這裡就直接繫結個管理員許可權了

kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: user-admin-test
subjects:
- kind: User
  name: "fanux" # Name is case sensitive
  apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: ClusterRole
  name: cluster-admin  # using admin role
  apiGroup: rbac.authorization.k8s.io複製程式碼

總結

證書與k8s的認證原理在叢集安裝以及開發多租戶容器平臺時非常有用,希望本文能讓大家有個整體細緻全面的瞭解。

```

本文由部落格一文多發平臺 OpenWrite 釋出!

相關文章