← 中文资源

简体中文版本

用只读根文件系统加固容器

在 Kubernetes 中以只读根文件系统运行容器,移除 Linux 能力,只为确需写入的路径挂载 emptyDir,缩小攻击面。

阅读英文原文

概述

多数容器镜像默认使用可写根文件系统,进程被攻破后可以修改应用代码、向静态目录写入 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。