MetaMaaz/docker-security-pipeline

GitHub: MetaMaaz/docker-security-pipeline

一个 DevSecOps 演示项目,通过多阶段 Docker 构建和 CI 安全门禁将漏洞镜像从 1213 个 CVE 加固至 28 个,完整记录了容器镜像安全加固的实践过程。

Stars: 0 | Forks: 0

# docker-security-pipeline ![CI 扫描未通过包含漏洞镜像的构建](https://static.pigsec.cn/wp-content/uploads/repos/2026/06/1bf7fc4908195646.png) **这证明了什么:** 我可以实现安全左移 —— 在带有漏洞的容器镜像到达 registry 之前,于 CI 阶段将其拦截,并配备文档化、可审计的例外处理流程。 ## Docker 镜像加固 Pipeline 我接手了一个原始的 Python/Flask 容器 —— 大小为 1.6 GB,包含超过 1,200 个严重和高危 CVE —— 并将其优化为一个仅有 28 个 CVE、大小为 100 MB 的 distroless 镜像。在此过程中的每一个版本都经过了扫描、在 CI 中进行了门禁拦截,并生成了 SBOM。这个仓库记录了我实现这一目标的过程,以及我在与扫描器“据理力争”时所学到的经验。 [![Security Scan](https://static.pigsec.cn/wp-content/uploads/repos/2026/06/3444290903195733.svg)](https://github.com/MetaMaaz/docker-security-pipeline/actions/workflows/security.yml) ![License](https://img.shields.io/badge/license-MIT-blue) ## 核心主旨 我不想仅仅知道加固是有用的。我想要具体的数据。因此,每个 Dockerfile 都是一个刻意设计的版本,使用 Trivy 进行扫描并与 Docker Scout 交叉验证,我保留了扫描结果,而不是主观的猜测。 ## 结果 Trivy `CRITICAL` + `HIGH`,每次都是同一个 Flask 应用: | 版本 | 基础镜像 | CRITICAL | HIGH | 总计 | 大小 | |---|---|--:|--:|--:|--:| | v1-naive | `python:3.9` | 190 | 1023 | 1213 | 1.6 GB | | v2-slim | `python:3.9-slim` | 5 | 34 | 39 | 226 MB | | v3-nonroot | 多阶段构建 `python:3.12-slim`,`USER appuser` | 2 | 9 | 11 | 210 MB | | v4-distroless | `gcr.io/distroless/python3-debian12` | 2 | 26 | 28 | 99.8 MB | CVE 数量从 1213 降至 28,体积从 1.6 GB 降至 100 MB。 ## 每个步骤实际带来的收益 slim 基础镜像发挥了绝大部分作用。将完整的 `python` 镜像替换为 `-slim`,仅仅通过剔除应用根本用不到的 OS 软件包,就消灭了大约 97% 的 CVE。此后的所有操作带来的都是更小、更精准的提升。 采用多阶段构建并使用非 root 用户,将构建工具保留在构建阶段,因此 runtime 镜像只发布应用及其依赖,而没有任何其他多余内容,并且以 `appuser` 而不是 root 身份运行。即使应用真的被攻破,攻击者能利用的资源和空间也会大幅减少。 Distroless 则更进一步:没有 shell,没有包管理器,没有 perl,体积只有 slim 镜像的一半。 ## 我没有预料到的一点:distroless 导致 CVE 数量增加了 v4 报告了 28 个 CVE,而 v3 只有 11 个。表面上看这似乎是一种退步,这曾让我困惑了一阵子。 原因在于基础镜像的滞后。官方的 distroless 镜像仍然基于 Debian 12,而 slim 已经升级到了 Debian 13,因此它包含较旧的软件包。但仅仅看数量会忽略掉实质性的变化。distroless 镜像的体积只有原来的一半,而且没有可以进入的 shell —— `docker run --rm -it myapp:v4-distroless sh` 会直接失败,因为 entrypoint 是 `python3`,而 `sh` 会被当作脚本名称来读取。v3 中存在的 perl 远程代码执行(RCE)和路径遍历 CVE 在这里消失了,它们没有被修补,而是压根不存在,因为 perl 根本就没有被安装。当我仔细阅读剩下的那 28 个 CVE 具体是什么时,发现它们几乎全都是应用从未调用过的库(如 sqlite、expat、ncurses、zlib)中被推迟修复或不予修复的 DoS 漏洞。 所以,虽然数字上升了,但镜像实际上更安全了。这就是浓缩在一行表格数据中的全部教训:去阅读 CVE 的细节,而不仅仅是数数。 ## CI 门禁 `.github/workflows/security.yml` 会在每次 push 和 PR 时重新构建 distroless 镜像,并使用 `aquasecurity/trivy-action` 对其进行扫描: ``` severity: CRITICAL,HIGH ignore-unfixed: true exit-code: 1 ``` `ignore-unfixed: true` 是最关键的一行。它告诉门禁仅在遇到你实际能够应用的 CVE 修复时才判定失败,这样构建就会因为真实的、可采取行动的风险而中断,而不是为了任何人都无法修补的基础 OS 噪音而唠叨不休。 尽管如此,第一次运行还是失败了。两个可修复的基础镜像 CVE —— `CVE-2025-13836` 和 `CVE-2026-45447` —— 已经在 Debian 中得到修补,但尚未同步到 distroless 基础镜像中,重新拉取依然只是给了我相同的 digest。我既不想无视红色的失败构建,也不想削弱门禁限制,所以我添加了一个 `.trivyignore`,准确列出了这两个 ID,注明了日期,并附注说明:一旦 distroless 重新构建,就重新固定基础镜像并删除这几行内容。风险已被接受,落实在纸面上,并制定了到期跟进计划。 为了证明门禁不是摆设,我推送了一个临时的 throwaway 分支,将构建目标指向 `Dockerfile.v1-naive` 并让它运行。结果完全按预期失败了 —— Trivy 统计了超过一千个 OS 级别的 CVE,步骤以非零状态退出,阻止了合并: ![CI 门禁在 naive 镜像上未通过](https://static.pigsec.cn/wp-content/uploads/repos/2026/06/1bf7fc4908195646.png) ## 供应链 `results/sbom.json` 是由 Syft 生成的 SPDX 物料清单(SBOM) —— 包含 41 个组件。你仅通过 SBOM 就能看出没有 shell、没有 apt、没有 perl,这意味着我关于“攻击面小”的声明并不是一句空口无凭的话。 扫描器之间的分歧是我最喜欢的发现。在完全相同的镜像上,Docker Scout 显示 `0C/0H/0M/1L`,而 Trivy 显示为 28。原因在于数据库不同:Scout 不屑于展示那些不可修复的基础 OS DoS 漏洞。但是,Scout 捕捉到而 Trivy 的门禁按设计过滤掉的那 1 个 LOW 漏洞,却是一个真实的 Flask 3.0.0 CVE(`CVE-2026-27205`)。因此,我将 Flask 升级到了 3.1.3 并重新扫描,Scout 的结果变干净了。任何单独的扫描器都没有展现出全貌 —— 这就是我在面试时会反复强调的核心要点。 最终镜像已发布到 `ghcr.io/metamaaz/myapp:v4-distroless`(以及 `:latest`),公开可访问,采用 MIT 许可证。 ## 布局 ``` .dockerignore .trivyignore # 2 risk-accepted CVE IDs, dated and documented .github/workflows/security.yml # CI security gate app/app.py app/requirements.txt # flask==3.1.3 dockerfiles/Dockerfile.v1-naive dockerfiles/Dockerfile.v2-slim dockerfiles/Dockerfile.v3-nonroot dockerfiles/Dockerfile.v4-distroless results/sbom.json results/scan-results.md # the full write-up results/scout-summary.txt LICENSE ``` 从仓库根目录构建任何版本: ``` docker build -f dockerfiles/Dockerfile.v4-distroless -t myapp:v4-distroless . ``` ## 对刚接触此项工作的人的建议 在尝试任何高招之前,先尝试使用 slim 基础镜像 —— 这能带来绝大部分的收益。在未检查基础发行版、修复状态以及易受攻击的代码是否真的可被访问(reachable)之前,不要把 CVE 数量变少等同于镜像更安全。而且要运行两个扫描器,因为它们产生分歧的地方,正是你能真正学到东西的地方。 ## 许可证 MIT
标签:DevSecOps, Docker, LLM防护, SBOM, 上游代理, 安全基线, 安全防御评估, 教学环境, 硬件无关, 请求拦截, 逆向工具