容器里跑着一个需要绑定 80 端口的 Web 服务,或者一个需要创建原始套接字做网络探测的 DaemonSet,默认配置往往就能启动。问题在于,容器运行时默认授予的 linux 权能通常多于业务实际所需:NET_RAW、CHOWN、SETUID 等能力对多数应用并不必要,却会在容器被突破后扩大攻击面。Kubernetes 的 securityContext.capabilities 提供了容器级的收敛手段:先 drop: ["ALL"],再按需 add。下面按准备、实施、验证和复盘四个阶段展开,适用于 linux 节点、容器运行时支持权能配置,并且团队能够接受一次可回滚的滚动更新。

先明确边界:权能收敛管什么,不管什么
Linux 权能把传统 root 的权限拆成数十个独立单元,例如 CAP_NET_BIND_SERVICE 允许绑定 1024 以下端口,CAP_NET_RAW 允许创建原始套接字,CAP_SYS_ADMIN 则覆盖挂载、命名空间管理等大量操作。Kubernetes 通过容器级 securityContext.capabilities 把 drop 和 add 列表传给容器运行时,由运行时在启动进程时设置对应的权能集合。它控制的是进程持有的内核权能,不改变 UID、文件系统视图、网络命名空间或主机挂载关系。
因此,权能收敛不能替代非 Root 运行、Seccomp、AppArmor/SELinux、只读根文件系统、NetworkPolicy 和准入控制。Kubernetes 的 Pod 安全性标准 也把权能要求与其他控制项并列:Restricted 级别要求容器弃用 ALL 权能,并且只允许添加 NET_BIND_SERVICE(Linux 容器)。这说明权能配置只是多层约束中的一层,单独使用并不足以构成完整的容器隔离方案。
还要区分 privileged: true 与权能收敛。特权模式基本放开了容器的设备访问和能力限制,与“只保留必要权限”的目标相反。把 privileged: true 和 capabilities.add 混在一起,既不会让权限边界更清晰,也可能在准入策略中触发独立告警。需要特定能力的常规工作负载,应优先走 drop 加 add 的路径,而不是用特权模式绕过。
准备:盘点业务权限与当前基线
改配置之前,先回答两个问题:业务到底需要哪一项权能,当前容器实际持有哪些权能。第一个问题不能靠“以前这样能跑”来回答,而要从具体操作反推:是否绑定低端口、是否创建 raw socket、是否修改网络接口或路由、是否挂载文件系统、是否 ptrace 其他进程、是否修改系统时间。第二个问题则直接从运行中的 Pod 读取。
常见权能的业务含义与风险可以对照下表,实际取舍时只保留业务明确需要的那一行:
| 权能 | 典型用途 | 风险提示 |
|---|---|---|
NET_BIND_SERVICE | 绑定 1024 以下端口 | 风险相对可控,但优先考虑改用高位端口并由 Service 映射 |
NET_RAW | 原始套接字、ICMP、部分网络探测工具 | 可用于构造异常报文,多数业务不需要,建议默认移除 |
NET_ADMIN | 配置接口、路由、防火墙、流量控制 | 可改变容器网络行为,敏感度高 |
SYS_ADMIN | 挂载、命名空间管理等 | 范围极广,通常应视为高危,需单独审批 |
SYS_PTRACE | 调试、注入、读取其他进程内存 | 可绕过进程隔离边界,非调试场景不应保留 |
SYS_TIME | 修改系统时钟 | 可能影响日志、证书校验和调度 |
DAC_READ_SEARCH | 绕过文件读权限与目录搜索检查 | 可扩大文件读取范围 |
CHOWN | 修改文件属主 | 常被默认授予,但多数应用并不需要 |
查看当前 Pod 的权能集合,可以直接读取进程状态:
kubectl get pod edge-agent-7d9c8f6b5-abcde
-o jsonpath='{.spec.containers[*].securityContext}'
kubectl exec edge-agent-7d9c8f6b5-abcde --
cat /proc/1/status | grep -E 'Cap(Inh|Prm|Eff|Bnd|Amb)'CapEff 是当前生效的权能位图,CapBnd 是上限集合。把十六进制值拿到本地装有 libcap 的机器上解码:
capsh --decode=00000000a80425fb记录基线时,同时保存旧的 Deployment YAML、当前镜像 digest、命名空间的安全策略标签,以及 initContainers、ephemeralContainers 和 sidecar 的配置。很多收敛遗漏不是因为主容器写错,而是初始化容器或边车仍然持有默认权能。
一个容易忽略的交互:非 Root 与 add
Kubernetes 的 capabilities 字段主要影响进程的 bounding、permitted 和 effective 集合,并不提供 ambient 权能的配置入口。实际效果是:当容器以非 Root 用户启动时,add 写入的权能通常不会自动出现在进程的 effective 集合中,因为切换 UID 的过程会清除相关集合。也就是说,配置里写了 add: ["NET_BIND_SERVICE"],不代表非 Root 进程一定能绑定 80 端口。
这一点必须在目标镜像上实测,不能只看 YAML。如果业务确实需要某项权能,又要求非 Root,可评估使用带文件权能的二进制、启动时短暂提权后降权,或者干脆改用高位端口。若无法满足,应把“保留 Root 启动”作为显式例外记录,而不是默默依赖默认配置。
实施:drop: ["ALL"] 后按需添加
收敛的基本形态是:每个容器先删除全部权能,再只加回业务需要的那一项。drop 和 add 同时存在时,运行时先丢弃 ALL,再重新添加列表中的权能。下面是一个只保留低端口绑定能力的示例:
apiVersion: apps/v1
kind: Deployment
metadata:
name: edge-agent
spec:
replicas: 2
selector:
matchLabels:
app: edge-agent
template:
metadata:
labels:
app: edge-agent
spec:
containers:
- name: agent
image: registry.example.com/edge-agent:1.4.2
securityContext:
runAsNonRoot: true
runAsUser: 10001
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
add: ["NET_BIND_SERVICE"]
ports:
- containerPort: 8080如果业务并不需要任何额外权能,add 列表应留空或直接省略。多数 Web 应用、API 服务和队列消费者都属于这种情况。不要因为“先加上,免得以后出问题”而保留 NET_RAW 或 CHOWN;这类预留会随着时间变成事实上的默认权限。
变更时注意几个影响面:
initContainers和ephemeralContainers需要分别设置,不能假设它们继承主容器的securityContext。- 滚动更新会重建 Pod,启动探针和就绪探针的时序可能变化,尤其是应用启动阶段依赖某项权能时。
- 使用
hostNetwork的工作负载,低端口绑定和网络配置的判定条件与普通 Pod 不同,需要在同类节点上验证。 - 不要在同一次变更中同时调整镜像、资源限制和网络策略,否则出现故障时很难判断是权能收敛还是其他变量导致的。
建议先在非生产命名空间用相同镜像和相同启动参数验证,再按单副本或小比例流量推进。对于 SYS_ADMIN、NET_ADMIN、SYS_PTRACE 这类高敏感权能,建议把 add 清单纳入变更审批,而不是由开发者在 Deployment 里自行添加。
验证:功能可用与边界收紧两条线
验证要同时回答“业务还能不能用”和“权限是不是真的收掉了”。只做前者,可能只是配置没生效;只做后者,可能忽略启动阶段的隐性依赖。
功能验证包括:Pod 能进入 Ready、启动日志没有 EPERM 或 EACCES、健康检查通过、关键业务路径可执行。边界验证则在获得授权的测试环境中,尝试执行应被拒绝的操作,并确认失败原因来自权能缺失,而不是 Seccomp、只读文件系统或网络策略等其他限制。
# 查看容器内 PID 1 的权能位图
kubectl exec deploy/edge-agent --
cat /proc/1/status | grep -E 'Cap(Eff|Prm|Bnd)'
# 若镜像内带有 capsh,可直接查看当前集合
kubectl exec deploy/edge-agent -- sh -c 'capsh --print 2>/dev/null || true'尝试创建原始套接字,验证 NET_RAW 是否已移除:
kubectl exec deploy/edge-agent -- python3 -c
'import socket; socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_ICMP)'如果 NET_RAW 已被丢弃,预期得到 PermissionError。若命令成功,先检查是否仍在旧 Pod 上执行,或检查是否误加了该权能。
尝试绑定低端口,验证 NET_BIND_SERVICE 的实际效果:
kubectl exec deploy/edge-agent -- python3 -c
'import socket; s=socket.socket(); s.bind(("0.0.0.0", 80))'这里有一个容易误判的点:如果节点把 net.ipv4.ip_unprivileged_port_start 设为 0,非特权进程绑定 80 端口可能不依赖 NET_BIND_SERVICE。因此绑定成功不一定说明权能仍在,绑定失败也不一定说明权能配置有误,需要结合 CapEff 和节点 sysctl 一起判断。
| 验证项 | 观察方式 | 期望结果 | 异常含义 |
|---|---|---|---|
| 权能集合 | /proc/1/status 的 CapEff | 只包含显式添加的权能 | 仍有默认权能,检查是否漏配容器 |
| 原始套接字 | 创建 SOCK_RAW | 权限拒绝 | 成功则 NET_RAW 未移除 |
| 低端口绑定 | 绑定 80 端口 | 无 NET_BIND_SERVICE 时拒绝 | 成功需排查权能或 sysctl |
| 挂载操作 | 在容器内执行 mount | 拒绝 | 成功说明仍有过宽权限 |
| 业务健康 | 探针与关键接口 | 正常 | 失败需查看启动日志中的 EPERM |
验证结果应附在变更记录中,包含执行时间、Pod 名称、镜像 digest、命令和实际输出。若使用临时调试容器,要注意它是否与目标容器共享进程命名空间;不共享时,读到的权能集合可能不是目标进程的。

复盘与回滚:把变更做成可逆操作
权能收敛最常见的故障不是安全事件,而是应用在某个冷路径上突然缺少权限。观察点应覆盖:Pod 重启次数、CrashLoopBackOff、启动日志中的 EPERM/EACCES、探针失败、业务错误率、网络连接异常,以及依赖特定系统调用的定时任务。某些问题只在低峰期或特定请求路径出现,变更后至少观察一个完整业务周期。
如果确认是权能移除导致,优先回滚,再分析:
kubectl rollout undo deployment/edge-agent
kubectl rollout status deployment/edge-agent使用 GitOps 的环境应回退对应提交,而不是直接在生产集群上手工改 YAML,避免下一次同步把变更覆盖回去。回滚后重新读取 CapEff,确认权能集合恢复到变更前状态,并验证业务指标恢复。保留旧 ReplicaSet 和镜像 digest,便于对比。
复盘时记录四类信息:本次 drop/add 清单及理由、验证命令与结果、故障现象与根因、回滚耗时。后续治理可以把权能清单写入镜像构建阶段的测试,用 Pod 安全性标准或 Kyverno、OPA 等准入策略做兜底,定期审查 add 列表中是否还有业务不再需要的项。权能收敛的价值不在于一次性把配置改得更严格,而在于把“默认给一堆”变成“明确需要哪一个”,并且每一次添加都能被验证、解释和回滚。
- edge
- it2021
- it2021.com
- Kubernetes





1F
先 drop 再 add 这个顺序太容易搞反了