在托管 Kubernetes 集群里,很多工作负载仍以镜像默认用户启动,容器内进程 UID 0 与宿主机内核之间只隔命名空间和 linux Capabilities。应用一旦被利用,攻击者就可能写入容器可写层、投放工具、尝试挂载或借助多余能力扩大影响。securityContext 是把这类默认权限收敛到最小集的声明式入口,但它同时涉及 Pod 级与容器级字段、镜像内目录权限、可写路径和准入策略。本文按准备、实施、验证和回滚组织,适用于自建或托管集群,不假设特定 CNI、容器运行时或策略引擎;改造前应先在测试命名空间验证。

先盘点字段层级与真实依赖
securityContext 分布在 Pod 和容器两个层级。Pod 级适合放统一身份和卷组设置,容器级适合放根文件系统、提权与能力等必须逐容器声明的字段。Kubernetes 官方文档说明,容器级 securityContext 中指定的字段会覆盖 Pod 级对应字段;因此不要只改 Pod 级就认为所有容器都已收敛。
| 字段 | 可设置层级 | 作用 | 常见误区 |
|---|---|---|---|
runAsNonRoot | Pod / 容器 | 要求容器以非 UID 0 启动,kubelet 会拒绝以 root 启动的容器 | 与 runAsUser: 0 同时设置会冲突 |
runAsUser / runAsGroup | Pod / 容器 | 指定进程 UID / GID | 容器级优先;UID 需与镜像内文件权限匹配 |
fsGroup | Pod | 调整卷的组所有权,影响 emptyDir、PVC 等 | 只设置 runAsUser 不保证挂载卷可写 |
readOnlyRootFilesystem | 容器 | 将容器根文件系统挂载为只读 | 必须逐容器设置,包括 init 容器和 sidecar |
allowPrivilegeEscalation | 容器 | 禁止子进程获得更多权限 | 特权容器或具有 CAP_SYS_ADMIN 时该值会被强制为 true |
capabilities.drop | 容器 | 丢弃 Linux 能力 | drop: ["ALL"] 后需要的能力要显式 add |
seccompProfile | Pod / 容器 | 限制系统调用集合 | RuntimeDefault 是常见基线,不是所有应用都无需验证 |
盘点时优先找出三类工作负载:镜像 USER 为 root 或未声明的;需要绑定低端口、原始套接字、修改网络配置的;启动时写 PID 文件、缓存、日志或上传目录的。前两类决定身份和能力改造范围,第三类决定只读根文件系统需要补哪些挂载。
从 Pod 级基线到容器级收敛
一个常见的改造前清单只声明镜像和端口,进程身份完全由镜像决定:
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-api
spec:
replicas: 2
selector:
matchLabels:
app: web-api
template:
metadata:
labels:
app: web-api
spec:
containers:
- name: app
image: registry.example.com/web-api:1.4.0
ports:
- containerPort: 8080
改造后把统一身份放在 Pod 级,把根文件系统、提权和能力放在每个容器级。下面的片段省略了探针和资源限制,实际清单应保留:
spec:
securityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
fsGroup: 10001
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: registry.example.com/web-api:1.4.0
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
volumeMounts:
- name: tmp
mountPath: /tmp
- name: cache
mountPath: /var/cache/web-api
volumes:
- name: tmp
emptyDir: {}
- name: cache
emptyDir: {}
这里的 runAsUser: 10001 不是装饰项。若镜像内应用目录只允许 root 读取,进程切换后会直接启动失败;若应用需要写 /var/run 或工作目录下的文件,只读根文件系统会返回 Read-only file system。因此改造顺序应是先构建非 root 镜像并验证目录权限,再在测试环境开启只读根文件系统,最后进入生产。
改造前后影响可以用下表对照:
| 项目 | 改造前 | 改造后 | 变更影响 |
|---|---|---|---|
| 进程身份 | 镜像默认,可能为 UID 0 | 强制 UID 10001 | 镜像内目录需可读,写目录需挂载 |
| 根文件系统 | 可写层可写 | 只读 | 启动时写 PID、缓存、日志会失败 |
| 能力集 | 运行时默认能力 | drop: ["ALL"] | 绑定低端口、原始套接字需显式 add |
| 提权 | 允许 | no_new_privs 生效 | 依赖 setuid 的程序可能无法工作 |
| 系统调用 | 运行时默认或未限制 | RuntimeDefault | 个别应用需验证或使用自定义配置 |
readOnlyRootFilesystem 只影响容器根文件系统,不会把挂载的 emptyDir 或 PVC 变成只读。Kubernetes 官方文档对 securityContext 字段和覆盖关系有完整说明,可参考为 Pod 或容器配置安全上下文。
只读根文件系统:先找写路径再挂载
开启只读根文件系统后,最常见的失败不是权限配置错误,而是应用仍在写根文件系统。排查时先看事件和上一次容器日志:
kubectl logs deploy/web-api --previous
kubectl get events --field-selector involvedObject.name=<pod-name> -o wide
日志中的 Read-only file system、Permission denied、Operation not permitted 分别指向只读挂载、UID/GID 权限和能力缺失。接着在测试 Pod 中验证可写路径。容器内不一定有 shell 或 touch,可以用应用自带工具,或通过 kubectl debug 临时容器排查;若准入策略禁止临时容器,就在测试命名空间用同一镜像复现。
kubectl exec deploy/web-api -- sh -c 'touch /tmp/health && echo tmp-writable'
kubectl exec deploy/web-api -- sh -c 'touch /app/root-write-test 2>&1 || echo root-readonly'
第一条成功、第二条失败,通常说明根文件系统已只读且 /tmp 已正确挂载。常见写路径和挂载建议如下:
| 路径 | 用途 | 建议卷类型 | 注意 |
|---|---|---|---|
/tmp | 临时文件、运行时解压 | emptyDir | Pod 删除后数据丢失 |
/var/cache/<app> | 应用缓存 | emptyDir | 需要评估缓存重建成本 |
/var/run 或 /run | PID、socket | emptyDir | 多容器共享时注意生命周期 |
| 上传目录 | 用户文件 | PVC 或对象存储 | emptyDir 不适合持久化 |
| 日志目录 | 文件日志 | 优先 stdout/stderr | 必须落盘时再挂载并接入采集 |

挂载时还要检查卷权限。fsGroup 可以让支持的文件系统卷获得组所有权,但并非所有存储驱动和访问模式都表现一致;对 PVC 应先在小规模副本验证写入,再扩大范围。不要把整个应用目录、/etc 或 /usr 挂成可写来绕过启动错误,这会抵消只读根文件系统的大部分收益。更合适的做法是构建镜像时预生成配置、把可变数据移到挂载卷,或让应用把日志写到标准输出。
准入阻断测试:先审计再强制
配置写进清单不等于集群会持续阻止错误配置。Pod Security Admission 可以通过命名空间标签执行基线,但 restricted 级别主要覆盖非 root、禁止提权、丢弃全部能力、限制卷类型和 seccomp 等要求;readOnlyRootFilesystem 通常不在其必填字段内,需要 Kyverno、Gatekeeper 或 CI 检查补足。Kubernetes 官方 Pod Security Standards 列出了各等级的字段范围,部署前应逐项核对。
建议先用 audit 和 warn 观察,不要直接在生产命名空间启用 enforce:
kubectl label namespace web-prod
pod-security.kubernetes.io/audit=restricted
pod-security.kubernetes.io/warn=restricted
--overwrite
然后用服务端 dry-run 走一遍准入链:
kubectl apply --dry-run=server -f deploy-web-api.yaml
准入测试至少覆盖以下用例:
| 测试项 | 操作 | 期望结果 |
|---|---|---|
| root 身份 | 提交未设置 runAsNonRoot 且镜像默认 root 的 Pod | 被拒绝或产生 restricted 警告 |
| 显式 UID 0 | 设置 runAsUser: 0 与 runAsNonRoot: true | 被拒绝或 Pod 无法启动 |
| 允许提权 | 不设置 allowPrivilegeEscalation: false | restricted 下被拒绝 |
| 保留能力 | capabilities.add 中加入 SYS_ADMIN | 被拒绝 |
| 只读根文件系统 | 提交未设置 readOnlyRootFilesystem 的容器 | PSA 可能放行,自定义策略应阻断 |
| 合法基线 | 使用本文改造后的清单 | 通过并产生预期警告 |
测试通过后再切换 enforce=restricted。切换前要确认现有工作负载不会因 hostPath、hostNetwork、hostPort 或未丢弃能力被批量阻断。对必须保留的例外,应使用策略引擎的例外机制或独立命名空间,并记录负责人和到期时间,而不是长期降低整个集群的强制级别。
验证、监控与紧急回滚
发布后先验证运行态,而不是只看 Pod 是否 Ready:
kubectl exec deploy/web-api -- id
kubectl exec deploy/web-api -- grep '^Cap' /proc/1/status
kubectl rollout history deployment/web-api
id 应返回非 0 UID;CapEff 应只包含显式保留的能力,drop: ["ALL"] 且未 add 时通常为零。再执行一次根文件系统写入测试,确认 /app 不可写、挂载目录可写。对多容器 Pod,init 容器和 sidecar 也要单独检查,因为 readOnlyRootFilesystem、allowPrivilegeEscalation 和 capabilities 都是容器级字段。
监控观察点应覆盖启动失败、重启和权限错误。部署后重点关注 CreateContainerConfigError、CrashLoopBackOff、就绪探针失败,以及日志中的 Permission denied、Read-only file system、Operation not permitted。如果新版本在几分钟内反复重启,优先回滚,不要在现场直接关闭只读根文件系统或恢复 root 身份。
回滚路径应提前写清楚:
| 场景 | 处理方式 | 注意 |
|---|---|---|
| 应用写根文件系统失败 | 补挂载 emptyDir 或 PVC 后重新发布 | 不要挂载整个 / |
| UID/GID 权限不匹配 | 调整镜像目录属主,或用 fsGroup 验证卷权限 | 先在小规模副本验证 |
| 能力缺失导致启动失败 | 仅 add 必要能力并记录理由 | 避免重新 add SYS_ADMIN |
| 准入策略阻断发布 | 查事件和策略报告,必要时临时加例外 | 例外设置到期时间并走审批 |
| 生产故障扩大 | kubectl rollout undo deployment/web-api 或 Helm/GitOps 回退 | 回滚后保留失败清单用于复盘 |
紧急回滚不是把安全配置全部删除。更稳妥的顺序是:先回滚到上一版本恢复业务,再在测试环境定位是身份、挂载还是能力问题;如果必须临时放宽准入,只对故障命名空间降低 enforce 级别,并设置明确的恢复时间。复盘时把遗漏的写路径、镜像目录权限和策略例外补进基线清单,避免同类问题在下一个服务重复出现。
- it2021
- it2021.com
- Kubernetes
- shell
- test

上海 上海 1F
这个改造顺序挺清晰的,先镜像后只读再准入