alice101-dev/supply-chain-secure-build

GitHub: alice101-dev/supply-chain-secure-build

该项目提供了一套从 SAST/SCA 扫描到镜像签名、SLSA 出处证明再到 Kubernetes 准入验证的完整容器供应链安全构建流水线参考实现。

Stars: 1 | Forks: 0

# 容器供应链安全 — 构建、证明、验证 [![CI](https://static.pigsec.cn/wp-content/uploads/repos/cas/ad/ad5834178f7599af9fdda11629d49cae07f2997beec49821b2920eff5bfd50e7.svg)](https://github.com/alice101-dev/supply-chain-secure-build/actions/workflows/ci.yml) ## 为什么这很重要 现代入侵越来越倾向于绕过你的防火墙,直接通过**构建流水线**潜入。攻击并不一定来自外部: - **恶意的内部人员**(或一台被盗的笔记本电脑 / CI token)构建了一个带后门的镜像,并直接 `kubectl apply` 到生产环境中 —— 没有审查、没有扫描、也没有二进制文件来源的痕迹。 - **被投毒的依赖项** —— 一次针对拼写错误或被入侵包的 `go get`(xz-utils / event-stream / SolarWinds 模式),后门就会*由你自己的 CI* 编译到你的二进制文件中,而且没有任何人签字确认。 - **流氓镜像** —— 被重新打标签、被篡改,或者从未经审查的 registry 拉取 —— 之所以能在集群中运行,是因为 Kubernetes 默认**会运行任何被指示运行的镜像**。`image: attacker/nginx:latest` 会和你的镜像一样顺利地被调度。 共同点在于:如果没有出处、签名和准入时的验证,集群就无法区分*你的*构建和攻击者的构建。本仓库端到端地弥补了这一漏洞 —— 为每个镜像提供 **SBOM**,在发布前进行**漏洞把关**,使用**无密钥签名**将镜像与*本仓库的 CI 工作流*进行加密绑定(内部人员无法从笔记本电脑上复现它),**SLSA provenance** 准确记录是哪个 commit 和 runner 构建了它,以及 **Kyverno 策略**让 Kubernetes 拒绝任何未签名、未扫描或在其他地方构建的镜像。 ``` graph LR C["commit / PR"] --> G["🔎 SAST
(Semgrep CE)"] G --> GV["🔗 SCA
(govulncheck)"] GV --> SK["🧩 Socket
dep risk (PR)"] SK --> B["🔨 build
(distroless, digest-pinned)"] B --> T["🛡️ Trivy gate
CRITICAL/HIGH ⇒ fail"] T --> S["📋 SBOM
(Syft, SPDX)"] S --> P["📦 push to GHCR"] P --> K["🔏 Cosign sign
keyless via OIDC"] K --> A["📜 attest
SBOM + SLSA provenance"] A --> R["🧾 Rekor
transparency log"] A --> D["☸️ deploy"] V["Kyverno verifyImages"] -->|signature valid| D V -.rejected.-> U["❌ unsigned /
foreign image"] ``` ## 流水线强制执行的内容 | 阶段 | 工具 | 保障 | | --- | --- | --- | | SAST | Semgrep CE (`p/golang`, `p/gosec`, `p/cwe-top-25`) | 在编译任何内容之前,不安全的代码模式会导致构建失败 | | SCA | govulncheck | 感知调用图:如果代码**实际触及**的依赖项和 stdlib 中存在漏洞,将导致构建失败 | | 供应链风险 | Socket | 对 PR 上的依赖代码进行行为分析(恶意软件、安装脚本、废弃包)—— 这是 CVE 数据库常常漏掉的零日风险;拦截由策略驱动 | | 密钥扫描 | Gitleaks | 每次运行都会扫描完整的 git 历史 —— 泄露的密钥会导致构建失败,即使它曾被提交随后又被删除 | | IaC 扫描 | Checkov | Dockerfile 和 Kubernetes manifest 配置不当(root 用户、缺少资源限制、可变 tag)会导致构建失败 | | 构建 | Docker 多阶段构建 → distroless/static | 无 shell,无包管理器,约 2 MB 的攻击面;基础镜像通过 digest 锁定 | | 漏洞把关 | Trivy | 存在可用修复方案的 CRITICAL/HIGH 漏洞 ⇒ 该镜像**永远不会被发布** | | 清单 | Syft | 生成 SPDX SBOM 并作为已签名的证明附加到镜像中 | | 签名 | Cosign **无密钥** | GitHub OIDC 证明了*是哪个仓库和工作流*构建了它;Fulcio 颁发短期证书;签名被记录在 Rekor 中。**无需存储、轮换或泄露任何密钥** | | 出处 | GitHub Attestations (SLSA) | 记录生成该镜像的确切 commit、工作流和 runner 的已签名声明 | | 准入 | Kyverno `verifyImages` | 集群**失败即关闭 (fail closed)**:只有由本仓库 CI 签名的镜像才能被调度;tag 会被修改为已验证的 digest | 该流水线还保护了**它自己**:每个第三方 action 都被锁定到完整的 commit SHA(并在注释中注明版本),因此被劫持的 action tag —— 例如 `tj-actions/changed-files` 攻击模式 —— 无法将代码注入到此构建中。这与通过 digest 锁定基础镜像的原则相同。 在 **PR** 上,除了发布/签名/证明外,上述所有把关环节都会运行 *except* —— 镜像会被构建和扫描,但永远不会被推送。签名、证明和推送到 registry 仅在 commit 合并到 `main` 分支时发生,因此任何未签名或未验证的内容都不会进入 GHCR。(Go 的 build/vet/test 在两者上都会运行。) ### 每次 PR 的供应链风险 (Socket) 当 PR 添加或更改依赖项时,Socket 会分析该包的实际代码,并在 PR 上发布风险评分卡。发现的问题是否会阻断是由 Socket **安全策略**(`error` 导致检查失败,`warn` 仅报告)设定的 —— 而不是在工作流中。下图:一个测试 PR 添加了故意设置的危险依赖项,一旦策略设置为 `error`,就会因 `High CVE` 警报而被拦截: ![Socket 在带有 High CVE 警报的 pull request 上标记有风险的依赖项](https://static.pigsec.cn/wp-content/uploads/repos/cas/8d/8d5f56fd154470a827a9adc23cf4ceacf340c47c9e14f5b0664725738945ee95.png) ## 自行验证 任何人都可以验证该镜像 —— 这正是无密钥签名 + 透明度日志的意义所在: ``` IMAGE=ghcr.io/alice101-dev/supply-chain-secure-build:latest # Signature: 它是由这个 repo 的 workflow 构建的吗? cosign verify \ --certificate-identity-regexp '^https://github.com/alice101-dev/supply-chain-secure-build/\.github/workflows/.*' \ --certificate-oidc-issuer https://token.actions.githubusercontent.com \ "$IMAGE" # SBOM: 里面究竟是什么? cosign verify-attestation --type spdxjson \ --certificate-identity-regexp '^https://github.com/alice101-dev/supply-chain-secure-build/\.github/workflows/.*' \ --certificate-oidc-issuer https://token.actions.githubusercontent.com \ "$IMAGE" | jq -r '.payload' | base64 -d | jq '.predicate.packages[].name' # Provenance: 哪个 commit,哪个 workflow,哪个 runner? gh attestation verify oci://$IMAGE --repo alice101-dev/supply-chain-secure-build ``` 成功后的效果(输出已简略): | 命令 | 成功结果中的关键行 | 证明了什么 | | --- | --- | --- | | `cosign verify` | `The cosign claims were validated` · `Existence of the claims in the transparency log was verified offline` · `The code-signing certificate was verified using trusted certificate authority certificates` | 签名是真实的,已公开记录在 Rekor 中,并链接到 Sigstore 的 CA —— 而不是自我声明的 | | `cosign verify-attestation` | 包含 `"type": "https://spdx.dev/Document"` 和 `"https://slsa.dev/provenance/v1"` 的 JSON 条目,全部绑定到**同一个** `docker-manifest-digest` | SBOM 和 provenance 附加在*正是这个镜像*上,逐字节对应 —— 替换镜像后,digest 将不再匹配 | | `gh attestation verify` | `✓ Verification succeeded!` · `Build repo: alice101-dev/supply-chain-secure-build` · `Build workflow: .github/workflows/ci.yml@refs/heads/main` | 该镜像是由*本仓库的* CI 在 `main` 分支上构建的 —— 不是在某个人的笔记本电脑上,也不是由 fork 构建的 | 如果任何检查失败 —— 错误的仓库、被篡改的层、未签名的重新构建 —— 命令将以非零状态退出,而正是这种相同的失败会导致 Kyverno 在准入时拒绝该 pod。 ## 在集群中强制执行 ``` # 要求在 cluster 中安装 Kyverno (https://kyverno.io) kubectl apply -f k8s/kyverno-verify-image-signature.yaml # 这样可以正常部署 — 该 image 由这个 repo 的 CI 签名: kubectl apply -f k8s/deployment.yaml # 这在 admission 时会被拒绝 — 未签名的 image: kubectl run bad --image=nginx:latest ``` 该策略**失败即关闭 (fail closed)**(`failurePolicy: Fail`)并将 tag 重写为已验证的 digest(`mutateDigest`),因此即使是 `:latest` 的部署也是可复现的。 ## 仓库布局 ``` . ├── .github/workflows/ci.yml # SAST · SCA · Socket · Gitleaks · Checkov · build→scan→SBOM→sign→attest→verify ├── Dockerfile # multi-stage, distroless, digest-pinned, version-stamped ├── cmd/server/main.go # entrypoint: wiring + build identity ├── internal/ │ ├── config/ # env-based config (twelve-factor) │ ├── handler/ # routes, probes, request logging (+ unit tests) │ └── server/ # hardened timeouts, graceful shutdown └── k8s/ ├── kyverno-verify-image-signature.yaml # admission: only OUR signatures pass └── deployment.yaml # hardened consumer (backend-api) ``` ## 服务本身 这不是一个单文件的 hello-world —— 而是一个生产级的 Go 后端: - **结构化的 JSON 日志** (`log/slog`),带有按请求记录的功能,并跳过探测端点。 - **强化的 `http.Server` 超时设置** (read/write/idle/header) —— 单个缓慢的客户端无法占用连接。 - **优雅关闭**:在收到 SIGTERM 时,`/readyz` 会翻转为 503,以便 Kubernetes *首先*排空流量,然后在进行中的请求会在 `SHUTDOWN_TIMEOUT` 内完成。 - **`/version` 报告确切的 commit**,该 commit 是在构建时通过 `-ldflags` 盖上的 —— 这与镜像的 SLSA provenance 所证明的 commit 相同,从而实现运行时身份与供应链证据的一致。 ## 相关项目 - [terraform-pr-gates](https://github.com/alice101-dev/terraform-pr-gates) — 将同样的安全左移哲学应用于 Terraform PR。 - [gke-pgbouncer-hardened](https://github.com/alice101-dev/gke-pgbouncer-hardened) — 此流水线生成的镜像在运行时加固方面的对应实现。
标签:DevSecOps, EVTX分析, LLM防护, Web截图, 上游代理, 子域名突变, 容器安全, 日志审计, 请求拦截, 软件供应链安全, 远程方法调用, 错误基检测, 静态代码分析