原生kubernetes 集群启动引导
一切的开始都是kubelet这个服务的启动
官方文档内容
TLS 启动引导
在一个 Kubernetes 集群中,工作节点上的组件(kubelet 和 kube-proxy)需要与 Kubernetes 控制平面组件通信,尤其是 kube-apiserver。 为了确保通信本身是私密的、不被干扰,并且确保集群的每个组件都在与另一个可信的组件通信, 我们强烈建议使用节点上的客户端 TLS 证书。
启动引导这些组件的正常过程,尤其是需要证书来与 kube-apiserver 安全通信的工作节点, 可能会是一个具有挑战性的过程,因为这一过程通常不受 Kubernetes 控制,需要不少额外工作。 这也使得初始化或者扩缩一个集群的操作变得具有挑战性。
为了简化这一过程,从 1.4 版本开始,Kubernetes 引入了一个证书请求和签名 API。 该提案可在这里看到。
本文档描述节点初始化的过程,如何为 kubelet 配置 TLS 客户端证书启动引导, 以及其背后的工作原理。
初始化过程
当工作节点启动时,kubelet 执行以下操作:
寻找自己的 kubeconfig 文件
检索 API 服务器的 URL 和凭据,通常是来自 kubeconfig 文件中的 TLS 密钥和已签名证书
尝试使用这些凭据来与 API 服务器通信
假定 kube-apiserver 成功地认证了 kubelet 的凭据数据,它会将 kubelet 视为一个合法的节点并开始将 Pod 分派给该节点。
注意,签名的过程依赖于:
kubeconfig 中包含密钥和本地主机的证书
证书被 kube-apiserver 所信任的一个证书机构(CA)所签名
负责部署和管理集群的人有以下责任:
- 创建 CA 密钥和证书
- 将 CA 证书发布到 kube-apiserver 运行所在的控制平面节点上
- 为每个 kubelet 创建密钥和证书;强烈建议为每个 kubelet 使用独一无二的、 CN 取值与众不同的密钥和证书
- 使用 CA 密钥对 kubelet 证书签名
- 将 kubelet 密钥和签名的证书发布到 kubelet 运行所在的特定节点上
- 本文中描述的 TLS 启动引导过程有意简化甚至完全自动化上述过程, 尤其是第三步之后的操作,因为这些步骤是初始化或者扩缩集群时最常见的操作。
启动引导初始化
在启动引导初始化过程中,会发生以下事情:
- kubelet 启动
- kubelet 看到自己没有对应的 kubeconfig 文件
- kubelet 搜索并发现 bootstrap-kubeconfig 文件
- kubelet 读取该启动引导文件,从中获得 API 服务器的 URL 和用途有限的一个“令牌(Token)”
- kubelet 建立与 API 服务器的连接,使用上述令牌执行身份认证
- kubelet 现在拥有受限制的凭据来创建和取回证书签名请求(CSR)
- kubelet 为自己创建一个 CSR,并将其 signerName 设置为 kubernetes.io/kube-apiserver-client-kubelet
- CSR 被以如下两种方式之一批复:
- 如果配置了,kube-controller-manager 会自动批复该 CSR
- 如果配置了,一个外部进程,或者是人,使用 Kubernetes API 或者使用 kubectl 来批复该 CSR
- kubelet 所需要的证书被创建
- 证书被发放给 kubelet
- kubelet 取回该证书
- kubelet 创建一个合适的 kubeconfig,其中包含密钥和已签名的证书
- kubelet 开始正常操作
- 可选地,如果配置了,kubelet 在证书接近于过期时自动请求更新证书
- 更新的证书被批复并发放;取决于配置,这一过程可能是自动的或者手动完成
本文的其余部分描述配置 TLS 启动引导的必要步骤及其局限性。
kubelet会寻找kubeconfig文件,如果没找到则
- 寻找bootstrap-kubeconfig文件开始引导过程
- 使用bootstrap-kubeconfig文件中的api server url和token(有限权限)进行身份认证
- 创建和取回证书签名请求(CSR),此时kube-controller-manager会自动批复该 CSR
- kubelet 取回签发的证书,创建
kubeconfig
文件,包含密钥和已签名的证书
在使用kubeadm初始化control plane时日志中的信息和引导配置一致
[apiclient] All control plane components are healthy after 15.003839 seconds
[upload-config] Storing the configuration used in ConfigMap "kubeadm-config" in the "kube-system" Namespace
[kubelet] Creating a ConfigMap "kubelet-config-1.18" in namespace kube-system with the configuration for the kubelets in the cluster
[upload-certs] Skipping phase. Please see --upload-certs
[bootstrap-token] Using token: 47wg36.zm1tr8lfosat2943
[bootstrap-token] Configuring bootstrap tokens, cluster-info ConfigMap, RBAC Roles
[bootstrap-token] configured RBAC rules to allow Node Bootstrap tokens to get nodes
[bootstrap-token] configured RBAC rules to allow Node Bootstrap tokens to post CSRs in order for nodes to get long term certificate credentials
[bootstrap-token] configured RBAC rules to allow the csrapprover controller automatically approve CSRs from a Node Bootstrap Token
[bootstrap-token] configured RBAC rules to allow certificate rotation for all node client certificates in the cluster
[bootstrap-token] Creating the "cluster-info" ConfigMap in the "kube-public" namespace
[kubelet-finalize] Updating "/etc/kubernetes/kubelet.conf" to point to a rotatable kubelet client certificate and key
同样,kubelet启动时需要以下参数来完成初始化
--cert-dir="/var/lib/kubelet/pki"
,path to store the key and certificate it generates (optional, can use default)--kubeconfig="/var/lib/kubelet/kubeconfig"
,A path to akubeconfig
file that does not yet exist; it will place the bootstrapped config file here--bootstrap-kubeconfig="/var/lib/kubelet/bootstrap-kubeconfig"
,A path to a bootstrapkubeconfig
file to provide the URL for the server and bootstrap credentials, e.g. a bootstrap token- Optional: instructions to rotate certificates
其中/var/lib/kubelet/bootstrap-kubeconfig文件的内容格式如下
- user字段下使用一个token进行身份认证
- certificate-authority由kubelet用来验证apiserver的服务器证书
apiVersion: v1
kind: Config
clusters:
- cluster:
certificate-authority: /var/lib/kubernetes/ca.pem
server: https://my.server.example.com:6443
name: bootstrap
contexts:
- context:
cluster: bootstrap
user: kubelet-bootstrap
name: bootstrap
current-context: bootstrap
preferences: {}
users:
- name: kubelet-bootstrap
user:
token: 07401b.f395accd246ae52d
对于kubeconfig
文件
在启动 kubelet 时,如果
--kubeconfig
标志所指定的文件并不存在,会使用通过标志--bootstrap-kubeconfig
所指定的启动引导 kubeconfig 配置来向 API 服务器请求客户端证书。 在证书请求被批复并被 kubelet 收回时,一个引用所生成的密钥和所获得证书的 kubeconfig 文件会被写入到通过--kubeconfig
所指定的文件路径下。 证书和密钥文件会被放到--cert-dir
所指定的目录中
引导节点拿到的证书是kubelet客户端证书,用来和apiserver通信。kubelet也可以作为服务向外暴露,证书的来源有
- 使用通过
--tls-private-key-file
和--tls-cert-file
所设置的密钥和证书 - 如果没有提供密钥和证书,则创建自签名的密钥和证书
- 通过 CSR API 从集群服务器请求服务证书
配置完成后最终kubelet
使用该kubeconfig
文件和apiserver
通信,注意这个文件名叫做kubelet-client-current.pem
apiVersion: v1
clusters:
- cluster:
certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0F...
server: https://192.168.1.10:6443
name: test-cluster
contexts:
- context:
cluster: test-cluster
user: system:node:test-cluster-node-1
name: system:node:test-cluster-node-1
current-context: system:node:test-cluster-node-1
kind: Config
preferences: {}
users:
- name: system:node:test-cluster-node-1
user:
client-certificate: /var/lib/kubelet/pki/kubelet-client-current.pem
client-key: /var/lib/kubelet/pki/kubelet-client-current.pem
eks集群的kubelet启动引导
eks节点使用bootstrap启动脚本来完成kubelet的初始化,启动的完整参数如下
没有指定
--bootstrap-kubeconfig
,意味着不需要进行TLS token的初始化/usr/bin/kubelet --cloud-provider aws --image-credential-provider-config /etc/eks/ecr-credential-provider/ecr-credential-provider-config --image-credential-provider-bin-dir /etc/eks/ecr-credential-provider --config /etc/kubernetes/kubelet/kubelet-config.json --kubeconfig /var/lib/kubelet/kubeconfig --container-runtime remote --container-runtime-endpoint unix:///run/containerd/containerd.sock --node-ip=192.168.27.37 --pod-infra-container-image=918309763551.dkr.ecr.cn-north-1.amazonaws.com.cn/eks/pause:3.5 --v=2
/var/lib/kubelet/pki/
路径下存在证书-rw------- 1 root root 1378 May 20 04:41 kubelet-server-2023-05-20-04-41-35.pem lrwxrwxrwx 1 root root 59 May 20 04:41 kubelet-server-current.pem -> /var/lib/kubelet/pki/kubelet-server-2023-05-20-04-41-35.pem
我们启动一个新的eks优化ami查看这些路径下有无这些文件,
aws ec2 run-instances --image-id ami-0b779da1a68e38cf1 \
--instance-type m4.large \
--key-name temp-key \
--count 1 \
--subnet-id subnet-027025e9d9760acdd \
--security-group-ids sg-096df1a0cb9a6d7e9 \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=testena}]' 'ResourceType=volume,Tags=[{Key=Name,Value=testena}]'
查看kubeconfig
实际上是由bootstrap替换得到的
$ cat /var/lib/kubelet/kubeconfig
apiVersion: v1
kind: Config
clusters:
- cluster:
certificate-authority: /etc/kubernetes/pki/ca.crt
server: MASTER_ENDPOINT
name: kubernetes
contexts:
- context:
cluster: kubernetes
user: kubelet
name: kubelet
current-context: kubelet
users:
- name: kubelet
user:
exec:
apiVersion: client.authentication.k8s.io/v1beta1
command: /usr/bin/aws-iam-authenticator
args:
- "token"
- "-i"
- "CLUSTER_NAME"
- --region
- "AWS_REGION"
/var/lib/kubelet/pki/
路径下并无文件,因此实际上是通过kubelet启动时生成的
通常的eks节点kubelet启动日志
我们修改并额外指定kubelet启动参数为/var/lib/kubelet/bootstrap-kubeconfig
mv /var/lib/kubelet/kubeconfig /var/lib/kubelet/bootstrap-kubeconfig
KUBELET_EXTRA_ARGS="--bootstrap-kubeconfig /var/lib/kubelet/bootstrap-kubeconfig $KUBELET_EXTRA_ARGS"
此时kubelet无法启动,等待客户端证书签发
/var/lib/kubelet/pki/
多了一个文件kubelet-client.key.tmp
- kubelet生成cliet私钥,使用该私钥生成csr
- 之后向apiserver发送csr,请求公钥证书验证签名
$ ll
total 8
-rw------- 1 root root 227 May 20 05:59 kubelet-client.key.tmp
再次重启kubelet,仍旧保持同样的结果
手动通过csr
$ kubectl get csr
node-csr-NvE0ty-HnP-dA435aze3oo3ubHfHI70ZX_9fTIss0Z0
[ec2-user@ip-172-31-22-99 ~]$ kubectl certificate approve node-csr-NvE0ty-HnP-dA435aze3oo3ubHfHI70ZX_9fTIss0Z0
certificatesigningrequest.certificates.k8s.io/node-csr-NvE0ty-HnP-dA435aze3oo3ubHfHI70ZX_9fTIss0Z0 approved
此时kubelet显示csr已经通过,等待签发证书,但是这一步显然是不行的(可能会绕过sts)
"Waiting for client certificate to be issued"
certificate signing request node-csr-NvE0ty-HnP-dA435aze3oo3ubHfHI70ZX_9fTIss0Z0 is approved, waiting to be issued
由于eks的apiserver我们看不到,因此无从知晓kubelet的集权是怎么完成的,目前推测和serviceaccount类似都是使用webhook的方式完成的。
接下来,进入节点并停止kubelet服务,手动使用预置参数启动kubelet
# /usr/bin/kubelet --config /etc/kubernetes/kubelet/kubelet-config.json --kubeconfig /var/lib/kubelet/kubeconfig --container-runtime-endpoinunix:///run/containerd/containerd.sock --image-credential-provider-config /etc/eks/image-credential-provider/config.json --image-credential-provider-bin-dir /etc/eks/image-credential-provider --node-ip=192.168.31.153 --pod-infra-container-image=918309763551.dkr.ecr.cn-north-1.amazonaws.com.cn/eks/pause:3.5 --v=2 --cloud-provider=aws --container-runtime=remote --node-labels=eks.amazonaws.com/sourceLaunchTemplateVersion=1,alpha.eksctl.io/cluster-name=test124,alpha.eksctl.io/nodegroup-name=test124-ng6,eks.amazonaws.com/nodegroup-image=ami-0b779da1a68e38cf1,eks.amazonaws.com/capacityType=ON_DEMAND,eks.amazonaws.com/nodegroup=test124-ng6,eks.amazonaws.com/sourceLaunchTemplateId=lt-0adf6b991b5366a98 --max-pods=58
查看kubelet日志
确实存在csr申请和kubelet证书签发
server.go:1175] "Started kubelet" server.go:155] "Starting to listen" address="0.0.0.0" port=10250 ... log.go:198] http: TLS handshake error from 192.168.9.40:33148: no serving certificate available for the kubelet ... csr.go:261] certificate signing request csr-2blqg is approved, waiting to be issued csr.go:257] certificate signing request csr-2blqg is issued certificate_manager.go:270] kubernetes.io/kubelet-serving: Certificate expiration is 2024-05-19 08:05:00 +0000 UTC, rotation deadline is 2024-04-09 08:32:10.925495309 +0000 UTC certificate_manager.go:270] kubernetes.io/kubelet-serving: Waiting 7800h21m57.699693545s for next certificate rotation certificate_manager.go:270] kubernetes.io/kubelet-serving: Certificate expiration is 2024-05-19 08:05:00 +0000 UTC, rotation deadline is 2024-02-19 02:05:12.905986938 +0000 UTC certificate_manager.go:270] kubernetes.io/kubelet-serving: Waiting 6593h54m58.680004839s for next certificate rotation
查看证书内容,kubelet
的用户组是
system:nodes, 用户名为system:node:ip-192-168-31-153.cn-north-1.compute.internal。kube-apiserver
就可以基于Node Authorizer来限制kubelet
只能读取和修改本节点上的资源$ sudo openssl x509 -noout -text -in /var/lib/kubelet/pki/kubelet-server-current.pem Certificate: Data: Version: 3 (0x2) Serial Number: 2c:26:45:8d:d2:56:39:64:33:b4:b0:c6:6f:82:e7:66:14:f7:55:b9 Signature Algorithm: sha256WithRSAEncryption Issuer: CN=kubernetes Validity Not Before: May 20 08:05:00 2023 GMT Not After : May 19 08:05:00 2024 GMT Subject: O=system:nodes, CN=system:node:ip-192-168-31-153.cn-north-1.compute.internal Subject Public Key Info: Public Key Algorithm: id-ecPublicKey Public-Key: (256 bit) pub: 04:25:36:df:f1:44:30:00:f7:62:43:7a:f3:cc:21: 22:ee:ed:40:40:0c:0b:28:2c:87:16:4f:bd:9b:c5: 70:83:e3:15:8a:2c:b9:f1:94:ca:53:95:d8:ee:42: b4:21:ab:85:a6:25:0f:71:b8:2d:c6:b2:08:ce:e0: d9:d1:c3:87:a0 ASN1 OID: prime256v1 NIST CURVE: P-256
这之后逻辑上kubelet会使用这个证书访问apiserver,但是实际上并没有用到这个证书。为了验证我们手动将/var/lib/kubelet/pki
下的文件全部删除
rm -rf /var/lib/kubelet/pki
但是经过一段时间后,节点并没有进入not ready状态,并且没有任何影响
那么接下来的问题在于
(1)如果不需要客户端证书,那为什么还需要申请呢?
目前看来是由于kubelet需要对外暴露服务,所以通过 CSR API 从集群服务器请求服务证书
kubelet
同样对外暴露了HTTPS服务,其客户端主要是kube-apiserver
和一些监控组件,如metric-server
。kube-apiserver
需要访问kubelet
来获取容器的日志和执行命令(kubectl logs/exec), 监控组件需要访问kubelet
暴露的cadvisor接口来获取监控信息在
kubelet
在启动时,如果没有指定服务端证书路径,会创建一个自签的CA证书,并使用该CA为自己签发服务端证书
在kubelet
配置文件配置serverTLSBootstrap
为true就可以启用这项特性,在eks节点上kubelet确实配置为"serverTLSBootstrap": true
也就是说,这里为kubelet签发的并不是客户端证书,而是服务端证书。
(2)eks节点究竟是使用什么来进行apiserver的认证呢?
上面的分析表明,eks节点不是通过签发客户端证书和apiserver通信的(手动签发实际上会卡在等待issued)。目前唯一能看到的和kubelet与apiserver通信的配置就只有/var/lib/kubelet/kubeconfig
文件
手动修改节点上的default凭证会导致节点进入not ready状态,可能是kubelet在使用/usr/bin/aws-iam-authenticator
获取tokne后,请求没有权限导致的。
我们直接拿/usr/bin/aws-iam-authenticator
生成的token请求apiserver
/usr/bin/aws-iam-authenticator token -i test124
curl -k --header "Authorization: Bearer xxxxxxxxxxxxxxxxxxxx" https://C9611E71A15AC11DE8CF33921D4BC09B.yl4.cn-north-1.eks.amazonaws.com.cn
{
"kind": "Status",
"apiVersion": "v1",
"metadata": {},
"status": "Failure",
"message": "forbidden: User \"system:node:ip-192-168-31-153.cn-north-1.compute.internal\" cannot get path \"/\"",
"reason": "Forbidden",
"details": {},
"code": 403
}
# 无权限的请求结果
{
"kind": "Status",
"apiVersion": "v1",
"metadata": {},
"status": "Failure",
"message": "Unauthorized",
"reason": "Unauthorized",
"code": 401
}
那么接下来的问题在于,为什么修改默认凭证后,过了10-15分钟才会出现无权限的问题
查看源码有如下描述,可见token的超时时间为15分钟
https://github.com/kubernetes-sigs/aws-iam-authenticator/blob/master/pkg/token/token.go#L82
const (
// The sts GetCallerIdentity request is valid for 15 minutes regardless of this parameters value after it has been
// signed, but we set this unused parameter to 60 for legacy reasons (we check for a value between 0 and 60 on the
// server side in 0.3.0 or earlier). IT IS IGNORED. If we can get STS to support x-amz-expires, then we should
// set this parameter to the actual expiration, and make it configurable.
requestPresignParam = 60
// The actual token expiration (presigned STS urls are valid for 15 minutes after timestamp in x-amz-date).
presignedURLExpiration = 15 * time.Minute
v1Prefix = "k8s-aws-v1."
maxTokenLenBytes = 1024 * 4
clusterIDHeader = "x-k8s-aws-id"
// Format of the X-Amz-Date header used for expiration
// https://golang.org/pkg/time/#pkg-constants
dateHeaderFormat = "20060102T150405Z"
kindExecCredential = "ExecCredential"
execInfoEnvKey = "KUBERNETES_EXEC_INFO"
stsServiceID = "sts"
)
单纯将kubeconfig文件修改后是不行的,可能是由于kubelet已经将配置文件读取到内存中了,之后手动删除authenticator,发现过一段时间后出现以下报错
kubelet_node_status.go:539] "Error updating node status, will retry" err="error getting node \"ip-192-168-13-54.cn-north-1.compute.internal\": Get \"https://xxxxxxxxB.yl4.cn-north-1.eks.amazonaws.com.cn/api/v1/nodes/ip-192-168-13-54.cn-north-1.compute.internal?resourceVersion=0&timeout=10s\": getting credentials: exec: fork/exec /usr/bin/aws-iam-authenticator: no such file or directory"
同样在检查5次节点状态后超时,可见kubelet确实是通过authenticator的token来与apiserver通信的