AEP-014: Supply Chain Security¶
| Field | Value |
|---|---|
| Status | completed |
| Priority | P0 |
| Effort | Medium (3-5 days) |
| Impact | High |
| Dependencies | AEP-011 (completed) |
Gap Analysis¶
Current Implementation¶
With AEP-011 landed, the project publishes multi-arch images to GHCR via .github/workflows/docker-publish.yml with buildx sbom: true and provenance: true flags. That's a strong starting point but stops short of what an enterprise procurement review will ask for:
- SBOM is generated but not attached/verified: buildx embeds an SBOM attestation on the image, but it isn't exported as a release artifact nor independently verifiable by downstream consumers.
- Images are not signed: anyone with GHCR write access (or a compromised token) can push a malicious
latesttag, and there is no way for the cluster to reject it. - No vulnerability scan gate: the CI pipeline has
banditfor Python source but no scan of the final container image. A vulnerable base image or transitive package would ship unnoticed. - Base image is pinned by tag, not digest:
FROM python:3.14-slim-bookworminDockerfileresolves to whatever taglatestpoints to at build time. Two builds on different days can produce different base images without any visible diff. - No dependency review in CI: new dependencies added via
uv addare not audited for known CVEs before merge. - No admission policy: the Kubernetes manifests don't enforce that only signed images from GHCR can run.
What's available¶
- Sigstore / cosign: keyless (OIDC) signing in GitHub Actions — no key material to rotate. Images are signed against the workflow identity (
https://github.com/BAHALLA/orrery/.github/workflows/docker-publish.yml@refs/tags/v0.2.0). - Trivy: industry-standard container + filesystem scanner, works as a GitHub Action and as a Kubernetes admission controller.
- Grype + Syft: alternative SBOM / scan stack from Anchore.
- CycloneDX Python plugin (
cyclonedx-py): emits a Python-specific SBOM that catches transitive LLM-provider dependencies that container scanners miss. - GitHub Dependency Review Action: gates PRs on new CVEs introduced by dependency changes.
- Kyverno / Sigstore Policy Controller: admission-time signature verification for Kubernetes clusters.
Gap¶
The platform has no chain of custody from source → build → deploy. A supply-chain attack on any upstream Python package (via uv.lock tampering or a malicious PyPI release) would not be caught.
Proposed Solution¶
Step 1: Pin the base image by digest¶
# Dockerfile — pin by digest, not tag
FROM ghcr.io/astral-sh/uv:python3.14-bookworm-slim@sha256:<digest> AS builder
FROM python:3.14-slim-bookworm@sha256:<digest>
Automate digest refresh with Renovate or Dependabot's docker ecosystem.
Step 2: Generate and publish SBOMs¶
Extend docker-publish.yml to emit a CycloneDX SBOM of the Python dependency graph (not just the OS layer) and upload it as a release artifact:
- name: Generate Python SBOM
run: |
uvx cyclonedx-py requirements uv.lock \
-o sbom-python.cdx.json
- name: Upload SBOM
uses: actions/upload-artifact@v4
with:
name: sbom
path: sbom-python.cdx.json
- name: Attach SBOM to release
if: startsWith(github.ref, 'refs/tags/v')
uses: softprops/action-gh-release@v2
with:
files: sbom-python.cdx.json
Step 3: Sign images with cosign (keyless)¶
- name: Install cosign
uses: sigstore/cosign-installer@v3
- name: Sign the image
env:
COSIGN_EXPERIMENTAL: "true"
run: |
cosign sign --yes \
ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }}
Verify from the command line:
cosign verify \
--certificate-identity-regexp "https://github.com/BAHALLA/orrery/.*" \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
ghcr.io/bahalla/orrery:0.2.0
Step 4: Scan images with Trivy as a CI gate¶
- name: Scan image with Trivy
uses: aquasecurity/trivy-action@0.28.0
with:
image-ref: ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }}
severity: CRITICAL,HIGH
exit-code: 1
ignore-unfixed: true
format: sarif
output: trivy-results.sarif
- name: Upload SARIF to code scanning
if: always()
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: trivy-results.sarif
Step 5: Gate PRs on dependency CVEs¶
# .github/workflows/ci.yml (new job)
dependency-review:
name: Dependency Review
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
steps:
- uses: actions/checkout@v6
- uses: actions/dependency-review-action@v4
with:
fail-on-severity: high
Step 6: Admission-time signature verification (optional)¶
For clusters that can run it, install the Sigstore Policy Controller and create a ClusterImagePolicy that only admits images signed by the expected workflow identity:
apiVersion: policy.sigstore.dev/v1beta1
kind: ClusterImagePolicy
metadata:
name: orrery-images
spec:
images:
- glob: "ghcr.io/bahalla/orrery*"
authorities:
- keyless:
url: https://fulcio.sigstore.dev
identities:
- issuer: https://token.actions.githubusercontent.com
subjectRegExp: "https://github.com/BAHALLA/orrery/.*"
Affected Files¶
| File | Change |
|---|---|
Dockerfile | Pin base images by digest |
.github/workflows/docker-publish.yml | Add SBOM generation, cosign signing, Trivy scan |
.github/workflows/ci.yml | Add dependency-review job |
.github/dependabot.yml | Enable docker ecosystem for base image updates |
deploy/k8s/imagepolicy.yaml | New — Sigstore Policy Controller rule (optional) |
docs/security.md | New — document the verification flow for downstream users |
Acceptance Criteria¶
- [x] Base images pinned by digest in
Dockerfile - [x] Dependabot opens PRs for base image digest updates (
dockerecosystem) - [x] CycloneDX Python SBOM generated on every CI build (uploaded as a build artifact from the security job)
- [x] SBOM attached to GitHub releases as a downloadable artifact
- [x] Images signed with cosign keyless identity in CI
- [x] Signature verification documented in Supply Chain Security (
docs/supply-chain.md—docs/security.mdwould collide with the existingdocs/config/security.mdauth page) - [x] Trivy scan gates image publish on HIGH/CRITICAL fixable CVEs (scans the pushed digest; the release job depends on it, so a vulnerable image never becomes a GitHub release)
- [x] Trivy SARIF uploaded to GitHub Code Scanning (
container-imagecategory) - [x] PR dependency review gate merged to
ci.yml(fail-on-severity: high) - [x] (Stretch) Admission-time signature verification via Sigstore Policy Controller — shipped as an opt-in manifest,
deploy/k8s/imagepolicy.yaml
Implementation notes (as landed)¶
- The SBOM is produced from the lockfile (
uv export --frozen→cyclonedx-py requirements), not fromuv.lockdirectly — cyclonedx-py has no uv-lock reader; the frozen export preserves the exact pinned graph. - The Trivy image scan runs after the push (it scans the pushed digest, which is what buildx produces for multi-arch builds) but gates the release:
docker-publishfailing the scan fails the workflow before thereleasejob runs, so no GitHub release is cut for a vulnerable image. ignore-unfixed: truekeeps the gate actionable — a CVE with no upstream fix would otherwise block every release until Debian patches it.
Notes¶
- Trivy can produce false positives on development dependencies; consider
--skip-filesfor files not shipped in the final image. - Keyless signing requires the GitHub Actions OIDC token — make sure
id-token: writeis set on the job (already set in AEP-011's workflow). - The Python SBOM is more valuable than the OS SBOM for this project because the primary attack surface is the LLM client libraries and provider SDKs, not the Debian base.
- Pair with AEP-013 (authentication) — supply-chain hardening and auth/auth are the two P0 gaps after AEP-011. Either can ship first.