跳到主要内容

容器安全入门

Docker 提供进程、文件系统和网络隔离,但容器与宿主机共享内核,不能把“运行在容器里”理解成“天然安全”。安全需要同时覆盖镜像来源、构建过程、运行权限、密钥、网络和宿主机。

完成后,你将能够:

  • 识别 Docker 中风险最高的配置;
  • 让应用以非 root 和最小权限运行;
  • 使用只读文件系统、资源限制和内部网络;
  • 避免把密钥写进镜像或仓库;
  • 使用工具检查镜像和运行配置。

1. 先建立威胁模型

可以从四个问题开始:

  1. 镜像可信吗? 基础镜像可能过期、被冒名或包含已知漏洞;
  2. 容器被攻破后能做什么? root、特权模式、危险挂载会放大影响;
  3. 容器能访问什么? 不必要的端口和网络连通会扩大攻击面;
  4. 宿主机是否安全? 所有容器共享宿主内核,Docker socket 还能控制整个 Engine。

高风险配置包括:

  • --privileged
  • 挂载 /var/run/docker.sock
  • 把宿主机 //etc/proc 等敏感路径写入容器;
  • 使用来源不明或长期不更新的镜像;
  • 将密码、token、私钥写入 Dockerfile、镜像或 Git;
  • 无限制地暴露端口、内存和进程数。

2. 选择并固定可信镜像

优先使用官方镜像或经过组织审核的镜像,并显式指定版本:

# 拉取明确版本和变体,避免隐式使用 latest
docker pull nginx:1.27-alpine

# 查看镜像来源标签、创建时间、用户和启动命令
docker image inspect nginx:1.27-alpine \
--format 'created={{.Created}} user={{json .Config.User}} labels={{json .Config.Labels}}'

# 查看仓库摘要;digest 可以固定到具体内容
docker image inspect nginx:1.27-alpine \
--format '{{json .RepoDigests}}'

版本标签便于阅读和升级,digest 提供不可变引用。实际项目可以同时记录两者,并通过自动化流程定期升级,而不是永远锁死一个含漏洞的旧版本。

控制构建上下文

.dockerignore 至少排除版本历史、依赖缓存和秘密文件:

# Git 历史可能包含已经删除的旧密钥
.git

# 本地依赖和构建产物应在受控步骤中生成
node_modules
dist

# 环境文件、私钥和云凭据绝不能进入构建上下文
.env
*.pem
.aws
.kube

不要把“后面再用 RUN rm 删除”当作补救。敏感文件一旦进入前面的镜像层,后续删除也可能从历史层恢复。

3. 使用非 root 用户

先观察镜像默认用户:

# 如果 Config.User 为空,容器通常默认以 root 启动
docker image inspect alpine:3.20 \
--format 'configured user={{json .Config.User}}'

# 输出当前容器进程身份;标准 alpine 默认显示 uid=0(root)
docker run --rm alpine:3.20 id

# 临时指定普通 UID/GID,再次查看身份
docker run --rm \
--user 10001:10001 \
alpine:3.20 \
id

应用镜像更适合在 Dockerfile 中创建固定用户:

# 使用最小化 Alpine 基础镜像
FROM alpine:3.20

# 创建固定 UID/GID 的系统用户,便于挂载目录正确授权
RUN addgroup -S -g 10001 appgroup \
&& adduser -S -D -H -u 10001 -G appgroup appuser

# 先由 root 创建并授权应用目录
WORKDIR /app
COPY --chown=appuser:appgroup ./app /app/app

# 后续构建步骤和容器进程都使用低权限用户
USER appuser:appgroup

# 使用 exec 形式启动应用,使信号直接传给主进程
ENTRYPOINT ["/app/app"]

非 root 不能消除所有风险,但能限制很多容器逃逸前的操作和误修改。应用若只需监听高端口、读取配置和写指定数据目录,就没有理由以 root 运行。

4. 只读根文件系统

容器通常只需要少数可写目录。让根文件系统只读,再显式提供临时目录或数据卷:

# 根文件系统只读,仅为 Nginx 运行目录提供受限 tmpfs
docker run --detach \
--name readonly-web \
--read-only \
--tmpfs /var/cache/nginx:rw,noexec,nosuid,size=32m \
--tmpfs /var/run:rw,noexec,nosuid,size=4m \
nginx:1.27-alpine

# 尝试写根目录应失败,从而验证只读限制
docker exec readonly-web \
sh -c 'touch /should-fail || echo "root filesystem is read-only"'

# 清理实验容器
docker rm --force readonly-web

tmpfs 数据存于内存,容器停止后消失,适合临时 PID、缓存和短期文件;持久数据仍应使用命名卷。

5. 丢弃 Linux capabilities

容器中的 root 默认也不是完整宿主机 root,Docker 会限制一部分 Linux capabilities。但仍应按需进一步缩减:

# 丢弃全部 capabilities,并禁止通过 setuid 等方式获得新权限
docker run --rm \
--cap-drop ALL \
--security-opt no-new-privileges:true \
alpine:3.20 \
sh -c 'grep -E "^(CapEff|NoNewPrivs):" /proc/self/status'

如果应用确实需要某项能力,再只加那一项。例如绑定低于 1024 的端口可能需要 NET_BIND_SERVICE

# 在全部丢弃后,只添加绑定低端口所需能力
docker run --rm \
--cap-drop ALL \
--cap-add NET_BIND_SERVICE \
--security-opt no-new-privileges:true \
your-app:1.0

不要遇到权限错误就直接加 --privileged。先查看错误、确定所需系统调用、设备或 capability,再提供最小权限。

6. 限制资源,降低拒绝服务风险

# 限制 CPU、内存、进程数,并设置日志轮转
docker run --detach \
--name bounded-web \
--cpus 1.0 \
--memory 256m \
--pids-limit 200 \
--log-driver local \
--log-opt max-size=10m \
--log-opt max-file=3 \
nginx:1.27-alpine

# 检查 Docker 实际记录的限制
docker inspect bounded-web \
--format 'memory={{.HostConfig.Memory}} pids={{.HostConfig.PidsLimit}} log={{.HostConfig.LogConfig.Type}}'

# 清理实验容器
docker rm --force bounded-web

限制过低会影响可用性,限制过高又失去保护作用。应基于压测和运行指标设定,并对 OOM、重启次数和磁盘增长告警。

7. 最小化网络暴露

数据库和缓存通常只需被应用容器访问,不应发布到所有宿主机接口:

# 创建内部服务使用的自定义网络
docker network create secure-backend

# Redis 只加入容器网络,不使用 -p 发布到宿主机
docker run --detach \
--name secure-cache \
--network secure-backend \
redis:7.4-alpine

# 从同一网络的临时客户端验证连接
docker run --rm \
--network secure-backend \
redis:7.4-alpine \
redis-cli -h secure-cache ping

# 清理容器和网络
docker rm --force secure-cache
docker network rm secure-backend

必须发布但只供本机反向代理访问时,绑定 127.0.0.1,例如 -p 127.0.0.1:8080:80。需要对外访问时,还应配置认证、TLS、防火墙和速率限制。

8. 正确处理密钥

不要这样做:

# 错误示例:ENV 会把密码保存在镜像配置中
ENV DATABASE_PASSWORD=do-not-put-secrets-here

# 错误示例:复制后再删除,秘密仍可能存在于历史镜像层
COPY production.key /tmp/production.key
RUN use-key /tmp/production.key && rm /tmp/production.key

构建时需要凭据,使用 BuildKit secret:

# secret 仅在当前构建步骤存在,不写入镜像层
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \
npm ci
# 从本机文件向 BuildKit 提供临时 secret
docker build \
--secret id=npmrc,src="$HOME/.npmrc" \
--tag private-app:1.0 .

运行时优先让应用从只读 secret 文件读取,而不是把值直接写进 Compose 文件。单机 Compose 的 secrets 主要提供文件挂载方式,并不自动等同于完整的密钥管理系统:

services:
api:
image: your-api:1.0
secrets:
# 容器内默认路径为 /run/secrets/db_password
- db_password

secrets:
db_password:
# 此文件必须由部署流程安全提供,且不能提交到 Git
file: ./secrets/db_password.txt

密钥还需要最小访问权限、审计、定期轮换和撤销机制。避免在日志中输出完整环境变量或请求头。

9. Docker socket 为什么危险

/var/run/docker.sock 是 Docker CLI 控制 Engine 的接口。能访问它的进程通常可以:

  • 创建特权容器;
  • 挂载宿主机任意目录;
  • 读取其他容器配置和环境;
  • 停止、删除或替换现有容器。

因此这类挂载通常近似授予宿主机 root 控制权:

services:
dangerous-service:
image: some-tool:latest
volumes:
# 危险示例:容器获得 Docker Engine 控制能力
- /var/run/docker.sock:/var/run/docker.sock

只有明确理解风险并有隔离措施的基础设施工具才应访问 socket。不要让普通 Web 应用、来源不明的 CI 任务或第三方镜像访问它。

10. 扫描镜像并保留供应链信息

Docker Desktop 某些版本提供 docker scout。如果当前环境没有该命令,可在 CI 中使用 Trivy、Grype 等受管扫描器:

# 检查当前 Docker 是否提供 Scout 子命令
docker scout version

# 查看镜像中已知漏洞及修复版本;命令可用时再执行
docker scout cves nginx:1.27-alpine

# 生成软件物料清单,便于追踪镜像包含的包
docker scout sbom nginx:1.27-alpine

漏洞扫描结果需要结合版本、实际暴露路径和可利用性评估。扫描“零高危”也不能证明镜像绝对安全。发布流程还应保留镜像 digest、SBOM、构建来源和签名或制品证明。

11. 主机和守护进程基线

  • 及时更新操作系统内核、Docker Engine 和 containerd;
  • 限制谁能登录宿主机、使用 sudo 和访问 Docker socket;
  • 配置日志轮转,并监控磁盘、inode、OOM 和异常重启;
  • 不要把未加密的 Docker API 暴露到 TCP 网络;
  • 定期清理已撤销的仓库凭据和无维护镜像;
  • 多租户或敌对代码场景需要评估 rootless、用户命名空间、专用节点、强化运行时或虚拟化边界。

检查 Engine 基本安全信息:

# 查看安全选项、rootless 状态、日志驱动和内核信息
docker info

# Linux 上确认谁可以访问 Docker socket
ls -l /var/run/docker.sock

# 查看当前用户所属组;docker 组应仅包含可信用户
id

12. 上线前检查清单

  • 镜像来自可信来源,标签明确,并记录 digest;
  • .dockerignore 排除了 Git、密钥、环境文件和本地依赖;
  • 最终镜像不包含编译工具和测试数据;
  • 应用使用非 root 用户;
  • 根文件系统尽量只读,可写目录单独挂载;
  • capabilities 已丢弃到最小,并启用 no-new-privileges
  • 未使用 --privileged,未挂载 Docker socket 和敏感宿主路径;
  • CPU、内存、进程数和日志大小有合理限制;
  • 只发布必需端口,数据库和缓存留在内部网络;
  • 密钥没有进入代码、镜像、Compose 文件或日志;
  • CI 已执行漏洞扫描,并保留 SBOM 和构建来源;
  • 数据已有备份、恢复演练和访问控制。

下一篇将学习一套固定的 Docker 故障诊断顺序,并亲手复现退出码、端口和网络故障。