容器镜像评审:进程、权限与构建产物
在大多数团队的代码评审(Code Review, CR)流程中,合并一个Dockerfile往往只耗费审查者不到 10 秒钟的时间——大家扫一眼 Base Image 是不是 Alpine,确认暴露了正确的端口,就随手点下了 Approve。
然而,生产环境出现的严重故障,往往就埋藏在这些被忽略的Dockerfile细节里:因为没有使用 Shell 格式导致的PID 1 僵尸进程收割失败、因为漏写USER指令引发的容器逃逸越权、以及在构建层里残留敏感 Token 带来的安全泄漏。
容器镜像安全不仅仅是 CVE 漏洞扫描,更是一套严密的工程质量门禁(Quality Gates)。
1. 被忽略的 ENTRYPOINT 信号传递与 PID 1 僵尸进程陷阱
在 CR 时,如果看到类似这样的代码,必须立即打回:
## 常见问题:命令格式影响主进程信号处理 ENTRYPOINT java -jar /app/service.jar- Shell 形式的祸害:当使用
ENTRYPOINT command param时,Docker 会启动/bin/sh -c作为 PID 1 主进程。而 Linux Shell 默认不会把SIGTERM优雅终止信号透传给子进程(Java/Node.js)。结果就是:每次 Pod 滚动更新时,应用都无法执行优雅关机(Graceful Shutdown),导致连接池强行中断、数据丢失。 - 正确的标准写法:必须使用 Exec 格式
ENTRYPOINT ["java", "-jar", "/app/service.jar"],或者在容器中引入tini/dumb-init专门收割僵尸进程。
2. Dockerfile 工程质量门禁九大检查清单 (Checklist)
在 CI 门禁中,必须强制校验以下规则:
- 禁止默认 USER root:必须显示指定
USER node或USER 10001,遵守最小权限原则。 - 严禁硬编码 Secret/Token:不允许
ENV API_KEY=xyz或在RUN步骤中使用curl -u user:pass。 - 启用多阶段构建 (Multi-Stage Builds):编译环境(如
golang:1.22-alpine)与运行环境(如distroless/static)彻底隔离。 - 确定性 Base Image 标签:严禁使用
ubuntu:latest或python:3,必须锁定版本号与 Hash(如golang:1.22.4-alpine3.20)。 - 正确处理 APT/APK 缓存:必须包含
rm -rf /var/lib/apt/lists/*,防止无用缓存污染镜像层。 - 显式声明 HEALTHCHECK 探针:为 Docker 单机或 Compose 部署提供强类型健康判定。
- 挂载临时目录 Noexec:将
/tmp与/run设置为noexec,nosuid,nodev。 - 清理敏感历史层:在同一
RUN指令中删除编译中间产物,不要在下一条RUN中才删除。 - 避免使用
ADD指令:除非需要解压本地 tar 包,一律使用COPY替代ADD,防止隐式 URL 下载攻击。
3. 基于 Open Policy Agent (OPA/Conftest) 的自动化 Dockerfile 质量门禁
为了在 CI 阶段自动化拦截不合格的 Dockerfile,我们可以编写基于 Rego 语言的 OPA 策略规则:
# policy/dockerfile_security.rego package main # 规则 1: 拦截使用 root 用户运行容器的代码 deny[msg] { input[i].Cmd == "user" val := input[i].Value val[0] == "root" msg := sprintf("Line %d: 严禁显式使用 USER root 运行容器!", [i]) } # 规则 2: 强制检测是否设置了 USER 指令 default has_user := false has_user { input[_].Cmd == "user" } deny[msg] { not has_user msg := "CR 门禁驳回:Dockerfile 中未发现 USER 指令,必须指定非 root 用户!" } # 规则 3: 拦截使用 latest 标签的 Base 镜像 deny[msg] { input[i].Cmd == "from" val := input[i].Value endswith(val[0], ":latest") msg := sprintf("Line %d: Base 镜像 %s 禁止使用 :latest 标签,必须锁定具体版本号!", [i, val[0]]) } # 规则 4: 检查是否混用了 Shell 形式的 ENTRYPOINT deny[msg] { input[i].Cmd == "entrypoint" not is_array(input[i].Value) msg := sprintf("Line %d: ENTRYPOINT 必须使用 JSON 数组格式 (Exec 形式),防止 PID 1 信号屏蔽!", [i]) } is_array(val) { is_array(val) }4. 生产现场静态检查与镜像审计命令
在 CI/CD 流水线中,通过集成开源工具,实现合规性门禁自动化阻断:
# 1. 使用 Hadolint 对 Dockerfile 进行静态语法与规范扫描 hadolint --ignore DL3007 --ignore DL3018 Dockerfile || exit 1 # 2. 使用 Trivy 进行镜像 CVE 漏洞与配置缺陷扫描 (拦截 High/Critical 漏洞) trivy image --severity HIGH,CRITICAL --exit-code 1 prod-registry.internal/app/order-api:v2.4.0 # 3. 使用 Docker BuildKit 挂载密钥机制(防止 Token 存入镜像 History 层) # ❌ 旧模式: docker build --build-arg GITHUB_TOKEN=xyz . # ✅ 新模式: 安全挂载 secret 文件,构建完即销毁 DOCKER_BUILDKIT=1 docker build --secret id=mysecret,src=./github_token.txt -t order-api:v2.4.0 .代码审查不是凭感觉叫好。建立起确定性的 Dockerfile 校验门禁(Conftest + Hadolint + Trivy),将 PID 1 信号传递、非 root 权限和零 Token 泄露写入 CI 刚性规则,才能在源头守住云原生镜像安全的生命线。
镜像门禁要配合运行时核验
静态检查只能发现构建期问题。镜像进入集群后,还应核对实际用户、挂载权限和网络策略,确保 Dockerfile 中的约束没有被部署配置覆盖。
补充说明
现场记录比结论更重要
运维变更最怕只留下一个“正常”。每次检查应保存对象范围、命令版本、时间窗和关键输出摘要;对异常结果,注明下一步由谁判断、什么条件下停止继续操作。脚本可以给出候选结论,但生产动作仍需要把原始指标、日志或事件链接回去。恢复以后也要核对队列、错误率和业务任务是否回到基线,避免只看进程存活就结束处理。
镜像评审除 Dockerfile 外,还要检查最终镜像里的实际文件和启动命令。构建阶段带入的配置、包管理缓存或调试工具都可能扩大攻击面。以非 root 用户启动后,验证临时目录、端口和卷挂载仍可工作;同时测试终止信号能否传到业务进程,避免发布时出现无法优雅退出的容器。