C2: Build authentik from source (Mikado chain) #274

Merged
eblume merged 16 commits from mikado/authentik-source-build into main 2026-03-01 13:45:01 -08:00
21 changed files with 868 additions and 85 deletions

View file

@ -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 <nixpkgs> { }, 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
''
)

View file

@ -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 <nixpkgs> { }
, 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;
}

View file

@ -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 <nixpkgs> { }
, 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
'';
}

View file

@ -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 <nixpkgs> { }, 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
'';
}

View file

@ -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 <nixpkgs> { }, 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
'';
}

View file

@ -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 <nixpkgs> { } }:
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

View file

@ -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 <nixpkgs> { }, 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;
}

View file

@ -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 <nixpkgs> { } }:
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;
};
}

View file

@ -0,0 +1,44 @@
# Test harness for building authentik components on ringtail
# Uses builtins.getFlake instead of <nixpkgs> (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; }
];
}

View file

@ -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 <nixpkgs> { }, 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;
}

View file

@ -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 <nixpkgs> { }
, 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";
}

View file

@ -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.

View file

@ -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(<chain>): 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(<chain>): 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.

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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]]

View file

@ -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