Harden Zot Registry
OIDC + API key authentication on zot with anonymous pull preserved, and tag immutability enforced server-side via accessControl. Completed as a C2 Mikado goal across PRs #236 and #237.
What Was Done
Updated ansible/roles/zot/templates/config.json.j2 with:
http.auth.openid— OIDC provider pointing to Authentik (authentik.ops.eblu.me)http.auth.apikey: true— API key generation for CI service accountshttp.accessControl— three-tier policy:anonymousPolicy: ["read"]— anyone can pullartifact-workloadsgroup:["read", "create"]— CI can push new tags but cannot overwrite or delete (immutable tags)adminsgroup:["read", "create", "update", "delete"]— break-glass
http.externalUrl—https://registry.ops.eblu.mefor OIDC callback redirectsaccessControl.metrics.users: [""]— allows anonymous Prometheus/Alloy scraping
Key Files
| File | Purpose |
|---|---|
ansible/roles/zot/templates/config.json.j2 | Zot config with auth + access control |
ansible/roles/zot/defaults/main.yml | OIDC issuer and external URL variables |
ansible/roles/zot/templates/oidc-credentials.json.j2 | OIDC client credentials |
.dagger/src/blumeops_ci/main.py | publish() with registry auth |
.forgejo/workflows/build-container.yaml | Dagger push with API key |
.forgejo/workflows/build-container-nix.yaml | Skopeo push with API key |
Verified
- Anonymous pull works (pull-through cache on gilbert)
- Unauthenticated push fails (401)
- OIDC browser login works (redirect to Authentik and back)
- API key push works (zot-ci API key)
- CI push succeeds (Dagger and Nix/skopeo paths)
- Pull-through caching still works
- Metrics endpoint accessible without auth
-
mise run services-checkpasses
Related
- register-zot-oidc-client — OIDC client registration in Authentik
- wire-ci-registry-auth — CI push path wiring
- enforce-tag-immutability — Server-side via accessControl
- adopt-commit-based-container-tags — Commit-SHA-based image tags