最新的 kubeadm 的设计文档在这里 ,如果把里面设计大概看一遍就能够理解里面的流程了,可以说设计的还是很缜密的,并且大大简化了 k8s 的运维工作。
kubeadm 安全设施 主要解释 kubeadm init 和 kubeadm join 的过程和实现
kubeadm init
首先进行 preflight-checks ,检查系统是否满足初始化的状态。
创建自签名的 CA,并且生成和签发各个 component 的私钥和证书 (/etc/kubernetes/pki),如果文件已存在就不会再生成了,比如要给 apiserver 添加域名可以重新签发一个证书,然后重启就好了。
写入各个服务的配置文件,以及一个 admin.conf (/etc/kubernetes/)
配置 kubelet 的动态配置加载 (disable by default)
配置静态 pod (/etc/kubernetes/manifests)
给 master 添加 taint 和 label,让其他 pod 默认不会运行在 master 上
生成用于让其他 kubelet 加入的 token
配置用 token 加入的可以自动确认 CSR(也就是用 CA 自动签 kubelet 的证书)
设置 kube-dns
检查 self-hosting,如果设置了,就把 static pod 转成 daemonset
是否使用外部 CA 的条件是,目录下有 CA 证书,但是没有 CA 私钥。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 if res, _ := certsphase.UsingExternalCA(i.cfg); !res { if err := certsphase.CreatePKIAssets(i.cfg); err != nil { return err } if err := kubeconfigphase.CreateInitKubeConfigFiles(kubeConfigDir, i.cfg); err != nil { return err } } else { fmt.Println("[externalca] The file 'ca.key' was not found, yet all other certificates are present. Using external CA mode - certificates or kubeconfig will not be generated." ) }
具体的生成 PKI 相关的配置的过程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 func CreatePKIAssets (cfg *kubeadmapi.MasterConfiguration) error { certActions := []func (cfg *kubeadmapi.MasterConfiguration) error { CreateCACertAndKeyFiles, CreateAPIServerCertAndKeyFiles, CreateAPIServerKubeletClientCertAndKeyFiles, CreateEtcdCACertAndKeyFiles, CreateEtcdServerCertAndKeyFiles, CreateEtcdPeerCertAndKeyFiles, CreateEtcdHealthcheckClientCertAndKeyFiles, CreateAPIServerEtcdClientCertAndKeyFiles, CreateServiceAccountKeyAndPublicKeyFiles, CreateFrontProxyCACertAndKeyFiles, CreateFrontProxyClientCertAndKeyFiles, } for _, action := range certActions { err := action(cfg) if err != nil { return err } } fmt.Printf("[certificates] Valid certificates and keys now exist in %q\n" , cfg.CertificatesDir) return nil }
列表中的函数都是用来生成所有证书和私钥的,主要依靠 k8s.io/kubernetes/cmd/kubeadm/app/phases/certs/pkiutil
来完成。私钥很容易生成,没什么需要特别配置的,主要看 CA 的证书里面配了啥,因为这个跟 k8s 的鉴权有关系。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 func NewSelfSignedCACert (cfg Config, key *rsa.PrivateKey) (*x509.Certificate, error) { now := time.Now() tmpl := x509.Certificate{ SerialNumber: new (big.Int).SetInt64(0 ), Subject: pkix.Name{ CommonName: cfg.CommonName, Organization: cfg.Organization, }, NotBefore: now.UTC(), NotAfter: now.Add(duration365d * 10 ).UTC(), KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, BasicConstraintsValid: true , IsCA: true , } certDERBytes, err := x509.CreateCertificate(cryptorand.Reader, &tmpl, &tmpl, key.Public(), key) if err != nil { return nil , err } return x509.ParseCertificate(certDERBytes) }
用 openssl x509 -in /etc/kubernetes/pki/ca.crt -text -noout
可以查看 ca 证书里面的内容,和我们看到的配置是一致的。
然后我们再看一下 apiserver 的私钥和证书是怎么签的,首先把生成的 CA 证书和私钥加载进来,然后生成私钥并且签出自己的证书。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 func CreateAPIServerCertAndKeyFiles (cfg *kubeadmapi.MasterConfiguration) error { caCert, caKey, err := loadCertificateAuthority(cfg.CertificatesDir, kubeadmconstants.CACertAndKeyBaseName) if err != nil { return err } apiCert, apiKey, err := NewAPIServerCertAndKey(cfg, caCert, caKey) if err != nil { return err } return writeCertificateFilesIfNotExist( cfg.CertificatesDir, kubeadmconstants.APIServerCertAndKeyBaseName, caCert, apiCert, apiKey, ) }
这里比较重要,因为 SAN 这个是用来匹配域名的,如果这里没写好,HTTPS 是拒绝访问的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 func NewAPIServerCertAndKey (cfg *kubeadmapi.MasterConfiguration, caCert *x509.Certificate, caKey *rsa.PrivateKey) (*x509.Certificate, *rsa.PrivateKey, error) { altNames, err := pkiutil.GetAPIServerAltNames(cfg) if err != nil { return nil , nil , fmt.Errorf("failure while composing altnames for API server: %v" , err) } config := certutil.Config{ CommonName: kubeadmconstants.APIServerCertCommonName, AltNames: *altNames, Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, } apiCert, apiKey, err := pkiutil.NewCertAndKey(caCert, caKey, config) if err != nil { return nil , nil , fmt.Errorf("failure while creating API server key and certificate: %v" , err) } return apiCert, apiKey, nil }
apiserver 的证书也是对的,马赛克的部分是我自己配的一些 IP 和 域名,和 kubeadm 配的一些 kube-dns 用的 overlay 网络上的 master 域名,其他 master 上的 components 也是类似的,在配高可用的时候要把每个 master 节点上的域名和 IP 都配上。kubelet 的证书也要配对,就在 join 里面介绍了,这个会用来鉴权 node 的身份。
其他部分的代码和设计文档的描述是一致的,所以没什么好看的,感觉一个好的设计文档是非常重要的,代码只是设计的实现,如果设计本身就可读性很强,阅读代码只是辅助理解一些细节和找 BUG 用的而已。
kubeadm join
首先用 token 鉴权的方式获取 apiserver 的 CA 证书,并且通过 SHA256 验证。
加载动态配置,如果 master 上有的话。
TLS 初始化,首先用 token 鉴权的方式把自己的 CSR 发给 apiserver,然后签发自己的证书,这个证书要用来检查 node 的身份的。
配置 kubelet 和 server 开始建立连接。
新版本的 kubelet 有些变动,支持把一些 featuregate 配置写到文件里面,比如这个阻止 fork bomb 的配置,新版本只能用配置文件写了,不能通过参数配置。
kubeadm 的详细过程如下:
首先会把 flags 传入 NodeConfiguration
中,开始 AddJoinConfigFlags(cmd.PersistentFlags()
,如果没有 nodeNamecfg
默认用 host 的 name 并且小写化通过 GetHostname
获得,和在宿主机上执行 hostname
是一致的。在初始化之前先 尝试启动 TryStartKubelet
过程,然后把 token 和 server 写入(证书 data 是怎么生成的?),先配置kubelet-bootstrap-config
整体流程如下,token 验证是基于 JWT 的,所以 token 的格式是 "^([a-z0-9]{6})\\.([a-z0-9]{16})$"
,分两部分,tokenID.tokenSecret
,
1 2 3 4 5 6 7 8 discovery.For ->GetValidatedClusterInfoObject 这个是获取 CA 证书的过程 ->token.RetrieveValidatedClusterInfo 这一步是 JWT 验证 ->tokenutil.ParseToken 得到 tokenID 和 tokenSecret ->pubKeyPins.Allow 加载用于检验 CA 证书的 HASH 值 ->buildInsecureBootstrapKubeConfig (用 token-bootstrap-client 身份)获取信息 -> 从kube-public 获取 configmap (和 kubectl describe configmap cluster-info -n kube-public 中的信息是一致的)这个configmap 里面包含 JWS 签名和 master 的 CA 证书,获取对应 tokenID 的 jws token,验证 token 成功,并且验证 CA 的证书 hash,如果通过说明这个 master 是可信的,然后拿到 CA 证书以后开始构建自己的证书,建立 secure config。 ->
kubeconfigutil.WriteToDisk 会把 bootstrap-kubelet.conf
写入到配置目录中,kubeadm 的任务就完成了,之前的 kubeadm 会代替 kubelet 生成 kubelet.conf(其中用的是公钥鉴权),现在移走了,kubelet 启动的时候会尝试使用这个配置文件建立 HTTPS 的鉴权配置文件。
可以看一下 kubelet 用这个配置给 master 发 CSR 以后得到了什么,可以看到是生成了自己的证书的。
并且这个证书里面的 SAN 也是用来进行 Node 鉴权的身份确认的信息。用 openssl x509 -in /var/lib/kubelet/pki/kubelet-client.crt -text -noout
查看信息。
红线部分就是 Node 鉴权的信息,基于这个确认 node 的身份。另外一个是 Role Based Access Control,那个主要是配给 pod,限制 pod 行为用的,类似于 linux 的 chmod
。
kubeadm 高可用配置 如果没有安全配置,其实高可用挺好配置的,现在加入了安全配置,虽然麻烦,但是还是能理解 k8s 的良苦用心的,理解了整个鉴权的过程以后就可以做,现在支持配置文件初始化其实更好配置了。可以基于这个文档配置 ,算了我还是不总结了,看懂上面的,照着配置就好了。主要是自己生成 CA,他们用的 cfssl
和 cfssljson
,这个工具比 openssl 好用一点,比较容易配置。生成 ca 证书和私钥以后,就可以构建 HTTPS 的 etcd。
先创建 ca 的配置文件 ca-config.json
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 { "signing" : { "default" : { "expiry" : "43800h" }, "profiles" : { "server" : { "expiry" : "43800h" , "usages" : [ "signing" , "key encipherment" , "server auth" , "client auth" ] }, "client" : { "expiry" : "43800h" , "usages" : [ "signing" , "key encipherment" , "client auth" ] }, "peer" : { "expiry" : "43800h" , "usages" : [ "signing" , "key encipherment" , "server auth" , "client auth" ] } } } }
然后生成用于自签名的 csr 的配置文件 ca-csr.json
,用于签发自签名的 CA 证书。
1 2 3 4 5 6 7 { "CN" : "etcd" , "key" : { "algo" : "rsa" , "size" : 2048 } }
结果就是目录下面生成了,ca-key.pem
和 ca.pem
,这个命令不太按规则,对应的叫 ca.key
和 ca.crt
,pem
是密钥保存的格式。
接下来用 client.json
获得自己的私钥和通过 ca 签的证书。
1 2 3 4 5 6 7 { "CN" : "client" , "key" : { "algo" : "ecdsa" , "size" : 256 } }
执行 cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=client client.json | cfssljson -bare client
,生成了 client-key.pem
,client.pem
,分别是私钥和证书,把文件 ca.pem
,ca-key.pem
, client.pem
,client-key.pem
,ca-config.json
, 拷贝到每台 master 机器上面,一般放到 /etc/kubernetes/pki/
下面。
然后
1 2 3 4 5 6 7 cfssl print-defaults csr > config.json sed -i '0,/CN/{s/example\.net/'"$PEER_NAME"'/}' config.json sed -i 's/www\.example\.net/'"$PRIVATE_IP"'/' config.json sed -i 's/example\.net/'"$PEER_NAME"'/' config.json cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=server config.json | cfssljson -bare server cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=peer config.json | cfssljson -bare peer
签出出两对密钥和证书,把每台的机器的 域名 和 IP 替换掉示例的 example 配置,就是这些地方可以改成自己的域名和地址,这个会被用来 check。
可以放到 kubelet 的 static pod 里面,启动 etcd,两个证书分别是用作 server 验证和 client 验证的,server 让别人访问的时候相信你,peer 是你访问别人的时候让别人相信你。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 cat >/etc/kubernetes/manifests/etcd.yaml <<EOF apiVersion: v1 kind: Pod metadata: labels: component: etcd tier: control-plane name: <podname> namespace: kube-system spec: containers: - command: - etcd --name ${PEER_NAME} \ - --data-dir /var/lib/etcd \ - --listen-client-urls https://${PRIVATE_IP}:2379 \ - --advertise-client-urls https://${PRIVATE_IP}:2379 \ - --listen-peer-urls https://${PRIVATE_IP}:2380 \ - --initial-advertise-peer-urls https://${PRIVATE_IP}:2380 \ - --cert-file=/certs/server.pem \ - --key-file=/certs/server-key.pem \ - --client-cert-auth \ - --trusted-ca-file=/certs/ca.pem \ - --peer-cert-file=/certs/peer.pem \ - --peer-key-file=/certs/peer-key.pem \ - --peer-client-cert-auth \ - --peer-trusted-ca-file=/certs/ca.pem \ - --initial-cluster etcd0=https://<etcd0-ip-address>:2380,etcd1=https://<etcd1-ip-address>:2380,etcd2=https://<etcd2-ip-address>:2380 \ - --initial-cluster-token my-etcd-token \ - --initial-cluster-state new image: k8s.gcr.io/etcd-amd64:3.1.10 livenessProbe: httpGet: path: /health port: 2379 scheme: HTTP initialDelaySeconds: 15 timeoutSeconds: 15 name: etcd env: - name: PUBLIC_IP valueFrom: fieldRef: fieldPath: status.hostIP - name: PRIVATE_IP valueFrom: fieldRef: fieldPath: status.podIP - name: PEER_NAME valueFrom: fieldRef: fieldPath: metadata.name volumeMounts: - mountPath: /var/lib/etcd name: etcd - mountPath: /certs name: certs hostNetwork: true volumes: - hostPath: path: /var/lib/etcd type: DirectoryOrCreate name: etcd - hostPath: path: /etc/kubernetes/pki/etcd name: certs EOF
然后每个节点的 master init 的配置文件按照下面这个配置,client.pem 是用来让 etcd 相信 apiserver 的。private-ip
和 load-balancer-ip
都是要写到证书的 SAN 里面的,不然用这些 ip 是访问不了 apiserver 的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 apiVersion: kubeadm.k8s.io/v1alpha1 kind: MasterConfiguration api: advertiseAddress: <private-ip> etcd: endpoints: - https://<etcd0-ip-address>:2379 - https://<etcd1-ip-address>:2379 - https://<etcd2-ip-address>:2379 caFile: /etc/kubernetes/pki/etcd/ca.pem certFile: /etc/kubernetes/pki/etcd/client.pem keyFile: /etc/kubernetes/pki/etcd/client-key.pem networking: podSubnet: <podCIDR> apiServerCertSANs: - <load-balancer-ip> apiServerExtraArgs: apiserver-count: "3" EOF
至于怎么做 loadbalance 可以用七层的也可以用四层的,七层把证书配到负载均衡服务上,四层的就不用,自己在裸机器上做可以用 vip + nginx 做一个四层的,也可以把证书放到 nginx 上做七层的,但是在云环境下,都不怎么支持自己配置 vip,需要用云厂商的 lb 服务,这个就看具体提供商的服务怎么配了。