PontoPe/ProvenancePipeline
GitHub: PontoPe/ProvenancePipeline
一套端到端的软件供应链安全流水线,通过 SBOM、无密钥签名、SLSA 来源证明和 Kyverno 准入控制,在 Kubernetes 集群中强制拒绝未经签名或身份不匹配的镜像。
Stars: 0 | Forks: 0
# Provenance Pipeline
软件供应链流水线:构建 → SBOM → 漏洞扫描 → 无密钥签名 → SLSA 来源证明 → 拒绝任何未签名内容的准入控制。
## 拒绝策略

四个 pod,一条策略,设置为 `Enforce` 且 `failurePolicy: Fail`:
| 镜像 | 结果 |
|---|---|
| 我们的镜像,基于 digest,由发布工作流签名 | **准许**,运行中 |
| `busybox`,基于 digest —— 一个普通的未签名镜像 | **拒绝** |
| `kyverno`,基于 digest —— 确实已签名,确实在 Rekor 中,但**身份错误** | **拒绝** |
| 我们的镜像,使用**标签 (tag)** 而非 digest | **拒绝** |
第三行值得停下来深思。`reg.kyverno.io/kyverno/kyverno` 确实由 Sigstore 签名,其条目也确实存在于公开的透明度日志中。但它依然被拒绝了,因为该签名不是*我们的*。一条仅仅检查“这是否已签名”的策略本会准许它。
## 为什么
“我们扫描了我们的镜像”只是基本要求,在运行时根本证明不了什么。本仓库闭环了这一流程:集群本身会拒绝运行任何签名和来源无法通过预期身份验证的镜像。有趣的产物不是流水线——而是这种拒绝行为。
## 架构
```
flowchart LR
SRC["git push"] --> BUILD["GitHub Actions
docker build"] BUILD --> SBOM["syft
SBOM (SPDX + CycloneDX)"] SBOM --> SCAN["grype / trivy
fail on CRITICAL"] SCAN --> PUSH["push to registry (GHCR)"] PUSH --> SIGN["cosign sign
keyless / Fulcio + Rekor"] SIGN --> ATT["cosign attest
SLSA provenance + SBOM attestation"] ATT --> REG[("Registry (GHCR)
image + Sigstore bundles")] REG --> ADM["Kyverno admission
verifyImages"] ADM -->|identity + issuer match| OK["Pod scheduled"] ADM -->|unsigned / wrong identity| DENY["AdmissionReview denied"] SIGN -.transparency log.-> REKOR[("Rekor")] ``` 详情:[docs/architecture.md](docs/architecture.md)。 ## 威胁模型 完整版本见 [docs/threat-model.md](docs/threat-model.md)。 | # | 威胁 | 控制 | |---|--------|---------| | T1 | 攻击者向 registry 推送恶意镜像 | 准入控制要求签名必须绑定到本仓库的 OIDC 身份;仅拥有 registry 写入权限无法伪造 | | T2 | 基础镜像受损 / 存在漏洞的依赖 | 每次构建生成 SBOM + `grype` 门禁;当新的 CVE 出现时,SBOM 证明允许我们重新查询旧镜像 | | T3 | 构建系统篡改 (注入步骤) | SLSA 来源证明记录了构建器 ID、源 ref 和材料;策略断言使用的是预期的构建器 | | T4 | 签名密钥被盗 | 无密钥签名 —— 临时 Fulcio 证书,没有可被盗用的长期密钥 | | T5 | 签名被剥离 / 在不同的镜像上重放 | 签名针对的是镜像 digest;策略固定了 digest,不信任标签 | | T6 | 通过被豁免准入控制的 namespace 绕过策略 | Kyverno 策略设置为 `Enforce` 且 `failurePolicy: Fail`;排除项已被列举并在行内进行了论证。后台扫描已关闭 —— 镜像验证需要 registry 交互,仅在准入时运行 | | T7 | 隐蔽签名 (没人注意到恶意签名) | Rekor 透明度日志;来源查询已记录在操作手册中。目前尚无自动化监控 | | T8 | 准入 webhook 不可用 → 故障开放 (fail-open) | `failurePolicy: Fail`。API server 会拒绝该 pod,而不是在未检查的情况下准入 | | T9 | 唯一可强制执行的证明停止发布 | **无控制措施。** 每个镜像的四个 Sigstore 包中,Kyverno 只能发现 GitHub 的那个,且该步骤受限于仓库必须公开。见 [B5](docs/BLOCKED.md) | ## 布局 ``` app/ # minimal demo service being built and signed (built) build/ # Dockerfile, build config (built) docs/evidence/ # captured output, generated not transcribed (built) policies/kyverno/ # verifyImages ClusterPolicy (Enforce) (built) cluster/bootstrap/ # Kyverno install + its NetworkPolicy (built) scripts/ # demo.sh + evidence collection (built) tests/ # policy tests, allow and deny paths (built) ``` ## 目前已验证的内容 每次推送到 `main` 都会运行 [`release.yml`](.github/workflows/release.yml),该工作流会构建镜像、使用 `syft` 生成 SBOM、在 `grype` 发现任何 CRITICAL 级别的结果时使构建失败、使用 `cosign` 对 digest 进行无密钥签名,并附加两项证明 —— SBOM 和 SLSA v1 来源断言。随后,一个独立的 `verify` 任务会从头开始重新检查所有内容,并同时固定证书身份和 OIDC 签发者。 该任务还运行了一个**阴性对照**:对一个从未被签名过的 digest 执行 `cosign verify`,如果验证成功,则使构建失败。如果没有这一步,一次绿色的运行只能证明理想路径,而一个从不说“不”的验证器根本算不上验证器。 真实的捕获输出,包含 Rekor 条目: [docs/evidence/supply-chain-verification.md](docs/evidence/supply-chain-verification.md)。 ``` make verify ``` GHCR 包是公开的,因此不需要任何凭证 —— 这是从一个从未登录过任何 registry 的集群节点上验证过的。 在集群端,Kyverno v1.18.2 在准入时运行相同的信任决策。该策略**同时**固定了证书身份和 OIDC 签发者;我们依次破坏了它们,每次都让原本准许的 pod 变成被拒绝,这是验证一项固定策略是否具有实际约束力(而非仅仅是装饰)的唯一方法。 ``` make policy-install # Kyverno, upstream sha256-enforced, images digest-pinned make policy-test # 9 assertions: 2 admit, 2 deny, 1 must-not-match make demo # the recording above ``` ## 未验证的内容 - **来源是安全的。** 来源证明说明了产物的出处,但绝不会说明出自该处的产物是安全的。对应威胁模型 T3,且没有任何 SLSA 等级能改变这一点。 - **每一个 namespace。** 拒绝所有未签名镜像的规则需要通过 namespace 标签选择性启用,因为这个集群属于 [KateClusters](../KateClusters) 并且运行着来自其他流水线的工作负载,集群范围内的规则会在它们下次重启时导致其崩溃。`kube-system`、`kyverno`、`calico-system` 和 `tigera-operator` 被排除在另一条规则之外,并且每一项排除都在策略文件中进行了论证。 - **独立于 GitHub 的证明步骤。** 附加在每个镜像上的四个 Sigstore 包中,Kyverno 只能发现由 `actions/attest-build-provenance` 写入的那一个,且该步骤受限于仓库必须公开。如果将仓库设为私有,强制执行策略将拒绝一切。这是设计中最大的开放性弱点 —— ADR-009 和 [B5](docs/BLOCKED.md)。 - **MEDIUM 和 HIGH 漏洞随镜像发布。** 漏洞门禁仅在 CRITICAL 时失效。对应 T2。 - **Rekor 监控。** T7 背后没有任何自动化机制。 ## 路线图 - [x] `app/` + `build/Dockerfile` —— 小型、可重现的演示服务 - [x] CI:构建 → `syft` SBOM → `grype` 门禁 - [x] CI:`cosign sign` 无密钥签名 + `cosign attest` (SLSA 来源,SBOM) - [x] `verify` 任务 + 捕获的证据,包含阴性对照 - [x] `cluster/bootstrap` —— Kyverno 安装 (目标为 KateClusters 节点) - [x] `policies/kyverno` —— 包含签发者和主体固定的 `verifyImages`,Enforce 模式 - [x] `tests/` —— 针对准许和拒绝路径的策略测试 - [x] 异常路径演示 + GIF - [x] 总结:实际达到了什么 SLSA 等级,以及哪些没有达到 - [ ] 移除对仓库可见性的依赖 (B5) - [ ] 对已发布的 digest 进行定时重新扫描,以便在无需重新构建的情况下暴露出已发布镜像的新 CVE ## 客观评价 SLSA 等级 构建等级为 L2,而非 L3,且这种差距绝非细节问题。 L2 要求一个托管的构建平台生成消费者可以验证的已签名来源。这一点已满足:GitHub 托管的 runner,来源通过 Fulcio 使用工作流自身的 OIDC 身份进行签名,条目存在于公开的 Rekor 中,可以通过 `cosign verify-attestation` 针对固定的身份和签发者进行验证。 L3 进一步要求该来源*无法被构建过程本身伪造* —— 必须生成于工作流自身步骤无法访问的隔离环境中。在这里,该断言是由构建镜像的同一任务中的一个步骤组装的,因此对该任务的任何破坏都可以随意写入它想要的任何来源。达到 L3 意味着要委托给受信任的生成器,例如 `slsa-framework/slsa-github-generator`。 [威胁模型](docs/threat-model.md) 在 T3 中陈述了同样的限制:来源证明了产物的*出处*,但绝不证明源码是安全的。 **集群层面的控制对该声明的补充,以及它未能做到的事。** SLSA 等级描述了产物是如何*生产*的;L1–L4 中没有任何内容涉及是否有人在运行它之前检查了来源证明。在准入时对其进行强制执行并不会提升等级 —— 构建仍然是 L2 —— 但正是这一步让该等级在运维上具有了实际意义。一个无人验证的已签名 L3 产物,其价值还不如一个经过验证的 L2 产物。因此:**构建等级为 L2,在准入时经过验证。** 这句话的两部分都起到了实际约束作用,且任何一部分的价值都不会超过其字面意思。 ## 接手此项目 [docs/PROVEhandoff.md](docs/PROVEhandoff.md) —— 当前状态、按顺序排列的下一步行动、阻碍问题及其确切的修复方案,以及明确陈述的局限性。 [docs/HANDBOOK.md](docs/HANDBOOK.md) —— CI 部分的构建过程,从头到尾的完整记录。 [docs/PROGRESS.md](docs/PROGRESS.md) —— 集群端已完成并验证的工作,并记录了那些走不通的死胡同,以免重蹈覆辙。 [docs/demo-recording.md](docs/demo-recording.md) —— 上面的 GIF 是如何制作的,编写初衷是为了让相关联的兄弟仓库也能复用。 ## 相关链接 运行在 [KateClusters](../KateClusters) 的集群上。镜像在那里被消费;而策略存放在这里。
docker build"] BUILD --> SBOM["syft
SBOM (SPDX + CycloneDX)"] SBOM --> SCAN["grype / trivy
fail on CRITICAL"] SCAN --> PUSH["push to registry (GHCR)"] PUSH --> SIGN["cosign sign
keyless / Fulcio + Rekor"] SIGN --> ATT["cosign attest
SLSA provenance + SBOM attestation"] ATT --> REG[("Registry (GHCR)
image + Sigstore bundles")] REG --> ADM["Kyverno admission
verifyImages"] ADM -->|identity + issuer match| OK["Pod scheduled"] ADM -->|unsigned / wrong identity| DENY["AdmissionReview denied"] SIGN -.transparency log.-> REKOR[("Rekor")] ``` 详情:[docs/architecture.md](docs/architecture.md)。 ## 威胁模型 完整版本见 [docs/threat-model.md](docs/threat-model.md)。 | # | 威胁 | 控制 | |---|--------|---------| | T1 | 攻击者向 registry 推送恶意镜像 | 准入控制要求签名必须绑定到本仓库的 OIDC 身份;仅拥有 registry 写入权限无法伪造 | | T2 | 基础镜像受损 / 存在漏洞的依赖 | 每次构建生成 SBOM + `grype` 门禁;当新的 CVE 出现时,SBOM 证明允许我们重新查询旧镜像 | | T3 | 构建系统篡改 (注入步骤) | SLSA 来源证明记录了构建器 ID、源 ref 和材料;策略断言使用的是预期的构建器 | | T4 | 签名密钥被盗 | 无密钥签名 —— 临时 Fulcio 证书,没有可被盗用的长期密钥 | | T5 | 签名被剥离 / 在不同的镜像上重放 | 签名针对的是镜像 digest;策略固定了 digest,不信任标签 | | T6 | 通过被豁免准入控制的 namespace 绕过策略 | Kyverno 策略设置为 `Enforce` 且 `failurePolicy: Fail`;排除项已被列举并在行内进行了论证。后台扫描已关闭 —— 镜像验证需要 registry 交互,仅在准入时运行 | | T7 | 隐蔽签名 (没人注意到恶意签名) | Rekor 透明度日志;来源查询已记录在操作手册中。目前尚无自动化监控 | | T8 | 准入 webhook 不可用 → 故障开放 (fail-open) | `failurePolicy: Fail`。API server 会拒绝该 pod,而不是在未检查的情况下准入 | | T9 | 唯一可强制执行的证明停止发布 | **无控制措施。** 每个镜像的四个 Sigstore 包中,Kyverno 只能发现 GitHub 的那个,且该步骤受限于仓库必须公开。见 [B5](docs/BLOCKED.md) | ## 布局 ``` app/ # minimal demo service being built and signed (built) build/ # Dockerfile, build config (built) docs/evidence/ # captured output, generated not transcribed (built) policies/kyverno/ # verifyImages ClusterPolicy (Enforce) (built) cluster/bootstrap/ # Kyverno install + its NetworkPolicy (built) scripts/ # demo.sh + evidence collection (built) tests/ # policy tests, allow and deny paths (built) ``` ## 目前已验证的内容 每次推送到 `main` 都会运行 [`release.yml`](.github/workflows/release.yml),该工作流会构建镜像、使用 `syft` 生成 SBOM、在 `grype` 发现任何 CRITICAL 级别的结果时使构建失败、使用 `cosign` 对 digest 进行无密钥签名,并附加两项证明 —— SBOM 和 SLSA v1 来源断言。随后,一个独立的 `verify` 任务会从头开始重新检查所有内容,并同时固定证书身份和 OIDC 签发者。 该任务还运行了一个**阴性对照**:对一个从未被签名过的 digest 执行 `cosign verify`,如果验证成功,则使构建失败。如果没有这一步,一次绿色的运行只能证明理想路径,而一个从不说“不”的验证器根本算不上验证器。 真实的捕获输出,包含 Rekor 条目: [docs/evidence/supply-chain-verification.md](docs/evidence/supply-chain-verification.md)。 ``` make verify ``` GHCR 包是公开的,因此不需要任何凭证 —— 这是从一个从未登录过任何 registry 的集群节点上验证过的。 在集群端,Kyverno v1.18.2 在准入时运行相同的信任决策。该策略**同时**固定了证书身份和 OIDC 签发者;我们依次破坏了它们,每次都让原本准许的 pod 变成被拒绝,这是验证一项固定策略是否具有实际约束力(而非仅仅是装饰)的唯一方法。 ``` make policy-install # Kyverno, upstream sha256-enforced, images digest-pinned make policy-test # 9 assertions: 2 admit, 2 deny, 1 must-not-match make demo # the recording above ``` ## 未验证的内容 - **来源是安全的。** 来源证明说明了产物的出处,但绝不会说明出自该处的产物是安全的。对应威胁模型 T3,且没有任何 SLSA 等级能改变这一点。 - **每一个 namespace。** 拒绝所有未签名镜像的规则需要通过 namespace 标签选择性启用,因为这个集群属于 [KateClusters](../KateClusters) 并且运行着来自其他流水线的工作负载,集群范围内的规则会在它们下次重启时导致其崩溃。`kube-system`、`kyverno`、`calico-system` 和 `tigera-operator` 被排除在另一条规则之外,并且每一项排除都在策略文件中进行了论证。 - **独立于 GitHub 的证明步骤。** 附加在每个镜像上的四个 Sigstore 包中,Kyverno 只能发现由 `actions/attest-build-provenance` 写入的那一个,且该步骤受限于仓库必须公开。如果将仓库设为私有,强制执行策略将拒绝一切。这是设计中最大的开放性弱点 —— ADR-009 和 [B5](docs/BLOCKED.md)。 - **MEDIUM 和 HIGH 漏洞随镜像发布。** 漏洞门禁仅在 CRITICAL 时失效。对应 T2。 - **Rekor 监控。** T7 背后没有任何自动化机制。 ## 路线图 - [x] `app/` + `build/Dockerfile` —— 小型、可重现的演示服务 - [x] CI:构建 → `syft` SBOM → `grype` 门禁 - [x] CI:`cosign sign` 无密钥签名 + `cosign attest` (SLSA 来源,SBOM) - [x] `verify` 任务 + 捕获的证据,包含阴性对照 - [x] `cluster/bootstrap` —— Kyverno 安装 (目标为 KateClusters 节点) - [x] `policies/kyverno` —— 包含签发者和主体固定的 `verifyImages`,Enforce 模式 - [x] `tests/` —— 针对准许和拒绝路径的策略测试 - [x] 异常路径演示 + GIF - [x] 总结:实际达到了什么 SLSA 等级,以及哪些没有达到 - [ ] 移除对仓库可见性的依赖 (B5) - [ ] 对已发布的 digest 进行定时重新扫描,以便在无需重新构建的情况下暴露出已发布镜像的新 CVE ## 客观评价 SLSA 等级 构建等级为 L2,而非 L3,且这种差距绝非细节问题。 L2 要求一个托管的构建平台生成消费者可以验证的已签名来源。这一点已满足:GitHub 托管的 runner,来源通过 Fulcio 使用工作流自身的 OIDC 身份进行签名,条目存在于公开的 Rekor 中,可以通过 `cosign verify-attestation` 针对固定的身份和签发者进行验证。 L3 进一步要求该来源*无法被构建过程本身伪造* —— 必须生成于工作流自身步骤无法访问的隔离环境中。在这里,该断言是由构建镜像的同一任务中的一个步骤组装的,因此对该任务的任何破坏都可以随意写入它想要的任何来源。达到 L3 意味着要委托给受信任的生成器,例如 `slsa-framework/slsa-github-generator`。 [威胁模型](docs/threat-model.md) 在 T3 中陈述了同样的限制:来源证明了产物的*出处*,但绝不证明源码是安全的。 **集群层面的控制对该声明的补充,以及它未能做到的事。** SLSA 等级描述了产物是如何*生产*的;L1–L4 中没有任何内容涉及是否有人在运行它之前检查了来源证明。在准入时对其进行强制执行并不会提升等级 —— 构建仍然是 L2 —— 但正是这一步让该等级在运维上具有了实际意义。一个无人验证的已签名 L3 产物,其价值还不如一个经过验证的 L2 产物。因此:**构建等级为 L2,在准入时经过验证。** 这句话的两部分都起到了实际约束作用,且任何一部分的价值都不会超过其字面意思。 ## 接手此项目 [docs/PROVEhandoff.md](docs/PROVEhandoff.md) —— 当前状态、按顺序排列的下一步行动、阻碍问题及其确切的修复方案,以及明确陈述的局限性。 [docs/HANDBOOK.md](docs/HANDBOOK.md) —— CI 部分的构建过程,从头到尾的完整记录。 [docs/PROGRESS.md](docs/PROGRESS.md) —— 集群端已完成并验证的工作,并记录了那些走不通的死胡同,以免重蹈覆辙。 [docs/demo-recording.md](docs/demo-recording.md) —— 上面的 GIF 是如何制作的,编写初衷是为了让相关联的兄弟仓库也能复用。 ## 相关链接 运行在 [KateClusters](../KateClusters) 的集群上。镜像在那里被消费;而策略存放在这里。
标签:DevSecOps, Kubernetes 准入控制, SBOM, 上游代理, 子域名突变, 硬件无关, 请求拦截, 软件供应链安全, 远程方法调用, 镜像签名