Update zot API key 1Password path and add rotation docs

- Fix op read path to use Forgejo Secrets item field zot-ci-api
  (was zot-ci-apikey/credential)
- Rewrite zot reference card security model for OIDC + API key auth
- Add API key rotation procedure with impersonation steps and op
  oneliner
- Document 90-day key expiry in wire-ci-registry-auth how-to

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Erich Blume 2026-02-21 12:05:25 -08:00
commit 281ffb7c0c
3 changed files with 28 additions and 4 deletions

View file

@ -110,7 +110,7 @@
- name: Fetch Zot CI API key for Forgejo Actions
ansible.builtin.command:
cmd: op read "op://vg6xf6vvfmoh5hqjjhlhbeoaie/zot-ci-apikey/credential"
cmd: op read "op://vg6xf6vvfmoh5hqjjhlhbeoaie/w3663ffnvkewbftncqxtcpeavy/zot-ci-api"
delegate_to: localhost
register: _zot_ci_api_key
changed_when: false

View file

@ -16,7 +16,7 @@ How CI pipelines authenticate to the zot registry after OIDC + apikey auth is en
The `zot-ci` service account (created in [[register-zot-oidc-client]]) belongs to the `artifact-workloads` group, granting `["read", "create"]` permissions — CI can push new tags but cannot overwrite or delete existing ones.
Authentication uses a zot API key generated after the service account's first OIDC login. The key is stored in 1Password (`zot-ci-apikey` item in blumeops vault) and synced to Forgejo Actions secrets via the `forgejo_actions_secrets` ansible role.
Authentication uses a zot API key generated after the service account's first OIDC login. The key is stored in 1Password (`Forgejo Secrets` item, field `zot-ci-api`, in blumeops vault) and synced to Forgejo Actions secrets via the `forgejo_actions_secrets` ansible role. The key expires every 90 days — see [[zot#API Key Rotation]] for the rotation procedure.
## Push Paths
@ -30,7 +30,7 @@ Authentication uses a zot API key generated after the service account's first OI
## Secret Flow
1Password item `zot-ci-apikey` → ansible pre_task fetches it → `forgejo_actions_secrets` role syncs to Forgejo API → both runners (k8s on indri, host on ringtail) access it as `${{ secrets.ZOT_CI_API_KEY }}`.
1Password `Forgejo Secrets` item (field `zot-ci-api`) → ansible pre_task fetches it → `forgejo_actions_secrets` role syncs to Forgejo API → both runners (k8s on indri, host on ringtail) access it as `${{ secrets.ZOT_CI_API_KEY }}`.
## Key Files

View file

@ -35,9 +35,33 @@ When [[cluster|minikube]] pulls an image, containerd checks zot first. If cached
## Security Model
Network access only (no authentication). Defense is the Tailscale ACL boundary.
OIDC authentication via [[authentik]], with API key support for CI. Three-tier access control:
| Role | Permissions | Use case |
|------|------------|----------|
| Anonymous | read | Pull images without auth |
| `artifact-workloads` group | read, create | CI push (new tags only, no overwrite/delete) |
| `admins` group | read, create, update, delete | Break-glass admin access |
CI authenticates with a zot API key generated from the `zot-ci` service account's OIDC session. The key is stored in the `Forgejo Secrets` 1Password item (field `zot-ci-api`) and synced to Forgejo Actions secrets via ansible.
## API Key Rotation
The `zot-ci` API key expires every **90 days**. To rotate:
1. In Authentik admin UI, impersonate the `zot-ci` user
2. Visit `https://registry.ops.eblu.me` — you'll land on the login page
3. Click "SIGN IN WITH OIDC" to authenticate as zot-ci
4. Navigate to `https://registry.ops.eblu.me/user/apikey`
5. Generate a new API key, copy it to clipboard
6. Update 1Password:
```fish
pbpaste | op item edit "Forgejo Secrets" --vault blumeops "zot-ci-api[password]=-"
```
7. Sync to Forgejo: `mise run provision-indri -- --tags forgejo_actions_secrets`
## Related
- [[forgejo]] - Container build CI
- [[cluster|Cluster]] - Registry consumer
- [[authentik]] - OIDC identity provider