Chapter 11 — Supply-Chain Controls
11.1 Current State and Scope
As-built supply-chain posture is minimal but deliberately clean so far: everything installed to date (Docker, Compose, Prometheus, nginx, certbot) came from Ubuntu official repos or the official Grafana apt repo — no curl-pipe-bash installs, no third-party PPAs, no unpinned container images yet, because no application containers exist yet. This chapter sets the controls before the application build starts pulling npm/crates/PyPI dependencies and publishing images — retrofitting provenance onto an already-polluted dependency tree is an order of magnitude harder than starting gated. Scope: container images, OS packages, language dependencies (Node/pnpm, Rust/Cargo, Python), the artifact registry, and the build pipeline that connects them. Controlling SEC-001 requirement: INF-03 (repository controls) plus the supply-chain family imported into KO-001; AI-02 intersects where MCP tool containers are concerned (§6.4).
11.2 Registry Policy
GitHub Container Registry (GHCR) under the SBS GitHub organization is the sole artifact registry:
- One registry, private visibility, org-owned — same ownership/revocability principle as §2.1 and §10.3. No Docker Hub publishing; no images pulled from Docker Hub except the official library base images enumerated in the allowlist (§11.3).
- Pull access from droplets via a registry-scoped read-only token stored in Vault (
secret/platform/ghcr/), rotated on the 90-day static schedule (§8.4). Push access belongs to CI only — no human pushes images; an image that wasn't built by the pipeline doesn't exist as far as deployment is concerned. - The §3.4 egress allowlist admits exactly: GHCR endpoints, Ubuntu archive/security mirrors, the Grafana apt repo, and the language registries (registry.npmjs.org, crates.io/static.crates.io, pypi.org/files.pythonhosted.org) from build contexts only — production droplets pull finished images, never raw dependencies; the language registries are unreachable from prod runtime (a compromised container cannot
pnpm addits way to a payload).
11.3 Image Provenance
- Base image allowlist, pinned by digest, reviewed quarterly: official
node:<LTS>-slim,rust:<pinned>-slim(build stage only),python:3.x-slim,nginx:stable,postgres/mongo/redisofficial images for the §6.5 interim,hashicorp/vault. Nothing outside the list without a PR touching the allowlist file (CODEOWNERS: security reviewer per §10.4). - Digest pinning everywhere:
FROM node:22-slim@sha256:…in Dockerfiles; compose files reference deployed images by digest, not floating tags (:latestis prohibited and CI-linted). Tag-based human readability is preserved by also tagging, but the digest is what deploys — a registry-side tag repoint cannot swap what prod runs. - Multi-stage builds — build stages (compilers, package managers) never ship; runtime stages carry the application and its production dependencies only. Runtime images run as non-root UID, no shell where the base permits (
distroless-style trimming is a later optimization, not a Day-1 gate). - Rebuild cadence: weekly CI rebuild of all images against updated base digests +
apt upgradelayer, so CVE patching is a pipeline event, not an emergency (§INF-05 patching applies to images as much as hosts). Trivy scan (§11.6) gates the result.
11.4 Dependency Pinning
Per-ecosystem, all enforced in CI (a lockfile that isn't verified is documentation, not control):
| Ecosystem | Pinning mechanism | CI enforcement |
|---|---|---|
| Node / pnpm | pnpm-lock.yaml committed; packageManager field + corepack pins pnpm itself |
pnpm install --frozen-lockfile — any drift fails the build; pnpm audit report attached |
| Rust / Cargo | Cargo.lock committed (binaries); rust-toolchain.toml pins the compiler |
cargo build --locked; cargo audit (RustSec) |
| Python | requirements.txt with --hash=sha256:… per package (pip-compile generated) |
pip install --require-hashes; pip-audit |
| OS packages | Versions asserted in the Ansible base role for security-relevant packages | Drift surfaces in the nightly --check run (§10.5) |
Dependency updates are PRs like any other change (Renovate bot, grouped weekly, auto-PR but never auto-merge) — the update path goes through the same review + CI gauntlet, so a poisoned upstream release sits in a diff a human looks at, not in a silent nightly pull. New top-level dependencies require a one-line justification in the PR body; transitive bloat is reviewed via the lockfile diff.
11.5 Artifact Signing and Verification
- Commits: signed commits required on both repos (already a §10.3 branch-protection condition) — SSH-key signing acceptable; the key inventory rides the per-person identity work of item 17.
- Images: CI signs every pushed image with cosign keyless (Sigstore, GitHub OIDC identity) — the signature attests "built by this workflow, from this commit, in this repo," which is the provenance claim that matters. No long-lived signing key to protect or rotate (the §8 lifecycle problem cosign keyless exists to delete).
- Verification at deploy: the deploy step (§6.7 / §10.4) runs
cosign verifyagainst the expected workflow identity beforecompose up; an unsigned or wrongly-attested image fails the deploy. This closes the loop: registry compromise alone cannot inject a runnable image, because the attacker must also produce a valid Sigstore attestation from the protected CI workflow. - SBOM: CI generates SPDX SBOMs (syft) per image, attached as a cosign attestation and archived to
sbs-dash-backups— when the next log4shell-class event lands, "are we exposed" is a query against stored SBOMs, not an archaeology project.
11.6 Pipeline Integrity and Scanning
The build pipeline is itself supply chain:
- GitHub Actions pinned by action digest (not
@v4floating tags) — the same rule applied to the tooling as to the images; third-party actions from a reviewed allowlist only. - Workflow permissions default
contents: read; the push job alone getspackages: write+id-token: write(for cosign OIDC). No self-hosted runners at this fleet size — GitHub-hosted runners keep runner compromise off the threat sheet at the cost of trusting GitHub, which the design already does (repo, registry). - Trivy scans every built image (CI gate: fail on CRITICAL with fix available; HIGH warns and files an issue) and re-scans the deployed digest set nightly against fresh CVE data — the nightly catches vulnerabilities published after the image shipped, feeding MON-06 (vulnerability scanning) with the §11.3 weekly rebuild as the remediation vehicle.
- gitleaks (already items 47/§10.4) covers the secrets half of pipeline hygiene.
11.7 Verification
Gates (LH-SBS-INST-001): base-image allowlist file exists, all Dockerfiles resolve FROM digests within it (lint); :latest absent from every compose/Dockerfile (grep gate); frozen-lockfile builds pass and a deliberately drifted lockfile fails (negative test); cosign verify passes on a pipeline image and fails on a manually-pushed unsigned test image (negative test); deploy refuses the unsigned image end-to-end; SBOM attestation present for every deployed digest; Trivy CI gate demonstrated on a seeded-CVE test image; language registries unreachable from prod runtime (egress probe, §3.4); Actions digests pinned (workflow lint); prod running-set digests ⊆ registry signed-set (drift cross-check with §10.5).