概述
多数容器镜像默认使用可写根文件系统,进程被攻破后可以修改应用代码、向静态目录写入 webshell,或篡改配置文件。只读根移除这条持久化路径,但不阻止进程写显式挂载的卷。关键是找出进程真正需要写入的路径:示例中的 nginx 需要 /var/cache/nginx 和 /tmp,其余路径应保持只读。日志应写到 stdout,由运行时收集;上传文件应进入对象存储或专用持久卷,而不是容器根目录。
步骤
先用 Docker 证明镜像能在只读模式下启动:
docker run --rm --read-only \
--tmpfs /tmp:rw,noexec,nosuid,size=32m \
--tmpfs /var/cache/nginx:rw,noexec,nosuid,size=64m \
nginxinc/nginx-unprivileged:1.27-alpine nginx -t
然后在 Deployment 中设置 runAsNonRoot、allowPrivilegeEscalation: false、readOnlyRootFilesystem: true、capabilities.drop: [ALL] 和 RuntimeDefault seccomp。为缓存和临时目录挂载有大小限制的 emptyDir:
securityContext:
runAsNonRoot: true
runAsUser: 101
runAsGroup: 101
fsGroup: 101
seccompProfile:
type: RuntimeDefault
containers:
- name: nginx
image: nginxinc/nginx-unprivileged:1.27-alpine
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
volumeMounts:
- { name: nginx-cache, mountPath: /var/cache/nginx }
- { name: nginx-tmp, mountPath: /tmp }
应用清单后执行 kubectl rollout status 和 kubectl get pods,确认副本 Running 且重启次数为 0。再用 kubectl exec deploy/nginx-readonly -- sh -c 'touch /usr/share/nginx/html/probe 2>&1 || true; touch /tmp/ok && echo tmp-writable' 验证镜像路径写入失败、挂载路径可写。最后给命名空间打上 pod-security.kubernetes.io/enforce=restricted 标签并重启 Deployment,确认新 Pod 能通过准入。
验证
kubectl get pod -o jsonpath='{.spec.containers[0].securityContext}' 应显示 readOnlyRootFilesystem: true、allowPrivilegeEscalation: false 和 capabilities.drop 含 ALL。exec 检查中,镜像路径应返回 Read-only file system,/tmp 应输出 tmp-writable。受限策略启用后,替换 Pod 必须变为 Ready,否则准入错误会指出缺失字段。
注意事项
emptyDir 在运行时仍可写,只读根并不让容器不可变。内存型 emptyDir 计入 Pod 内存限制,增长过快会触发 OOM。需要在启动时安装软件包或重写 /etc 的应用,要先改造或使用 init container。不要为让应用启动而把整个根目录挂成可写卷;同时固定镜像 digest,不要只依赖可变 tag。