非Root容器为何无法获得权能?

1 人参与

一个常见的配置组合:容器声明 runAsNonRoot、drop: ["ALL"]、add: ["NET_BIND_SERVICE"],镜像启动后却在绑定 80 端口时报权限错误。YAML 的语义看不出问题,症结在内核的权能继承规则上——add 写进配置的能力,最后没有落在进程真正生效的那一层集合里。

权能分散在多个集合

linux 进程的权能是分层的:bounding 是上限,permitted 是进程可以持有的集合,effective 是当下真正起作用的子集,inheritable 与 ambient 则决定权能能否跨越 execve 这道边界。Kubernetes 的 securityContext.capabilities 只向运行时传递 drop 和 add 两个列表,由运行时在启动进程时设置对应集合,但这项字段并不提供配置 ambient 权能的入口。

关键在于 UID 切换。内核的规则是:进程从 UID 0 切换到非零 UID 时,permitted、effective 和 ambient 集合会被清空。容器运行时通常先完成权能设置、再切换到 runAsUser 指定的用户,于是 add 的项停留在 bounding 集合里作为“允许的上限”,而进程的 CapEff 往往是空的。之后 execve 一个普通二进制,既没有文件权能,也没有 ambient 可用于继承,permitted 与 effective 自然无从建立。这也解释了为什么以 Root 启动的同一镜像表现正常:没有 UID 跨越,权能留在原处。

判断与取舍

排查时先对比两个位图。CapEff 为空、CapBnd 含目标位,就是上述现象的典型特征,不必再怀疑字段拼写或运行时版本;反过来,CapEff 里出现未声明的权能,才说明配置遗漏或旧 Pod 未被重建。

确实需要“非 Root + 特定权能”时,可选路径有限。给二进制设置文件权能,可以让 execve 依据文件授予权限,但若同时启用了禁止提权的选项,这条路径会被内核拦截,需要单独评估。启动时短暂提权、完成绑定后再降权也可行,代价是启动阶段仍存在 Root 进程。多数场景下更务实的做法是改用高位端口,由 Service 完成映射,让非 Root 与权能收敛同时成立。若业务不得不保留 Root 启动,应把它记为显式例外并写明理由,而不是靠默认配置含糊带过。

权能收敛约束的是能力上限,非 Root 约束的是身份,两者并不在同一维度。把 drop 加 add 当成“给非 Root 进程发权限”的开关,正是对这套机制最常见的误读。

参与讨论

1 条评论
  • 麻辣兔

    原来卡在UID切换这里了