Fold enforce-tag-immutability into harden-zot-registry #235

Merged
eblume merged 1 commit from enforce-tag-immutability into main 2026-02-21 08:05:17 -08:00
4 changed files with 30 additions and 31 deletions
Showing only changes of commit b42660f881 - Show all commits

Fold enforce-tag-immutability into harden-zot-registry

Tag immutability requires auth to be meaningful, so it can't be resolved
independently. Replace client-side push checks with server-side
accessControl policy: artifact-workloads group gets read+create (no
update/delete), enforcing immutability at the registry level.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Erich Blume 2026-02-21 07:52:49 -08:00

View file

@ -1,7 +1,6 @@
---
title: Enforce Tag Immutability
modified: 2026-02-20
status: active
modified: 2026-02-21
tags:
- how-to
- zot
@ -12,33 +11,18 @@ tags:
Prevent accidental overwrite of version tags during CI push.
## Approach
## Resolution
Push-side enforcement: before pushing a version tag, query `GET /v2/blumeops/<name>/tags/list` and fail if the tag already exists. Commit SHA tags are inherently unique and skip this check.
Tag immutability is enforced server-side via `accessControl` policies in [[harden-zot-registry]], not by client-side push checks. The three-tier access model makes push-side enforcement unnecessary:
## Two Push Paths to Update
- **Anonymous:** `["read"]` — pull only, no push at all
- **`artifact-workloads` group (CI):** `["read", "create"]` — can push new tags but cannot overwrite or delete existing ones
- **Admins:** `["read", "create", "delete"]` — break-glass for removing bad images
1. **Dagger path:** Add tag-existence check before `ctr.publish()` in `.dagger/src/blumeops_ci/main.py`
2. **Nix/skopeo path:** Add tag-existence check before `skopeo copy` in `.forgejo/workflows/build-container-nix.yaml`
Since CI only has `create` (not `update`), pushing an existing version tag is rejected by zot itself. Commit SHA tags are inherently unique and never collide.
## Key Files
| File | Purpose |
|------|---------|
| `.dagger/src/blumeops_ci/main.py` | Add pre-publish tag check |
| `.forgejo/workflows/build-container-nix.yaml` | Add pre-copy tag check |
## Notes
- After auth is enabled, the tag check API call may need auth too — or it can rely on anonymous read access from `anonymousPolicy: ["read"]`
- Only version tags (e.g., `v1.2.0`) need the check; commit SHA tags are unique by nature
## Verification
- [ ] Pushing a new version tag succeeds
- [ ] Pushing an existing version tag fails with a clear error
- [ ] Pushing a commit SHA tag always succeeds
This approach requires authentication to be meaningful — without auth, everyone is anonymous. The requirements are therefore part of the root [[harden-zot-registry]] goal's `accessControl` configuration.
## Related
- [[harden-zot-registry]] — Parent goal
- [[harden-zot-registry]] — Parent goal (includes this requirement)

View file

@ -1,6 +1,6 @@
---
title: Harden Zot Registry
modified: 2026-02-20
modified: 2026-02-21
status: active
requires:
- register-zot-oidc-client
@ -32,9 +32,14 @@ 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`** — anonymous read, authenticated write
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. **`externalUrl`** — `https://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 |
@ -49,6 +54,8 @@ Update `ansible/roles/zot/templates/config.json.j2` to add:
- [ ] 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
@ -56,6 +63,6 @@ Update `ansible/roles/zot/templates/config.json.j2` to add:
- [[register-zot-oidc-client]] — Prereq: register OIDC client in Authentik
- [[wire-ci-registry-auth]] — Prereq: update CI push paths with credentials
- [[enforce-tag-immutability]] — Prereq: prevent version tag overwrites
- [[enforce-tag-immutability]] — Folded into this card (server-side via accessControl)
- [[adopt-commit-based-container-tags]] — Prereq: commit-SHA-based image tags
- [[agent-change-process]] — C2 methodology

View file

@ -1,6 +1,6 @@
---
title: Register Zot OIDC Client
modified: 2026-02-20
modified: 2026-02-21
status: active
tags:
- how-to
@ -30,11 +30,16 @@ Register a zot OAuth2 provider and application in Authentik via blueprint, follo
- New template `ansible/roles/zot/templates/oidc-credentials.json.j2` containing `client_id` and `client_secret`
- Source `client_secret` from 1Password via a new pre_task in `ansible/playbooks/indri.yml`
6. **Create `artifact-workloads` group** in Authentik blueprint:
- Add a group resource to the blueprint with name `artifact-workloads`
- Create a service account user in the `artifact-workloads` group for CI push operations
- This group gets `["read", "create"]` in zot's `accessControl` (no update/delete — enforces tag immutability)
## Key Files
| File | Purpose |
|------|---------|
| `argocd/manifests/authentik/configmap-blueprint.yaml` | Add zot blueprint (provider + app + policy) |
| `argocd/manifests/authentik/configmap-blueprint.yaml` | Add zot blueprint (provider + app + policy + group) |
| `argocd/manifests/authentik/external-secret.yaml` | Add `AUTHENTIK_ZOT_CLIENT_SECRET` env var |
| `ansible/roles/zot/templates/oidc-credentials.json.j2` | New: OIDC credentials for zot |
| `ansible/playbooks/indri.yml` | New pre_task for zot OIDC client secret |
@ -44,6 +49,7 @@ Register a zot OAuth2 provider and application in Authentik via blueprint, follo
- [ ] Authentik admin UI shows zot application
- [ ] OIDC discovery endpoint includes zot client
- [ ] Blueprint status is `successful` (check via API, not just logs)
- [ ] `artifact-workloads` group exists with CI service account
## Related

View file

@ -1,6 +1,6 @@
---
title: Wire CI Registry Auth
modified: 2026-02-20
modified: 2026-02-21
status: active
tags:
- how-to
@ -23,6 +23,8 @@ There are two push paths to update:
> **Note:** The API key must be generated manually after OIDC login is working — log in to zot UI via browser, generate an API key, and store it in 1Password. This is a manual step between [[register-zot-oidc-client]] and this card, but not modeled as a formal `requires` dependency.
CI authenticates as a service account in the `artifact-workloads` group (created in [[register-zot-oidc-client]]). This group grants `["read", "create"]` — CI can push new tags but cannot overwrite or delete existing ones, enforcing tag immutability server-side.
## Secret Flow
### Indri runner (minikube)