第 10 章 生产化与安全
本章目标:掌握镜像与容器的安全加固要点(非 root、最小化、只读文件系统、secret 管理、健康检查),了解镜像扫描与私有仓库分发,能写出"生产级"的 Dockerfile。
1. 容器不等于安全
容器只是内核级隔离,不是安全边界:
- 共享内核:容器与宿主机共用同一个 Linux 内核,容器逃逸漏洞(如 CVE-2022-0185)一旦被利用,影响的是宿主机;
- 默认 root:官方基础镜像默认以 root 运行,容器内 root 与宿主机 root 同 UID 0,配合内核漏洞风险更大;
- 多余内容 = 攻击面:镜像里多装的每个包、每个工具,都可能藏漏洞(如遗留的 curl、编译器、调试器)。
所以生产化的核心思路是:最小化内容、最小化权限、最小化暴露。
2. 镜像加固清单
① 非 root 运行(最重要的一条):
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:
docker pull nginx:1.27-alpine@sha256:<digest>
# 或 Dockerfile 里 FROM nginx:1.27-alpine@sha256:<digest>④ 健康检查:让编排/负载均衡知道容器是否真的可用(第 9 章讲过 docker inspect 里的健康状态就是它产生的):
HEALTHCHECK --interval=5s --timeout=3s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"3. 运行期安全配置
镜像加固是"静态",运行时还有一组开关(docker run 或 compose):
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-appcompose 等价写法(注意 read_only 与 cap_drop):
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 任何人都能看):
# 坏:密码进环境变量,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:
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 做全套加固:
# 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()# 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"]构建并加固运行:
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 扫描 → 私有仓库 → 受信拉取;供应链依赖也要固定与扫描。
练习
- 在
examples/10-production-and-security/构建并加固运行,验证:curl /health返回 ok、docker exec app id是非 root、docker inspect健康状态为 healthy。 - 去掉
--read-only再运行,对比docker inspect中文件系统挂载的差异;把某个路径换成--tmpfs观察可写行为。 - 用
docker run给容器加-e SECRET=x,再docker inspect,确认环境变量明文可见——体会"为什么密码不能放环境变量"。 - 修改 Dockerfile 用
ENTRYPOINT ["python"]+CMD ["app.py"],用docker stop观察 SIGTERM 的传递(可给应用加signal处理器验证优雅退出)。 - 思考题:为什么
--read-only和--tmpfs /tmp通常成对出现?(提示:只读后应用写临时文件会失败。)
可运行示例见
examples/10-production-and-security/,按其中的 README 步骤执行。