securityContext 实战:从一次组权限排查说起
上周排查一个 Pod 写 PVC 报 Permission denied 的问题。Deployment 里明明配了 runAsUser: 1000,容器起来也能跑,可一写挂载卷就报错。进容器看 id:
uid=1000(service) gid=0(root) groups=0(root)
而 PVC 的目录属主组是 2000,权限 770。进程主组是 0,不在 2000 组里,组权限位不匹配,other 位又是 0,直接被拒。根因很朴素:只设了 runAsUser,没设 runAsGroup,Kubernetes 不会替你猜主组,进程主组默认就是 root(0)。
这个坑值得展开聊聊。
字段先归位:Pod 级还是 Container 级
securityContext 分两层:
Pod 级(PodSecurityContext)
runAsUser/runAsGroupfsGroup/supplementalGroups/fsGroupChangePolicyseccompProfile/sysctls
Container 级(SecurityContext)
runAsUser/runAsNonRootprivileged/allowPrivilegeEscalationcapabilities/readOnlyRootFilesystem/procMountseccompProfile/appArmorProfile/seLinuxOptions
关键覆盖关系:Container 级同名字段覆盖 Pod 级。但注意,这种覆盖是作用在容器进程上的,不影响 Pod 级控制的卷属性。比如你在 Container 级把 runAsUser 改成 1000,Pod 卷的属主组依然由 Pod 级 fsGroup 决定,两者互不替代。
坑一:runAsGroup 不设,主组就是 root(0)
前面就是真实案例。修复方式很简单:显式声明主组。
securityContext:
runAsUser: 1000
runAsGroup: 1000
但这里有个容易混淆的替代方案:fsGroup。fsGroup 的作用是改卷文件属主组,而不是改进程主组。如果你只是想让容器能访问某个 gid 2000 的卷,但又不方便改进程主组,可以用:
securityContext:
runAsUser: 1000
fsGroup: 2000
kubelet 会在挂载时把卷的属主组调整成 2000,容器内进程就能通过组权限访问。但要注意,fsGroup 改的是卷,不是进程 id 输出的主组,这两条路径的语义完全不同。
选哪条,取决于你要「换进程身份」还是「换卷的归属」。
坑二:privileged: true 直接越过所有加固
另一个高频问题:容器启动异常,有人习惯性加 privileged: true。
privileged: true 的实际语义是:赋予容器全部 capabilities,并越过 seccomp / AppArmor / SELinux 这套加固。原文原话是「覆盖并使许多加固选项失效」。也就是说,如果你同时在 securityContext 里写了 seccompProfile: RuntimeDefault,只要 privileged 开着,这个 seccomp 配置基本等于没写。
失败表现通常是:明明配置了 seccomp、AppArmor,容器里依然能执行被拦截的系统调用;或者你列了一长串 capabilities.drop,容器内照样 capsh --print 看到一堆 cap。
别用 privileged。需要什么权限,用 capabilities 精确加:
securityContext:
capabilities:
add: ["NET_ADMIN"]
drop: ["ALL"]
capabilities 给的是 Linux capabilities 的细粒度授权,不是全量 root。绝大多数场景下,NET_ADMIN、SYS_TIME、CHOWN 这类单项能力足够,不需要把整扇门拆了。
坑三:runAsNonRoot 依赖镜像元数据
runAsNonRoot: true 的本意是强制容器不能以 root 跑。但它的判断依据是镜像元数据里的 USER 字段,不是你自己在 runAsUser 里写的值。
如果镜像没声明非 root USER,kubelet 会直接拒绝创建 Pod。失败表现就是 Pod 起不来,kubelet 事件里会报 runAsNonRoot 与镜像 USER 冲突相关的错误(原文提到「kubelet 拒绝」但未给出完整报错文案)。
所以这个字段要配合镜像使用。要么镜像构建时就 USER 1000,要么你在 securityContext 里 runAsUser: 1000 且镜像元数据能对上。别指望 runAsNonRoot 能绕过镜像本身的 root 默认值。
坑四:fsGroup 与 Container 覆盖的错位
Container 级 runAsUser 可以覆盖 Pod 级 runAsUser,但Pod 卷的属主组只认 Pod 级 fsGroup。这是很多人配置完发现「容器里 uid 对了,卷还是进不去」的原因之一。
比如你在两个容器里分别用不同 runAsUser 跑同一个 PVC,卷的属主组是同一个 fsGroup 值,两个容器都能通过组权限访问——这是 fsGroup 的典型适用场景。反过来,如果你为了让某个容器能访问卷,去改 Container 级 runAsGroup,卷的属主组不会变,照样被拒。
fsGroupChangePolicy 这个字段控制的是 kubelet 何时调整卷属主组,原文只列了字段名没有展开取值,真要用的时候再对着文档查,别想当然。
另一个思路:hostUsers: false
如果容器镜像里硬编码了 root 路径、或者 entrypoint 里有 chown 逻辑,又不想在宿主机上暴露 root 权限,可以考虑 hostUsers: false。这个配置下,容器内看到的 uid 是 root,但对宿主机来说是非 root 用户。语义是借助用户命名空间做隔离。
适用范围跟普通 securityContext 不太一样:适合「镜像要求 root、但宿主不允许 root」的场景。注意这是用户命名空间机制,不是万灵药,原文提到它但没展开限制条件,用之前确认集群是否开了对应支持。
关于 seccomp / AppArmor / SELinux 的 type
seccompProfile 和 appArmorProfile 都有三个取值:
RuntimeDefault:用容器运行时的默认配置Unconfined:关闭Localhost:指向节点上的自定义配置
RuntimeDefault 是性价比最高的起点。但就像前面说的,privileged: true 会让这些配置失效。所以如果你发现配置了 RuntimeDefault 但容器行为跟没配一样,先检查有没有人动了 privileged。
Windows 是另一套体系
以上字段基本都是 Linux 语义。Windows 节点要用 windowsOptions,字段体系和 Linux 不是一一对应的,别把 Linux 配置直接搬过去。
适用与不适用场景速记
| 配置 | 适用场景 | 不适用场景 |
|---|---|---|
runAsGroup | 进程需要明确主组身份,访问按组授权的卷 | 只想解决卷访问权限——那是 fsGroup 的活 |
fsGroup | 多个 Pod 共享 PVC,按组授权 | 需要精确控制进程 gid 的场景 |
capabilities | 需要单项特权,如 NET_ADMIN | 需要完整内核能力——此时应考虑是不是架构有问题 |
runAsNonRoot | 镜像已声明非 root USER,加一道防线 | 镜像默认 root 且无法改——kubelet 会直接拒绝 |
hostUsers: false | 镜像强制 root,宿主不想给 root | 需要宿主级用户隔离语义的场景,先验证特性支持 |
结论
这次排障的最终修复是 runAsGroup: 1000,因为业务就是要以组身份访问卷,而不是改卷归属。
securityContext 的核心不是「配了就行」,而是搞清楚每一条字段作用在进程上还是卷上、是 Pod 级还是 Container 级、会不会被 privileged 一把掀翻。优先级永远是:精确授权 > 开特权。