| | @@ -0,0 +1,87 @@ |
| + | # CI/CD Supply Chain Security |
| + | |
| + | This picks up where the [secure CI/CD pipeline](../secure-cicd-pipeline) leaves off. |
| + | That repo proves the *source* is clean. This one proves the *artifact* is - that the |
| + | container image a cluster is about to run was built by my pipeline, hasn't been |
| + | tampered with since, and ships with a verifiable bill of materials. |
| + | |
| + | The mechanism is Sigstore. The pipeline signs every image keylessly with Cosign |
| + | using the workflow's OIDC identity, attaches an SBOM as a signed attestation, and a |
| + | Kyverno policy refuses to admit anything to the cluster that can't produce both. |
| + | |
| + | ```mermaid |
| + | flowchart LR |
| + | build[build image] --> sbom[syft SBOM] |
| + | sbom --> scan[grype scan<br/>fail on high] |
| + | scan --> sign[cosign sign<br/>keyless OIDC] |
| + | sign --> attest[cosign attest<br/>SBOM] |
| + | attest --> verify[cosign verify<br/>in-pipeline] |
| + | verify --> admit[Kyverno admission<br/>at deploy time] |
| + | ``` |
| + | |
| + | ## Keyless signing |
| + | |
| + | There is no private key to manage or leak. Cosign requests a short-lived |
| + | certificate from Fulcio bound to the GitHub Actions OIDC identity |
| + | (`https://github.com/<owner>/<repo>`), signs, and logs the signature to the Rekor |
| + | transparency log. Verification checks the certificate identity and issuer rather |
| + | than a key you have to rotate. |
| + | |
| + | That is why the workflow asks for `id-token: write` - without it there's no OIDC |
| + | token to exchange for the signing certificate. |
| + | |
| + | Cosign signing and verification, and what happens the moment a signed artifact is |
| + | modified - the signature is rejected: |
| + | |
| + |  |
| + | |
| + | > The screenshot uses a local key pair to show the sign/verify mechanics offline; |
| + | > the pipeline itself signs keylessly with the GitHub OIDC identity (no stored key). |
| + | |
| + | ## What the pipeline does |
| + | |
| + | | Stage | Tool | Output | |
| + | |-------|------|--------| |
| + | | build | buildx + build-push-action | image pushed to GHCR by digest, with provenance + SBOM attestations | |
| + | | scan | syft + grype | SPDX SBOM artifact; build fails on a high/critical CVE | |
| + | | sign | cosign | keyless signature in Rekor | |
| + | | attest | cosign | signed SPDX attestation attached to the image | |
| + | | verify | cosign | signature + attestation checked before the run is called good | |
| + | |
| + | Everything operates on the image **digest**, never a mutable tag, so the thing that |
| + | gets signed and verified is exactly the thing that was built. |
| + | |
| + | ## Enforcing it at deploy time |
| + | |
| + | `policy/kyverno-verify-images.yaml` is a `ClusterPolicy` in `Enforce` mode that: |
| + | |
| + | 1. requires a Cosign signature from this repo's GitHub identity, and |
| + | 2. requires an SPDX SBOM attestation from the same identity, |
| + | |
| + | for any image under `ghcr.io/zionboggan/*`. It also rewrites tags to digests on |
| + | admission (`mutateDigest: true`) so a pod can't be pinned to a tag that later moves. |
| + | |
| + | Apply it: |
| + | |
| + | ```bash |
| + | kubectl apply -f policy/kyverno-verify-images.yaml |
| + | ``` |
| + | |
| + | A signed image from the pipeline is admitted; an arbitrary `nginx:latest` is |
| + | rejected. `policy/test/pods.yaml` has one of each for `kyverno apply` to test |
| + | against, which is what the `admission-policy-check` workflow runs on PRs that touch |
| + | the policy. |
| + | |
| + | ## Verify an image by hand |
| + | |
| + | ```bash |
| + | OWNER=zionboggan ./policy/verify.sh ghcr.io/zionboggan/cicd-supply-chain-security@sha256:<digest> |
| + | ``` |
| + | |
| + | ## Notes |
| + | |
| + | Keyless is the right default for CI - nothing to store, everything in the |
| + | transparency log. If you needed an air-gapped or offline verifier you'd switch to a |
| + | key pair (or a KMS key) and Cosign supports that, but for a GitHub-hosted pipeline |
| + | the OIDC identity is the cleaner trust anchor. More in |
| + | [docs/supply-chain.md](docs/supply-chain.md). |