Harden zot registry, pt 1 (#231)

## Summary
- Enable OIDC + API key authentication on zot with anonymous pull preserved
- Enforce tag immutability for version tags
- Adopt commit-SHA-based container image tagging

Details in the [[harden-zot-registry]] Mikado chain (`mise run docs-mikado harden-zot-registry`).

## Test plan
- [ ] Anonymous pull still works
- [ ] Unauthenticated push fails (401)
- [ ] CI container builds pass with new auth and tagging
- [ ] `mise run services-check` passes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Reviewed-on: https://forge.ops.eblu.me/eblume/blumeops/pulls/231
This commit is contained in:
Erich Blume 2026-02-20 22:50:01 -08:00
commit 0e2c10176d
28 changed files with 743 additions and 30 deletions

View file

@ -1 +1 @@
Create C2 Mikado cards for harden-zot-registry: root goal and three prerequisite cards (register-zot-oidc-client, wire-ci-registry-auth, enforce-tag-immutability).
Expand harden-zot-registry Mikado chain: add prereqs for container version sync check, pin container versions, and Dagger nix build function.

View file

@ -16,7 +16,7 @@ Discovered while attempting [[deploy-authentik]]: the deployment references `reg
## What to Do
1. Verify `containers/authentik/default.nix` builds on ringtail (the Nix builder runs there)
1. Verify `containers/authentik/default.nix` builds — locally via Dagger (`dagger call build-nix --src=. --container-name=authentik`) or on ringtail (the CI nix builder runs there)
2. The `ak` entrypoint needs bash (included via `bashInteractive`) and orchestrates both `server` and `worker` subcommands
3. Tag and release: `mise run container-tag-and-release authentik v1.0.0`
4. Verify the `-nix` tagged image appears in the registry

View file

@ -1,6 +1,6 @@
---
title: Build Container Image
modified: 2026-02-19
modified: 2026-02-20
last-reviewed: 2026-02-15
tags:
- how-to
@ -38,7 +38,13 @@ A container can have one or both build files. The directory name becomes the ima
dagger call build --src=. --container-name=<name>
```
**Nix** — test with nix-build (requires nix, e.g. on [[ringtail]]):
**Nix** — test with Dagger (no local nix required):
```bash
dagger call build-nix --src=. --container-name=<name> export --path=./<name>.tar.gz
```
Or with nix-build directly (requires nix, e.g. on [[ringtail]]):
```bash
nix-build containers/<name>/default.nix -o result

View file

@ -73,6 +73,10 @@ Mikado chain for hardening the zot registry. Track progress with `mise run docs-
- [[wire-ci-registry-auth]]
- [[enforce-tag-immutability]]
- [[adopt-commit-based-container-tags]]
- [[add-container-version-sync-check]]
- [[pin-container-versions]]
- [[add-dagger-nix-build]]
- [[fix-ntfy-nix-version]]
## Authentik

View file

@ -0,0 +1,82 @@
---
title: Add Container Version Sync Check
modified: 2026-02-20
requires:
- pin-container-versions
- add-dagger-nix-build
- fix-ntfy-nix-version
tags:
- how-to
- containers
- ci
- zot
---
# Add Container Version Sync Check
Add a pre-commit check that validates version consistency across the three places container versions are declared: Dockerfile ARGs, `service-versions.yaml`, and nix derivations. No VERSION files needed — the existing sources are the source of truth, and the check enforces they agree.
## Context
Discovered during analysis of [[adopt-commit-based-container-tags]]: the new commit-SHA-based image tags need a reliable version source (`vX.Y.Z-<sha>`). Versions are currently scattered across Dockerfile ARGs (varying naming conventions), `service-versions.yaml` entries (many still `null`), and nix derivations (implicit from nixpkgs). A sync check ensures these stay consistent without adding a redundant fourth source.
## What Was Done
### 1. Created `mise run container-version-check` task
A typer-based uv-script that iterates over `containers/*/` and validates five rules per container:
1. Any Dockerfile must declare `ARG CONTAINER_APP_VERSION=<value>`
2. Any `default.nix` must produce a version via `dagger call nix-version`
3. At least one build file must exist (Dockerfile or default.nix)
4. A matching `service-versions.yaml` entry must exist with non-null `current-version`
5. All resolved versions from (1), (2), and (4) must agree (v-prefix stripped for comparison)
Scoping: by default only checks containers changed vs main. `--all-files` checks everything. If `service-versions.yaml` itself changed, all containers are checked.
Blacklisted containers (utility images, not tracked services): `kubectl`, `nettest`.
Container-to-service name mapping: `quartz``docs`, `kiwix-serve``kiwix`.
### 2. Added pre-commit hook
```yaml
- id: container-version-check
name: container-version-check
entry: mise run container-version-check
language: system
files: ^(containers/|service-versions\.yaml)
pass_filenames: false
```
### 3. Populated `service-versions.yaml`
Filled in `current-version` for all hybrid services: navidrome (v0.60.3), miniflux (2.2.17), teslamate (v2.2.0), transmission (4.0.6-r4), kiwix (3.8.1), forgejo-runner (0.19.11). Added authentik (2025.10.1) as a new hybrid entry.
### ntfy nix version skew (resolved)
The check discovered that ntfy's Dockerfile pins v2.17.0 but nixpkgs has ntfy-sh 2.15.0. This was resolved in [[fix-ntfy-nix-version]] by building a custom nix derivation from the forge mirror. The version check now extracts the version from local nix files via regex, falling back to Dagger for unmodified nixpkgs packages.
## Key Files
| File | Change |
|------|--------|
| `mise-tasks/container-version-check` | New: typer CLI sync validation script |
| `.pre-commit-config.yaml` | Add `container-version-check` hook |
| `service-versions.yaml` | Populate `current-version` for all hybrid services + authentik |
## Verification
- [x] `mise run container-version-check --all-files` passes with no errors
- [x] Intentionally changing a Dockerfile ARG without updating `service-versions.yaml` fails the check
- [x] `service-versions.yaml` has `current-version` populated for all hybrid services
- [x] Nix-only container versions (authentik) checked via Dagger
- [x] ntfy nix version resolved via [[fix-ntfy-nix-version]]
## Related
- [[pin-container-versions]] — Prereq: containers need parseable version ARGs first
- [[add-dagger-nix-build]] — Prereq: nix version extraction
- [[fix-ntfy-nix-version]] — Prereq: ntfy nix derivation version skew
- [[adopt-commit-based-container-tags]] — Parent: CI uses the same version extraction at build time
- [[harden-zot-registry]] — Root goal

View file

@ -0,0 +1,97 @@
---
title: Add Dagger Nix Build Function
modified: 2026-02-20
status:
tags:
- how-to
- containers
- ci
- dagger
- zot
---
# Add Dagger Nix Build Function
Add Dagger functions for building nix container images and extracting version info from nix derivations. This enables local nix container evaluation and provides the version extraction mechanism needed by [[add-container-version-sync-check]].
## Context
Discovered during analysis of [[adopt-commit-based-container-tags]]: nix containers (authentik, ntfy, nettest) derive their bundled app version from the nixpkgs pin, not from an explicit declaration. To validate that a VERSION file matches the actual nix-built version, we need a way to query the version from nix.
Currently, nix containers can only be built on ringtail (the `nix-container-builder` runner). There is no local build path for developers — the only option is to push and wait for CI. Adding a Dagger-based nix build gives both local evaluation and version extraction.
## What to Do
### 1. Add `build_nix` Dagger function
A new function in `.dagger/src/blumeops_ci/main.py` that builds a nix container inside a `nixos/nix` container:
```python
@function
async def build_nix(
self, src: dagger.Directory, container_name: str
) -> dagger.File:
"""Build a nix container from containers/<name>/default.nix. Returns the image tarball."""
# Uses NIX_IMAGE (nixos/nix:2.33.3) — already defined in the module
# Runs nix-build inside the container
# Returns the docker-archive tarball
```
This mirrors the existing `build` function (Dockerfile) but for nix. The result is a docker-archive tarball that can be loaded with `docker load` or pushed with `skopeo`.
### 2. Add `nix_version` Dagger function
A function that extracts the version of a specific nix package from the nixpkgs pin:
```python
@function
async def nix_version(
self, src: dagger.Directory, package: str
) -> str:
"""Extract the version of a nixpkgs package. Returns version string."""
# nix eval --raw nixpkgs#<package>.version
```
This lets the version sync check run `dagger call nix-version --src=. --package=authentik` to get the actual version that would be built.
### 3. Add `publish_nix` Dagger function (optional)
If useful, a combined build-and-push that mirrors `publish` but for nix images:
```python
@function
async def publish_nix(
self, src: dagger.Directory, container_name: str, version: str,
registry: str = "registry.ops.eblu.me",
) -> str:
"""Build nix container and push to registry via skopeo."""
```
This would give a `dagger call publish-nix` path parallel to the existing `dagger call publish`.
## Nix in Dagger
The `flake_lock` function already demonstrates running nix inside Dagger using `nixos/nix:2.33.3`. The nix build function follows the same pattern but needs:
- `NIX_PATH` set to resolved nixpkgs (same as the CI workflow does)
- `--extra-experimental-features "nix-command flakes"` for `nix eval`
- The full repo source mounted (nix files may reference other files like `test-connectivity.sh`)
## Key Files
| File | Change |
|------|--------|
| `.dagger/src/blumeops_ci/main.py` | Add `build_nix`, `nix_version`, optionally `publish_nix` |
## Verification
- [ ] `dagger call build-nix --src=. --container-name=nettest` produces a valid docker-archive tarball
- [ ] `dagger call nix-version --src=. --package=ntfy-sh` returns the correct version string
- [ ] `dagger call nix-version --src=. --package=authentik` returns the Authentik version
- [ ] Tarball from `build-nix` can be loaded with `docker load` and run locally
## Related
- [[add-container-version-sync-check]] — Parent: needs nix version extraction for sync check
- [[adopt-commit-based-container-tags]] — Grandparent goal
- [[dagger]] — Dagger reference

View file

@ -2,6 +2,8 @@
title: Adopt Commit-Based Container Tags
modified: 2026-02-20
status: active
requires:
- add-container-version-sync-check
tags:
- how-to
- containers
@ -35,7 +37,12 @@ Both the Dockerfile and Nix workflows fire for each trigger, each bailing out if
### Version Source
Each container declares the version of its primary bundled app. The mechanism for declaring this (e.g., a `VERSION` file, parsing a Dockerfile `ARG`, or a convention per container) should be determined during implementation.
Each container's version is extracted at build time from existing declarations — no separate VERSION file:
- **Dockerfile builds**: parsed from `ARG CONTAINER_APP_VERSION=<value>` in the Dockerfile
- **Nix builds**: extracted via `dagger call nix-version` or `nix eval`
The [[add-container-version-sync-check]] pre-commit check ensures these declarations stay in sync with `service-versions.yaml`. See [[pin-container-versions]] for the work to ensure every container has a parseable version.
### Image Tag Format

View file

@ -0,0 +1,41 @@
---
title: Fix ntfy Nix Version
modified: 2026-02-20
tags:
- how-to
- containers
- nix
- zot
---
# Fix ntfy Nix Version
Override the nixpkgs ntfy-sh derivation to build v2.17.0 from the forge mirror, aligning the nix-built container with the Dockerfile version.
## Context
Discovered during [[add-container-version-sync-check]]: the ntfy container has both a Dockerfile and a `default.nix`. The Dockerfile builds v2.17.0 from `forge.ops.eblu.me/eblume/ntfy.git`, but the nix derivation uses `pkgs.ntfy-sh` from nixpkgs which is pinned at 2.15.0. The version sync check currently excludes ntfy from nix version validation as a workaround.
## What Was Done
Replaced the nixpkgs `pkgs.ntfy-sh` reference in `containers/ntfy/default.nix` with a custom derivation that builds v2.17.0 from the forge mirror using `fetchgit`, `buildNpmPackage` (web UI), and `buildGoModule` (server). Docs are skipped (placeholder for `go:embed`, matching the Dockerfile approach).
The `container-version-check` script was updated to extract versions from local nix files via regex (`version = "X.Y.Z"`) before falling back to the Dagger `nix-version` function for unmodified nixpkgs packages. This avoids the issue where `nix eval nixpkgs#ntfy-sh.version` returns the upstream 2.15.0 instead of our overridden 2.17.0.
## Key Files
| File | Change |
|------|--------|
| `containers/ntfy/default.nix` | Custom derivation building v2.17.0 from forge |
| `mise-tasks/container-version-check` | Regex-based local nix version extraction |
## Verification
- [x] `dagger call build-nix --src=. --container-name=ntfy` produces a working image
- [x] Version extractable from local `default.nix` via regex (2.17.0)
- [x] `mise run container-version-check --all-files` passes with ntfy included
## Related
- [[add-container-version-sync-check]] — Parent: needs ntfy in NIX_PACKAGE_MAP
- [[harden-zot-registry]] — Root goal

View file

@ -0,0 +1,53 @@
---
title: Pin Container Versions
modified: 2026-02-20
tags:
- how-to
- containers
- ci
- zot
---
# Pin Container Versions
Ensure every container has an explicit, parseable version declaration so that [[add-container-version-sync-check]] has something to validate against.
## Context
Discovered during analysis of [[adopt-commit-based-container-tags]]: containers needed a uniform, parseable version declaration for the sync check. Most containers already had version ARGs (miniflux, navidrome, ntfy, etc.), but with inconsistent naming (`NAVIDROME_VERSION`, `MINIFLUX_VERSION`, etc.), and several containers (devpi, cv, quartz, nettest) had none.
## What Was Done
Every container Dockerfile now declares `ARG CONTAINER_APP_VERSION=X.Y.Z` as its first ARG, providing a uniform parsing target. Containers that use the version in build commands chain it to a semantic ARG:
```dockerfile
ARG CONTAINER_APP_VERSION=v0.60.3
ARG NAVIDROME_VERSION=${CONTAINER_APP_VERSION}
```
Specific changes:
- **devpi**: Pinned devpi-server==6.19.1 and devpi-web==5.0.1
- **cv**: `CONTAINER_APP_VERSION=1.0.3` (matches latest Forgejo package release)
- **quartz**: `CONTAINER_APP_VERSION=1.28.2` (pinned nginx:1.28.2-alpine base)
- **nettest**: `CONTAINER_APP_VERSION=0.1.0` (internal, no upstream)
- **All others**: Existing versions carried forward with new uniform ARG pattern
## Key Files
| File | Change |
|------|--------|
| `containers/*/Dockerfile` | Add `ARG CONTAINER_APP_VERSION` to all 13 containers |
| `service-versions.yaml` | Populate `current-version` for devpi, cv, docs |
## Verification
- [x] Every container Dockerfile has `ARG CONTAINER_APP_VERSION=X.Y.Z`
- [x] ARG chaining tested with Docker build (nginx:1.28.2-alpine)
- [x] devpi container pins pip package versions
- [x] cv version matches Forgejo package release (1.0.3)
- [x] quartz pins nginx base image to stable (1.28.2)
## Related
- [[add-container-version-sync-check]] — Parent: needs parseable versions for sync check
- [[adopt-commit-based-container-tags]] — Grandparent goal

View file

@ -1,6 +1,6 @@
---
title: Dagger
modified: 2026-02-12
modified: 2026-02-20
tags:
- reference
- ci-cd
@ -27,7 +27,10 @@ Build engine for BlumeOps CI/CD pipelines. Replaces shell-based build scripts wi
|----------|-----------|-------------|
| `build` | `(src, container_name) → Container` | Build a container from `containers/<name>/Dockerfile` |
| `publish` | `(src, container_name, version, registry?) → str` | Build and push to registry (default: `registry.ops.eblu.me`) |
| `build_nix` | `(src, container_name) → File` | Build a nix container from `containers/<name>/default.nix`, return docker-archive tarball |
| `nix_version` | `(package) → str` | Extract the version of a nixpkgs package |
| `build_docs` | `(src, version) → File` | Build Quartz docs site, return docs tarball |
| `flake_lock` | `(src, flake_path?) → File` | Resolve flake inputs, return updated `flake.lock` |
## CLI Examples
@ -44,6 +47,12 @@ dagger call --interactive build --src=. --container-name=devpi
# Publish a container to zot
dagger call publish --src=. --container-name=devpi --version=v1.1.0
# Build a nix container (no local nix required)
dagger call build-nix --src=. --container-name=nettest export --path=./nettest.tar.gz
# Check a nixpkgs package version
dagger call nix-version --package=authentik
# Build docs tarball locally
dagger call build-docs --src=. --version=dev export --path=./docs-dev.tar.gz