WEBKT

无需重启Pod:如何动态调整Kubernetes临时容器的安全上下文与特权

17 0 0 0

在 Kubernetes 集群中,当线上服务出现死锁、内存泄露或异常网络丢包时,我们通常会使用 kubectl debug 注入一个临时容器(Ephemeral Container)进行排查。

然而,默认注入的临时容器往往遵循极低的权限限制(受限于命名空间默认的 Pod 安全标准 PSA 或底层安全策略)。当你尝试在临时容器中运行 tcpdumpgdbstrace 时,往往会遭遇 Permission denied 错误。

由于 Pod 的 Spec 是高度不可变的,一旦临时容器启动,其自身的 securityContext 就无法被直接修改。难道为了获取调试权限,我们必须重启业务 Pod 吗?答案是否定的。

本文将介绍两种在不重启业务 Pod 的前提下,获取或调整临时容器特权通道的硬核方案。


方案一:声明式追加法 —— 注入全新的高特权临时容器

Kubernetes 的 ephemeralcontainers 子资源(Subresource)虽然不支持修改已有的临时容器,但支持向已运行的 Pod 中追加(Append)新的临时容器

如果当前的临时容器权限不足,我们无需重启 Pod,只需向该 Pod 追加一个带有 privileged: true 或特定 capabilities 的全新临时容器即可。

1. 现代化声明式注入(Kubernetes 1.25+)

在较新版本的 Kubernetes 中,kubectl debug 已经内置了安全上下文配置文件(Profiles)。你可以通过 --profile 参数直接注入具备特权的临时容器:

# 注入一个拥有 sysadmin 特权配置文件的临时容器,共享宿主机 PID 命名空间并允许特权操作
kubectl debug -it <pod-name> \
  --image=nicolaka/netshoot \
  --target=<target-container-name> \
  --profile=sysadmin

sysadmin 配置文件会自动为临时容器配置以下安全上下文:

  • privileged: true
  • 允许访问宿主机的命名空间与特权系统调用

2. 绕过 CLI 限制:通过 API 注入精准的 Capabilities

如果你的集群版本较老,或者你需要极度精细的安全上下文控制(例如:仅需要 NET_ADMIN 而非完整的 privileged),可以通过直接调用 Kubernetes API 的方式追加临时容器。

首先,获取当前 Pod 的临时容器列表并保存为 JSON:

kubectl get pod <pod-name> --output=jsonpath='{.spec.ephemeralContainers}' > ephemeral.json

ephemeral.json 中追加一个包含自定义 securityContext 的全新容器定义:

[
  {
    "name": "debugger-net-admin",
    "image": "nicolaka/netshoot",
    "imagePullPolicy": "IfNotPresent",
    "stdin": true,
    "tty": true,
    "targetContainerName": "business-app",
    "securityContext": {
      "capabilities": {
        "add": ["NET_ADMIN", "SYS_PTRACE"]
      },
      "privileged": false,
      "runAsUser": 0
    }
  }
]

使用 kubectl replace --raw 接口将更新后的配置发送给 Pod 的 ephemeralcontainers 子资源。此操作完全不会引发业务容器的重启

kubectl replace --raw "/api/v1/namespaces/<namespace>/pods/<pod-name>/ephemeralcontainers" -f ephemeral.json

接着,直接附加(Attach)到新注入的特权容器上:

kubectl attach -it <pod-name> -c debugger-net-admin

此时,你就可以在不破坏业务容器运行状态的前提下,对业务容器进行抓包(tcpdump)或系统调用追踪(strace)。


方案二:极客应急通道 —— 在 Node 节点动态篡改 OCI 容器状态

如果你的 Kubernetes 集群启用了极为苛刻的 Admission Controller(如 Gatekeeper 或 Kyverno),在 API 层面直接拒绝了任何含有 privileged: true 或特权 capabilities 的临时容器注入请求,那么上述方案一将会失效。

在不重启 Pod、不修改 API 对象的前提下,如果拥有集群 Node 节点的 root 访问权,我们可以绕过 Kubernetes 控制平面,直接在 OCI 运行时(Containerd / CRI-O)层面动态提升容器的权限

警告:此方法属于非正常运维手段,通常用于紧急故障排查,且需要具备 Node 节点的登录和管理权限。

1. 寻找目标临时容器的 Container ID

首先在 master 或通过 kubectl 确定临时容器的 Runtime ID:

kubectl get pod <pod-name> -o jsonpath='{.status.ephemeralContainerStatuses}'

找到对应临时容器的 containerID,格式通常为 containerd://<container-id-hash>

2. 登录业务所在的 Node 节点并使用 runc 接管

Kubernetes 底层大多使用 containerd 作为容器运行时,而 containerd 最终会通过 runc 来创建和管理容器空间。

登录到 Pod 调度所在的宿主机,使用 runc state 找到对应的 OCI 容器:

# 切换到 containerd 的 k8s 命名空间
alias crictl="crictl -runtime-endpoint unix:///run/containerd/containerd.sock"
ctr -n k8s.io containers list | grep <container-id-hash>

3. 修改 OCI 运行时的 config.json 强行提权

在 Containerd 的存储路径下(默认通常在 /run/containerd/io.containerd.runtime.v2.task/k8s.io/<container-id-hash>/),保存着容器运行时的状态配置文件 config.json

  1. 备份并打开该目录下的 config.json

    cd /run/containerd/io.containerd.runtime.v2.task/k8s.io/<container-id-hash>/
    cp config.json config.json.bak
    vi config.json
    
  2. 找到 capabilities 节点,将你需要的权限强行写入(例如加入 CAP_NET_ADMINCAP_SYS_PTRACE):

    "capabilities": {
      "bounding": [
        "CAP_NET_ADMIN",
        "CAP_SYS_PTRACE",
        "CAP_CHOWN",
        "CAP_DAC_OVERRIDE",
        "CAP_FOWNER",
        "CAP_SETGID",
        "CAP_SETUID"
      ],
      "effective": [
        "CAP_NET_ADMIN",
        "CAP_SYS_PTRACE",
        "CAP_CHOWN",
        "CAP_DAC_OVERRIDE",
        "CAP_FOWNER",
        "CAP_SETGID",
        "CAP_SETUID"
      ],
      "inheritable": [
        "CAP_NET_ADMIN",
        "CAP_SYS_PTRACE"
      ],
      "permitted": [
        "CAP_NET_ADMIN",
        "CAP_SYS_PTRACE",
        "CAP_CHOWN",
        "CAP_DAC_OVERRIDE",
        "CAP_FOWNER",
        "CAP_SETGID",
        "CAP_SETUID"
      ]
    }
    
  3. 动态更新容器配置。通过 runc update 命令将新的安全上下文热应用到运行中的容器进程中:

    runc --root /run/containerd/runc/k8s.io update --resources ... <container-id-hash>
    

    注意:runc update 原生主要支持资源限制(CPU/Memory)的动态调整。若要完全重载安全特权,可在修改 config.json 后,通过向容器发送 SIGHUP 信号让运行时进程重载配置,或利用 nsenter 工具直接将拥有特权的 Shell 进程“套壳”注入到临时容器的 Namespace 中。

4. 更优雅的替代方案:使用 nsenter 绕过安全限制

如果你已经拥有 Node 节点的 root 权限,其实无需拘泥于 K8s 的临时容器。可以直接使用 nsenter 动态加入目标 Pod 的命名空间,这在实质上等同于创建了一个“无限特权”的本地临时容器:

# 获取业务容器在宿主机上的 pid
SIB_PID=$(crictl inspect --output go-template --template '{{.info.pid}}' <business-container-id>)

# 利用 nsenter 进入该容器的 network、pid、mount 命名空间
nsenter -t $SIB_PID -n -p -m /bin/bash

利用 nsenter 启动的 Bash 会完整继承宿主机 root 的所有 Capability,同时置身于业务容器的网络与进程空间内。由于此操作完全在 OS 内核层面进行,不仅不需要重启 Pod,甚至不会在 Kubernetes API Server 中留下任何特权容器的创建记录,从而绕过了所有的 Admission Control 审计。


最佳实践与防御性思考

在实际的生产环境中,上述操作虽然能有效解决排查故障时的权限燃眉之急,但也对集群安全提出了极高的挑战:

  1. 临时容器的生命周期管理:向 Pod 追加的临时容器是一次性的,一旦创建就无法删除,只能随 Pod 的销毁而销毁。频繁追加高特权临时容器会导致 Pod Spec 臃肿,建议排查完毕后,在业务低峰期主动重建 Pod。
  2. 审计策略的收紧:鉴于通过 API 追加临时容器可以实现权限跃升,集群管理员应在 Kyverno 或 OPA Gatekeeper 中,针对 pods/ephemeralcontainers 子资源实施严格的 capabilities 白名单审计。
  3. 节点侧防线:限制开发或运维人员直接登录 Node 节点,防止滥用 nsenter 或篡改 OCI 运行时配置造成越权与容器逃逸。
云原生摸鱼大师 Kubernetes临时容器安全上下文

评论点评