很多团队在 Pod 安全上下文里写了 runAsNonRoot: true,就认为容器逃逸的风险已经被兜住了。这个判断只对了一半:runAsNonRoot 约束的是容器内进程的 UID 不为 0,它并不改变内核视角下这个 UID 与宿主机 UID 的对应关系。当容器以 root 运行、又恰好持有某些 capability 时,一旦发生逃逸,攻击者在宿主机侧拿到的仍然是 root 权限。用户命名空间(user namespaces)补的正是这一层:把容器内的 root 映射为宿主机上的非特权 UID,让"容器内是 root、宿主机上不是 root"成为默认状态。
Kubernetes 通过 Pod 的 spec.hostUsers: false 触发这一行为。该字段在 v1.25 以 Alpha 引入,v1.30 进入 Beta,v1.33 起默认启用,v1.36 转为 GA 并锁定特性门控——也就是说,从 v1.36 开始你无法再通过关闭 UserNamespacesSupport 来退出这一能力,设置该门控会被忽略且不报错。这决定了本文的落点不是"要不要开启",而是"在默认开启的环境里,哪些工作负载会因此行为改变,怎么验证映射生效,出问题怎么回滚"。

runAsNonRoot 与 hostUsers=false 的边界差异
把两者放在一起对比,能看清各自拦住了什么。
| 维度 | runAsNonRoot: true | hostUsers: false |
|---|---|---|
| 作用对象 | 容器内进程的 UID 取值 | 容器内 UID 与宿主机 UID 的映射关系 |
| 拦截方式 | 准入阶段拒绝以 UID 0 启动的容器 | 运行时由 kubelet 分配宿主机侧 UID/GID |
| 被绕过的典型场景 | 镜像内已有非 root 用户但进程仍可通过 setuid 程序提权;容器内非 root 却持有 CAP_NET_ADMIN 等能力 | 无法通过容器内提权获得宿主机 root,因为映射目标本身无特权 |
| 逃逸后的宿主机权限 | 若逃逸路径涉及内核缺陷,仍可能落到宿主机 root | 落到宿主机上被映射的非特权 UID |
| 对镜像的要求 | 需要镜像内存在非 root 用户 | 无需改动镜像,runAsUser: 0 也可用 |
关键点在于:runAsNonRoot 是准入层的静态检查,hostUsers=false 是内核层的运行时隔离。前者防的是"配置写错",后者防的是"配置写对但仍被攻破"。官方文档明确提到,一批被评为 HIGH 或 CRITICAL 的安全脆弱性问题在启用用户命名空间后无法被利用;同时,hostUsers: false 会让 CAP_NET_ADMIN 这类能力变成命名空间内的能力,只对容器本地资源生效,不影响宿主机。这也是它相比 runAsNonRoot 更适合承载"需要特权但不想给宿主机特权"的工作负载的原因。
需要强调的是,两者不是替代关系。runAsNonRoot 仍然应该保留,它降低了容器内的攻击面;hostUsers=false 是在此之上的第二层,缩小逃逸成功后的爆炸半径。
前置条件与适用范围
在动手改配置之前,先确认三件事。
内核与运行时。 用户命名空间是 linux 特性,非 linux 节点不适用。容器运行时需要支持该能力,containerd、CRI-O 等主流运行时在新版本中已具备支持,但具体版本行为应以你所使用发行版和运行时的官方说明为准,不要直接套用他人集群的结论。
集群版本。 v1.30 起为 Beta,v1.33 起默认启用,v1.36 起 GA 且不可退出。低于 v1.30 的集群需要显式开启特性门控,且行为与 GA 版本存在差异,评估时要把版本作为独立变量。
UID 范围。 默认情况下,启用该特性后有效的 UID/GID 范围是 0–65535,这对文件和进程同时生效。这意味着 runAsUser、runAsGroup、fsGroup 的取值都落在这个区间内。如果你的工作负载依赖大于 65535 的 UID,需要单独确认当前版本的映射策略,不能假设它一定可用。
一个容易被忽略但很重要的性质:pod.spec 中的 runAsUser、runAsGroup、fsGroup 始终指容器内的用户,卷挂载也按这些用户处理,因此宿主机上的 UID/GID 不影响 Pod 对挂载卷的读写。换句话说,由 Pod 在卷中创建或读取的 inode 归属,与未启用用户命名空间时保持一致。这带来一个实际好处:同一个卷可以在启用和未启用用户命名空间的 Pod 之间共享,切换 hostUsers 不会导致卷内文件所有权错乱。该结论适用于 Pod 可挂载的任何卷,包括在策略允许时的 hostPath。
配置示例与变更影响
最小改动是在 Pod 或 PodTemplate 的 spec 中加一行:
apiVersion: v1
kind: Pod
metadata:
name: isolated-workload
spec:
hostUsers: false
containers:
- name: app
image: fedora:42
securityContext:
runAsUser: 0注意这里的 runAsUser: 0 是刻意保留的:容器内进程依然是 root,但它在宿主机上对应的是 kubelet 分配的非特权 UID。这正是该特性的价值所在——不需要为了安全去改镜像里的用户体系。
对工作负载控制器而言,把 hostUsers: false 写进 Deployment、StatefulSet、DaemonSet 的 Pod 模板即可。变更影响集中在三点:
- Pod 需要重建。
hostUsers属于 Pod 级不可变字段,修改模板会触发滚动更新,评估时要考虑副本数和 PDB。 - 宿主机侧 UID 变化。 kubelet 会为每个 Pod 挑选映射的宿主机 UID/GID,并保证同一节点上没有两个 Pod 使用相同的映射方式。任何在宿主机侧按 UID 做审计、配额或文件归属判断的脚本,都需要重新校准。
- 特权能力的语义变化。 原本"给了就能管宿主机"的能力,现在被限制在命名空间内。依赖这些能力操作宿主机资源的边车或初始化容器,行为会改变。
用 kubectl 验证实际 UID 映射
配置写对不等于映射生效。验证要落到"容器内看到什么、宿主机上是什么"这两个视角。
先确认 Pod 正常调度并运行:
kubectl get pod isolated-workload -o jsonpath='{.spec.hostUsers}{"n"}'
kubectl get pod isolated-workload -o jsonpath='{.status.phase}{"n"}'第一条应输出 false,第二条应为 Running。如果 hostUsers 字段没有出现在输出里,说明你的集群版本尚未默认启用该特性,或该字段被上层策略剥离。
接着看容器内的身份:
kubectl exec isolated-workload -- id在容器内应看到 uid=0(root)。这一步确认容器内仍是 root,符合预期。
然后回到节点上对照宿主机侧进程。在 Pod 所在节点执行:
# 先取得容器进程 PID
kubectl get pod isolated-workload -o jsonpath='{.status.containerStatuses[0].containerID}{"n"}'
# 在节点上用 crictl 或运行时工具定位进程后查看
ps -o pid,uid,gid,comm -p <容器主进程PID>宿主机侧看到的 UID 应当是一个非 0 的高位值,而不是 0。这一步是整个验证的核心:容器内 id 显示 root、宿主机 ps 显示非 root,两者同时成立,才说明映射真正生效。
对于批量核查,可以用如下方式快速筛出未启用隔离的 Pod:
kubectl get pods -A -o json |
jq -r '.items[] | select(.spec.hostUsers != false) | "(.metadata.namespace)/(.metadata.name)"'这条命令列出所有没有显式设置 hostUsers: false 的 Pod。在 v1.33 及以上默认启用的集群中,未显式设置的 Pod 行为取决于版本默认值,因此这份清单更适合作为"待确认"列表,而不是"违规"列表。
常见故障与排查路径
卷权限与 fsGroup 相关报错
最典型的现象是 Pod 启动后写入卷时报 Permission denied。由于 runAsUser、fsGroup 指的是容器内用户,卷内 inode 归属理论上不受影响,但如果工作负载在宿主机侧直接操作卷路径(例如通过 hostPath 且以宿主机视角写入),就会出现两边 UID 不一致的问题。
排查顺序:先确认容器内 id 与卷内文件归属是否匹配,再检查是否有宿主机侧进程或 DaemonSet 以宿主机 UID 写入同一路径。修复方向通常是让所有访问方都通过 Pod 挂载访问,而不是在宿主机侧直接读写。
NFS 与其他网络文件系统挂载失败
NFS 的权限判定发生在服务端,服务端看到的是宿主机侧 UID。启用用户命名空间后,Pod 在 NFS 服务端呈现的 UID 不再等于容器内的 runAsUser,如果 NFS 导出配置基于 UID 做限制(如 root_squash 配合固定 UID 白名单),挂载会失败或只读。
这类问题的处理不在 Kubernetes 侧,而在存储侧:需要让 NFS 导出策略接受映射后的 UID 范围,或改用支持身份映射的存储方案。评估阶段就应把"工作负载是否依赖基于 UID 的存储授权"列为准入条件。
特权容器与能力冲突
设置了 privileged: true 的容器与用户命名空间存在语义冲突:特权容器期望获得宿主机层面的全部能力,而用户命名空间恰恰要剥夺这种能力。表现为容器启动失败,或启动后原本可用的操作返回 EPERM。
处理原则是先判断该容器是否真的需要特权。多数情况下,把 privileged: true 换成精确的 capability 列表即可,因为 hostUsers: false 下这些 capability 依然在命名空间内有效。确实需要宿主机级特权的(如某些 CNI 组件、节点级监控代理),应显式设置 hostUsers: true 并为该类工作负载单独建立准入策略,而不是让它们悄悄回退。
启动即失败的快速定位
当 Pod 卡在 ContainerCreating 或反复重启时,按以下顺序收敛:
kubectl describe pod isolated-workload
kubectl get events -n <namespace> --sort-by=.lastTimestamp | tail -30重点看事件中是否出现与用户命名空间、UID 映射、运行时能力相关的错误。若错误指向运行时而非 Kubernetes API,需要到对应节点的 kubelet 与容器运行时日志中确认,因为映射由 kubelet 与运行时协同完成。

回滚方案与灰度节奏
回滚的粒度取决于变更范围,建议按"单 Pod → 单工作负载 → 命名空间"逐级推进,每一级都保留可观测窗口。
单 Pod 回滚。 对已部署的 Pod 无法原地修改 hostUsers,需要删除重建。如果只是验证阶段,直接删除即可;如果 Pod 由控制器管理,改回模板字段后等待滚动更新完成。
工作负载回滚。 把 Pod 模板中的 hostUsers: false 移除或显式设为 true,然后执行滚动更新。对 Deployment 可用:
kubectl rollout status deployment/<name> -n <namespace>
kubectl rollout undo deployment/<name> -n <namespace>rollout undo 回退的是 ReplicaSet 版本,前提是变更通过模板提交且历史版本保留。如果变更同时改动了镜像或挂载,回退后要重新验证卷权限,因为 hostUsers 切换本身不影响卷内 inode 归属,但其他改动可能影响。
DaemonSet 的回滚。 节点级代理类工作负载回滚风险更高,因为一次更新覆盖所有节点。DaemonSet 支持通过 kubectl rollout undo daemonset/<name> 回退到上一修订版本,但更稳妥的做法是先在一小批打了标签的节点上验证,再逐步放开。回滚期间要监控节点上代理进程是否恢复正常,以及是否有节点因代理缺失而被标记为不可用。
灰度观察点。 每一级变更后至少观察以下指标:Pod 启动成功率、容器重启次数、卷写入错误率、以及依赖特权能力的组件是否出现 EPERM。这些指标比"Pod 是否 Running"更能反映映射带来的实际影响。
落地建议
把这项工作拆成评估与推广两个阶段会更稳。评估阶段的核心产出是一份清单:哪些工作负载依赖宿主机级特权、哪些依赖基于 UID 的存储授权、哪些在宿主机侧有按 UID 判断的运维脚本。这份清单决定了 hostUsers: false 能覆盖的范围。
推广阶段则从无状态、无特权、不依赖 NFS UID 授权的工作负载开始,用本文的验证步骤确认映射生效,再逐步扩展到有状态负载。对于确实需要宿主机特权的工作负载,显式写 hostUsers: true 并配套准入策略,让"例外"可见、可审计,而不是留在默认值里含糊过去。
最后提醒一点:v1.36 之后该特性已锁定,特性门控不再是退路。真正的回滚手段是 Pod 级别的 hostUsers 字段和滚动更新机制,因此把这两条路径在变更前演练一遍,比事后查文档更有效。
热门话题
- it2021
- it2021.com
- Kubernetes





1F
原来 runAsNonRoot 管不到宿主机 UID 这一层