supply-chain.yml 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
  1. # Supply-chain gate (task 46). Coordinates with task 42's `ci.yml`.
  2. #
  3. # Per-PR legs: `manifest-lint` (kustomize build + scripts/check_manifests.py,
  4. # including the L7-vs-L4 mTLS pairing check).
  5. # Per-push-to-master legs: multi-arch `docker buildx bake` with SBOM +
  6. # provenance, trivy/grype gating HIGH/CRITICAL, keyless cosign signing, and
  7. # digest pinning of `deploy/k8s/kustomization.yaml` (strict lint re-run).
  8. # Scheduled legs (weekly rebuild, Monday 04:00 UTC): rebuild + rescan + resign
  9. # so base-image digests advance even without code changes.
  10. #
  11. # The L7 mTLS constraint is DOCUMENTED (docs/DEPLOYMENT.md) and lint-checked
  12. # here; it is explicitly NOT exercised end-to-end in CI.
  13. name: supply-chain
  14. on:
  15. push:
  16. branches: ["master"]
  17. pull_request: {}
  18. schedule:
  19. # Weekly rebuild (Monday 04:00 UTC).
  20. - cron: "0 4 * * 1"
  21. workflow_dispatch: {}
  22. permissions:
  23. contents: read
  24. env:
  25. REGISTRY_IMAGE: ghcr.io/d-oauth/d-oauth-server
  26. SQLX_OFFLINE: "true"
  27. jobs:
  28. manifest-lint:
  29. name: manifest-lint
  30. runs-on: ubuntu-24.04
  31. steps:
  32. - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
  33. - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
  34. with:
  35. python-version: "3.x"
  36. - name: Install pyyaml + kustomize
  37. run: |
  38. pip install pyyaml
  39. curl -sSfL https://github.com/kubernetes-sigs/kustomize/releases/download/kustomize%2Fv5.4.3/kustomize_v5.4.3_linux_amd64.tar.gz \
  40. | tar -xzf - kustomize && sudo mv kustomize /usr/local/bin/
  41. - name: Kustomize build
  42. run: kustomize build deploy/k8s
  43. - name: Manifest policy lint (probes, PDB, no-baked-secrets, L7-vs-L4 mTLS)
  44. run: python3 scripts/check_manifests.py
  45. build:
  46. name: build
  47. if: github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
  48. runs-on: ubuntu-24.04
  49. permissions:
  50. contents: read
  51. packages: write
  52. id-token: write
  53. outputs:
  54. digest: ${{ steps.bake.outputs.digest }}
  55. steps:
  56. - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
  57. - uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3
  58. - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
  59. - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
  60. with:
  61. registry: ghcr.io
  62. username: ${{ github.actor }}
  63. password: ${{ secrets.GITHUB_TOKEN }}
  64. - name: Multi-arch bake with SBOM + provenance
  65. id: bake
  66. run: |
  67. TAG="sha-${GITHUB_SHA::12}"
  68. echo "tag=${TAG}" >> "${GITHUB_OUTPUT}"
  69. docker buildx bake image \
  70. --set "image.tags=${REGISTRY_IMAGE}:${TAG}" \
  71. --set image.output="type=registry" \
  72. --sbom=true \
  73. --provenance=mode=max \
  74. --metadata-file /tmp/bake-metadata.json
  75. python3 -c "
  76. import json
  77. meta = json.load(open('/tmp/bake-metadata.json'))
  78. node = meta['image']['containerimage.digest']
  79. open('${GITHUB_OUTPUT}', 'a').write(f'digest={node}\n')
  80. "
  81. - name: SBOM (syft, committed as artifact)
  82. uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0
  83. with:
  84. image: ${{ env.REGISTRY_IMAGE }}@${{ steps.bake.outputs.digest }}
  85. artifact-name: sbom.spdx.json
  86. - name: Scan (trivy, gate HIGH/CRITICAL)
  87. uses: aquasecurity/trivy-action@6e7b7d1fd3e4fef0c5fa8cce1229c54b2c9bd0d8 # v0.24.0
  88. with:
  89. image-ref: ${{ env.REGISTRY_IMAGE }}@${{ steps.bake.outputs.digest }}
  90. format: sarif
  91. output: trivy.sarif
  92. severity: HIGH,CRITICAL
  93. exit-code: "1"
  94. - name: Scan (grype, gate high/critical)
  95. uses: anchore/scan-action@64a33b277ea7a1215a3c142735a1091341939ff5 # v4
  96. with:
  97. image: ${{ env.REGISTRY_IMAGE }}@${{ steps.bake.outputs.digest }}
  98. fail-build: true
  99. severity-cutoff: high
  100. - uses: sigstore/cosign-installer@398d4b0eeef1380460a10c8013a76f728fb906ac # v3
  101. - name: Keyless sign (cosign, OIDC)
  102. env:
  103. COSIGN_YES: "true"
  104. run: |
  105. cosign sign "${REGISTRY_IMAGE}@${{ steps.bake.outputs.digest }}"
  106. cosign attest --predicate /tmp/bake-metadata.json \
  107. "${REGISTRY_IMAGE}@${{ steps.bake.outputs.digest }}"
  108. pin-digest:
  109. name: pin-digest
  110. needs: [build]
  111. runs-on: ubuntu-24.04
  112. steps:
  113. - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
  114. - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
  115. with:
  116. python-version: "3.x"
  117. - name: Pin kustomization to the built digest + strict lint
  118. env:
  119. DIGEST: ${{ needs.build.outputs.digest }}
  120. run: |
  121. pip install pyyaml
  122. python3 - "${DIGEST}" <<'EOF'
  123. import sys
  124. from pathlib import Path
  125. import yaml
  126. digest = sys.argv[1]
  127. assert digest.startswith("sha256:"), f"bad digest: {digest}"
  128. path = Path("deploy/k8s/kustomization.yaml")
  129. doc = yaml.safe_load(path.read_text())
  130. for img in doc.get("images", []):
  131. if img.get("name") == "ghcr.io/d-oauth/d-oauth-server":
  132. img.pop("newTag", None)
  133. img["digest"] = digest
  134. path.write_text(yaml.safe_dump(doc, sort_keys=False))
  135. EOF
  136. python3 scripts/check_manifests.py --strict-digest