在 Kubernetes 里,容器进程默认以 root 用户(UID 0)启动,一旦应用存在远程代码执行之类的漏洞,攻击者就能在容器内直接拿到最高权限,进而挂载宿主目录、篡改系统文件。securityContext 是 K8s 提供的原生安全原语,它让我们无需改动镜像,就可以通过声明式配置把 Pod 收敛为「最小权限」运行:降权到非 root、只读根文件系统、丢弃危险的 Linux capabilities、过滤系统调用。本文用一套生产级 YAML 实战讲解每个字段该怎么填、为什么要填。
一、为什么需要安全上下文
很多团队把注意力放在网络隔离和镜像扫描上,却忽略了「容器里进程以谁的身份跑」这件最基础的事。默认情况下,Dockerfile 里没写 USER 指令,或者基础镜像本身就以 root 打包,最终在集群里运行的进程就是 root。这会带来三类典型风险:
- 容器内 root 与宿主 root 共享同一个内核,只是被 namespace 和 cgroup 隔离;一旦逃逸成功,危害被放大。
- 攻击者拿到 root 后,可以安装提权工具、修改 /etc,进一步横向移动。
- 挂载了 hostPath 或特权容器(privileged)时,root 几乎等于拿到宿主控制权。
安全上下文(securityContext)正是为了堵住这条路径:它在 Pod 和容器两个层级提供一组字段,用来约束进程的身份、文件系统和内核能力,是纵深防御里性价比最高的一环。它与 Cilium 网络策略 形成互补——一个管「网络谁能动」,一个管「进程能做什么」。
二、安全上下文作用在哪些层级
securityContext 可以写在两个地方,作用范围不同:
| 层级 | 作用范围 | 典型字段 |
|---|---|---|
| Pod 级(spec.securityContext) | 对该 Pod 下所有容器生效 | runAsUser、runAsGroup、runAsNonRoot、fsGroup、seccompProfile |
| 容器级(container.securityContext) | 仅对单个容器生效,优先级更高 | 以上全部,外加 readOnlyRootFilesystem、capabilities、allowPrivilegeEscalation、privileged |
当 Pod 级和容器级都设置了同一字段时,容器级覆盖 Pod 级。推荐的做法是:把通用的身份约束放到 Pod 级,把容器特有的能力裁剪放到容器级,这样既统一又灵活。
三、身份降权:runAsNonRoot 与 runAsUser
最基础的一步是让进程不再以 root 运行。K8s 提供了两种写法:一种是显式指定 UID/GID,另一种是让 K8s 帮你校验「不能是 root」。
apiVersion: v1
kind: Pod
metadata:
name: app-nonroot
spec:
securityContext:
runAsNonRoot: true # 拒绝以 UID 0 启动,镜像若为 root 则 Pod 起不来
runAsUser: 1000 # 显式指定非特权 UID
runAsGroup: 3000 # 主 GID
fsGroup: 2000 # 挂载卷的属组,便于容器读写
containers:
- name: app
image: nginx:1.27
securityContext:
runAsUser: 1001 # 容器级覆盖 Pod 级
这里有个常见的坑:镜像里的工作目录或文件属主是 root,你却用 runAsUser: 1000 去跑,应用会因为权限不足而启动失败。解决方法是重新打镜像(用非 root USER 构建),或者在 Dockerfile 里 chown 目录。如果图省事用 Docker 多阶段构建 时,记得在最终阶段显式声明 USER。
四、文件系统加固:只读根文件系统
很多攻击依赖向磁盘写入 webshell 或下载恶意脚本。把根文件系统设为只读,可以大幅压缩攻击者的操作空间。配合一个可写的 emptyDir 给真正需要落盘的路径即可。
spec:
containers:
- name: app
image: myapp:1.0
securityContext:
readOnlyRootFilesystem: true
volumeMounts:
- name: tmp
mountPath: /tmp
- name: cache
mountPath: /app/cache
volumes:
- name: tmp
emptyDir: {}
- name: cache
emptyDir: {}
只读根文件系统还有一个额外好处:配合 Kyverno 策略即代码 做准入校验,可以强制所有生产负载都开启这一项,从集群层面兜底。
五、能力最小化:capabilities 裁剪
Linux capabilities 把传统 root 的权限拆成了几十个细粒度单元。容器默认会带一组 capabilities(如 CHOWN、NET_RAW、SYS_TIME),其中很多应用根本用不到。最佳实践是「先全部丢弃,再按需添加」。
securityContext:
capabilities:
drop:
- ALL # 丢弃全部默认能力
add:
- NET_BIND_SERVICE # 仅允许绑定 1024 以下端口(如 80/443)
allowPrivilegeEscalation: false
NET_RAW 是尤其该丢弃的能力——它允许构造原始 socket,常被用于内网探测与攻击,而绝大多数 Web 服务并不需要它。drop ALL 之后再只加 NET_BIND_SERVICE,是生产环境的标准姿势。
六、防止提权:allowPrivilegeEscalation
allowPrivilegeEscalation 控制进程是否能通过 setuid 之类的方式获得比父进程更高的权限。把它设为 false,等价于内核的 no_new_privs 标志,能挡住大量本地提权利用。它与必须丢弃 SETPCAP 等能力配合使用,效果最佳。
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
- SETPCAP
七、系统调用过滤:seccompProfile
即使做了能力裁剪,进程仍然能调用全部系统调用。seccomp(secure computing mode)可以限制进程能用的 syscall 集合。K8s 内置了 RuntimeDefault 这个开箱即用的 profiles,由容器运行时维护一份经过裁剪的安全 syscall 白名单,性价比极高。
securityContext:
seccompProfile:
type: RuntimeDefault # 使用容器运行时提供的默认安全 profile
对于安全要求更高的场景,可以自定义 seccomp profile 文件并挂载进节点,但在 90% 的常规业务里,RuntimeDefault 已经能拦掉相当大一部分内核攻击面。
八、与 Pod Security Admission 的关系
上面这些字段是「应用自己声明」,而 Pod Security Admission(PSA)是「集群层面强制」。PSA 提供三个标准:Privileged(完全放开)、Baseline(适度限制)、Restricted(最严格,强制要求 runAsNonRoot、readOnlyRootFilesystem、drop ALL capabilities 等)。
建议把命名空间标记为 restricted,这样即使开发者忘记写 securityContext,集群也会直接拒绝不符合要求的 Pod 创建。PSA 与 Kubernetes ValidatingAdmissionPolicy 可以组合使用,前者管通用基线,后者管业务定制规则。
apiVersion: v1
kind: Namespace
metadata:
name: prod
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/enforce-version: latest
九、一份生产级完整示例
把前面所有要点合在一起,下面是一个可以直接落地的 Web 服务 Pod 模板:身份非 root、只读根文件系统、能力全丢只加绑端口、禁止提权、启用 RuntimeDefault seccomp。
apiVersion: v1
kind: Pod
metadata:
name: web-secure
namespace: prod
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: web
image: nginx:1.27
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
add:
- NET_BIND_SERVICE
ports:
- containerPort: 8080
volumeMounts:
- name: tmp
mountPath: /tmp
- name: var-cache
mountPath: /var/cache/nginx
volumes:
- name: tmp
emptyDir: {}
- name: var-cache
emptyDir: {}
十、常见坑与排错清单
| 现象 | 根因 | 解决办法 |
|---|---|---|
| Pod 一直 CreateContainerConfigError / 起不来 | runAsNonRoot 为 true,但镜像以 root 打包 | 重打镜像显式 USER 非 root,或确认镜像 entrypoint 不要求 root |
| 应用报 read-only file system 写失败 | 开了 readOnlyRootFilesystem 但程序要写日志/临时文件 | 把 /tmp、缓存目录挂 emptyDir,或改日志输出到 stdout |
| 绑定 80 端口报 permission denied | drop ALL 后缺少 NET_BIND_SERVICE | 在 add 列表补 NET_BIND_SERVICE |
| PSA restricted 下被拒绝创建 | 缺 runAsNonRoot 或仍带特权能力 | 补齐第九节模板中的字段,或调整命名空间标签 |
十一、落地 Checklist
| 检查项 | 是否必选 | 说明 |
|---|---|---|
| runAsNonRoot: true | 必选 | 杜绝以 root 运行 |
| readOnlyRootFilesystem: true | 推荐 | 压缩写入型攻击面 |
| capabilities drop ALL | 必选 | 能力最小化 |
| allowPrivilegeEscalation: false | 必选 | 防止本地提权 |
| seccompProfile RuntimeDefault | 推荐 | 限制系统调用 |
| 命名空间 PSA=restricted | 推荐 | 集群级强制兜底 |
十二、小结
securityContext 不要求你改写一行业务代码,却能把容器的攻击面砍掉一大半:身份非 root、根文件系统只读、能力只留必要项、禁止提权、过滤系统调用。把这套配置沉淀进 Helm 模板或 Kustomize 基线,再叠加命名空间的 PSA restricted 策略,就能让「最小权限」从一句口号变成集群的默认行为。在 CI 里用 GitHub Actions 跑 kube-score 或 conftest 校验,可以在合并前就拦下不安全的安全上下文配置。




