Chainguard base migration runbook
Phase 2 §5.1 + §5.2 + §5.3 + §5.4 of RESTRUCTURING_PLAN.md. Owns the cutover from Debian-slim base images to Chainguard Wolfi bases, the cosign + SBOM signing pipeline, and the Kyverno admission policies that gate signed-only images in production.
Scope
Four AlphaSwarm-owned images are targeted to move to Chainguard Wolfi in
Phase 2 §5.1. As of this writing only alphaswarm-client has actually
landed on Chainguard bases on the mainline branch; the other three still
build from Debian-based images (alphaswarm-api/alphaswarm-worker/
alphaswarm-controller are on a pinned python:3.12-slim, and
alphaswarm-ui is on node:20-bookworm/node:20-bookworm-slim, not
node:20-alpine). A Chainguard migration for these three exists on an
unmerged branch; treat the "Base after" column below as the target state
this runbook produces, not the current state, until that branch lands:
| Image | Dockerfile | Base before (current) | Base after (target) |
|---|---|---|---|
alphaswarm-api / alphaswarm-worker (shared api target) | alphaswarm_platform/Dockerfile (or alphaswarm_platform/build/docker/alphaswarm-runtime/Dockerfile, used by the current build-publish.yml) | python:3.12-slim | cgr.dev/chainguard/python:3.11-dev |
alphaswarm-controller | alphaswarm_platform/build/docker/alphaswarm_controller/Dockerfile | python:3.12-slim (pinned by digest, via BUILD_BASE/RUNTIME_BASE build args) | cgr.dev/chainguard/python:3.11-dev (builder) + cgr.dev/chainguard/python:3.11 (runtime) |
alphaswarm-client | alphaswarm_platform/build/docker/alphaswarm_client/Dockerfile | — (already migrated) | cgr.dev/chainguard/node:20-dev + cgr.dev/chainguard/python:3.11-dev (builders) + cgr.dev/chainguard/python:3.11 (runtime) — confirmed in place |
alphaswarm-ui | alphaswarm_platform/build/docker/alphaswarm_ui/Dockerfile | node:20-bookworm (builder) + node:20-bookworm-slim (runtime) | cgr.dev/chainguard/node:20-dev (builder) + cgr.dev/chainguard/node:20 (runtime) |
Two images carry documented exemptions and stay on their current bases:
| Image | Dockerfile | Reason |
|---|---|---|
alphaswarm-bots standard | alphaswarm_bots/Dockerfile | Already on gcr.io/distroless/python3-debian12:nonroot — smaller and more locked-down than Chainguard Python, no shell at all. Builder stage stays on python:3.12-slim-bookworm for build-essential availability. |
alphaswarm-bots HFT | alphaswarm_bots/Dockerfile.hft | Kernel-bypass libs (DPDK, Onload, Mellanox OFED) require kernel headers + libnuma1 + linuxptp + ethtool + kmod which Chainguard's nonroot Wolfi runtime image does not ship. Per ADR 007. |
Two future-phase scaffolds are created in Phase 2 §5.6:
| Image | Dockerfile | Activation phase |
|---|---|---|
alphaswarm-edge (Envoy cell router) | alphaswarm_platform/build/docker/alphaswarm-edge/Dockerfile | Phase 3 §6.4 (cell topology) |
alphaswarm-agent-sandbox (gVisor target) | alphaswarm_platform/build/docker/alphaswarm-agent-sandbox/Dockerfile | Phase 5 §8 (per-tenant MCP + agent sandbox) |
Why Chainguard Wolfi
- glibc, not musl — keeps native wheel compatibility for
numpy,pyarrow,torch,psycopg2, etc. The RESTRUCTURING_PLAN footnote at §5.1 explicitly notes that Alpine/musl-style minimalism breaks the native-wheel toolchain. - Continuously rebuilt — Chainguard ships a fresh image set every ~24 hours, so CVE patches land without us doing anything beyond a rebuild. Pair with Renovate (Phase 1 §4.7) to re-trigger the build matrix on a base-image bump.
- No CVEs in the base — Chainguard runs distroless-style
scans and publishes a daily-zero-CVE SLO for
latesttags. Application-level CVEs are still our responsibility: the currentbuild-sign-pushcomposite action runs a Trivy scan (severity: HIGH,CRITICAL) that blocks the build on findings. Thegrype/syftcommands below are a manual/local verification path — neither tool is currently wired into CI (.github/workflowsand.github/actionshave nogrypeorsyftstep). - Single nonroot UID convention (65532) — matches the Phase 2
§5.4 PSS restricted profile. The runtime stages never run as
root; the
-devbuilder runs as root only forapk add.
Build verification
Local one-off build (no push, no signing — for inner-loop dev):
docker buildx build \
--platform linux/amd64,linux/arm64 \
--target api \
--file alphaswarm_platform/Dockerfile \
--tag alphaswarm-api:dev \
.
Multi-arch build via build-publish.yml (CI canonical path — the workflow
actually named build-multi-arch.yml does not exist; alphaswarm_platform
and alphaswarm each carry their own build-publish.yml, triggered on
version tags and via workflow_dispatch):
gh workflow run build-publish.yml \
--ref feat/phase-2-supply-chain
The workflow signs every pushed image with cosign keyless OIDC via the
build-sign-push composite action
(alphaswarm_platform/.github/actions/build-sign-push/action.yml), which
also attaches build provenance + an SBOM (docker/build-push-action's
provenance: true / sbom: true) and runs a Trivy vulnerability scan
(HIGH/CRITICAL findings block the build). There is currently no separate
inspect job — verify signatures/attestations locally with cosign verify
as below.
Verify cosign signature locally
cosign verify \
--certificate-identity-regexp 'https://github.com/Alpha-Swarm-ai/alphaswarm_platform/.github/workflows/build-publish\.yml@refs/.*' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
docker.io/julianwiley/alphaswarm-api:latest
Expected exit code: 0. The output prints the signature payload including the Rekor transparency log entry index.
Verify CycloneDX SBOM attestation locally
cosign verify-attestation \
--certificate-identity-regexp 'https://github.com/Alpha-Swarm-ai/alphaswarm_platform/.github/workflows/build-publish\.yml@refs/.*' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--type cyclonedx \
docker.io/julianwiley/alphaswarm-api:latest > sbom-attestation.json
The predicate field of the attestation is the base64-encoded
CycloneDX document.
Re-run grype against the SBOM
syft docker.io/julianwiley/alphaswarm-api:latest -o cyclonedx-json=sbom.json
grype sbom:sbom.json --fail-on high
Exit code 0 = no HIGH or CRITICAL CVEs; non-zero = the gate fires in CI.
Kyverno audit-to-enforce ratchet
The six Phase 2 §5.3 cluster policies below (deployments/kubernetes/security/kyverno/cluster-policies/00-* through 05-*
in alphaswarm_platform) have already been ratcheted from Audit to
validationFailureAction: Enforce in the current tree — including
02-require-runtime-class.yaml, which each in-tree policy comment now
notes was flipped once the referenced follow-up phase landed. The workflow
below is kept for reference (e.g. for onboarding a new policy through the
same ratchet), not because these six are still pending:
| Policy | Audit-mode soak | Enforce gate |
|---|---|---|
00-verify-signatures.yaml | 7 days zero violations across all AlphaSwarm-owned namespaces | Phase 2.5 — done, now Enforce |
01-require-pss-restricted.yaml | 7 days zero violations | Phase 2.5 — done, now Enforce |
02-require-runtime-class.yaml | Was held until Phase 5 §8.3 landed the gVisor RuntimeClass | Phase 5.1 — done, now Enforce |
03-no-host-network.yaml | 7 days zero violations after alphaswarm-edge namespace carries alphaswarm.io/host-network-allowed: "true" | Phase 2.5 — done, now Enforce |
04-no-privilege-escalation.yaml | 7 days zero violations | Phase 2.5 — done, now Enforce |
05-required-labels.yaml | 7 days zero violations on namespaces that carry alphaswarm.io/component | Phase 2.5 — done, now Enforce |
Three additional policies (06-money-plane-progressive-delivery.yaml,
07-restrict-image-registries.yaml, 08-deployment-version-labels.yaml)
now also exist in the same directory; they postdate Phase 2 §5.3 and are
out of scope for this runbook.
Operator workflow to flip Audit → Enforce
# 1. Verify zero violations for the target policy:
kubectl get clusterpolicyreport -o jsonpath='{range .items[*].results[?(@.policy=="alphaswarm-verify-image-signatures")]}{.result}{"\n"}{end}' \
| sort | uniq -c
# Expected output: only "pass" lines. Any "fail" lines block the ratchet.
# 2. Patch the policy in place:
kubectl patch clusterpolicy alphaswarm-verify-image-signatures \
--type=merge \
-p '{"spec":{"validationFailureAction":"Enforce"}}'
# 3. Update the YAML in tree so the audit-only state is preserved:
sed -i 's/validationFailureAction: Audit/validationFailureAction: Enforce/' \
alphaswarm_platform/deployments/kubernetes/security/kyverno/cluster-policies/00-verify-signatures.yaml
# 4. Commit + open PR with `[Phase 2.5 ratchet]` in the title.
Rollback
The Chainguard migration is reversible per Dockerfile. Each
Dockerfile carries a Phase 2 §5.1 comment at the top documenting
the previous base image. To roll back a single image:
- Revert that file in
alphaswarm_platform/Dockerfileoralphaswarm_platform/build/docker/<service>/Dockerfileto its pre-Phase-2 state. - Trigger
build-multi-arch.ymlon the revert branch. - The cosign keyless signature still applies (it signs by digest, not base image). The grype scan may fail differently because the Debian-slim base ships different CVEs.
Cosign signing on PRs
The Phase 2 §5.2 cosign + SBOM + grype steps gate on
if: github.event_name != 'pull_request' because cosign keyless
requires OIDC, which is unavailable on PRs from forked
repositories. PRs from internal branches still build (and pull-
through cache), but they neither push nor sign. The inspect job
that runs cosign verify on :latest tags is only useful for
merged commits.
If you need to verify a signature on a PR build, push to a feature branch in the canonical repo (not a fork) and check the registry manually:
docker pull docker.io/julianwiley/alphaswarm-api:feat-phase-2-supply-chain-<sha>
cosign verify \
--certificate-identity-regexp 'https://github.com/Alpha-Swarm-ai/.*' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
docker.io/julianwiley/alphaswarm-api:feat-phase-2-supply-chain-<sha>
Related documents
- RESTRUCTURING_PLAN.md §5
- alphaswarm_platform/deployments/kubernetes/security/README.md
.cursor/plans/alphaswarm-index-debt-phase-2-supply-chain.md— thealphaswarmrepo no longer carries a.cursor/plans/directory; this plan now lives archived inalphaswarm_internal- ADR 007 — QuantBot Latency Classes (explains the HFT Debian-slim exemption)