Wire CI Registry Auth

How CI pipelines authenticate to the zot registry after OIDC + apikey auth is enabled.

Overview

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 (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 API Key Rotation for the rotation procedure.

Push Paths

Dagger path (Dockerfile containers)

.forgejo/workflows/build-container.yaml passes --registry-password=env:ZOT_CI_API_KEY to the Dagger publish() function, which calls with_registry_auth() before pushing.

Nix/skopeo path (Nix containers)

.forgejo/workflows/build-container-nix.yaml passes --dest-creds=zot-ci:$ZOT_CI_API_KEY to skopeo copy.

Secret Flow

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

FilePurpose
.dagger/src/blumeops_ci/main.pypublish() accepts optional registry_password
.forgejo/workflows/build-container.yamlPasses API key to Dagger
.forgejo/workflows/build-container-nix.yamlPasses API key to skopeo
ansible/playbooks/indri.ymlPre_task fetches API key from 1Password
ansible/roles/forgejo_actions_secrets/defaults/main.ymlSecret entry for ZOT_CI_API_KEY