## Summary - Enable OIDC + API key authentication on zot registry with three-tier accessControl - `anonymousPolicy: ["read"]` — anyone can pull - `artifact-workloads` group: `["read", "create"]` — CI push, no overwrite/delete - `admins` group: `["read", "create", "update", "delete"]` — break-glass - Wire both CI push paths (Dagger and Nix/skopeo) with `ZOT_CI_API_KEY` credentials - Add `artifact-workloads` PolicyBinding in Authentik blueprint for zot app access - Add `ZOT_CI_API_KEY` to Forgejo Actions secrets via existing ansible role Completes the `wire-ci-registry-auth` and `harden-zot-registry` Mikado cards. ## Manual Deployment Steps (after merge) 1. Deploy Authentik blueprint: `argocd app sync authentik` 2. In Authentik admin UI: set a password for the `zot-ci` service account 3. Deploy zot config: `mise run provision-indri -- --tags zot` 4. Log in to `https://registry.ops.eblu.me` as `zot-ci` via OIDC → generate API key 5. Store API key in 1Password as `zot-ci-apikey` in blumeops vault 6. Sync Forgejo secrets: `mise run provision-indri -- --tags forgejo_actions_secrets` 7. Trigger a test container build to verify CI push 8. Verify anonymous pull: `curl -sf https://registry.ops.eblu.me/v2/_catalog` ## Uncertainties - **Zot `accessControl` group matching with OIDC:** Groups from Authentik's `profile` scope claim should map to zot policy groups, but the exact claim-to-group matching needs runtime verification - **`http.auth.apikey: true`:** This config key is documented but needs verification against the specific zot version built from source on indri - **API key permissions:** Need to confirm zot API keys inherit the generating user's group for accessControl evaluation ## Test Plan - [ ] `mise run provision-indri -- --check --diff --tags zot` shows expected config changes - [ ] Anonymous pull works after deploy - [ ] Unauthenticated push fails (401) - [ ] OIDC browser login redirects to Authentik and back - [ ] API key push works after key generation - [ ] CI push succeeds with both Dagger and skopeo paths - [ ] `mise run services-check` passes 🤖 Generated with [Claude Code](https://claude.com/claude-code) Reviewed-on: https://forge.ops.eblu.me/eblume/blumeops/pulls/237
145 lines
4.8 KiB
YAML
145 lines
4.8 KiB
YAML
# Nix container build workflow
|
|
# Triggers on pushes to main that modify containers/*, or via manual dispatch.
|
|
# Detects which containers changed, builds from default.nix, and pushes via
|
|
# skopeo with commit-SHA-based tags: vX.Y.Z-<sha>-nix
|
|
name: Build Container (Nix)
|
|
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
paths: ['containers/**']
|
|
workflow_dispatch:
|
|
inputs:
|
|
container:
|
|
description: 'Container name (directory under containers/)'
|
|
required: true
|
|
type: string
|
|
ref:
|
|
description: 'Commit SHA to build (defaults to current HEAD)'
|
|
required: false
|
|
type: string
|
|
|
|
jobs:
|
|
detect:
|
|
runs-on: nix-container-builder
|
|
outputs:
|
|
containers: ${{ steps.list.outputs.containers }}
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v4
|
|
with:
|
|
fetch-depth: 2
|
|
|
|
- name: Detect changed containers
|
|
id: list
|
|
run: |
|
|
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
|
CONTAINERS='["${{ inputs.container }}"]'
|
|
else
|
|
CONTAINERS=$(git diff --name-only HEAD~1 HEAD -- containers/ \
|
|
| cut -d/ -f2 | sort -u \
|
|
| jq -R -s -c 'split("\n") | map(select(length > 0))')
|
|
fi
|
|
echo "containers=$CONTAINERS" >> "$GITHUB_OUTPUT"
|
|
echo "Containers to build: $CONTAINERS"
|
|
|
|
build:
|
|
needs: detect
|
|
if: needs.detect.outputs.containers != '[]'
|
|
runs-on: nix-container-builder
|
|
strategy:
|
|
matrix:
|
|
container: ${{ fromJson(needs.detect.outputs.containers) }}
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Check for default.nix
|
|
id: check
|
|
run: |
|
|
if [ -f "containers/${{ matrix.container }}/default.nix" ]; then
|
|
echo "exists=true" >> "$GITHUB_OUTPUT"
|
|
else
|
|
echo "No default.nix for ${{ matrix.container }} — skipping"
|
|
echo "exists=false" >> "$GITHUB_OUTPUT"
|
|
fi
|
|
|
|
- name: Extract version and SHA
|
|
if: steps.check.outputs.exists == 'true'
|
|
id: meta
|
|
run: |
|
|
CONTAINER="${{ matrix.container }}"
|
|
NIX_FILE="containers/$CONTAINER/default.nix"
|
|
|
|
# Try extracting version = "..." from the nix file (e.g. ntfy)
|
|
VERSION=$(grep -m1 '^\s*version\s*=\s*"' "$NIX_FILE" \
|
|
| sed 's/.*"\(.*\)".*/\1/' || true)
|
|
|
|
# Fall back to CONTAINER_APP_VERSION from Dockerfile (e.g. nettest)
|
|
if [ -z "$VERSION" ] && [ -f "containers/$CONTAINER/Dockerfile" ]; then
|
|
VERSION=$(grep -m1 '^ARG CONTAINER_APP_VERSION=' \
|
|
"containers/$CONTAINER/Dockerfile" \
|
|
| sed 's/^ARG CONTAINER_APP_VERSION=//')
|
|
fi
|
|
|
|
# Last resort: nix eval for nixpkgs packages (e.g. authentik)
|
|
if [ -z "$VERSION" ]; then
|
|
VERSION=$(nix --extra-experimental-features "nix-command flakes" \
|
|
eval --raw "nixpkgs#${CONTAINER}.version")
|
|
fi
|
|
|
|
if [ -z "$VERSION" ]; then
|
|
echo "Error: Could not determine version for $CONTAINER"
|
|
exit 1
|
|
fi
|
|
|
|
REF="${{ inputs.ref }}"
|
|
if [ -z "$REF" ]; then
|
|
REF="${GITHUB_SHA}"
|
|
fi
|
|
SHORT_SHA=$(echo "$REF" | head -c 7)
|
|
|
|
# Ensure version starts with 'v'
|
|
case "$VERSION" in
|
|
v*) ;;
|
|
*) VERSION="v${VERSION}" ;;
|
|
esac
|
|
|
|
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
|
echo "sha=$SHORT_SHA" >> "$GITHUB_OUTPUT"
|
|
echo "Version: $VERSION, SHA: $SHORT_SHA"
|
|
|
|
- name: Resolve nixpkgs
|
|
if: steps.check.outputs.exists == 'true'
|
|
id: nixpkgs
|
|
run: |
|
|
NIXPKGS_PATH=$(nix flake metadata nixpkgs --json | jq -r '.path')
|
|
echo "Resolved nixpkgs: $NIXPKGS_PATH"
|
|
echo "path=$NIXPKGS_PATH" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Build with nix
|
|
if: steps.check.outputs.exists == 'true'
|
|
env:
|
|
NIX_PATH: "nixpkgs=${{ steps.nixpkgs.outputs.path }}"
|
|
run: |
|
|
echo "Building containers/${{ matrix.container }}/default.nix"
|
|
echo "NIX_PATH=$NIX_PATH"
|
|
nix-build "containers/${{ matrix.container }}/default.nix" -o result
|
|
echo "Build complete: $(readlink result)"
|
|
|
|
- name: Push to registry
|
|
if: steps.check.outputs.exists == 'true'
|
|
env:
|
|
ZOT_CI_API_KEY: ${{ secrets.ZOT_CI_API_KEY }}
|
|
run: |
|
|
CONTAINER="${{ matrix.container }}"
|
|
VERSION="${{ steps.meta.outputs.version }}"
|
|
SHORT_SHA="${{ steps.meta.outputs.sha }}"
|
|
IMAGE="registry.ops.eblu.me/blumeops/$CONTAINER:${VERSION}-${SHORT_SHA}-nix"
|
|
|
|
echo "Pushing to $IMAGE"
|
|
skopeo copy \
|
|
--dest-creds="zot-ci:$ZOT_CI_API_KEY" \
|
|
"docker-archive:result" \
|
|
"docker://$IMAGE"
|
|
echo "Push complete: $IMAGE"
|