编程 securityContext 实战:从一次 PVC 组权限排查说起

2026-09-01 00:06:17

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 / runAsGroup
  • fsGroup / supplementalGroups / fsGroupChangePolicy
  • seccompProfile / sysctls

Container 级(SecurityContext)

  • runAsUser / runAsNonRoot
  • privileged / allowPrivilegeEscalation
  • capabilities / readOnlyRootFilesystem / procMount
  • seccompProfile / appArmorProfile / seLinuxOptions

关键覆盖关系:Container 级同名字段覆盖 Pod 级。但注意,这种覆盖是作用在容器进程上的,不影响 Pod 级控制的卷属性。比如你在 Container 级把 runAsUser 改成 1000,Pod 卷的属主组依然由 Pod 级 fsGroup 决定,两者互不替代。

坑一:runAsGroup 不设,主组就是 root(0)

前面就是真实案例。修复方式很简单:显式声明主组。

securityContext:
  runAsUser: 1000
  runAsGroup: 1000

但这里有个容易混淆的替代方案:fsGroupfsGroup 的作用是改卷文件属主组,而不是改进程主组。如果你只是想让容器能访问某个 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_ADMINSYS_TIMECHOWN 这类单项能力足够,不需要把整扇门拆了。

坑三: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

seccompProfileappArmorProfile 都有三个取值:

  • 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 一把掀翻。优先级永远是:精确授权 > 开特权

复制全文 生成海报 Security Context Kubernetes 云原生 运维

推荐文章

程序员茄子在线接单