讲解

容器的隔离是「内核级进程隔离」而非硬件级,共享内核意味着安全水位天然低于虚拟机——所以容器安全的核心思路是缩减权限与暴露面。第一条铁律:不要用 root 跑应用。镜像默认 USER 是 root,容器里 root 与宿主机 root 共享内核(靠 user namespace 才有进一步隔离),容器逃逸漏洞的破坏力与容器内权限正相关。Dockerfile 里建普通用户并 USER 切换,或运行时 --user 指定 uid——注意挂载卷的属主要对应上。

第二条:砍掉能力(capabilities)。Linux 把 root 特权拆成 30+ 个能力项,容器默认只带一小部分(如 CHOWN、SETUID、NET_BIND_SERVICE),还可以继续收紧:--cap-drop ALL 全丢,按需 --cap-add 加回个别项(比如绑定低端口才需要 NET_BIND_SERVICE)。配合 --security-opt no-new-privileges(禁止 setuid 提权),应用即使被攻破也做不了什么。

第三条:控制来源与内容。基础镜像用官方或可信发布者的,锁定版本甚至 digest;用 docker scout 或 trivy 扫镜像漏洞(CI 里跑);镜像越小越好——alpine、distroless 系镜像连 shell 都没有,攻击者进去了也没有工具可用。最后提醒一个宿主侧事实:能操作 Docker socket 等于 root,/var/run/docker.sock 别随便挂进容器,CI 需要构建时优先考虑 rootless 构建工具(kaniko、buildah)。

示例

以非 root 身份运行容器(65534 是 nobody 用户的 uid):

docker run --rm --user 65534:65534 alpine:3.21 id

默认 capabilities 对比全丢之后的差异——去掉 NET_RAW 后 ping 不可用(这正是收紧的意义):

docker run --rm --cap-drop ALL alpine:3.21 sh -c 'ping -c 1 -W 2 127.0.0.1 2>&1 | head -1; echo "cap 全丢后仍然能跑普通命令: $(echo ok)"'

no-new-privileges 与只读文件系统的组合(资源限制章已见只读,这里看提权禁令):

docker run --rm --security-opt no-new-privileges=true alpine:3.21 sh -c 'echo "提权被禁止的容器正常启动"; id -u'

在 Dockerfile 里固化非 root 用户才是正路——构建一个「天生低权限」的镜像:

mkdir -p secctx && cat > secctx/Dockerfile <<'EOF'
FROM alpine:3.21
RUN adduser -D -u 10001 appuser
USER appuser
CMD ["id"]
EOF
cd secctx && docker build -t ohmydocs-verify-nonroot:1.0 . && cd ..
docker run --rm ohmydocs-verify-nonroot:1.0

常见坑

  • --user 之后挂载卷写不进:容器内 uid 与宿主机目录属主不匹配;提前 chown 或让镜像用户与部署用户的 uid 对齐。
  • 为了 ping 加回一堆 cap:排查网络用临时容器,别给业务容器加 NET_RAW。
  • 基础镜像图省事用 unknown 源:供应链投毒真实存在;官方镜像 + 锁 digest + 定期扫描是底线。
  • 把 docker.sock 挂进容器:等于交出宿主机 root;监控类容器需要时至少挂只读,并清楚自己在做什么。

小结

非 root 运行、cap 最小化、no-new-privileges、镜像小而可信;docker.sock 是宿主机 root 的钥匙。下一章做卫生:磁盘清理。