THEKROLL-LTD/oss-mirror-build

GitHub: THEKROLL-LTD/oss-mirror-build

一个 GitHub Actions 模板,用于自动构建、扫描和加固上游 OSS 容器镜像,通过双重漏洞扫描、SBOM 和差异审查保障容器供应链安全。

Stars: 0 | Forks: 0

# oss-mirror-build 一个用于从上游源代码构建、扫描和加固 OSS 容器镜像的 GitHub Actions 模板。在每次上游版本更新时生成审计所需的产物 —— SBOM、扫描报告、差异审查 —— 并通过 Pull Request 作为人工审核关卡。 专为那些需要以合理方式自托管第三方 OSS 容器,但又不想支付 Chainguard 或 Docker Hardened Images 定价的团队而设计。默认变基到 [Google 的 distroless](https://github.com/GoogleContainerTools/distroless);任何最小化基础镜像均可适用。 关于此流水线背后的“为什么以及何时”的论述,请参阅配套文章:*[停止拉取随机的 Docker 镜像](https://medium.thekroll.ltd/stop-pulling-random-docker-images-c19e94559cc6)*。 ## 它的功能 每天晚上(默认为 UTC 03:00),您 fork 仓库中的流水线将执行以下操作: 1. 检查您配置的上游仓库是否有新的 release 标签 2. 在该标签处克隆上游源代码 3. 应用您的 Dockerfile 覆盖(如果存在)—— 这是您变基到 distroless 或任何加固基础镜像的方式 4. 生成差异审查产物:包含上次构建标签与新标签之间的提交日志和完整补丁 4a. **(仅在存在覆盖时)** 对比上游 `Dockerfile` 路径(可通过 `UPSTREAM_DOCKERFILE_PATHS` 配置)在上次审核标签与新标签之间的差异。如果发生任何更改,将提交一个 `override-review` issue 并阻止构建,直到人工关闭该 issue。这可以捕获 `Dockerfile.override` 与重构后的上游 Dockerfile 之间的静默漂移(例如新增 apt 包、重命名 COPY 路径、重构构建阶段),这是 Trivy 无法检测到的。 5. 对克隆的源代码运行 [Trivy](https://github.com/aquasecurity/trivy) **文件系统扫描**,读取每个语言层的 lockfile(`mix.lock`, `package-lock.json`, `requirements.txt`, `Gemfile.lock`, `Cargo.lock`, `go.sum`, ……) 6. 构建容器镜像一次,并在本地加载 7. 运行 Trivy **镜像扫描**,为 Security 标签页生成一份 SARIF 报告,并生成 CycloneDX SBOM 作为构建产物 8. 如果任一扫描发现存在可用修复方案的 `CRITICAL` 或 `HIGH` 级别漏洞:阻止推送,打开一个涵盖两次扫描的单个 issue,并保留所有产物 9. 如果一切正常:推送到您的 GHCR 命名空间,并打开一个 Pull Request,在 `image-pin.yml` 中固定新的摘要 为什么需要两次扫描:镜像扫描只能看到构建后存活下来的内容。对于 Go 和 Rust 二进制文件来说这已经足够——每个依赖项都在二进制文件的 BuildInfo 中。但对于 Elixir、Python、Ruby、Node 及类似运行时,lockfile 仅存在于构建阶段,而运行时镜像只是编译后的字节码,因此仅通过镜像扫描无法看到语言层的 CVE 漏洞情况。文件系统扫描填补了这一空白。 合并 PR 后,您的 GitOps 系统(Flux、ArgoCD 或任何固定了 `image-pin.yml` 的系统)将推出新的摘要。 ## 快速开始 五个步骤。假设您之前使用过静态编译的上游项目,例如 Go 或 Rust;对于更复杂的运行时(带有原生依赖的 Python、带有运行时加载库的 C),首次配置覆盖需要花费更长时间。 **1. 通过 "Use this template" 按钮进行 Fork。** **2. 在您的新仓库中启用 Actions。** GitHub 默认会在基于模板生成的仓库中禁用它们。路径为 Settings → Actions → General → "Allow all actions and reusable workflows"。 **3. 在 `.github/workflows/mirror-build.yml` 中配置两个必需的变量:** ``` env: UPSTREAM_REPO: "Forceu/Gokapi" IMAGE_NAME: "ghcr.io/your-org/gokapi" ``` `IMAGE_NAME` 必须为小写。容器注册表会拒绝仓库名称部分的大写字母。在两个值均被设置(且为小写)之前,流水线会快速报错并给出明确的错误提示。 **4. (推荐)添加 Dockerfile 覆盖** 以使用加固的基础镜像。将 `examples/gokapi/Dockerfile.override` 中的参考文件复制到 `dockerfiles/Dockerfile.override`,并根据您的上游进行调整: ``` FROM golang:1.25-alpine AS build WORKDIR /src RUN apk add --no-cache git COPY . . RUN go generate ./... && \ CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" \ -o /out/gokapi github.com/forceu/gokapi/cmd/gokapi FROM gcr.io/distroless/static:nonroot COPY --from=build /out/gokapi /gokapi USER nonroot:nonroot EXPOSE 53842 ENTRYPOINT ["/gokapi"] ``` 如果没有覆盖,流水线将原样构建上游的 Dockerfile。这依然可以为您提供 SBOM、扫描报告和受控的注册表——但 Trivy 会暴露上游基础层中存在的任何 CVE。如果在静态 Go 二进制文件上使用 `distroless/static` 覆盖:通常零操作系统层发现,仅剩应用层的 CVE。 **5. 手动触发一次工作流**(Actions 标签页 → OSS Mirror Build → Run workflow)。如果首次运行成功并打开了 PR,后续的夜间定时计划将接管。 ## Dockerfile 覆盖机制 如果您的仓库中存在 `dockerfiles/Dockerfile.override`,流水线会将其复制到克隆的上游源代码中,完全替换上游的 `Dockerfile`。然后构建将针对您的覆盖文件运行。 有意设计为全文件替换,而不是补丁。原因如下: - 可以稳健应对上游的空白或布局更改 - 当上游升级语言版本时,只需在一处进行协调 - CI 中不会出现 `patch --reject` 的麻烦 您仍然在跟踪上游源代码——流水线每次运行时都会重新克隆它。只有容器构建配方是您自己的。 ### 模块路径补丁(发布 v2+ 但在 go.mod 中没有 `/v2` 的 Go 项目) 一些 Go 项目发布 v2.x.x 或更高版本的 release 标签,但在 `go.mod` 中保留了 `module foo/bar` 而不是必需的 `module foo/bar/v2`。 Go 会将每一个此类标签视为无效,并在二进制文件的 `runtime/debug.BuildInfo.Main.Version` 中打上伪版本号(如 `v0.0.0--`)而不是该标签。漏洞扫描器(Trivy、Grype)随后无法根据 semver 匹配 CVE 修复版本,并对主模块报告所有已知 CVE 的误报——不是因为任何东西真的有漏洞,而是因为它们无法确认修复已存在。 如果您的上游遇到此问题,请在 `go build` 之前在您的 `Dockerfile.override` 中修补模块路径: 1. 对 `go.mod` 使用 `sed` 命令将 `/v2`(或 `/vN`)附加到 module 指令中 2. 对所有 `.go` 文件使用 `find | xargs sed` 重写仓库内的自身导入 3. 对补丁执行 `git commit` 并在生成的提交上 `git tag -f `——如果没有干净的代码树,Go 会回退到 `(devel)`;如果没有重新打标签,它会输出伪版本 工作流将上游标签作为 `--build-arg VERSION=` 传递给每次构建,因此您的覆盖可以直接引用它。具体示例请参见 `examples/gokapi/Dockerfile.override`。 经验法则:如果 `head -1 go.mod` 显示为 `module foo/bar` 并且上游发布了 v2.x.x+ 标签,则您需要此补丁。如果 `go.mod` 已经以匹配标签主版本号的 `/vN` 结尾,则直接覆盖即可,无需补丁。 ## 每次运行的产物 **成功路径:** - 镜像推送到 `ghcr.io//:` 和 `ghcr.io//@sha256:` - 在 `main` 分支上针对 `mirror-build/` 分支打开 Pull Request - PR 正文包含:摘要、Dockerfile 来源(`override` 或 `upstream`)、扫描摘要、产物链接 - CycloneDX SBOM(镜像 + fs)、Trivy SARIF(镜像 + fs)、差异审查 markdown、完整差异补丁——全部作为工作流产物保留 90 天 - Trivy 发现项上传到仓库的 Security 标签页,类别为 `trivy-image`、`trivy-fs` 和 `trivy-rescan` **受阻路径(发现 CVE):** - 不推送镜像 - 打开带有 `security`、`cve`、`blocked` 标签的 issue,链接到工作流运行和 Security 标签页 - 保留相同的产物以供审查 - 流水线失败,阻止下一次夜间构建,直到问题解决(出现修复后的上游版本,或者您在 `.trivyignore` 中添加了记录在案的豁免) ## 检测发布后公布的 CVE 主构建任务只在构建时扫描镜像——如果明天公布了一个针对昨天镜像中依赖项的新 CVE,主任务直到下一次上游发布更新受影响依赖项时才会察觉。 第二个计划任务 `rescan-last-build` 弥补了这一空白。它在相同的夜间计划上运行,拉取 `.last-built-tag` 中记录的最后一次构建的镜像,使用 Trivy 针对当前数据库重新扫描,并在出现新发现时自动提交 issue(标签为 `rescan`)。镜像本身永远不会被修改——这是信息提供,而非修复。 “等待上游” 还是 “通过 Dockerfile 覆盖进行补丁和重建” 的决定仍由人工做出。 如果您的 GHCR 包是私有的,请确保包设置明确链接了 mirror 仓库,否则重新扫描任务在拉取镜像时会因 403 错误而失败。 ## 配置参考 | 变量 | 必需 | 默认值 | 描述 | |---|---|---|---| | `UPSTREAM_REPO` | 是 | 空 | github.com 上的 `owner/repo` | | `IMAGE_NAME` | 是 | 空 | 包含注册表的完整镜像引用(小写),例如 `ghcr.io/my-org/name` | | `TRIVY_SEVERITY` | 否 | `CRITICAL,HIGH` | 阻止推送的逗号分隔 Trivy 严重级别 | 计划时间在工作流文件中硬编码为 UTC `0 3 * * *`。如果需要不同的频率,请进行更改。 ## 常见问题解答 **上游移动了 Dockerfile 或重命名了主包。** 相应地更新您的 `Dockerfile.override`。克隆步骤总是拉取完整的目录树,因此 `upstream-src/` 中的路径反映了上游目前的任何内容。 **上游将 Go(或 Python、Node)升级到了我的覆盖无法编译的版本。** 覆盖声明了自己的构建阶段基础镜像;上游的运行时版本是无关紧要的。只需更新您覆盖中的 `FROM :` 行。对于静态的 Go 二进制文件,这通常是一行更改。 **Trivy 标记了一个我无法处理的 CVE。** 在 `.trivyignore` 中添加一个条目,并附上注释解释原因: ``` # CVE-2025-12345 — upstream base 中的 libcurl。无法从我们的部署中触达 # (容器运行在 mTLS ingress 之后,无出站 HTTP)。于 2026-08 重新评估。 CVE-2025-12345 ``` Trivy 会读取 CVE ID 和注释,此文件的 Git 历史将成为您审计轨迹的一部分。 **`main` 分支上的分支保护规则阻止我作为唯一维护者合并自己的 PR。** 要么授予自己管理员覆盖权限,要么专门针对此仓库放宽“需要批准”规则。`mirror-build/*` 分支是由机器人创建的,因此“要求作者以外的人审查”在技术上并不会阻碍您——但许多团队会将其配置得更加严格。 **上游没有 GitHub releases 也没有 git 标签。** 流水线至少需要 git 标签。仅通过滚动 `main` 分支发布的超出了此模板的讨论范围——您将在没有明确审查单元的情况下镜像一个不断变动的目标。 **第一次成功的运行显示带有 `Initial build of ` 且没有差异内容。** 正常现象。没有之前的标签可供对比。从下一次成功构建开始,差异审查产物将显示版本之间的更改。 **我想调试正在运行的 pod,但我的 distroless 镜像中没有 shell。** 使用 `kubectl debug --image=busybox -it -- sh` 运行一个临时容器,或者在非生产环境中基于 `gcr.io/distroless/static:debug` 构建一个 `:debug` 变体。Distroless 故意不提供 shell,以减少运行时攻击面。 ## 固定 action 依赖 附带的工作流通过主要标签(`@v4`、`@v6`)引用 action,并通过固定发布版本引用 Trivy。在生产环境中使用时,请将每个 action 固定到 commit SHA。对于此模板,[Renovate](https://docs.renovatebot.com/) 比 Dependabot 更合适——它可以将 Action 更新分组到单个每周 PR 中,避免与此流水线生成的 `mirror-build/*` PR 发生冲突。参考配置以 `renovate.json.example` 的形式提供;fork 后将其复制为 `renovate.json`。Dependabot 也可以,但需要更多调整。 ## 镜像的许可证 上方的 Apache-2.0 许可证涵盖了此模板自身的内容——工作流 YAML、文档和参考覆盖。它**不**管辖您使用它构建的容器镜像。 Mirror 镜像继承了它们所包含的上游的许可证。示例: - 如果您镜像了一个 **AGPL-3.0** 的上游(例如 Gokapi),您发布的镜像是 AGPL-3.0 的。该镜像的运营者受 AGPL §13(网络使用)的约束——他们必须向网络用户提供源代码访问权限。 - 如果您镜像了一个 **GPL-2.0 GPL-3.0** 的上游,发布的镜像将相应地继承这些条款。 - 如果您镜像了一个 **MIT / Apache-2.0 / BSD** 的上游(例如 root-gg/plik 是 MIT),镜像则遵循这些宽松条款。 在将镜像发布到公共注册表之前,请验证上游的 `LICENSE` 文件,并在您的 fork 中添加一个 `NOTICE.md` 来声明镜像许可证并指向上游源代码。具体示例请参阅 [`THEKROLL-LTD/mirror-Gokapi`](https://github.com/THEKROLL-LTD/mirror-Gokapi) 的 `NOTICE.md`。 ## 许可证 Apache-2.0。随意 fork、修改和发布。 ## 相关 - **博客文章:** *[停止拉取随机的 Docker 镜像](https://medium.thekroll.ltd/stop-pulling-random-docker-images-c19e94559cc6)* —— 此模板实现的理论依据和背景 - **在线示例:** [`THEKROLL-LTD/mirror-gokapi`](https://github.com/THEKROLL-LTD/mirror-gokapi) —— 一个每夜构建并扫描 `Forceu/Gokapi` 的工作 fork。流水线运行、PR 被合并、镜像保存在 `ghcr.io/thekroll-ltd/gokapi`。无 SLA;仅供参考或自行承担风险拉取 ## 维护者 [THEKROLL](https://thekroll.ltd)。欢迎提交 Issue 和 Pull Request——尤其是针对 `examples/` 中尚未涵盖的上游项目的 Dockerfile 覆盖。
标签:Chainguard替代, CI/CD模板, CycloneDX, DevSecOps, distroless, Docker, GitHub Actions, SARIF, SBOM(软件物料清单), Web截图, 上游代理, 上游同步, 人工智能安全, 合规性, 夜间构建, 安全防御评估, 容器安全, 差异化审查, 开源软件(OSS), 自动笔记, 请求拦截, 镜像加固, 镜像构建