3.k8s-拓展知识

Pasted image 20250412124829

1. ServiceAccount Token 在不同K8s版本中的使用

1.1. 前言

ServiceAccount 为 Pod 中运行的进程提供了一个身份,Pod 内的进程可以使用其关联服务账号的身份,向集群的 APIServer 进行身份认证。

当创建 Pod 的时候规范下面有一个 spec.serviceAccount 的属性用来指定该 Pod 使用哪个 ServiceAccount,如果没有指定的话则默认使用 default 这个 sa,然后通过投射卷,在 Pod的目录 /run/secrets/kubernetes.io/serviceaccount/ 下有一个 token 令牌文件。我们通过 RBAC 对该 sa 授予了什么权限,那么容器里的应用拿着这个 token 后,就具备了对应的权限。

但是需要注意的是不同的 K8s 版本对该 token 文件的使用是不一样的,所以我们这里分别进行下简单说明。

Note

个人感觉这个 SAtoken 与 AD域委派 有点相似

维度 Kubernetes ServiceAccount AD 委派
身份代理 Pod 内进程使用 ServiceAccount 身份访问 API Server 服务代表用户或计算机身份执行操作
权限控制 通过 RBAC 绑定角色,限制 ServiceAccount 的权限 通过委派权限控制服务能代表的操作范围
令牌机制 使用 Token 或 projected volume 进行身份认证 使用 Kerberos 票据或 OAuth 令牌

1.2. <=1.20 版本

使用 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

Pasted image 20250409233232
我们可以看到创建 sa 后自动生成了一个 secret,格式为 <saname>-token-xxxx ,比如我们创建了一个名字为 sa-demo 的 sa 之后,系统自动创建了一个名字为 sa-demo-token-4gvbw 的 secret,这个 secret 里就包含了一个 token

kubectl describe secrets sa-demo-token-xmmqm

Pasted image 20250409233239
可以看到自动生成的这个 secret 对象里面包含一个 token,我们也可以通过下面的命令来获取,
kubectl get secrets sa-demo-token-xmmqm -o jsonpath='{.data.token}' | base64 -d

可以把这个 token 复制到 jwt.io 网站进行解码,
Pasted image 20250409233244
可以看到里面没有过期时间,也说明了该 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 数据并没有变化。
Pasted image 20250409233258
如果需要在 Pod 中去访问 K8s 集群的资源对象,现在就可以为使用的 sa 绑定上相应的权限,然后在 Pod 应用中使用该对应的 token 去和 APIServer 进行通信就可以了,这个时候的 token 就能识别到对应的权限了。

1.3. >=1.21版本 && <=1.23版本

使用 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

Pasted image 20250409235510
同样可以看到创建 sa 后系统也自动创建了一个对应的 secret 对象,和以前版本没什么区别,我们也可以通过下面的命令来获得该 secret 对象里面包含的 token 值:

kubectl get secrets sa-demo-token-gxqzw -o jsonpath='{.data.token}' | base64 -d

Pasted image 20250409235540
同样将该 token 值拷贝到 jwt.io 网站进行解码,从解码后的值可以看到该 token 值里面同样不包含任何过期时间,也说明了我们创建 sa 之后,所对应的 token 是永不过期的。
Pasted image 20250409235617
同样我们再次使用上面的 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 值。
Pasted image 20250410152905
可以看到该 token 值解码后的 PAYLOAD 数据中包含了很多不同的数据,其中的 exp 字段表示该 token 的过期时间,可以看到过期时间是 1 年。

这里我们可以总结下在 v1.21 到 v1.23 版本的 K8s 集群,当创建 ServiceAccount 对象后,系统仍然会自动创建一个 secret 对象,该 secret 对象里面包含的 token 仍然是永不过期的,但是 Pod 里面并不会使用该 secret 的 token 值了。

从上面查看创建后的 Pod 资源清单可以看出,现在创建 Pod 后,Kubernetes 控制平面会自动添加一个投射卷到 Pod,此卷包括了访问 Kubernetes API 的 token,该清单片段定义了由三个数据源组成的投射卷,这三个数据源是:

  • serviceAccountToken数据源 :包含 kubelet 从 kube-apiserver 获取的令牌,kubelet 使用 TokenRequest API 获取有时间限制的令牌。为 TokenRequest 服务的这个令牌会在 Pod 被删除或定义的生命周期(默认为 1 小时)结束之后过期。该令牌绑定到特定的 Pod, 并将其受众设置为与 kube-apiserver 的 audience 相匹配。 这种机制取代了之前基于 Secret 添加卷的机制,之前 Secret 代表了针对 Pod 的 ServiceAccount 但不会过期。
  • configMap 数据源 :ConfigMap 包含一组证书颁发机构数据,Pod 可以使用这些证书来确保自己连接到集群的 kube-apiserver(而不是连接到中间件或意外配置错误的对等点上)。
  • downwardAPI 数据源 :用于查找包含 Pod 的名字空间的名称,并使该名称信息可用于在 Pod 内运行的应用程序代码。

所以我们应该要指定现在版本的 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

1.4. >=1.24 版本

使用 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

Pasted image 20250410153232
我们可以看到该 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 将使其绑定的令牌过期。

1.5. 总结

  • 1.20(含 1.20)之前的版本,在创建 sa 时会自动创建一个 secret,然后这个会把这个 secret 通过投射卷挂载到 pod 里 /var/run/secrets/kubernetes.io/serviceaccount/token ,该 secret 里面包含的 token 是永久有效的。
  • 1.21~1.23 版本,在创建 sa 时也会自动创建 secret,但是在 pod 里并不会使用 secret 里的 token,而是由 kubelet 到 TokenRequest API 去申请一个 token,该 token 默认有效期为一年,但是 pod 每一个小时会更新一次 token。
  • 1.24 版本及以上,在创建 sa 时不再自动创建 secret 了,只保留由 kubelet 到 TokenRequest API 去申请 token。

当然我们仍然可以手动创建 Secret 来保存 ServiceAccount 令牌,例如在你需要一个永不过期的令牌的时候。一旦手动创建一个 Secret 并将其关联到 ServiceAccount,Kubernetes 控制平面就会自动将令牌填充到该 Secret 中。

可以发现随着版本更新,再逐渐淘汰使用secret中的token,因为其具有永不过期的特性,导致危害很大
替代方案就是使用kubelet 到TokenRequest API去申请一个token ,这个token具有有效期且会定期更新

2. 非标准 pod 和容器的限制和利用

2.1. 实验环境

Kind 部署 1master 2worker(v1.23) 集群。

2.2. 多种 pod

2.2.1. 标准 pod

通常使用 kubectl 命令创建标准 pod,创建过程涉及与 K8S API 服务器的交互,是使用 K8S API 创建和定义的,因此可以在 K8S API 中查询它们。

kubectl get pods --all-namespaces

Pasted image 20250410154320

2.2.2. 静态 pod

静态 pod 是由 kubelet 管理的 pod,kubelet 是集群中每个节点上运行的主要节点代理,而不是 K8S API。静态 pod 通常用于引导 K8S 控制平面本身及内部服务。因为静态 pod 是由 kubelet 管理,所以它们不能引用其他 K8S 对象(如 secrets、config maps、服务账号等)。

因为静态pod不与 k8s apiserver进行交互

2.2.2.1. 创建静态 pod

要创建静态 pod,必须将 kubelet 配置为在调用时接受静态 pod 清单。这可以通过在配置文件中指定相关字段或使用指定静态 pod 清单位置的专用命令行参数调用它来实现。kubelet 可以在本地路径或 Web 托管位置中使用 and 命令行参数查找静态 pod 清单。

进入 master 节点: docker exec -it demo-control-plane /bin/bash
找 kubelet 配置文件位置: ps -ef | grep kubelet
Pasted image 20250410154412

查看静态 pod 清单路径: cat /var/lib/kubelet/config.yaml | grep staticPod Pasted image 20250410154420

ls /etc/kubernetes/manifests
Pasted image 20250410154428
在上面的路径目录中创建一个 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 会自动创建
Pasted image 20250410154504

2.2.2.2. 删除静态 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 的存在,但不会对它们进行管理。

2.2.3. 镜像 pod

镜像 pod 是由 kubelet 生成的对象,用于表示控制平面上的静态 pod,为此,必须将 kubelet 配置为报告静态 pod,并授权在控制平面上创建镜像 pod。这意味着,根据控制平面设置,镜像 pod 可能未启用,因此可能对仅依赖 API 的 K8S 管理员、开发人员和监控产品不可见

2.2.3.1. 发现静态 pod

如果 kubelet 配置为通过镜像 pod 报告静态 pod,那么 K8S 控制平面就可以发现它们。

2.3. 多种容器

2.3.1. 标准容器

2.3.1.1. 发现标准容器
kubectl get pods --all-namespaces -o json | jq '.items[].spec.containers[].name'

Pasted image 20250410154626

2.3.2. 非标准容器

这些容器可以在 Kubernetes 清单的其他部分中以 spec 键进行列出,也可以在 spec 键之外列出,或者根本不列出;它们包括 init 容器、Pod infra(暂停容器)和临时容器。

2.3.3. init 容器

init 容器设计为在主应用程序容器启动之前完成运行。init 容器使得可以为 Pod 中的主应用程序容器执行 Pod 级设置任务成为可能。init 容器在其可用资源和应用程序方面与标准容器不同。由于可以使用多个 init 容器,它们还具有类似组的共享资源请求和限制。init 容器的常见用例包括下载配置文件、准备数据库和延迟启动应用程序容器。考虑到 init 容器监督标准容器的引导和设置阶段,攻击者可能利用它们来污染设置(例如获得持久性)。因此,尽管经常被忽视,但 init 容器也应进行监控。

2.3.3.1. 创建 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 -
2.3.3.2. 发现 init 容器
kubectl get pods --all-namespaces -o json | jq '.items[] | select(.spec.initContainers) | {pod: .metadata.name, initContainers: .spec.initContainers[].name}'

Pasted image 20250410154735

2.3.4. 暂停容器

Pod infra 容器又叫暂停容器,即使 Pod 中没有其他容器运行,暂停容器也保持活动状态,暂停容器的镜像由 kubelet 通过命令行参数或 kubelet 配置文件中的标识定义。 pod-infra-container-imagepodInfraContainerImage pod-infra-container-imagepodInfraContainerImage 由于 kubelet 管理此类容器,Kubernetes API 不知道其存在。目前还没有机制可以让 kubelet 对这些容器进行报告。API 缺乏可见性,加上暂停容器在 pod/container 创建时的自动执行,使得任何修改这些容器为攻击者控制的镜像的威胁行为者都能实现潜在持久性。它们附加到每个 pod,并且能够检查 Pod 的内容、流量和数据。

2.3.4.1. 修改暂停容器获取隐蔽持久化

docker exec -it demo-control-plane /bin/bash

检查节点上 kubelet 守护程序来识别指定的当前暂停容器,需要一定的权限。
systemctl status kubelet
Pasted image 20250410154834

"10-kubeadm.conf"中存在暂停容器的标识, pod-infra-container-image
Pasted image 20250410154846

cat /etc/systemd/system/kubelet.service.d/10-kubeadm.conf
Pasted image 20250410154852

kind 集群中暂停容器的具体设置看来是在 kubelet 环境变量中,
cat /var/lib/kubelet/kubeadm-flags.env
Pasted image 20250410154904

ctr -n k8s.io i ls -q 暂停容器的镜像在节点中
Pasted image 20250410154913

2.3.4.2. 小插曲:发现暂停容器

暂停容器是经过设计隐藏的,并且无法通过 K8s 客户端(例如 kubectl)对 Kubernetes 用或管理员可见或可访问。出于同样的原因,它们对云提供商也不可见。因此,识别当前运行的暂停容器需要节点级访问权限和查询容器运行时的能力。

要发现暂停容器,可以执行以下步骤:

  1. 获取节点上的容器运行时信息,例如使用 Docker 命令检查运行中的容器。
  2. 对于云平台,可能需要使用平台特定的工具或 API 查询当前运行的容器。

创建 Pod 后运行的暂停容器,
ps -aux | grep pause master 节点中一共运行了 9 个暂停容器,
Pasted image 20250410155017
这是因为 master 节点中有 9 个 Pod,每个运行中的 Pod 都有一个关联的 pause 容器

kubectl get pods -A -owide | grep demo-control-plane
Pasted image 20250410155038
要替换现有的暂停镜像,新镜像必须在其 Docker 文件中指定一个默认命令,并且默认情况下该预期进程应该是长期运行的。在此示例中,一个适用的镜像是 nginx,因此我们更改配置文件中镜像为 nginx