Skip to content

第 10 章 生产化与安全 ​

本章目标:掌握镜像与容器的安全加固要点(非 root、最小化、只读文件系统、secret 管理、健康检查),了解镜像扫描与私有仓库分发,能写出"生产级"的 Dockerfile。

1. 容器不等于安全 ​

容器只是内核级隔离,不是安全边界:

  • 共享内核:容器与宿主机共用同一个 Linux 内核,容器逃逸漏洞(如 CVE-2022-0185)一旦被利用,影响的是宿主机;
  • 默认 root:官方基础镜像默认以 root 运行,容器内 root 与宿主机 root 同 UID 0,配合内核漏洞风险更大;
  • 多余内容 = 攻击面:镜像里多装的每个包、每个工具,都可能藏漏洞(如遗留的 curl、编译器、调试器)。

所以生产化的核心思路是:最小化内容、最小化权限、最小化暴露。

2. 镜像加固清单 ​

① 非 root 运行(最重要的一条):

dockerfile
FROM python:3.12-alpine
# 显式创建非特权用户(alpine 用 adduser;Debian 系用 useradd)
RUN adduser -D -u 1000 app
WORKDIR /app
COPY app.py .
USER app                    # 之后的所有层与运行进程都以 app 身份
CMD ["python", "app.py"]

USER 之前的 RUN 仍以 root 执行(构建期需要装包),USER 之后以非 root 运行。验证:docker exec <容器> id 应看到 uid=1000(app)。

② 最小化基础镜像:alpine(musl)/ slim 系,多阶段构建去掉构建工具(回顾第 7 章),并删除运行不需要的工具——生产镜像里不该有编译器、包管理器、调试器。

③ 固定版本标签:nginx:alpine 会漂移,生产用 nginx:1.27-alpine(回顾第 2 章);更进一步锁定 digest:

bash
docker pull nginx:1.27-alpine@sha256:<digest>
# 或 Dockerfile 里 FROM nginx:1.27-alpine@sha256:<digest>

④ 健康检查:让编排/负载均衡知道容器是否真的可用(第 9 章讲过 docker inspect 里的健康状态就是它产生的):

dockerfile
HEALTHCHECK --interval=5s --timeout=3s --retries=3 \
  CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"

3. 运行期安全配置 ​

镜像加固是"静态",运行时还有一组开关(docker run 或 compose):

bash
docker run -d --name app \
  --read-only \                  # 容器文件系统只读,杜绝写入恶意文件
  --tmpfs /tmp \                 # 只读后需要可写临时目录,挂 tmpfs(回顾第 4 章)
  --cap-drop ALL \               # 丢弃全部 Linux 能力(网络管理、挂载等)
  --security-opt no-new-privileges \   # 禁止进程提升权限
  --pids-limit 100 \             # 限制进程数,防 fork 炸弹
  --memory 256m --cpus 0.5 \     # 资源限制(回顾第 9 章)
  secure-app

compose 等价写法(注意 read_only 与 cap_drop):

yaml
services:
  app:
    image: secure-app
    read_only: true
    tmpfs: [/tmp]
    cap_drop: [ALL]
    security_opt: [no-new-privileges:true]
    pids_limit: 100
    deploy:
      resources:
        limits: { cpus: "0.5", memory: 256M }

secret 管理(不要用环境变量传密码——docker inspect 任何人都能看):

bash
# 坏:密码进环境变量,inspect 可见
docker run -e DB_PASSWORD=secret ...

# 好:文件挂载 + 只读 + 权限最小化(或 docker secret / 密钥管理系统)
docker run -v "$PWD/db_password:/run/secrets/db_password:ro" ...

应用从文件读密钥(如 open("/run/secrets/db_password").read()),镜像里不残留任何密码。

4. 优雅退出:让应用体面收尾 ​

docker stop 先发 SIGTERM,宽限期(默认 10 秒)内应用应自行收尾(保存状态、关连接),超时才 SIGKILL:

bash
docker stop --time 30 app     # 把宽限期调成 30 秒

应用侧要求:主进程直接响应 SIGTERM。这就是第 6 章建议"CMD/ENTRYPOINT 用 JSON 数组形式"的原因——shell 包裹会吞掉信号,应用收不到 SIGTERM,只能被强杀。

5. 镜像扫描与分发 ​

  • 漏洞扫描:docker scan 已停服,社区主流用 Trivy(trivy image <镜像>),CI 里扫描失败即阻断发布;私有仓库(Harbor 等)也内置扫描。
  • 私有仓库分发:docker tag 打上仓库前缀 → docker push(回顾第 2 章);生产集群只允许从受信仓库拉取。
  • 供应链意识:基础镜像、依赖库都来自第三方——固定版本、扫描、最小化,缺一不可。

6. 完整示例:加固版镜像(本机可复现) ​

配套 examples/10-production-and-security/,基于第 6 章的零依赖 Python 应用加"健康检查端点",Dockerfile 做全套加固:

python
# examples/10-production-and-security/app.py(含 /health 端点)
import http.server

class Handler(http.server.BaseHTTPRequestHandler):
    def do_GET(self):
        if self.path == "/health":
            body, code = b"ok\n", 200
        else:
            body, code = b"Hello from hardened image!\n", 200
        self.send_response(code)
        self.send_header("Content-Type", "text/plain")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)
    def log_message(self, *args):
        pass

http.server.HTTPServer(("0.0.0.0", 8000), Handler).serve_forever()
dockerfile
# examples/10-production-and-security/Dockerfile
FROM python:3.12-alpine
ENV PYTHONDONTWRITEBYTECODE=1      # 禁止写 __pycache__(配合只读文件系统)
RUN adduser -D -u 1000 app
WORKDIR /app
COPY app.py .
USER app                           # 非 root 运行
EXPOSE 8000
HEALTHCHECK --interval=5s --timeout=3s --retries=3 \
  CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"
CMD ["python", "app.py"]

构建并加固运行:

bash
cd examples/10-production-and-security
docker build -t secure-app .
docker run -d --name app --read-only --tmpfs /tmp \
  --cap-drop ALL --security-opt no-new-privileges --pids-limit 100 \
  -p 8087:8000 secure-app

curl http://localhost:8087/health          # ok
docker exec app id                          # uid=1000(app),非 root
docker inspect app --format '{{.State.Health.Status}}'   # healthy
docker rm -f app

若因只读文件系统报错,多半是应用在写文件——把可写路径挂 tmpfs 或卷(如 /tmp)。

常见误区 ​

  • 误区:容器是安全的隔离边界。 正确:只是内核级隔离;逃逸漏洞影响宿主机,权限与内容要最小化。
  • 误区:USER app 只是让进程"看起来"非 root。 正确:它真正以非特权 UID 运行,容器内没有 root 权限。
  • 误区:密码放环境变量没问题。 正确:docker inspect 明文可见,应用镜像/配置里也不该留密钥;用 secret 文件或密钥管理。
  • 误区:EXPOSE 或健康检查 = 负载均衡自动摘除。 正确:健康检查状态由编排系统读取(如 compose 的 depends_on、K8s 探针),要配合使用。
  • 误区:加固 = 一个开关搞定。 正确:镜像(非 root/最小化/固定版本)+ 运行期(只读/cap-drop/资源限制/secret)+ 流程(扫描/受信仓库)三层缺一不可。

小结 ​

  • 容器共享内核,安全靠"最小化内容、最小化权限、最小化暴露"。
  • 镜像加固:非 root(USER + 显式建用户)、最小化/多阶段、固定版本与 digest、HEALTHCHECK。
  • 运行期加固:--read-only、--cap-drop ALL、no-new-privileges、--pids-limit、资源限制。
  • secret 用文件挂载/密钥管理,别放环境变量;应用要优雅响应 SIGTERM。
  • 发布链路:Trivy 扫描 → 私有仓库 → 受信拉取;供应链依赖也要固定与扫描。

练习 ​

  1. 在 examples/10-production-and-security/ 构建并加固运行,验证:curl /health 返回 ok、docker exec app id 是非 root、docker inspect 健康状态为 healthy。
  2. 去掉 --read-only 再运行,对比 docker inspect 中文件系统挂载的差异;把某个路径换成 --tmpfs 观察可写行为。
  3. 用 docker run 给容器加 -e SECRET=x,再 docker inspect,确认环境变量明文可见——体会"为什么密码不能放环境变量"。
  4. 修改 Dockerfile 用 ENTRYPOINT ["python"] + CMD ["app.py"],用 docker stop 观察 SIGTERM 的传递(可给应用加 signal 处理器验证优雅退出)。
  5. 思考题:为什么 --read-only 和 --tmpfs /tmp 通常成对出现?(提示:只读后应用写临时文件会失败。)

可运行示例见 examples/10-production-and-security/,按其中的 README 步骤执行。

📖本文阅读--次|📊全站访问--次|👥访客--人