blumeops/docs/how-to/zot/harden-zot-registry.md
Erich Blume c86b5d7772
All checks were successful
Build Container / detect (push) Successful in 3s
Build Container / build-dagger (navidrome) (push) Successful in 22m26s
Native Dagger container builds + Navidrome v0.61.1 (#330)
## Summary
- Move Dagger module from `.dagger/` to repo root (`src/blumeops/`), rename `blumeops-ci` → `blumeops`
- Replace opaque `docker_build()` with native Dagger pipelines that surface full build errors per step
- Migrate navidrome as the first container (`containers/navidrome/container.py`)
- Upgrade navidrome from v0.60.3 to v0.61.1 (major artwork overhaul, SQLite FTS5 search, server-managed transcoding)
- Add `dagger call container-version` for CI version extraction without Dockerfile parsing
- All mise tasks (`container-list`, `container-version-check`, `container-build-and-release`) updated for hybrid mode
- Legacy `docker_build()` fallback preserved for all other containers

## Motivation
When navidrome v0.61.0 added a new Go build tag (`sqlite_fts5`), `docker_build()` showed only "exit code: 1". We had to run `docker build --progress=plain` manually to find `undefined: buildtags.SQLITE_FTS5`. Native Dagger pipelines show the full error inline.

## Container build dispatch needed
After merge, dispatch container build for navidrome:
```
mise run container-build-and-release navidrome --ref 470b4bd
```

## Deploy steps
1. Wait for container build to complete
2. Back up navidrome-data PVC (non-reversible DB migrations)
3. `argocd app set navidrome --revision main && argocd app sync navidrome`
4. Verify at https://dj.ops.eblu.me

## Future
Remaining containers migrate incrementally in follow-up PRs using the same pattern.

Reviewed-on: #330
2026-04-11 17:11:56 -07:00

2.2 KiB

title modified tags
Harden Zot Registry 2026-04-11
how-to
zot
registry
security

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:

  1. http.auth.openid — OIDC provider pointing to Authentik (authentik.ops.eblu.me)
  2. http.auth.apikey: true — API key generation for CI service accounts
  3. http.accessControl — three-tier policy:
    • 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", "update", "delete"] — break-glass
  4. http.externalUrlhttps://registry.ops.eblu.me for OIDC callback redirects
  5. accessControl.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
src/blumeops/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-check passes