起因
有一个 CNI 组件以 DaemonSet 的方式运行在所有的 node 上,这个 CNI Pod 会将自己的 Service Account Token 转换成 kubeconfig 并存储到主机的目录下。当 kubelet 调用 cni 插件时,cni 插件会使用这个 kubeconfig 去获取集群 Pod 的一些信息。
在 k8s 1.24 上出现了问题,当 CNI Pod 重启后,使用生成的 kubeconfig 就会返回 Unauthorized 的错误,即这个 token 已经过不了 APIServer 的认证了。
原因
k8s 1.24 上,ServiceAccount(下文缩写为 SA) 的 token 生成逻辑已经发生了变化,不再会自动为 SA 生成 token 并保存到 secret 中,Pod 中使用 token 时也不会再挂载这个 secret。当 Pod 使用 SA 时,默认行为如下:
- Pod 创建出来后,在 admission 阶段,有一个 serviceaccount admission 会为 Pod 挂载 token,路径同样还是在
/var/run/secrets/kubernetes.io/serviceaccount
下。但是 volume 字段不再是通过 secret,而是通过 projected。
projected:
defaultMode: 420
sources:
# source 类型是 serviceAccountToken
- serviceAccountToken:
expirationSeconds: 3607
path: token
- configMap:
items:
- key: ca.crt
path: ca.crt
name: kube-root-ca.crt
- downwardAPI:
items:
- fieldRef:
apiVersion: v1
fieldPath: metadata.namespace
path: namespace
- Pod 调度到 Node 上后,kubelet 中的 projected volume mounter 会根据 volumesMount 中的 volume 类型,为 Pod 挂载对应的文件。当发现存在 ServiceAccountToken 类型的 projected source 时,就会调用 apiserver 的 TokenRequest 接口,为当前 Pod 请求临时的 Token。并且这个 token 的有效期只有 3607s。kubelet 会自动刷新这个 token 来保证它不会过期。
case source.ServiceAccountToken != nil: tp := source.ServiceAccountToken // When FsGroup is set, we depend on SetVolumeOwnership to // change from 0600 to 0640. mode := *s.source.DefaultMode if mounterArgs.FsUser != nil || mounterArgs.FsGroup != nil { mode = 0600 } var auds []string if len(tp.Audience) != 0 { auds = []string{tp.Audience} } tr, err := s.plugin.getServiceAccountToken(s.pod.Namespace, s.pod.Spec.ServiceAccountName, &authenticationv1.TokenRequest{ Spec: authenticationv1.TokenRequestSpec{ Audiences: auds, ExpirationSeconds: tp.ExpirationSeconds, BoundObjectRef: &authenticationv1.BoundObjectReference{ APIVersion: "v1", Kind: "Pod", Name: s.pod.Name, UID: s.pod.UID, }, }, }) if err != nil { errlist = append(errlist, err) continue } payload[tp.Path] = volumeutil.FileProjection{ Data: []byte(tr.Status.Token), Mode: mode, FsUser: mounterArgs.FsUser, }
这样带来的好处就是 service account 默认不再会有永久性 token,而是每个 Pod 有一个临时的 token,这个 token 默认有效期是 3607s,由 kubelet 自动刷新。并且当 Pod 删除后,该 token 也会自动失效。这在安全性上带来了很大的提升。
解决
为了和之前组件的行为保持一致,需要保证这个 token 是永久有效的。最简单的解决办法就是手动创建 service account 的 token secret。例如:
apiVersion: v1 kind: Secret # 表示这个 secret 类型 type: kubernetes.io/service-account-token metadata: name: mycontroller namespace: kube-system annotations: # service account 名称 kubernetes.io/service-account.name: "mycontroller"
k8s 的
tokens-controller
在 watch 到该 secret 时,会发现 ca, namespace, token 字段均为空,因此会自动为该 secret 填充这些字段。这样我们就获得了永久性的 token,并使用该 token 生成 kubeconfig 了。func (e *TokensController) secretUpdateNeeded(secret *v1.Secret) (bool, bool, bool) { caData := secret.Data[v1.ServiceAccountRootCAKey] needsCA := len(e.rootCA) > 0 && !bytes.Equal(caData, e.rootCA) needsNamespace := len(secret.Data[v1.ServiceAccountNamespaceKey]) == 0 tokenData := secret.Data[v1.ServiceAccountTokenKey] needsToken := len(tokenData) == 0 return needsCA, needsNamespace, needsToken }
Token 是如何做身份认证的
service account token 在不同版本下的行为不同,那么 token 本身又是如何做身份认证的呢?
token 是一个符合 JWT 规范的字符串。
对于永久性 token 来说,其中保存了 service account 的信息。
{
"iss": "kubernetes/serviceaccount",
"kubernetes.io/serviceaccount/namespace": "kube-system",
"kubernetes.io/serviceaccount/secret.name": "mycontroller",
"kubernetes.io/serviceaccount/service-account.name": "mycontroller",
"kubernetes.io/serviceaccount/service-account.uid": "2f0ab840-064c-4168-b9b2-932c361e13d6",
"sub": "system:serviceaccount:kube-system:mycontroller"
}
apiserver 在获取到这个 token 后,根据 JWT 的规范对内容进行完整性校验。校验通过后就根据 token 中 service account 进行认证鉴权了。
对于临时性(pod) token 来说,内容就稍有不同了。
{
"aud": [
"https://kubernetes.default.svc.cluster.local"
],
"exp": 1705344168,
"iat": 1673808168,
"iss": "https://kubernetes.default.svc.cluster.local",
"kubernetes.io": {
"namespace": "kube-system",
"pod": {
"name": "mycontroller-lr99n",
"uid": "f8a3c6c7-c41c-4a33-9329-f40d208a03e6"
},
"serviceaccount": {
"name": "mycontroller",
"uid": "2f0ab840-064c-4168-b9b2-932c361e13d6"
},
"warnafter": 1673811775
},
"nbf": 1673808168,
"sub": "system:serviceaccount:kube-system:mycontroller"
}
可以看到 token 中除了 service account 的信息,还有 pod 的信息。这样 token 的有效期是由 pod 的生命周期以及 nbf, exp 来确定了。nbf 代表 Not valid before
,exp 代表 Expiration time
,都是使用 unix time 来保存的。并且在 pod 删除后,token 就自动失效了。同时鉴权还是使用 service account 进行。