blumeops/docs/how-to/zot/harden-zot-registry.md
Erich Blume 04e036c603 Fold enforce-tag-immutability into harden-zot-registry (#235)
## Summary

- Removed `status: active` from `enforce-tag-immutability` card — its requirements are folded into the parent `harden-zot-registry` goal's `accessControl` configuration
- Updated `harden-zot-registry` with three-tier access control spec (anonymous read, artifact-workloads read+create, admins full)
- Added `artifact-workloads` group creation step to `register-zot-oidc-client`
- Added service account context to `wire-ci-registry-auth`

## Rationale

Tag immutability requires authentication to be meaningful. Without auth, everyone is anonymous and gets the same policy. Rather than client-side push checks, the registry enforces immutability server-side: CI gets `["read", "create"]` (no update/delete), so pushing an existing tag is rejected by zot itself.

## Test plan

- [ ] `mise run docs-check-links` passes
- [ ] `mise run docs-mikado` shows enforce-tag-immutability as resolved

Reviewed-on: https://forge.ops.eblu.me/eblume/blumeops/pulls/235
2026-02-21 08:05:16 -08:00

2.6 KiB

title modified status requires tags
Harden Zot Registry 2026-02-21 active
register-zot-oidc-client
wire-ci-registry-auth
enforce-tag-immutability
adopt-commit-based-container-tags
how-to
zot
registry
security

Harden Zot Registry

Enable OIDC + API key authentication on zot with anonymous pull preserved, and enforce tag immutability for version tags. This is the C2 Mikado root goal.

Context

Zot currently has no authentication — security relies entirely on the Tailscale ACL boundary. Any tailnet client can push images, and tags are mutable.

Both prerequisites from the original plan are now complete:

Core Change

Update ansible/roles/zot/templates/config.json.j2 to add:

  1. http.auth.openid — OIDC provider pointing to Authentik
  2. http.auth.apikey: true — enable API key generation for CI
  3. accessControl — three-tier policy enforcing tag immutability:
    • anonymousPolicy: ["read"] — anyone can pull
    • artifact-workloads group: ["read", "create"] — CI can push new tags but cannot overwrite or delete (immutable tags)
    • admins group: ["read", "create", "delete"] — break-glass for removing bad images
  4. externalUrlhttps://registry.ops.eblu.me for OIDC callback redirects

The artifact-workloads group must be created in Authentik (see register-zot-oidc-client) and a service account added to it for CI use.

Key Files

File Purpose
ansible/roles/zot/templates/config.json.j2 Zot config — add auth + access control
ansible/roles/zot/defaults/main.yml New OIDC variables
ansible/roles/zot/tasks/main.yml Deploy OIDC credentials file

Verification

  • Anonymous pull works (curl -sf https://registry.ops.eblu.me/v2/_catalog)
  • Unauthenticated push fails (401)
  • OIDC browser login works (redirect to Authentik and back)
  • API key push works (docker login with zak_... token)
  • Pushing an existing version tag as CI user fails (no update permission)
  • Admin can delete a tag if needed
  • Pull-through caching still works
  • mise run services-check passes