From efa9806bfa0587d98708d4a0bd4b949f3b852408 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Sun, 1 Mar 2026 13:45:00 -0800 Subject: [PATCH] C2: Build authentik from source (Mikado chain) (#274) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Mikado Chain: build-authentik-from-source Replace `pkgs.authentik` from nixpkgs with a custom Nix derivation built from source. This removes the dependency on the nixpkgs packaging timeline and gives full version control. Target version: **2025.12.4** (nixpkgs reference, upgrading from deployed 2025.10.1). ### Dependency Graph ``` build-authentik-from-source (goal) ├── authentik-go-server-derivation │ ├── authentik-api-client-generation ← IN PROGRESS │ └── authentik-python-backend-derivation ├── authentik-web-ui-derivation │ └── authentik-api-client-generation ← IN PROGRESS └── authentik-python-backend-derivation ``` ### Ready Leaves - `authentik-api-client-generation` — Go + TypeScript client generation from OpenAPI schema - `authentik-python-backend-derivation` — Django backend with 60+ deps, 4 in-tree packages ### Architecture Ported from [nixpkgs `pkgs/by-name/au/authentik/package.nix`](https://github.com/NixOS/nixpkgs/tree/master/pkgs/by-name/au/authentik): - `source.nix` — shared version/source fetch - `client-go.nix` — Go API client generation - `client-ts.nix` — TypeScript API client generation - `api-go-vendor-hook.nix` — Go vendor directory injection hook - (more components to follow as leaves are closed) ### Related Cards - [[build-authentik-from-source]] — Goal card - [[authentik-api-client-generation]] - [[authentik-python-backend-derivation]] - [[authentik-web-ui-derivation]] - [[authentik-go-server-derivation]] Reviewed-on: https://forge.ops.eblu.me/eblume/blumeops/pulls/274 --- containers/authentik/api-go-vendor-hook.nix | 28 ++++ containers/authentik/authentik-django.nix | 153 ++++++++++++++++++ containers/authentik/authentik-server.nix | 64 ++++++++ containers/authentik/client-go.nix | 47 ++++++ containers/authentik/client-ts.nix | 36 +++++ containers/authentik/default.nix | 49 ++++-- containers/authentik/python-deps.nix | 125 ++++++++++++++ containers/authentik/sources.nix | 30 ++++ containers/authentik/test-build.nix | 44 +++++ containers/authentik/webui-deps.nix | 51 ++++++ containers/authentik/webui.nix | 80 +++++++++ .../authentik-source-build.infra.md | 2 +- docs/how-to/agent-change-process.md | 2 +- .../authentik-api-client-generation.md | 27 +++- .../authentik-go-server-derivation.md | 3 +- .../authentik-python-backend-derivation.md | 69 +++++--- .../authentik/authentik-web-ui-derivation.md | 37 ++--- .../authentik/build-authentik-from-source.md | 61 ++++--- .../authentik/mirror-authentik-build-deps.md | 40 +++++ docs/how-to/how-to.md | 1 + service-versions.yaml | 4 +- 21 files changed, 868 insertions(+), 85 deletions(-) create mode 100644 containers/authentik/api-go-vendor-hook.nix create mode 100644 containers/authentik/authentik-django.nix create mode 100644 containers/authentik/authentik-server.nix create mode 100644 containers/authentik/client-go.nix create mode 100644 containers/authentik/client-ts.nix create mode 100644 containers/authentik/python-deps.nix create mode 100644 containers/authentik/sources.nix create mode 100644 containers/authentik/test-build.nix create mode 100644 containers/authentik/webui-deps.nix create mode 100644 containers/authentik/webui.nix create mode 100644 docs/how-to/authentik/mirror-authentik-build-deps.md diff --git a/containers/authentik/api-go-vendor-hook.nix b/containers/authentik/api-go-vendor-hook.nix new file mode 100644 index 0000000..3c7e9d6 --- /dev/null +++ b/containers/authentik/api-go-vendor-hook.nix @@ -0,0 +1,28 @@ +# Setup hook that injects generated Go API client into the vendor directory +# Replaces vendor/goauthentik.io/api/v3/ with freshly generated client-go output +# Skips during FOD (fixed-output derivation) builds to keep vendorHash stable +{ pkgs ? import { }, sources ? import ./sources.nix { inherit pkgs; } }: + +let + client-go = import ./client-go.nix { inherit pkgs sources; }; +in +pkgs.makeSetupHook + { + name = "authentik-api-go-vendor-hook"; + } + ( + pkgs.writeShellScript "authentik-api-go-vendor-hook" '' + authentikApiGoVendorHook() { + chmod -R +w vendor/goauthentik.io/api + rm -rf vendor/goauthentik.io/api/v3 + cp -r ${client-go} vendor/goauthentik.io/api/v3 + + echo "Finished authentikApiGoVendorHook" + } + + # don't run for FOD, e.g. the goModules build + if [ -z ''${outputHash-} ]; then + postConfigureHooks+=(authentikApiGoVendorHook) + fi + '' + ) diff --git a/containers/authentik/authentik-django.nix b/containers/authentik/authentik-django.nix new file mode 100644 index 0000000..eb07abd --- /dev/null +++ b/containers/authentik/authentik-django.nix @@ -0,0 +1,153 @@ +# Authentik Python/Django backend +# +# Assembles the final package from: +# 1. python-deps FOD (venv with stripped store references) +# 2. opencontainers git dependency (fetched via Nix) +# 3. Workspace packages (ak-guardian, django-channels-postgres, etc.) +# 4. Authentik application source +# 5. Lifecycle scripts, blueprints, manage.py +# +# autoPatchelfHook restores RPATHs that were stripped in the FOD. +# +# Optional input: webui derivation. When provided, resolves @webui@ store +# path placeholders in Python source. When null (default), leaves placeholders +# for isolated testing. +# +# Output: +# $out/bin/python3.14 venv python (symlink to nix python314) +# $out/lib/python3.14/site-packages/ all Python packages +# $out/lifecycle/ lifecycle scripts (symlink) +# $out/blueprints/ YAML blueprints +# $out/manage.py Django management script +{ pkgs ? import { } +, sources ? import ./sources.nix { inherit pkgs; } +, webui ? null +}: + +let + python-deps = import ./python-deps.nix { inherit pkgs sources; }; + + # opencontainers is a git dependency not on PyPI — fetch separately + opencontainers-src = pkgs.fetchFromGitHub { + owner = "vsoch"; + repo = "oci-python"; + rev = "ceb4fcc090851717a3069d78e85ceb1e86c2740c"; + hash = "sha256-Q6SJed0K6eIrqQ9mNAD4RGx+YCJvnI5E+0KGp5fBtTU="; + }; + + # When webui is provided, resolve paths directly; otherwise use placeholder + webuiPath = if webui != null then "${webui}" else "@webui@"; + + sp = "$out/lib/python3.14/site-packages"; +in + +pkgs.stdenv.mkDerivation { + pname = "authentik-django"; + version = sources.version; + inherit (sources) meta; + + src = sources.src; + + nativeBuildInputs = with pkgs; [ + autoPatchelfHook # restores RPATHs stripped in the FOD + ]; + + # Libraries that autoPatchelfHook resolves NEEDED entries against + buildInputs = with pkgs; [ + python314 + stdenv.cc.cc.lib # libstdc++, libgcc_s + libxml2 + libxslt + xmlsec + openssl + libpq + krb5.lib + libtool.lib + libffi + zlib + ]; + + dontBuild = true; + + installPhase = '' + runHook preInstall + + # --- Copy venv from FOD --- + cp -r ${python-deps} $out + chmod -R +w $out + + # Restore python path in pyvenv.cfg (was replaced with @python@ in FOD) + sed -i "s|@python@|${pkgs.python314}|g" $out/pyvenv.cfg + + # Recreate bin/ (was removed in FOD to strip python store refs) + mkdir -p $out/bin + ln -s ${pkgs.python314}/bin/python3.14 $out/bin/python3.14 + ln -s python3.14 $out/bin/python3 + ln -s python3.14 $out/bin/python + + # Recreate entry point scripts that were in the venv's bin/ + # (gunicorn, etc. — use python from this venv) + for ep in gunicorn uvicorn dramatiq dumb-init; do + if [ -e ${sp}/$ep ] || $out/bin/python3.14 -c "import $ep" 2>/dev/null; then + cat > $out/bin/$ep << SCRIPT + #!$out/bin/python3.14 + import sys + from importlib.metadata import entry_points + eps = entry_points(group='console_scripts', name='$ep') + if eps: + sys.exit(eps[0].load()()) + SCRIPT + chmod +x $out/bin/$ep + fi + done 2>/dev/null || true + + # --- opencontainers (git dependency, pure Python) --- + cp -r ${opencontainers-src}/opencontainers ${sp}/opencontainers + + # --- Workspace packages (pure Python — direct copy) --- + # ak-guardian: hatch config maps to "guardian" package + cp -r packages/ak-guardian/guardian ${sp}/guardian + cp -r packages/django-channels-postgres/django_channels_postgres ${sp}/ + cp -r packages/django-dramatiq-postgres/django_dramatiq_postgres ${sp}/ + cp -r packages/django-postgres-cache/django_postgres_cache ${sp}/ + + # --- Authentik application + lifecycle --- + cp -r authentik ${sp}/authentik + cp -r lifecycle ${sp}/lifecycle + chmod +x ${sp}/lifecycle/ak + + # --- Patches for Nix store paths --- + + # BASE_DIR: point to $out instead of computing from settings.py's location + substituteInPlace ${sp}/authentik/root/settings.py \ + --replace-fail \ + 'BASE_DIR = Path(__file__).absolute().parent.parent.parent' \ + "BASE_DIR = Path(\"$out\")" + + # blueprints_dir: point to $out/blueprints + substituteInPlace ${sp}/authentik/lib/default.yml \ + --replace-fail 'blueprints_dir: /blueprints' \ + "blueprints_dir: $out/blueprints" + + # Web asset paths: placeholder @webui@ for Go server card to resolve + substituteInPlace ${sp}/authentik/stages/email/utils.py \ + --replace-fail 'Path("web/icons/icon_left_brand.png")' \ + 'Path("${webuiPath}/icons/icon_left_brand.png")' \ + --replace-fail 'Path("web/dist/assets/icons/icon_left_brand.png")' \ + 'Path("${webuiPath}/dist/assets/icons/icon_left_brand.png")' + + # Lifecycle bash script: use Nix store bash (no /usr/bin/env in containers) + substituteInPlace ${sp}/lifecycle/ak \ + --replace-fail '#!/usr/bin/env -S bash' '#!${pkgs.bash}/bin/bash' + + # --- Top-level structure --- + ln -s ${sp}/lifecycle $out/lifecycle + cp -r blueprints $out/blueprints + cp manage.py $out/manage.py + + runHook postInstall + ''; + + # autoPatchelfHook runs in fixupPhase — don't disable it + dontPatchShebangs = true; +} diff --git a/containers/authentik/authentik-server.nix b/containers/authentik/authentik-server.nix new file mode 100644 index 0000000..a99d9f8 --- /dev/null +++ b/containers/authentik/authentik-server.nix @@ -0,0 +1,64 @@ +# Authentik Go HTTP server binary +# +# Builds cmd/server from the authentik source using buildGoModule. +# The compiled binary serves the web UI, REST API, spawns gunicorn +# for the Django backend, and runs the embedded reverse proxy outpost. +# +# Two runtime path dependencies are baked in at compile time: +# - authentik-django: lifecycle scripts (gunicorn launcher) +# - webui: static web assets (dist/ and authentik/ directories) +# +# The apiGoVendorHook replaces vendored goauthentik.io/api/v3 with +# freshly generated client-go output, but only during the real build +# (not the FOD module-download phase), so vendorHash stays stable. +# +# Output: $out/bin/authentik +{ pkgs ? import { } +, sources ? import ./sources.nix { inherit pkgs; } +, authentik-django ? import ./authentik-django.nix { inherit pkgs sources; } +, webui ? null +}: + +let + apiGoVendorHook = import ./api-go-vendor-hook.nix { inherit pkgs sources; }; + + # Web assets path: use real webui derivation if provided, otherwise + # a placeholder directory. The placeholder allows the binary to compile + # and pass --help verification, but web serving won't work at runtime. + webAssetsPath = + if webui != null then webui + else pkgs.runCommand "webui-placeholder" { } '' + mkdir -p $out/dist $out/authentik + ''; +in + +pkgs.buildGoModule { + pname = "authentik-server"; + inherit (sources) version src meta; + + subPackages = [ "cmd/server" ]; + + nativeBuildInputs = [ apiGoVendorHook ]; + + env.CGO_ENABLED = 0; + + postPatch = '' + substituteInPlace internal/gounicorn/gounicorn.go \ + --replace-fail './lifecycle' "${authentik-django}/lifecycle" + substituteInPlace web/static.go \ + --replace-fail './web' "${webAssetsPath}" + substituteInPlace internal/web/static.go \ + --replace-fail './web' "${webAssetsPath}" + ''; + + # Clear postPatch during the module-download FOD phase so that + # substituteInPlace (which references authentik-django and webui + # store paths) doesn't affect vendorHash computation. + overrideModAttrs.postPatch = ""; + + vendorHash = "sha256-bdILiCQgDuzp+VJDVW3z2JxTtxlHkm9tmMHiA/Sx6ts="; + + postInstall = '' + mv $out/bin/server $out/bin/authentik + ''; +} diff --git a/containers/authentik/client-go.nix b/containers/authentik/client-go.nix new file mode 100644 index 0000000..5b8911d --- /dev/null +++ b/containers/authentik/client-go.nix @@ -0,0 +1,47 @@ +# Generate Go API client bindings from authentik's OpenAPI schema +# Uses openapi-generator-cli to produce Go code from schema.yml +{ pkgs ? import { }, sources ? import ./sources.nix { inherit pkgs; } }: + +pkgs.stdenvNoCC.mkDerivation { + pname = "authentik-client-go"; + version = "3.${sources.version}"; + inherit (sources) meta; + + src = sources.client-go-src; + + # Docker volume path /local → local pwd + postPatch = '' + substituteInPlace ./config.yaml \ + --replace-fail '/local' "$(pwd)" + ''; + + nativeBuildInputs = with pkgs; [ + openapi-generator-cli + go + ]; + + buildPhase = '' + runHook preBuild + + openapi-generator-cli generate \ + -i ${sources.src}/schema.yml -o $out \ + -g go \ + -c ./config.yaml + + gofmt -w $out + + runHook postBuild + ''; + + installPhase = '' + runHook preInstall + + cp go.mod go.sum $out + + cd $out + rm -rf test + rm -f .travis.yml git_push.sh + + runHook postInstall + ''; +} diff --git a/containers/authentik/client-ts.nix b/containers/authentik/client-ts.nix new file mode 100644 index 0000000..8ad395b --- /dev/null +++ b/containers/authentik/client-ts.nix @@ -0,0 +1,36 @@ +# Generate TypeScript fetch client bindings from authentik's OpenAPI schema +# Uses openapi-generator-cli to produce TypeScript code, then compiles with tsc +{ pkgs ? import { }, sources ? import ./sources.nix { inherit pkgs; } }: + +pkgs.stdenvNoCC.mkDerivation { + pname = "authentik-client-ts"; + inherit (sources) version src meta; + + # Docker volume path /local → local pwd + postPatch = '' + substituteInPlace ./scripts/api/ts-config.yaml \ + --replace-fail '/local' "$(pwd)" + ''; + + nativeBuildInputs = with pkgs; [ + nodejs + openapi-generator-cli + typescript + ]; + + buildPhase = '' + runHook preBuild + + openapi-generator-cli generate \ + -i ./schema.yml -o $out \ + -g typescript-fetch \ + -c ./scripts/api/ts-config.yaml \ + --additional-properties=npmVersion=${sources.version} \ + --git-repo-id authentik --git-user-id goauthentik + + cd $out + npm run build + + runHook postBuild + ''; +} diff --git a/containers/authentik/default.nix b/containers/authentik/default.nix index 01cc2f1..8c34cb9 100644 --- a/containers/authentik/default.nix +++ b/containers/authentik/default.nix @@ -1,19 +1,41 @@ -# Nix-built Authentik identity provider -# Uses nixpkgs authentik package (ak entrypoint wrapping Go server + Python worker) -# Built with dockerTools.buildLayeredImage for efficient layer caching +# Nix-built Authentik identity provider (from source) +# +# Assembles four component derivations into a container image: +# 1. webui — Lit frontend (esbuild + rollup) +# 2. authentik-django — Python backend + lifecycle scripts +# 3. authentik-server — Go HTTP server binary +# 4. ak wrapper — sets PATH/VIRTUAL_ENV, delegates to lifecycle/ak +# +# Built with dockerTools.buildLayeredImage for efficient layer caching. { pkgs ? import { } }: let - # Wrapper entrypoint that sets up /blueprints symlinks before running ak. - # buildLayeredImage's extraCommands can't access store paths from contents (they're - # in separate layers), so we create the symlinks at container start instead. + sources = import ./sources.nix { inherit pkgs; }; + # Duplicated from sources.nix so build-container-nix.yaml can grep it + version = "2026.2.0"; + webui = import ./webui.nix { inherit pkgs sources; }; + authentik-django = import ./authentik-django.nix { inherit pkgs sources webui; }; + authentik-server = import ./authentik-server.nix { inherit pkgs sources authentik-django webui; }; + + # Wrapper that provides bin/ak with the correct runtime environment. + # lifecycle/ak dispatches: "server" → Go binary, "worker"/"migrate"/etc → Python. + ak = pkgs.writeShellScriptBin "ak" '' + export PYTHONDONTWRITEBYTECODE=1 + export PATH="${authentik-server}/bin:${authentik-django}/bin:$PATH" + export VIRTUAL_ENV="${authentik-django}" + cd "${authentik-django}" + exec "${authentik-django}/lifecycle/ak" "$@" + ''; + + # Container entrypoint: symlink built-in blueprints then run ak. + # buildLayeredImage's extraCommands can't access store paths from contents + # (they're in separate layers), so we create the symlinks at container start. entrypoint = pkgs.writeShellScript "authentik-entrypoint" '' - # Link built-in blueprint dirs from the Nix store into /blueprints - for item in /nix/store/*authentik-django*/blueprints/*/; do + for item in ${authentik-django}/blueprints/*/; do name=$(basename "$item") [ ! -e "/blueprints/$name" ] && ln -s "$item" "/blueprints/$name" 2>/dev/null || true done - exec ${pkgs.authentik}/bin/ak "$@" + exec ${ak}/bin/ak "$@" ''; in @@ -22,7 +44,9 @@ pkgs.dockerTools.buildLayeredImage { tag = "latest"; contents = [ - pkgs.authentik + ak + authentik-django + authentik-server pkgs.bashInteractive pkgs.coreutils pkgs.cacert @@ -30,9 +54,8 @@ pkgs.dockerTools.buildLayeredImage { ]; # Create /blueprints as world-writable so user 65534 can create symlinks at runtime. - # The nixpkgs authentik-django package hardcodes blueprints_dir to its Nix store path, - # making custom blueprints mounted at /blueprints/custom invisible. The entrypoint - # wrapper populates this directory with symlinks to built-in blueprints on each start. + # authentik-django hardcodes blueprints_dir to $out/blueprints; the AUTHENTIK_BLUEPRINTS_DIR + # env var overrides it to /blueprints, where custom blueprints are mounted by k8s ConfigMap. extraCommands = '' mkdir -p blueprints chmod 777 blueprints diff --git a/containers/authentik/python-deps.nix b/containers/authentik/python-deps.nix new file mode 100644 index 0000000..17d557c --- /dev/null +++ b/containers/authentik/python-deps.nix @@ -0,0 +1,125 @@ +# Fixed-output derivation (FOD): download and install all external Python +# dependencies into a venv using uv sync. +# +# FODs get network access because the output hash is declared upfront. +# However, FODs must not reference other Nix store paths in their output. +# Compiled .so files (from sdist builds) contain RPATHs to system libraries +# (libxml2, krb5, etc.) which are Nix store paths. We strip these references +# here; authentik-django.nix restores them via autoPatchelfHook. +# +# The venv's bin/ and pyvenv.cfg also reference the python store path, so we +# replace them with placeholders that the main derivation restores. +# +# When uv.lock changes, reset outputHash to pkgs.lib.fakeHash, build to +# get the correct hash from the error message, then update. +{ pkgs ? import { }, sources ? import ./sources.nix { inherit pkgs; } }: + +pkgs.stdenv.mkDerivation { + pname = "authentik-python-deps"; + version = sources.version; + + src = sources.src; + + nativeBuildInputs = with pkgs; [ + python314 + uv + git # opencontainers is a git dependency in uv.lock + cacert # HTTPS verification for PyPI + GitHub + pkg-config + removeReferencesTo + # Build tools on PATH for sdist compilation + postgresql.pg_config # pg_config for psycopg-c + krb5 # krb5-config for gssapi + ]; + + # System libraries for packages that must build from sdist: + # lxml, xmlsec — pyproject.toml [tool.uv] no-binary-package + # psycopg-c — sdist only on PyPI + # gssapi — no Linux wheels on PyPI + buildInputs = with pkgs; [ + libxml2 + libxslt + xmlsec + openssl + libpq # psycopg-c links against libpq + libtool # libltdl for xmlsec dynamic crypto backend loading + libffi + zlib + ]; + + buildPhase = '' + runHook preBuild + + export HOME=$TMPDIR + export SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt + export GIT_SSL_CAINFO=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt + export UV_PYTHON=${pkgs.python314}/bin/python3.14 + export UV_LINK_MODE=copy + + # gssapi's pre-generated C code uses S4U functions declared in gssapi_ext.h + # but doesn't include it — force-include via compiler flag + export NIX_CFLAGS_COMPILE="''${NIX_CFLAGS_COMPILE:-} -include gssapi/gssapi_ext.h" + + uv sync \ + --frozen \ + --no-install-project \ + --no-install-workspace \ + --no-dev + + runHook postBuild + ''; + + installPhase = '' + runHook preInstall + mv .venv $out + + # --- Strip Nix store references (FODs must be self-contained) --- + # autoPatchelfHook in authentik-django.nix restores correct RPATHs. + + # Replace python store path in pyvenv.cfg with placeholder + sed -i "s|${pkgs.python314}|@python@|g" $out/pyvenv.cfg + + # Remove bin/ entirely — main derivation recreates it + rm -rf $out/bin + + # Strip store refs from .pyc files (contain embedded paths) + find $out -type f -name '*.pyc' -delete + + # Dynamically discover ALL remaining Nix store paths in the output. + # This is more robust than a static list of store paths — any new + # build/runtime dependency is automatically handled. + # Note: || true needed because xargs returns 123 if grep returns 1 + # (no match) on any batch, and pipefail propagates that. + { find $out -type f -print0 \ + | xargs -0 grep -aohE '/nix/store/[a-z0-9]{32}-[^/"[:space:]]+' 2>/dev/null \ + || true; } | sort -u > $TMPDIR/store-refs.txt + echo "Found $(wc -l < $TMPDIR/store-refs.txt) unique store path references to strip" + + # Build remove-references-to args from discovered paths + refs_args="" + while IFS= read -r ref; do + refs_args="$refs_args -t $ref" + done < $TMPDIR/store-refs.txt + + # Strip all discovered references from all files + if [ -n "$refs_args" ]; then + find $out -type f -exec remove-references-to $refs_args {} + 2>/dev/null || true + fi + + # Verify — report any remaining references + remaining=$({ find $out -type f -print0 | xargs -0 grep -cl '/nix/store/' 2>/dev/null || true; } | wc -l) + echo "Files with remaining store references: $remaining" + if [ "$remaining" -gt 0 ]; then + echo "WARNING: Files still containing store references:" + { find $out -type f -print0 | xargs -0 grep -l '/nix/store/' 2>/dev/null || true; } + fi + + runHook postInstall + ''; + + outputHashMode = "recursive"; + outputHashAlgo = "sha256"; + outputHash = "sha256-DtpcYQyI07m7v84D/UC28Tj35R9wye6IX+1D0gMZPgY="; + + dontFixup = true; +} diff --git a/containers/authentik/sources.nix b/containers/authentik/sources.nix new file mode 100644 index 0000000..9134fa8 --- /dev/null +++ b/containers/authentik/sources.nix @@ -0,0 +1,30 @@ +# Centralized version and source pinning for authentik 2026.2.0 +# All sources fetched from forge mirrors for supply chain control +{ pkgs ? import { } }: + +let + version = "2026.2.0"; +in +{ + inherit version; + + # Main authentik repo — provides schema.yml, Python backend, web UI, Go server + src = pkgs.fetchgit { + url = "https://forge.ops.eblu.me/mirrors/authentik.git"; + rev = "version/${version}"; + hash = "sha256-pVQ34cZYX3hlk6hF1aZ/n32xMqTF4Jmp0G0VGDU7iXc="; + }; + + # Go API client repo — provides config.yaml, go.mod, go.sum, templates + client-go-src = pkgs.fetchgit { + url = "https://forge.ops.eblu.me/mirrors/authentik-client-go.git"; + rev = "v3.${version}"; + hash = "sha256-DwXw/0QcSDYQKVhPA8tStrSoZooriQex/9FxSJtR/QY="; + }; + + meta = with pkgs.lib; { + description = "Authentik identity provider"; + homepage = "https://goauthentik.io"; + license = licenses.mit; + }; +} diff --git a/containers/authentik/test-build.nix b/containers/authentik/test-build.nix new file mode 100644 index 0000000..57b7569 --- /dev/null +++ b/containers/authentik/test-build.nix @@ -0,0 +1,44 @@ +# Test harness for building authentik components on ringtail +# Uses builtins.getFlake instead of (ringtail has flakes, no NIX_PATH) +# +# Usage: +# nix-build test-build.nix -A python-deps --extra-experimental-features 'nix-command flakes' +# nix-build test-build.nix -A authentik-django --extra-experimental-features 'nix-command flakes' +# nix-build test-build.nix -A client-go --extra-experimental-features 'nix-command flakes' +# nix-build test-build.nix -A client-ts --extra-experimental-features 'nix-command flakes' +# nix-build test-build.nix -A authentik-server --extra-experimental-features 'nix-command flakes' +# nix-build test-build.nix -A webui-deps --extra-experimental-features 'nix-command flakes' +# nix-build test-build.nix -A webui --extra-experimental-features 'nix-command flakes' +# nix-build test-build.nix -A assembled --extra-experimental-features 'nix-command flakes' +let + pkgs = (builtins.getFlake "nixpkgs").legacyPackages.x86_64-linux; + sources = import ./sources.nix { inherit pkgs; }; + + # Individual components (isolated, no cross-wiring) + _webui = import ./webui.nix { inherit pkgs sources; }; + + # Fully wired assembly (webui → authentik-django → authentik-server) + _authentik-django-assembled = import ./authentik-django.nix { inherit pkgs sources; webui = _webui; }; + _authentik-server-assembled = import ./authentik-server.nix { + inherit pkgs sources; + authentik-django = _authentik-django-assembled; + webui = _webui; + }; +in +{ + # Individual component builds (for debugging in isolation) + python-deps = import ./python-deps.nix { inherit pkgs sources; }; + authentik-django = import ./authentik-django.nix { inherit pkgs sources; }; + client-go = import ./client-go.nix { inherit pkgs sources; }; + client-ts = import ./client-ts.nix { inherit pkgs sources; }; + authentik-server = import ./authentik-server.nix { inherit pkgs sources; }; + webui-deps = import ./webui-deps.nix { inherit pkgs sources; }; + webui = _webui; + + # Fully assembled stack — tests that all components wire together + assembled = pkgs.linkFarm "authentik-assembled-${sources.version}" [ + { name = "authentik-django"; path = _authentik-django-assembled; } + { name = "authentik-server"; path = _authentik-server-assembled; } + { name = "webui"; path = _webui; } + ]; +} diff --git a/containers/authentik/webui-deps.nix b/containers/authentik/webui-deps.nix new file mode 100644 index 0000000..5364a4b --- /dev/null +++ b/containers/authentik/webui-deps.nix @@ -0,0 +1,51 @@ +# Fixed-output derivation for authentik web UI npm dependencies +# +# Runs `npm ci` in the web/ directory to fetch all Node.js dependencies. +# This is a FOD (fixed-output derivation) so it has network access during build +# but the output hash must match exactly. +# +# The output hash is platform-specific because npm downloads platform-specific +# native binaries for esbuild, rollup, and SWC. +# +# Workspace packages (under web/packages/*) have their own node_modules, +# so we collect all node_modules directories via find. +# +# Output: all node_modules directories from the web/ tree +{ pkgs ? import { }, sources ? import ./sources.nix { inherit pkgs; } }: + +pkgs.stdenvNoCC.mkDerivation { + pname = "authentik-webui-deps"; + inherit (sources) version src meta; + + sourceRoot = "${sources.src.name}/web"; + + outputHash = + { + "x86_64-linux" = "sha256-+4cWvFuixCcO7P+z701/0H+Ah/Z5sbLNsdx2Uowqwf4="; + } + .${pkgs.stdenvNoCC.hostPlatform.system} + or (throw "authentik-webui-deps: unsupported host platform ${pkgs.stdenvNoCC.hostPlatform.system}"); + outputHashMode = "recursive"; + + nativeBuildInputs = with pkgs; [ + nodejs_24 + cacert + ]; + + buildPhase = '' + npm ci --cache ./cache --ignore-scripts + rm -r ./cache node_modules/.package-lock.json + ''; + + # Workspace packages install dependencies into separate node_modules + # directories with symlinks between them — copy all of them + installPhase = '' + mkdir $out + find -type d -name node_modules -prune -print \ + -exec mkdir -p $out/{} \; \ + -exec cp -rT {} $out/{} \; + ''; + + dontCheckForBrokenSymlinks = true; + dontPatchShebangs = true; +} diff --git a/containers/authentik/webui.nix b/containers/authentik/webui.nix new file mode 100644 index 0000000..43b4177 --- /dev/null +++ b/containers/authentik/webui.nix @@ -0,0 +1,80 @@ +# Authentik web UI build +# +# Builds the Lit-based TypeScript frontend from the web/ directory. +# Uses esbuild (via wireit) for the main build and rollup for the SFE +# (Standalone Frontend Engine) sub-package. +# +# Inputs: +# - webui-deps: FOD with npm dependencies (node_modules trees) +# - client-ts: generated TypeScript API client from schema.yml +# +# Output: +# $out/dist/ esbuild bundle (admin, user, flow, rac, etc.) +# $out/authentik/ static icons for authentication sources/connectors +{ pkgs ? import { } +, sources ? import ./sources.nix { inherit pkgs; } +, webui-deps ? import ./webui-deps.nix { inherit pkgs sources; } +, client-ts ? import ./client-ts.nix { inherit pkgs sources; } +}: + +pkgs.stdenvNoCC.mkDerivation { + pname = "authentik-webui"; + inherit (sources) version src meta; + + sourceRoot = "${sources.src.name}/web"; + + nativeBuildInputs = with pkgs; [ + nodejs_24 + ]; + + # Hardcode version string instead of importing from package.json + # (the JSON import-with-assertion may not resolve in the Nix build sandbox) + postPatch = '' + substituteInPlace packages/core/version/node.js \ + --replace-fail \ + 'import PackageJSON from "../../../../package.json" with { type: "json" };' \ + "" \ + --replace-fail \ + '(PackageJSON.version);' \ + '"${sources.version}";' + ''; + + buildPhase = '' + runHook preBuild + + # Copy node_modules from the FOD into the build tree + buildRoot=$PWD + pushd ${webui-deps} + find -type d -name node_modules -prune -print \ + -exec cp -rT {} $buildRoot/{} \; + popd + + # Replace the npm-published @goauthentik/api with our generated client + chmod -R +w node_modules/@goauthentik + rm -rf node_modules/@goauthentik/api + ln -sn ${client-ts} node_modules/@goauthentik/api + + # Patch shebangs on build tool binaries so they can run in the sandbox + pushd node_modules/.bin + for tool in rollup wireit lit-localize esbuild; do + [ -L "$tool" ] && patchShebangs "$(readlink "$tool")" 2>/dev/null || true + done + popd + + npm run build + npm run build:sfe + + runHook postBuild + ''; + + installPhase = '' + runHook preInstall + mkdir $out + cp -r dist $out/dist + cp -r authentik $out/authentik + runHook postInstall + ''; + + NODE_ENV = "production"; + NODE_OPTIONS = "--openssl-legacy-provider"; +} diff --git a/docs/changelog.d/authentik-source-build.infra.md b/docs/changelog.d/authentik-source-build.infra.md index 8385723..757ad9e 100644 --- a/docs/changelog.d/authentik-source-build.infra.md +++ b/docs/changelog.d/authentik-source-build.infra.md @@ -1 +1 @@ -Start C2 Mikado chain: build authentik from a custom Nix derivation (from source) to replace nixpkgs dependency and gain full version control. +Build authentik 2026.2.0 from source via custom Nix derivation, replacing the nixpkgs `pkgs.authentik` dependency. Four components (API client generation, Python backend, web UI, Go server) assembled into a single container image with full supply chain control via forge mirrors. diff --git a/docs/how-to/agent-change-process.md b/docs/how-to/agent-change-process.md index e01bbad..b9f70ff 100644 --- a/docs/how-to/agent-change-process.md +++ b/docs/how-to/agent-change-process.md @@ -163,7 +163,7 @@ The `mikado-branch-invariant-check` commit-msg hook validates this convention an 2. **Open a PR** after the first card commits so the user can review the Mikado graph 3. **Work leaf nodes** — pick a leaf (a card with `status: active` and no unmet `requires`): - Commit code changes (`C2(): impl ...`) that progress toward closing it - - **Verify the change works** (deploy from branch, run tests, etc.) before closing + - **Verify the card's own deliverables** (deploy from branch, run tests, etc.) before closing. "Works" means the card's stated outputs are correct — not that downstream consumers have integrated them. If a downstream card later discovers the output doesn't fit, that's a new prerequisite discovery handled by the normal reset mechanism. - Commit the card closure (`C2(): close ...`) — remove `status: active` - Push to origin — this is the save point 4. **End the cycle** — after pushing a closed leaf node, prompt the user to review the PR and suggest ending the session. Each closed leaf is a natural stopping point; the chain is designed to be resumed later. Don't rush into the next leaf without the user's go-ahead. diff --git a/docs/how-to/authentik/authentik-api-client-generation.md b/docs/how-to/authentik/authentik-api-client-generation.md index 1624f72..dcb045b 100644 --- a/docs/how-to/authentik/authentik-api-client-generation.md +++ b/docs/how-to/authentik/authentik-api-client-generation.md @@ -1,7 +1,8 @@ --- title: Generate Authentik API Clients modified: 2026-02-28 -status: active +requires: + - mirror-authentik-build-deps tags: - how-to - authentik @@ -32,6 +33,30 @@ Both clients are generated from the same `schema.yml` OpenAPI spec in the main a - TypeScript client replaces `web/node_modules/@goauthentik/api/` in the web UI build - The nixpkgs derivation patches the generated Go client (`client-go-config.patch`) — check if still needed +## Testing on Ringtail + +Use this ad-hoc `test-build.nix` harness (not committed to the repo): + +```nix +# test-build.nix +let + pkgs = (builtins.getFlake "nixpkgs").legacyPackages.x86_64-linux; + sources = import ./sources.nix { inherit pkgs; }; +in +{ + client-go = import ./client-go.nix { inherit pkgs sources; }; + client-ts = import ./client-ts.nix { inherit pkgs sources; }; + api-go-vendor-hook = import ./api-go-vendor-hook.nix { inherit pkgs sources; }; +} +``` + +```fish +set tmpdir (ssh ringtail 'mktemp -d /tmp/authentik-test.XXXXXX') +scp containers/authentik/*.nix ringtail:$tmpdir/ +ssh ringtail "cd $tmpdir && nix-build test-build.nix -A client-go --extra-experimental-features 'nix-command flakes'" +ssh ringtail "rm -rf $tmpdir" +``` + ## Related - [[build-authentik-from-source]] — Parent goal diff --git a/docs/how-to/authentik/authentik-go-server-derivation.md b/docs/how-to/authentik/authentik-go-server-derivation.md index f83275e..65224c3 100644 --- a/docs/how-to/authentik/authentik-go-server-derivation.md +++ b/docs/how-to/authentik/authentik-go-server-derivation.md @@ -1,7 +1,6 @@ --- title: Build Authentik Go Server -modified: 2026-02-28 -status: active +modified: 2026-03-01 requires: - authentik-api-client-generation - authentik-python-backend-derivation diff --git a/docs/how-to/authentik/authentik-python-backend-derivation.md b/docs/how-to/authentik/authentik-python-backend-derivation.md index 5df7365..47b3ed8 100644 --- a/docs/how-to/authentik/authentik-python-backend-derivation.md +++ b/docs/how-to/authentik/authentik-python-backend-derivation.md @@ -1,7 +1,8 @@ --- title: Build Authentik Python Backend -modified: 2026-02-28 -status: active +modified: 2026-03-01 +requires: + - mirror-authentik-build-deps tags: - how-to - authentik @@ -14,32 +15,60 @@ Build `authentik-django` — the Python/Django application that forms the core b ## Context -This is the most complex component. The nixpkgs derivation uses `python3.override` with extensive `packageOverrides` to handle authentik's non-standard dependencies: +Authentik 2026.2.0 requires Python 3.14 (`requires-python = "==3.14.*"`). The nixpkgs reference derivation (2025.12.4) builds all 60+ Python deps through nix's `python3.override` with `packageOverrides`. This approach breaks on Python 3.14 because many nixpkgs python314 packages haven't been updated — astor, dacite, exceptiongroup, and pydantic-core all fail to build. -- **4 in-tree Python packages** built from the monorepo: `ak-guardian`, `django-channels-postgres`, `django-dramatiq-postgres`, `django-postgres-cache` -- **Forked `djangorestframework`** from `authentik-community/django-rest-framework` (specific commit) -- **Pinned `dramatiq`** at 1.17.1 (upstream uses newer versions that break authentik) -- **Django 5** forced via `django_5` -- **60+ Python dependencies** from nixpkgs +Instead of carrying individual overrides for each broken package, we use **`uv`** to install Python dependencies from PyPI, where upstream maintainers have already published Python 3.14-compatible wheels. Nix provides only the Python interpreter and system libraries. -Post-install, the derivation patches hardcoded paths in `settings.py`, `default.yml`, `email/utils.py`, and `files/backends/file.py` to reference Nix store paths. +## Approach: uv sync FOD + autoPatchelfHook + +Nix builds are sandboxed with no network access. The pattern is: + +1. **Fixed-output derivation (FOD)** — `uv sync --frozen` fetches and installs all dependencies into a venv. FODs are allowed network access because the output hash is declared upfront. Compiled `.so` files reference Nix store paths (RPATHs to libxml2, krb5, etc.), which FODs must not contain, so we strip references with `remove-references-to` and delete `bin/` and `.pyc` files. +2. **Main derivation** — copies the FOD's `lib/python3.14/site-packages/`, recreates `bin/` with proper python symlinks, restores `pyvenv.cfg`, and runs `autoPatchelfHook` to re-link `.so` files against the correct Nix store libraries. + +**Why not `uv pip download` + `uv pip install --no-index`?** `uv pip download` does not exist in uv 0.9.29 (nixpkgs). And the download-only approach has further complications with sdist-only packages (psycopg-c, gssapi) that must be compiled anyway. ## What to Do -1. Create a Python package override set that builds the 4 in-tree packages from source -2. Pin the forked `djangorestframework` and `dramatiq` versions -3. Build `authentik-django` using `hatchling` as the build backend -4. Apply the 4 `substituteInPlace` patches for Nix store path references -5. Copy lifecycle scripts, `manage.py`, blueprints, and web assets into the output -6. Verify: `python -c "import authentik"` succeeds +1. Create the FOD (`python-deps.nix`) that runs `uv sync --frozen --no-install-project --no-install-workspace --no-dev`, then strips all Nix store references from the output +2. Create the main derivation (`authentik-django.nix`) that: + - Copies the FOD's site-packages + - Recreates venv `bin/` and `pyvenv.cfg` + - Runs `autoPatchelfHook` to restore `.so` RPATHs + - Copies 4 in-tree workspace packages directly into site-packages + - Copies `authentik/` and `lifecycle/` into site-packages + - Copies `opencontainers` from `fetchFromGitHub` into site-packages +3. Apply `substituteInPlace` patches for Nix store paths in `settings.py`, `default.yml`, `email/utils.py` +4. Copy lifecycle scripts, `manage.py`, blueprints into the output +5. Verify: `$out/bin/python3.14 -c "import authentik"` succeeds ## Key Details -- Build backend: `hatchling` -- Entry point: `manage.py` (Django management commands) -- Lifecycle scripts: `lifecycle/` directory (used by Go server and `ak` wrapper) -- Blueprints: `blueprints/` directory (YAML IaC definitions) -- The output must include `web/` assets (email templates reference them) +- Nix provides: `python314`, `uv`, system libraries (`libxml2`, `libxslt`, `openssl`, `libffi`, `zlib`, etc.) +- PyPI provides: all Python packages (via pre-built `cp314` wheels where available, sdist builds otherwise) +- The FOD hash must be recomputed when `uv.lock` changes +- `manylinux` wheels bundle some `.so` files — acceptable for a container image +- The 4 in-tree packages are installed from monorepo source, not PyPI +- Standard `djangorestframework` 3.16.1 from PyPI (no longer forked as of 2026.2.0) + +## Lessons Learned + +Build issues encountered and resolved: + +| Issue | Fix | +|-------|-----| +| `pg_config` not found for psycopg-c | Use `pkgs.postgresql.pg_config` (separate derivation), not `pkgs.postgresql` | +| gssapi `gss_acquire_cred_impersonate_name` undeclared | `NIX_CFLAGS_COMPILE="-include gssapi/gssapi_ext.h"` — function is in `gssapi_ext.h`, not auto-included | +| xmlsec linker error `-lltdl` | Add `pkgs.libtool` to buildInputs (provides libltdl) | +| psycopg-c needs `libpq` | Add `pkgs.libpq` to buildInputs | +| Static `refTargets` list missed 6 store refs | Replaced with dynamic discovery: `grep -aohE '/nix/store/...'` finds all refs, `remove-references-to` strips them | +| `xargs grep` exit code 123 under `pipefail` | Wrap pipeline in `{ ... \|\| true; }` — grep returning 1 (no match) causes xargs to return 123 | +| `grep -aoE` includes filename prefix in output | Use `grep -aohE` (`-h` suppresses filenames) to get clean store paths | +| autoPatchelfHook can't find libraries | `buildInputs` in main derivation must include all libraries that `.so` files link against | + +The `uv sync` completes in ~3.5 minutes. Dynamic reference discovery finds 19 unique store paths and strips all of them. After stripping, `remove-references-to` mangles hashes to `eeee...` bytes — about 40 files still "contain" `/nix/store/` strings but with invalid hashes, which is expected and harmless. `autoPatchelfHook` in the main derivation resolves all NEEDED entries with 0 unsatisfied dependencies. + +Build verified: `$out/bin/python3.14 -c "import authentik"` succeeds, along with all key dependencies (django 5.2.11, lxml, xmlsec, psycopg, guardian, opencontainers). ## Related diff --git a/docs/how-to/authentik/authentik-web-ui-derivation.md b/docs/how-to/authentik/authentik-web-ui-derivation.md index d6c29dd..cc71254 100644 --- a/docs/how-to/authentik/authentik-web-ui-derivation.md +++ b/docs/how-to/authentik/authentik-web-ui-derivation.md @@ -1,7 +1,6 @@ --- title: Build Authentik Web UI -modified: 2026-02-28 -status: active +modified: 2026-03-01 requires: - authentik-api-client-generation tags: @@ -14,30 +13,28 @@ tags: Build the Lit-based TypeScript web frontend for authentik. -## Context +## Overview -The web UI lives in `web/` in the authentik repo. It's built with Rollup and uses Lit web components. The nixpkgs derivation builds this in two phases: +The web UI lives in `web/` in the authentik repo. As of 2026.2.0, the main build uses **esbuild** (via wireit) and the SFE sub-package uses **rollup**. The Nix build uses a two-phase approach: -1. **`webui-deps`** — Fixed-output derivation that runs `npm ci` to fetch Node dependencies. Uses platform-specific output hashes (aarch64-linux vs x86_64-linux). -2. **`webui`** — Patches in the generated TypeScript API client (`client-ts`), then runs `npm run build`. Output includes `dist/` and `authentik/` static directories. +1. **`webui-deps.nix`** — Fixed-output derivation that runs `npm ci` to fetch Node dependencies. Platform-specific output hash (npm downloads architecture-specific native binaries for esbuild, rollup, and SWC). +2. **`webui.nix`** — Copies deps, patches in the generated TypeScript API client (`client-ts`), patches shebangs, then runs `npm run build` (wireit/esbuild) and `npm run build:sfe` (rollup). Output includes `dist/` and `authentik/` static directories. -There's also a **`website`** derivation (Docusaurus-based API docs at `website/`) that produces the `/help` endpoint. This is optional but included in the nixpkgs build. +## Build Details -## What to Do +- **Node.js:** `nodejs_24` (authentik requires Node >= 24, npm >= 11.6.2) +- **Build time:** ~33s on ringtail (x86_64-linux) +- **FOD hash:** Platform-specific — will need updating on each authentik version bump +- **Output:** `$out/dist/` (JS/CSS bundles) and `$out/authentik/` (static SVG/PNG icons) +- **Consumed by:** Go server (`authentik-server.nix` via `webui` parameter) for static file serving, and `authentik-django.nix` for email template icon paths +- **Docusaurus website** (`/help` endpoint) is not built — optional and can be added later -1. Create a fixed-output derivation for `npm ci` in `web/` (platform-specific hashes) -2. Patch the generated TypeScript client into `web/node_modules/@goauthentik/api/` -3. Build with `npm run build` — produces `dist/` and `authentik/` directories -4. Optionally build the Docusaurus website (`website/`) for the `/help` endpoint -5. Verify: static assets exist and reference correct paths +## Key Lessons -## Key Details - -- Build tool: Rollup (via npm scripts) -- Node.js version: `nodejs_24` in current nixpkgs (check upstream requirements) -- The TypeScript API client must be patched in before the build -- Fixed-output hashes break on any npm dependency change — will need updating per release -- Output is consumed by both `authentik-django` (email templates) and the Go server (static serving) +- The 2026.2.0 build switched from rollup to esbuild for the main frontend. Only the SFE sub-package still uses rollup. +- The version string in `packages/core/version/node.js` uses a JSON import-with-assertion that doesn't resolve in the Nix sandbox — must be patched to hardcode the version. +- `NODE_OPTIONS=--openssl-legacy-provider` is needed for compatibility. +- Workspace packages have separate `node_modules/` directories — the FOD must collect all of them via `find`. ## Related diff --git a/docs/how-to/authentik/build-authentik-from-source.md b/docs/how-to/authentik/build-authentik-from-source.md index f0c1cee..fdf1f8c 100644 --- a/docs/how-to/authentik/build-authentik-from-source.md +++ b/docs/how-to/authentik/build-authentik-from-source.md @@ -1,8 +1,6 @@ --- title: Build Authentik from Source -modified: 2026-02-28 -status: active -branch: mikado/authentik-source-build +modified: 2026-03-01 requires: - authentik-go-server-derivation - authentik-web-ui-derivation @@ -15,45 +13,58 @@ tags: # Build Authentik from Source -Replace `pkgs.authentik` from nixpkgs with a custom Nix derivation that builds authentik from source. This removes the dependency on the nixpkgs packaging timeline and gives full version control. +Custom Nix derivation that builds authentik from source, replacing the `pkgs.authentik` nixpkgs dependency. This gives full version control independent of the nixpkgs release cycle. ## Motivation -The nix-container-builder runner on ringtail resolves `nixpkgs` via the NixOS nix registry, which pins to `nixos-25.11`. That channel lags behind upstream authentik releases — e.g. nixos-25.11 has 2025.10.1 while upstream is at 2025.12.4+. Building from source lets us target any release. - -This also serves as practice for packaging services from source using Nix, relying on nixpkgs only for satellite dependencies (Python interpreter, Node.js, Go toolchain, system libraries). +The nix-container-builder runner on ringtail resolves `nixpkgs` via the NixOS nix registry, which pins to `nixos-25.11`. That channel lags behind upstream authentik releases. Building from source lets us target any release by updating `sources.nix`. ## Architecture -Authentik has four build components that must be assembled: +Authentik has four build components assembled by `containers/authentik/default.nix`: -1. **API client generation** — Go and TypeScript bindings generated from `schema.yml` (OpenAPI) -2. **Python backend** (`authentik-django`) — Django application with 60+ Python dependencies, including 4 in-tree packages and a forked `djangorestframework` -3. **Web UI** — Lit-based TypeScript frontend built with Rollup -4. **Go server** — HTTP server binary (`cmd/server`) that serves the web UI and spawns gunicorn for Django +1. **API client generation** (`client-go.nix`, `client-ts.nix`) — Go and TypeScript bindings generated from `schema.yml` (OpenAPI) +2. **Python backend** (`authentik-django.nix`) — Django application with 60+ Python dependencies installed via `uv` from PyPI (see [[authentik-python-backend-derivation]]) +3. **Web UI** (`webui.nix`) — Lit-based TypeScript frontend built with esbuild + rollup +4. **Go server** (`authentik-server.nix`) — HTTP server binary that serves the web UI and spawns gunicorn for Django -The final package is the `ak` bash wrapper that orchestrates Go server + Python worker. +The `ak` wrapper script in `default.nix` sets PATH/VIRTUAL_ENV and delegates to `lifecycle/ak`, which dispatches `server` to the Go binary and everything else to Python/Django. + +**Python packaging strategy:** Nix provides the Python 3.14 interpreter and system libraries. Python packages are installed from PyPI using `uv`, locked by authentik's `uv.lock`. This avoids nixpkgs' Python 3.14 compatibility issues and aligns with upstream's build process. ## Source -Forge mirror: https://forge.ops.eblu.me/mirrors/authentik (upstream: `goauthentik/authentik`) +All derivations fetch from forge mirrors for supply chain control: +- https://forge.ops.eblu.me/mirrors/authentik (upstream: `goauthentik/authentik`) +- https://forge.ops.eblu.me/mirrors/authentik-client-go (upstream: `goauthentik/client-go`) -Reference derivation: [nixpkgs `pkgs/by-name/au/authentik/package.nix`](https://github.com/NixOS/nixpkgs/tree/master/pkgs/by-name/au/authentik) +Version and hashes are centralized in `containers/authentik/sources.nix`. -## What to Do +## Updating to a New Version -Once all prerequisites are complete: +1. Update `version` in `sources.nix` and `default.nix` +2. Update `src` and `client-go-src` hashes in `sources.nix` (use `nix-prefetch-git` on ringtail) +3. Rebuild `python-deps.nix` FOD — hash changes when `uv.lock` changes +4. Rebuild `webui-deps.nix` FOD — hash changes when `package-lock.json` or platform-specific npm binaries change +5. Recompute `vendorHash` in `authentik-server.nix` if Go dependencies changed +6. Test on ringtail: `nix-build test-build.nix -A assembled` +7. Build and push the container via CI -1. Assemble the component derivations into a final `ak`-wrapped package in `containers/authentik/` -2. Update `containers/authentik/default.nix` to use the custom derivation instead of `pkgs.authentik` -3. Test locally via Dagger before pushing to CI: `dagger call build-nix --src=. --container-name=authentik` -4. Build and push the container: `mise run container-build-and-release authentik` -5. Update `argocd/manifests/authentik/kustomization.yaml` with the new image tag -6. Update `service-versions.yaml` with the new version -7. Verify deployment: ArgoCD sync, UI login, OAuth2 flows +## Testing + +Nix derivations target `x86_64-linux`. Test incrementally on ringtail: + +```fish +set tmpdir (ssh ringtail 'mktemp -d /tmp/authentik-test.XXXXXX') +scp containers/authentik/*.nix ringtail:$tmpdir/ +ssh ringtail "cd $tmpdir && nix-build test-build.nix -A assembled --extra-experimental-features 'nix-command flakes'" +ssh ringtail "rm -rf $tmpdir" +``` + +`test-build.nix` provides both individual component targets and a fully-wired `assembled` target. ## Related -- [[build-authentik-container]] — Current nixpkgs-based build (to be replaced) +- [[build-authentik-container]] — Container build reference - [[deploy-authentik]] — Parent deployment goal - [[agent-change-process]] — C2 methodology diff --git a/docs/how-to/authentik/mirror-authentik-build-deps.md b/docs/how-to/authentik/mirror-authentik-build-deps.md new file mode 100644 index 0000000..e4cf806 --- /dev/null +++ b/docs/how-to/authentik/mirror-authentik-build-deps.md @@ -0,0 +1,40 @@ +--- +title: Mirror Authentik Build Dependencies +modified: 2026-02-28 +tags: + - how-to + - authentik +--- + +# Mirror Authentik Build Dependencies + +Mirror the external repositories needed to build authentik from source onto the forge, ensuring full supply chain control. + +## Context + +Building authentik from source requires fetching code from three GitHub repositories. The main `goauthentik/authentik` repo is already mirrored, but two companion repos are not: + +- **`goauthentik/client-go`** — Go API client bindings, versioned in lockstep with authentik (e.g. `v3.2026.2.0` matches `version/2026.2.0`). Used by the Go server build. +- **`authentik-community/django-rest-framework`** — Fork of DRF pinned to a specific commit. Authentik's Python backend requires this custom version. The upstream org name (`authentik-community`) differs from the main repo org (`goauthentik`), so the mirror name must be explicit. + +## What to Do + +1. Mirror `goauthentik/client-go`: + ```fish + mise run mirror-create https://github.com/goauthentik/client-go.git \ + --name authentik-client-go \ + --description "Go API client for authentik (lockstep versioned)" + ``` +2. Mirror `authentik-community/django-rest-framework`: + ```fish + mise run mirror-create https://github.com/authentik-community/django-rest-framework.git \ + --name authentik-django-rest-framework \ + --description "Authentik fork of Django REST Framework" + ``` +3. Verify both mirrors sync: check tags appear on forge + +## Related + +- [[build-authentik-from-source]] — Parent goal +- [[authentik-api-client-generation]] — Consumes client-go mirror +- [[authentik-python-backend-derivation]] — Consumes django-rest-framework mirror diff --git a/docs/how-to/how-to.md b/docs/how-to/how-to.md index 33a9b89..2a80d7d 100644 --- a/docs/how-to/how-to.md +++ b/docs/how-to/how-to.md @@ -101,6 +101,7 @@ Mikado chain for deploying Authentik. Track progress with `mise run docs-mikado Mikado chain for building Authentik from a custom Nix derivation (from source). Track progress with `mise run docs-mikado build-authentik-from-source`. - [[build-authentik-from-source]] +- [[mirror-authentik-build-deps]] - [[authentik-api-client-generation]] - [[authentik-python-backend-derivation]] - [[authentik-web-ui-derivation]] diff --git a/service-versions.yaml b/service-versions.yaml index d64f8d3..fa5e24f 100644 --- a/service-versions.yaml +++ b/service-versions.yaml @@ -131,8 +131,8 @@ services: - name: authentik type: argocd - last-reviewed: null - current-version: "2025.10.1" + last-reviewed: "2026-03-01" + current-version: "2026.2.0" upstream-source: https://github.com/goauthentik/authentik/releases - name: navidrome