ServiceAccount 为 Pod 中运行的进程提供了一个身份,Pod 内的进程可以使用其关联服务账号的身份,向集群的 APIServer 进行身份认证。
当创建 Pod 的时候规范下面有一个 spec.serviceAccount
的属性用来指定该 Pod 使用哪个 ServiceAccount,如果没有指定的话则默认使用 default 这个 sa,然后通过投射卷,在 Pod的目录 /run/secrets/kubernetes.io/serviceaccount/
下有一个 token 令牌文件。我们通过 RBAC 对该 sa 授予了什么权限,那么容器里的应用拿着这个 token 后,就具备了对应的权限。
但是需要注意的是不同的 K8s 版本对该 token 文件的使用是不一样的,所以我们这里分别进行下简单说明。
个人感觉这个 SAtoken 与 AD域委派 有点相似
维度 | Kubernetes ServiceAccount | AD 委派 |
---|---|---|
身份代理 | Pod 内进程使用 ServiceAccount 身份访问 API Server | 服务代表用户或计算机身份执行操作 |
权限控制 | 通过 RBAC 绑定角色,限制 ServiceAccount 的权限 | 通过委派权限控制服务能代表的操作范围 |
令牌机制 | 使用 Token 或 projected volume 进行身份认证 | 使用 Kerberos 票据或 OAuth 令牌 |
使用 kind 快速创建一个小于等于 v1.20 版本的集群,
docker pull kindest/node:v1.20.7
kind create cluster --name kind120 --image kindest/node:v1.20.7
先创建一个名字为 sa-demo
的 ServiceAccount 对象,
#在当前命名空间创建一个名为 sa-demo 的服务账户
kubectl create sa sa-demo
#列出当前命名空间的所有服务账户
kubectl get sa
#列出当前命名空间的 Secret(包括自动生成的服务账户令牌)
kubectl get secret
我们可以看到创建 sa 后自动生成了一个 secret,格式为 <saname>-token-xxxx
,比如我们创建了一个名字为 sa-demo
的 sa 之后,系统自动创建了一个名字为 sa-demo-token-4gvbw
的 secret,这个 secret 里就包含了一个 token
kubectl describe secrets sa-demo-token-xmmqm
可以看到自动生成的这个 secret 对象里面包含一个 token,我们也可以通过下面的命令来获取,
kubectl get secrets sa-demo-token-xmmqm -o jsonpath='{.data.token}' | base64 -d
可以把这个 token 复制到 jwt.io 网站进行解码,
可以看到里面没有过期时间,也说明了该 token 是永不过期的。
现在我们使用上面我们创建的 sa 来运行一个 Pod,
apiVersion: v1
kind: Pod
metadata:
name: demo
spec:
serviceAccount: sa-demo
containers:
- name: demo
image: nginx:1.7.9
ports:
- containerPort: 80
直接创建该 Pod,
kubectl apply -f demo-pod.yaml
kubectl get pod demo -oyaml
apiVersion: v1
kind: Pod
metadata:
name: demo
namespace: default
resourceVersion: "1460"
uid: 61176f36-7475-4a6b-9ff4-afc2a4530175
spec:
containers:
- image: nginx:1.7.9
imagePullPolicy: IfNotPresent
name: demo
ports:
- containerPort: 80
protocol: TCP
resources: {}
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
volumeMounts:
- mountPath: /var/run/secrets/kubernetes.io/serviceaccount
name: sa-demo-token-xmmqm
readOnly: true
serviceAccount: sa-demo
serviceAccountName: sa-demo
volumes:
- name: sa-demo-token-xmmqm
secret:
defaultMode: 420
secretName: sa-demo-token-xmmqm
Pod 创建后我们可以看到会自动将指定 sa 对应的 secret 挂载到容器的 /var/run/secrets/kubernetes.io/serviceaccount
目录下去,所以现在该目录下面肯定包含对应的 token 文件,我们可以查看该值来验证下:
kubectl exec -it demo -- cat /run/secrets/kubernetes.io/serviceaccount/token
可以看到 Pod 里通过投射卷所挂载的 token 跟 sa-demo 对应的 secret 包含的 token 是模一样的,这个 token 是永不过期的,所以即使删除了 Pod 之后重新创建,Pod 里的 token 仍然是不变的,因为 secret 对象里面的 token 数据并没有变化。
如果需要在 Pod 中去访问 K8s 集群的资源对象,现在就可以为使用的 sa 绑定上相应的权限,然后在 Pod 应用中使用该对应的 token 去和 APIServer 进行通信就可以了,这个时候的 token 就能识别到对应的权限了。
使用 kind 快速创建一个 v1.23.4 版本的集群
docker pull kindest/node:v1.23.4
kind create cluster --name kind123 --image kindest/node:v1.23.4
同样首先创建一个名为 sa-demo 的 ServiceAccount 对象
kubectl create sa sa-demo
kubectl get sa
kubectl get secret
同样可以看到创建 sa 后系统也自动创建了一个对应的 secret 对象,和以前版本没什么区别,我们也可以通过下面的命令来获得该 secret 对象里面包含的 token 值:
kubectl get secrets sa-demo-token-gxqzw -o jsonpath='{.data.token}' | base64 -d
同样将该 token 值拷贝到 jwt.io 网站进行解码,从解码后的值可以看到该 token 值里面同样不包含任何过期时间,也说明了我们创建 sa 之后,所对应的 token 是永不过期的。
同样我们再次使用上面的 sa 来创建一个 Pod,如下所示,
apiVersion: v1
kind: Pod
metadata:
name: demo
spec:
serviceAccount: sa-demo
containers:
- name: demo
image: nginx:1.7.9
ports:
- containerPort: 80
直接创建该 Pod,
kubectl apply -f demo-pod.yaml
kubectl get pod demo -oyaml
root@lavm-cf3dw4rf9c:~# kubectl get pod demo -oyaml
apiVersion: v1
kind: Pod
metadata:
annotations:
kubectl.kubernetes.io/last-applied-configuration: |
{"apiVersion":"v1","kind":"Pod","metadata":{"annotations":{},"name":"demo","namespace":"default"},"spec":{"containers":[{"image":"nginx:1.7.9","name":"demo","ports":[{"containerPort":80}]}],"serviceAccount":"sa-demo"}}
creationTimestamp: "2025-04-10T07:18:16Z"
name: demo
namespace: default
resourceVersion: "94493"
uid: f4e90148-e10d-4538-837a-9a95deda142c
spec:
containers:
- image: nginx:1.7.9
imagePullPolicy: IfNotPresent
name: demo
ports:
- containerPort: 80
protocol: TCP
resources: {}
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
volumeMounts:
- mountPath: /var/run/secrets/kubernetes.io/serviceaccount
name: kube-api-access-mvpx9
readOnly: true
dnsPolicy: ClusterFirst
enableServiceLinks: true
nodeName: kind123-control-plane
preemptionPolicy: PreemptLowerPriority
priority: 0
restartPolicy: Always
schedulerName: default-scheduler
securityContext: {}
serviceAccount: sa-demo
serviceAccountName: sa-demo
terminationGracePeriodSeconds: 30
tolerations:
- effect: NoExecute
key: node.kubernetes.io/not-ready
operator: Exists
tolerationSeconds: 300
- effect: NoExecute
key: node.kubernetes.io/unreachable
operator: Exists
tolerationSeconds: 300
volumes:
- name: kube-api-access-mvpx9
projected:
defaultMode: 420
sources:
- 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 创建后查看对应的资源对象,可以看到和之前的版本已经有一个很大的区别了,并不是将上面自动创建的 secret 挂载到容器的 /var/run/secrets/kubernetes.io/serviceaccount
目录。我们可以查看下 Pod 中的 token 值来和 secret 包含的 token 值进行对比,
kubectl exec -it demo -- cat /run/secrets/kubernetes.io/serviceaccount/token
可以很明显看到现在 Pod 中的 token 值和自动创建 secret 的 token 值不一样了,同样在 jwt.io 解码该 token 值。
可以看到该 token 值解码后的 PAYLOAD 数据中包含了很多不同的数据,其中的 exp 字段表示该 token 的过期时间,可以看到过期时间是 1 年。
这里我们可以总结下在 v1.21 到 v1.23 版本的 K8s 集群,当创建 ServiceAccount 对象后,系统仍然会自动创建一个 secret 对象,该 secret 对象里面包含的 token 仍然是永不过期的,但是 Pod 里面并不会使用该 secret 的 token 值了。
从上面查看创建后的 Pod 资源清单可以看出,现在创建 Pod 后,Kubernetes 控制平面会自动添加一个投射卷到 Pod,此卷包括了访问 Kubernetes API 的 token,该清单片段定义了由三个数据源组成的投射卷,这三个数据源是:
所以我们应该要指定现在版本的 K8s 集群创建的 Pod 里面包含的 token 不是使用 ServiceAccount 自动关联的 secret 对象里面的 token 了,而是 kubelet 会向 TokenRequest API 发送一个请求,申请一个新的 token 放在 Pod 的 /run/secrets/kubernetes.io/serviceaccount/token 里。这个 token 会在 1 个小时后由 kubelet 重新去申领一个新的 token,所以 1 小时之后再次查看这个 token 的话会发现 token 的内容是变化的,如果删除此 Pod 重新创建的话,则会重新申领 token,被删除 Pod 里的 token 会立即过期。
而且我们还可以手动使用 kubectl create token <sa>
命令来请求 ServiceAccount
kubectl create token sa-demo
kubectl create token kube-proxy -n kube-system
# 请求具有自定义过期时间的令牌
kubectl create token sa-demo --duration 10m
使用 kind 快速创建一个 v1.27.1 版本的集群,
docker pull kindest/node:v1.27.1
kind create cluster --name kind127 --image kindest/node:v1.27.1
同样首先创建一个名为 sa-demo
的 ServiceAccount 对象,
kubectl create sa sa-demo
kubectl get sa
kubectl get secret
我们可以看到该 ServiceAccount 创建后并没有创建对应的 Secret 对象。
同样我们再次使用上面的 sa 来创建一个 Pod,如下所示
apiVersion: v1
kind: Pod
metadata:
name: demo
spec:
serviceAccount: sa-demo
containers:
- name: demo
image: nginx:1.7.9
ports:
- containerPort: 80
直接创建该 Pod,
kubectl apply -f demo-pod.yaml
kubectl get pod demo -oyaml
apiVersion: v1
kind: Pod
metadata:
name: demo
namespace: default
resourceVersion: "2446"
uid: 1644b00f-6d10-44f9-bab1-d6a662c7f6f7
spec:
containers:
- image: nginx:1.7.9
imagePullPolicy: IfNotPresent
name: demo
ports:
- containerPort: 80
protocol: TCP
resources: {}
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
volumeMounts:
- mountPath: /var/run/secrets/kubernetes.io/serviceaccount
name: kube-api-access-mvrxm
readOnly: true
serviceAccount: sa-demo
serviceAccountName: sa-demo
volumes:
- name: kube-api-access-mvrxm
projected:
defaultMode: 420
sources:
- 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 后同样会自动添加一个投射卷到 Pod,此卷包括了访问 Kubernetes API 的令牌,和 >=1.21
版本 && <=1.23
版本 表现是一致的。同样我们可以下查看 Pod 中的 token 值来进行验证,
kubectl exec -it demo -- cat /run/secrets/kubernetes.io/serviceaccount/token
把上面输出的 token 值拷贝到 jwt.io 里进行解码。发现 token 的有效期也为 1 年,这个 token 在 Pod 里也是每 1 小时会更新一次,如果 Pod 被删除重建,那么会重新申领一个新的 token,被删除 Pod 里的 token 立即过期。
需要注意的没有特定的机制使通过 TokenRequest 签发的令牌无效,如果你不再信任为某个 Pod 绑定的 ServiceAccount 令牌,你可以删除该 Pod,删除 Pod 将使其绑定的令牌过期。
当然我们仍然可以手动创建 Secret 来保存 ServiceAccount 令牌,例如在你需要一个永不过期的令牌的时候。一旦手动创建一个 Secret 并将其关联到 ServiceAccount,Kubernetes 控制平面就会自动将令牌填充到该 Secret 中。
可以发现随着版本更新,再逐渐淘汰使用secret中的token,因为其具有永不过期的特性,导致危害很大
替代方案就是使用kubelet 到TokenRequest API去申请一个token ,这个token具有有效期且会定期更新
Kind 部署 1master 2worker(v1.23) 集群。
通常使用 kubectl 命令创建标准 pod,创建过程涉及与 K8S API 服务器的交互,是使用 K8S API 创建和定义的,因此可以在 K8S API 中查询它们。
kubectl get pods --all-namespaces
静态 pod 是由 kubelet 管理的 pod,kubelet 是集群中每个节点上运行的主要节点代理,而不是 K8S API。静态 pod 通常用于引导 K8S 控制平面本身及内部服务。因为静态 pod 是由 kubelet 管理,所以它们不能引用其他 K8S 对象(如 secrets、config maps、服务账号等)。
因为静态pod不与 k8s apiserver进行交互
要创建静态 pod,必须将 kubelet 配置为在调用时接受静态 pod 清单。这可以通过在配置文件中指定相关字段或使用指定静态 pod 清单位置的专用命令行参数调用它来实现。kubelet 可以在本地路径或 Web 托管位置中使用 and 命令行参数查找静态 pod 清单。
进入 master 节点: docker exec -it demo-control-plane /bin/bash
找 kubelet 配置文件位置: ps -ef | grep kubelet
查看静态 pod 清单路径: cat /var/lib/kubelet/config.yaml | grep staticPod
ls /etc/kubernetes/manifests
在上面的路径目录中创建一个 Pod 配置文件
echo 'apiVersion: v1
kind: Pod
metadata:
name: nginx-static-pod
spec:
containers:
- name: nginx-static-pod
image: nginx
command:
- "/bin/bash"
- "-c"
- "--"
args:
- "while true; do sleep 30; done;"' | tee /etc/kubernetes/manifests/nginx-static.yaml
该 Pod 会自动创建
通过 kubectl 删除静态 pod 是失败的,因为 K8S API 不生成静态pod,因此不知道它们的存在。可以直接删除 nginx-static.yaml 即可自动删除静态 pod。
上述静态 pod 创建过程中,并不是由 K8S API 主导的,K8S API 不生成静态 pod ,因此它不知道 静态 pod 的存在,那上图中静态 pod 为什么能被 kubectl 命令列出来了?
因为将 kubelet 配置为使用镜像 pod ,意味着 kubelet 将会创建这些不受 Kubernetes 控制的容器,然后将它们报告给 K8S 控制平面。这样 K8S 控制平面可能会知道这些镜像 pod 的存在,但不会对它们进行管理。
镜像 pod 是由 kubelet 生成的对象,用于表示控制平面上的静态 pod,为此,必须将 kubelet 配置为报告静态 pod,并授权在控制平面上创建镜像 pod。这意味着,根据控制平面设置,镜像 pod 可能未启用,因此可能对仅依赖 API 的 K8S 管理员、开发人员和监控产品不可见
如果 kubelet 配置为通过镜像 pod 报告静态 pod,那么 K8S 控制平面就可以发现它们。
kubectl get pods --all-namespaces -o json | jq '.items[].spec.containers[].name'
这些容器可以在 Kubernetes 清单的其他部分中以 spec 键进行列出,也可以在 spec 键之外列出,或者根本不列出;它们包括 init 容器、Pod infra(暂停容器)和临时容器。
init 容器设计为在主应用程序容器启动之前完成运行。init 容器使得可以为 Pod 中的主应用程序容器执行 Pod 级设置任务成为可能。init 容器在其可用资源和应用程序方面与标准容器不同。由于可以使用多个 init 容器,它们还具有类似组的共享资源请求和限制。init 容器的常见用例包括下载配置文件、准备数据库和延迟启动应用程序容器。考虑到 init 容器监督标准容器的引导和设置阶段,攻击者可能利用它们来污染设置(例如获得持久性)。因此,尽管经常被忽视,但 init 容器也应进行监控。
init 容器在 Pod 的规范中通过数组字段定义。当 Pod 启动时,Kubernetes 将按照它们在 Pod 配置文件中定义的顺序运行每个 init 容器。 spec.initContainers
echo 'apiVersion: v1
kind: Pod
metadata:
name: init-containers-pod
spec:
containers:
- name: main-container
image: wordpress
initContainers:
- name: init-container-1
image: busybox
command: [ "sleep", "3"]
- name: init-container-2
image: ubuntu
command: [ "touch", "/tmp/demoInitSetupFile"]' | kubectl apply -f -
kubectl get pods --all-namespaces -o json | jq '.items[] | select(.spec.initContainers) | {pod: .metadata.name, initContainers: .spec.initContainers[].name}'
Pod infra 容器又叫暂停容器,即使 Pod 中没有其他容器运行,暂停容器也保持活动状态,暂停容器的镜像由 kubelet 通过命令行参数或 kubelet 配置文件中的标识定义。 pod-infra-container-imagepodInfraContainerImage
pod-infra-container-imagepodInfraContainerImage
由于 kubelet 管理此类容器,Kubernetes API 不知道其存在。目前还没有机制可以让 kubelet 对这些容器进行报告。API 缺乏可见性,加上暂停容器在 pod/container 创建时的自动执行,使得任何修改这些容器为攻击者控制的镜像的威胁行为者都能实现潜在持久性。它们附加到每个 pod,并且能够检查 Pod 的内容、流量和数据。
docker exec -it demo-control-plane /bin/bash
检查节点上 kubelet 守护程序来识别指定的当前暂停容器,需要一定的权限。
systemctl status kubelet
"10-kubeadm.conf"中存在暂停容器的标识, pod-infra-container-image
cat /etc/systemd/system/kubelet.service.d/10-kubeadm.conf
kind 集群中暂停容器的具体设置看来是在 kubelet 环境变量中,
cat /var/lib/kubelet/kubeadm-flags.env
ctr -n k8s.io i ls -q
暂停容器的镜像在节点中
暂停容器是经过设计隐藏的,并且无法通过 K8s 客户端(例如 kubectl)对 Kubernetes 用或管理员可见或可访问。出于同样的原因,它们对云提供商也不可见。因此,识别当前运行的暂停容器需要节点级访问权限和查询容器运行时的能力。
要发现暂停容器,可以执行以下步骤:
创建 Pod 后运行的暂停容器,
ps -aux | grep pause master
节点中一共运行了 9 个暂停容器,
这是因为 master 节点中有 9 个 Pod,每个运行中的 Pod 都有一个关联的 pause 容器
kubectl get pods -A -owide | grep demo-control-plane
要替换现有的暂停镜像,新镜像必须在其 Docker 文件中指定一个默认命令,并且默认情况下该预期进程应该是长期运行的。在此示例中,一个适用的镜像是 nginx,因此我们更改配置文件中镜像为 nginx