Jesse A. Green / notebook

Live demo: the DocCompiler guide library as a static, read-only snapshot (October 2026). No backend, no accounts, nothing to submit.

authentik — Admin & Developer Guide

Generated May 2026 from goauthentik/authentik at 9e75f62

AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it.

This guide covers the architecture, source organization, deployment, configuration internals, security posture, performance tuning, and extension points of authentik version 2026.8.0-rc1 (commit 9e75f62, 2026-05-28). It targets operators running authentik in production, developers extending it, and security engineers vetting it for deployment.

For end-user installation and operation, see the User Guide.


Table of Contents

  1. Executive Summary
  2. System Architecture
  3. Repository Structure
  4. Module Inventory
  5. Request Lifecycle
  6. The Flow Engine
  7. Provider Implementations
  8. Outpost Architecture
  9. The Blueprint System
  10. Configuration Reference
  11. Database Schema and Migrations
  12. Background Workers and Scheduling
  13. API Surface (/api/v3/)
  14. Frontend Architecture (web/)
  15. Operations Runbooks
  16. Security Analysis
  17. Performance Tuning
  18. Code Quality and Patterns
  19. Testing Strategy
  20. Suggested Improvements
  21. Contributing
  22. Quick Reference

1. Executive Summary

Attribute Value
Repo github.com/goauthentik/authentik
Commit documented 9e75f62 (2026-05-28)
Version 2026.8.0-rc1
Total source 10,403,381 chars across 5,814 files
Languages Python 3.14 (Django 5.2), Go, Rust (axum), TypeScript (Lit)
License model MIT (core) + EE (authentik/enterprise/ + web/src/admin/enterprise/) + CC BY-SA 4.0 (website/) + proprietary (website/static/img/)
Primary deployment Docker Compose, Kubernetes Helm, AWS CloudFormation, DigitalOcean Marketplace
Sole stateful dependency PostgreSQL (no Redis required — django-postgres-cache + django-channels-postgres use LISTEN/NOTIFY)
External binaries 6 (1 Python server entry + 5 Go outpost binaries)
Default network ports 9000 (HTTP), 9443 (HTTPS), 3389/6636 (LDAP/LDAPS), 1812 (RADIUS UDP), 9300 (metrics), 9900 (debug)

What authentik is: a multi-protocol Identity Provider implemented as a Django REST framework backend (authentik/) with a Web Components admin UI (web/), augmented by purpose-built Go outposts (cmd/, internal/outpost/) for protocol families that need raw-socket handling (LDAP, RADIUS, RDP/SSH gateway via RAC, and HTTP forward-auth via the Proxy provider). The core is dual-licensed: the MIT-licensed open core and an EE add-on tier (authentik/enterprise/) that adds SSF, MS Entra/Google Workspace sync, audit log, MTLS stage, account lockdown, RAC, unique-password policy, and a few connectors.

Why three runtimes (Python + Go + Rust)? Python/Django handles the business logic, ORM, admin REST API, and flow execution where developer velocity matters most. Go handles outpost wire protocols where latency and concurrent connection count matter (a single LDAP outpost handles thousands of concurrent binds; Django could not). Rust (in packages/ak-axum/ and packages/ak-common/) is reserved for the high-performance API surface and the PROXY-protocol-aware reverse-proxy frontend; it's the newest layer (introduced for the Server v2 refactor) and currently a thin slice of the surface.


2. System Architecture

                    ┌─────────────────────────────────────────────────────────┐
                    │                          USERS                           │
                    └──┬───────────────────────────┬──────────────────────────┘
                       │ HTTPS                     │ LDAP / RADIUS / RDP / SSH
                       ▼                           ▼
            ┌──────────────────────┐    ┌────────────────────────┐
            │  Reverse Proxy       │    │    Outposts (Go)        │
            │  (Traefik/Nginx/Caddy)│   │  ldap / radius / rac /  │
            └──────────┬───────────┘    │  proxy (separate)        │
                       │ HTTP            └────────┬───────────────┘
                       ▼                           │  websocket+REST
            ┌──────────────────────┐               │
            │  authentik server    │◄──────────────┘
            │  ┌────────────────┐  │
            │  │ Web (TS/Lit)   │  │  /if/admin/, /if/user/, /if/flow/
            │  │ Static assets  │  │
            │  └────────────────┘  │
            │  ┌────────────────┐  │
            │  │ Django + DRF   │  │  /api/v3/*
            │  │ ASGI (uvicorn) │  │
            │  └────────┬───────┘  │
            │  ┌────────▼───────┐  │
            │  │ Embedded       │  │  inline Proxy outpost (Go process)
            │  │ Outpost (Go)   │  │  shares port 9000 with router
            │  └────────────────┘  │
            └──────────┬───────────┘
                       │
                       │       ┌─────────────────────────┐
                       │       │   authentik worker      │
                       │       │   (Django + dramatiq)   │
                       │       │   - background tasks    │
                       │       │   - email send          │
                       │       │   - sync jobs           │
                       │       │   - event processors    │
                       │       └────────────┬────────────┘
                       │                    │
                       ▼                    ▼
            ┌──────────────────────────────────────┐
            │            PostgreSQL                 │
            │  - all object state                   │
            │  - sessions (authenticatedsession)    │
            │  - cache (django-postgres-cache)      │
            │  - pubsub (LISTEN/NOTIFY via          │
            │     django-channels-postgres)         │
            │  - task queue (django-dramatiq-postgres) │
            └──────────────────────────────────────┘

Components

Component Process Language Mounted volumes
server python -m gunicorn -k uvicorn.workers.UvicornWorker authentik.root.asgi:application Python 3.14 /data, /blueprints, /certs, /media
worker python -m dramatiq authentik.tasks (via lifecycle/worker.py) Python 3.14 /blueprints, /templates, /data, /var/run/docker.sock (optional)
embedded outpost child of server; Go binary authentik Go n/a (in-container)
proxy outpost (separate) authentik-proxy Go n/a
ldap outpost authentik-ldap Go optional /certs
radius outpost authentik-radius Go n/a
rac outpost authentik-rac Go n/a
postgresql postgres C /var/lib/postgresql/data

Why the embedded outpost?

For small installs, running a separate Go process for the Proxy provider is overkill. The server container starts a Go binary as a goroutine that shares port 9000 via a UNIX domain socket. The lightweight router in internal/web/ decides per-request whether to route to the Django ASGI app (most paths), the embedded outpost (forward-auth paths under /outpost.goauthentik.io/), or to static asset serving. See cmd/server/server.go:65-100 for the attemptProxyStart loop that brings up the embedded outpost once the API is healthy.

Why PostgreSQL-only?

Older versions of authentik used Redis. The current architecture replaces Redis with three PostgreSQL-based libraries:

  • django-postgres-cache (packages/django-postgres-cache/) — a Django cache backend that uses a PostgreSQL table with a TTL column.
  • django-channels-postgres (packages/django-channels-postgres/) — a Django Channels layer using LISTEN/NOTIFY for pub/sub, eliminating the need for a Redis channel layer.
  • django-dramatiq-postgres (packages/django-dramatiq-postgres/) — a dramatiq broker implementation that uses a PostgreSQL queue table.

The trade-off: PostgreSQL pub/sub has higher latency than Redis (~20-50 ms vs <5 ms) and lower throughput. But for an IdP, this is negligible compared to the simplification: one stateful service, one backup target, one network firewall hole. Larger installs can still configure an external cache.url and channel.url in default.yml to point at Redis if they prefer.


3. Repository Structure

authentik/                                  # repo root
├── Cargo.toml                              # Rust workspace (ak-axum, ak-common, client-rust, docsmg)
├── Cargo.lock
├── Makefile                                # 'make gen-dev-config', 'make docs', 'make test', etc.
├── README.md                               # marketing-tier overview (53 lines)
├── SECURITY.md, CODE_OF_CONDUCT.md, CONTRIBUTING.md
├── LICENSE                                 # MIT (core)
├── manage.py                               # Django entrypoint (Python)
├── pyproject.toml                          # Python 3.14, 60+ runtime deps
├── package.json                            # npm workspace root
├── package-lock.json
├── go.mod, go.sum                          # Go modules root for cmd/* + internal/*
├── build.rs                                # Cargo build script (PyO3 bridge)
├── rust-toolchain.toml                     # pinned Rust toolchain
├── schema.yml                              # generated OpenAPI 3.0 spec
├── cspell.config.jsonc                     # spellcheck config (dictionaries under web/e2e/utils/)
│
├── authentik/                              # ★ Python application (Django)
│   ├── root/                               # Django project: settings.py, asgi.py, urls.py
│   ├── api/                                # ViewSets for /api/v3/*, schema customization
│   ├── lib/                                # shared utilities: config loader, expressions, logging
│   │   └── default.yml                     # ★ canonical runtime configuration
│   ├── core/                               # User, Application, Source, Group, Token, Brand models
│   ├── flows/                              # Flow engine: Flow, Stage, FlowStageBinding, FlowPlanner
│   ├── stages/                             # 22 stage implementations (identification, password, ...)
│   ├── providers/                          # 8 provider implementations (saml, oauth2, ldap, radius, proxy, rac, scim, ldap)
│   ├── sources/                            # 7 source implementations (ldap, oauth, saml, kerberos, scim, plex, telegram)
│   ├── policies/                           # 7 policy implementations + base classes
│   ├── outposts/                           # Outpost model + Docker/Kubernetes controllers
│   ├── blueprints/                         # YAML import/export + apply engine
│   ├── events/                             # Event model + notification system + context processors
│   ├── crypto/                             # X.509 cert generation/import (CertificateKeyPair model)
│   ├── brands/                             # Brand (multi-tenancy by domain) model + views
│   ├── tenants/                            # django-tenants integration (different from Brands)
│   ├── tasks/                              # Dramatiq task registry + schedules
│   ├── rbac/                               # Django-Guardian based RBAC
│   ├── admin/                              # admin REST APIs (apps, files, settings, version)
│   ├── recovery/                           # `ak create_recovery_key`, `ak create_admin_group`
│   ├── commands/                           # generic shell commands (`ak shell_plus` etc.)
│   ├── endpoints/                          # Endpoint device model (EE)
│   ├── common/                             # shared OAuth/SAML XML constants
│   └── enterprise/                         # ★ EE-licensed code (SSF, Entra/GWS sync, audit, MTLS, RAC, account_lockdown, unique_password, reports)
│
├── cmd/                                    # ★ Go binaries (one per outpost type)
│   ├── server/                             # main authentik server binary + embedded outpost
│   ├── proxy/                              # Forward-auth proxy outpost
│   ├── ldap/                               # LDAP outpost
│   ├── radius/                             # RADIUS outpost
│   └── rac/                                # Remote Access Connection outpost
│
├── internal/                               # Go shared libraries
│   ├── common/                             # PreRun, version info, exit channel
│   ├── config/                             # config.yml loader (Go side)
│   ├── constants/                          # version + user-agent string
│   ├── crypto/                             # FIPS-aware backend selection
│   ├── debug/                              # pprof + dump_config debug HTTP server (port 9900)
│   ├── gounicorn/                          # gunicorn supervisor (used inside the Python container)
│   ├── outpost/                            # outpost framework: API controller, healthcheck, entrypoint
│   │   ├── ak/                             # AuthAPIController, healthchecks, websocket loop
│   │   ├── ldap/                           # LDAP outpost handler (bind, search, group, metrics)
│   │   ├── proxyv2/                        # Proxy outpost (the modern one — v1 was retired)
│   │   ├── radius/                         # RADIUS outpost (PAP, CHAP, MS-CHAPv2)
│   │   ├── rac/                            # RAC outpost (Guacamole-protocol bridge)
│   │   └── ak/healthcheck/                 # shared /health/live/, /health/ready/
│   ├── utils/                              # web (TLS, transports), sentry helpers, unix
│   ├── crypto/backend/                     # FIPS backend factory
│   └── web/                                # WebServer, BrandTLS lookup, routing
│
├── packages/                               # ★ npm + Cargo workspaces (shared libraries)
│   ├── ak-axum/                            # Rust axum extensions (PROXY-protocol, extractors)
│   ├── ak-common/                          # Rust common helpers (config, tokio extensions)
│   ├── ak-guardian/                        # Python: django-guardian fork with management commands
│   ├── client-go/                          # Go SDK (auto-generated from schema.yml)
│   ├── client-ts/                          # TypeScript SDK (auto-generated)
│   ├── client-rust/                        # Rust SDK (auto-generated)
│   ├── django-channels-postgres/           # PostgreSQL pub/sub channel layer
│   ├── django-dramatiq-postgres/           # PostgreSQL task broker for dramatiq
│   ├── django-postgres-cache/              # PostgreSQL cache backend
│   ├── docusaurus-config/                  # shared Docusaurus theme config
│   ├── esbuild-plugin-live-reload/         # dev-only esbuild plugin
│   ├── eslint-config/, prettier-config/, tsconfig/  # shared lint/format/TS config
│   └── logger-js/                          # shared JS logger
│
├── web/                                    # ★ TypeScript frontend (Lit web components)
│   ├── src/
│   │   ├── admin/                          # Admin interface (entrypoint: admin/index.entrypoint.ts)
│   │   ├── user/                           # User interface (entrypoint: user/index.entrypoint.ts)
│   │   ├── flow/                           # Flow executor UI (entrypoint: flow/index.entrypoint.ts)
│   │   ├── rac/                            # RAC client (entrypoint: rac/index.entrypoint.ts)
│   │   ├── elements/                       # Shared web components
│   │   ├── common/                         # API client glue, helpers, ws, errors
│   │   ├── components/                     # Form fields, search-ql, hints
│   │   └── standalone/                     # api-browser (Swagger UI), loading splash
│   ├── packages/sfe/                       # Simplified Flow Executor for IE-11 / MSEdge-18 (Office 365)
│   ├── packages/core/                      # Browser shims (env, paths, scripting, version)
│   ├── packages/lex/                       # Tokenizer for the search-ql component
│   ├── packages/formdata-polyfill/         # IE-11 polyfill
│   ├── packages/node-domexception/         # Node DOMException polyfill
│   ├── e2e/                                # Playwright e2e tests
│   ├── bundler/                            # esbuild config + vite-plugin-lit-css
│   └── tools/                              # Refactor scripts (kept for posterity)
│
├── website/                                # ★ Docusaurus documentation site (goauthentik.io)
│   ├── docs/                               # User-facing docs
│   ├── integrations/                       # Per-app integration recipes (150+)
│   ├── api/                                # API browser embed
│   ├── docusaurus-theme/                   # Custom theme components
│   └── scripts/docsmg/                     # Rust CLI for bulk docs migration
│
├── blueprints/                             # ★ Default blueprints applied on startup
│   ├── default/                            # Required blueprints (default flows, providers, etc.)
│   ├── example/                            # Reference blueprints to clone/adapt
│   ├── system/                             # Internal-use blueprints
│   ├── migrations/                         # Schema-evolution blueprints
│   └── testing/                            # Used by CI only
│
├── locale/                                 # ★ i18n translations (django-translatable + lit-localize)
│   ├── cs_CZ/, de_DE/, en/, es_ES/, fr_FR/, it_IT/, ja_JP/, ko_KR/, nl_NL/,
│   │   no_NO/, pl_PL/, pt_BR/, pt_PT/, ru_RU/, tr_TR/, zh-Hans/, zh-Hant/,
│   │   bg_BG/, fi_FI/                      # each contains LC_MESSAGES/*.po
│
├── lifecycle/                              # ★ container init scripts
│   ├── ak                                  # `ak` shell wrapper (sources venv, exec manage.py)
│   ├── ak.py                               # Python entrypoint
│   ├── gunicorn.conf.py                    # gunicorn worker config
│   ├── migrate.py                          # idempotent migration runner
│   ├── wait_for_db.py                      # pg_isready loop
│   ├── worker.py, worker_process.py        # dramatiq worker entrypoint
│   ├── system_migrations/                  # one-shot fix-up scripts run on startup
│   ├── aws/                                # CDK app for AWS CloudFormation
│   └── container/                          # Docker entrypoint scripts
│
├── tests/                                  # ★ test harnesses
│   ├── e2e/                                # Playwright + docker-compose stacks
│   ├── integration/                        # Python integration tests
│   ├── openid_conformance/                 # OIDC compliance suite
│   └── geoip/                              # GeoIP test data
│
├── schemas/                                # JSON Schemas for blueprints, etc.
├── scripts/                                # node scripts (build helpers)
└── src/                                    # Rust workspace (axum frontend, metrics, worker)
    ├── main.rs                             # axum entry (Server v2, in beta)
    ├── server/                             # axum router
    ├── worker/                             # tokio worker harness
    └── metrics/                            # Prometheus middleware

Important

The Rust workspace under src/ is a next-generation frontend that is in active development. Production deployments today still use the Python/Go architecture. The Rust src/main.rs is destined to replace cmd/server/main.go (Go) as the high-performance HTTP frontend, terminating TLS, applying PROXY-protocol, and reverse-proxying to a Unix-domain socket served by Django/uvicorn. Treat it as a forward-looking surface; the canonical entry remains cmd/server/.


4. Module Inventory

The briefing extracted 168 module files (most are 1:1 with a Python package). The top-level Python modules under authentik/, with their primary responsibility:

Module Purpose Key files
authentik/root Django project config settings.py (1500+ lines), asgi.py, urls.py, routing.py, celery.py (legacy)
authentik/api DRF ViewSets & schema generation v3/urls.py, views/__init__.py, search.py, custom schema patches in schema/
authentik/lib Shared utilities default.yml (config baseline), config.py + config.go (twin loaders), expression/ (Python-expression evaluator), generators.py, logging.py
authentik/core Identity primitives models.py (User, Application, Source, Group, Token, AuthenticatedSession, PropertyMapping), api/ (CoreViewSets)
authentik/flows Flow execution engine models.py (Flow, Stage, FlowStageBinding, FlowToken), planner.py (FlowPlan, FlowPlanner — the heart), views/executor.py, markers.py (reevaluate, stage skip)
authentik/stages/* 22 stage types Each is its own Django app with models.py, stage.py (StageView subclass), forms.py, tests/
authentik/providers/* 8 provider types oauth2/, saml/, ldap/, radius/, proxy/, rac/, scim/ (plus 4 EE under enterprise/providers/)
authentik/sources/* 7 source types oauth/, saml/, ldap/, kerberos/, scim/, plex/, telegram/
authentik/policies/* 7 policy types + base expression/, reputation/, password/, expiry/, event_matcher/, geoip/, dummy/ + EE unique_password/
authentik/outposts Outpost orchestration models.py (Outpost, OutpostServiceConnection), controllers/ (docker.py, k8s/), apps.py (auto-discovery)
authentik/blueprints YAML import/export engine v1/ (loader, exporter, tags: !Env, !Find, !KeyOf, !Format, !If, !Context), apps.py (file watcher), management/commands/apply_blueprint.py
authentik/events Event sourcing models.py (Event, Notification, NotificationTransport, NotificationRule), context_processors/ (geoip, asn), monitored_tasks.py, notification dispatchers per transport
authentik/crypto X.509 management models.py (CertificateKeyPair), builder.py (CSR generation), management/commands/import_certificate.py
authentik/brands Multi-domain branding models.py (Brand), views.py (brand resolution middleware)
authentik/tenants Schema-per-tenant (advanced) django-tenants integration; defaults disabled
authentik/tasks Task registry + schedules apps.py (dramatiq autodiscovery), models.py (TaskSchedule), schedules expressed as cron-like specs
authentik/rbac Object-level permissions models.py (Role, InitialPermissions), api/ (RBAC ViewSets) wraps django-guardian
authentik/admin Admin REST + system info api/applications.py, api/files/ (storage backends), api/settings.py, api/version.py
authentik/recovery Break-glass tooling management/commands/create_recovery_key.py, create_admin_group.py, views.py (token consumption)
authentik/endpoints Endpoint devices (EE) models.py (Endpoint, EndpointDevice), connectors under endpoints/connectors/
authentik/enterprise EE-licensed code audit/ (tamper-evident events), providers/ (entra, gws, ssf, ws_federation), stages/ (mtls, source, account_lockdown, authenticator_endpoint_gdtc), policies/unique_password/, reports/, endpoints/connectors/ (fleet, google_chrome, agent)

Go internals (internal/)

Package Purpose
internal/common Init, PreRun (cobra hook), exit-signal channel
internal/config Reads /etc/authentik/config.yml + env overrides (Go-side twin of authentik/lib/config.py)
internal/constants Version string assembly (GIT_BUILD_HASH env var read; see extracted/config_keys.tsv:15)
internal/crypto FIPS-aware factory: returns AWS-LC-RS when compliance.fips.enabled=true, else stdlib
internal/debug HTTP debug server on port 9900: pprof endpoints + /debug/dump_config
internal/gounicorn Supervisor that wraps gunicorn (used inside the Python container)
internal/outpost/ak APIController — REST + websocket to authentik server; healthcheck loop
internal/outpost/ak/entrypoint OutpostMain(name, ServerFactory) — common bootstrap pattern
internal/outpost/ldap LDAP server (bind handlers in bind/, search handlers in search/, group sync in group/, metrics in metrics/)
internal/outpost/proxyv2 HTTP forward-auth proxy (modern: v2 supersedes v1). Sessionstore backends: filesystem, postgres, sessionstore (default in-memory)
internal/outpost/radius RADIUS server (PAP, CHAP, MS-CHAPv2; metrics)
internal/outpost/rac RDP/SSH/VNC gateway (connection/)
internal/outpost/flow Internal SDK for invoking flow execution from outposts
internal/utils/{web,sentry,unix} HTTP transport with User-Agent, Sentry tracesampler, Unix socket helpers
internal/web The lightweight router that fronts both Django and the embedded outpost; BrandTLS for per-brand SNI cert serving

Frontend modules (web/src/)

Module Purpose
web/src/admin/ Admin Console SPA — entrypoint admin/index.entrypoint.ts. Lazy-loaded route per resource.
web/src/user/ User self-service SPA — user/index.entrypoint.ts. Application library, settings, MFA management.
web/src/flow/ Flow executor SPA — flow/index.entrypoint.ts. Renders one stage at a time, receives next-stage JSON from server.
web/src/rac/ RAC client (Guacamole-protocol over WebSocket)
web/src/standalone/api-browser/ Embedded Swagger UI for schema.yml
web/src/standalone/loading/ First-paint splash before the entrypoint hydrates
web/src/elements/ Shared Lit web components: tables, dialogs, charts, dual-select, search-ql, ak-mdx, etc.
web/src/common/ API client glue (api/), error formatting (errors/), websocket (ws/), sentry init, helpers, locale, ui
web/src/components/ Higher-level composed components: ak-search-ql, ak-wizard, sync widget, notifications
web/packages/sfe/ Simplified Flow Executor — limited fallback for IE-11/MSEdge-18 (specifically Office 365/Teams). Supports only: identification, password, redirect, autosubmit, authenticator_validate (code + WebAuthn). See web/packages/sfe/README.md.

Build infrastructure

Tooling Where
Python packaging pyproject.toml (uv-compatible)
Go module go.mod (single module at repo root for all cmd/* and internal/*)
Rust workspace Cargo.toml (workspace at repo root; members under packages/ak-axum, packages/ak-common, packages/client-rust, website/scripts/docsmg)
npm workspace package.json with workspaces: ["packages/*", "web/*", "website/*"]
Bundler esbuild (custom config in web/bundler/) — fast, simple, no webpack
Test runners pytest (Python), go test ./... (Go), cargo test (Rust), Playwright (web e2e), wtr (web unit)
Lint/format ruff + black (Python), gofmt + staticcheck (Go), rustfmt + clippy (Rust), eslint + prettier (TS/JS)

5. Request Lifecycle

A typical OIDC login walks through every layer of authentik. Here's the journey of one request:

  Browser                Reverse Proxy        authentik server          PostgreSQL
   ───────                ──────────────       ─────────────────        ──────────

  GET https://grafana → … redirects → GET /application/o/authorize/?client_id=…&redirect_uri=…
                                            │
                                            ▼
                           ┌──────────────────────────────────────────┐
                           │ internal/web/ router                     │
                           │  - reads SNI → BrandTLS                  │
                           │  - matches /application/o/ → Django      │
                           └─────────────────┬────────────────────────┘
                                             ▼
                           ┌─────────────────────────────────────────┐
                           │ Django + DRF (Python)                   │
                           │  authentik/providers/oauth2/views/      │
                           │     authorize.py: AuthorizationFlowInit │
                           │  - loads OAuth2Provider by client_id    │
                           │  - resolves bound Flow                  │
                           │  - kicks off FlowPlanner.plan()         │
                           └─────────────────┬───────────────────────┘
                                             ▼
                           ┌─────────────────────────────────────────┐
                           │ authentik/flows/planner.py              │
                           │  FlowPlanner.plan(flow, request, ctx)   │
                           │  - cache lookup (django-postgres-cache) │
                           │  - if miss: walks FlowStageBindings,    │
                           │    evaluates PolicyBindings per stage   │
                           │  - returns FlowPlan(stages=[...])       │
                           └─────────────────┬───────────────────────┘
                                             ▼
                           ┌─────────────────────────────────────────┐
                           │ FlowExecutorView.dispatch()             │
                           │  - pop next stage from plan             │
                           │  - render its template/JSON to client   │
                           └─────────────────┬───────────────────────┘
                                             ▼
  302 redirect to /if/flow/default-authentication-flow/...
                                             │
                                             ▼
  Browser loads /if/flow/... → web/src/flow/ SPA hydrates
                                             │
                                             ▼
  POST stage payload (username, password) → FlowExecutorView
                                             │
                                             ▼
                           ┌─────────────────────────────────────────┐
                           │ Per-stage logic                         │
                           │ - identification: User.objects.filter() │
                           │ - password: verify_password()           │
                           │ - mfa_validate: WebAuthn challenge      │
                           │ - user_login: session = create_session()│
                           └─────────────────┬───────────────────────┘
                                             ▼
                           ┌─────────────────────────────────────────┐
                           │ Final stage: redirect back to provider  │
                           │ - issues authorization code             │
                           │ - 302 → grafana.example.com/...?code=…  │
                           └─────────────────────────────────────────┘
                                             │
                                             ▼
  Grafana → POST /application/o/token/?code=…
                                             │
                                             ▼
                           ┌─────────────────────────────────────────┐
                           │ TokenView: validate code, issue JWT     │
                           │  (signed with provider's CertificateKey)│
                           └─────────────────────────────────────────┘
                                             │
                                             ▼
  Grafana → GET /application/o/userinfo/ → 200 with claims
                                             │
                                             ▼
  Grafana session created → user lands on dashboard

Key insight: every stage transition is a fresh HTTP roundtrip with form POST. The flow's state lives server-side in the session — the client sees only the rendered HTML/JSON for one stage at a time. This is what makes flows safe to interrupt, customize, and policy-gate.

Embedded outpost path

For Proxy provider requests:

  Browser → GET https://app.example.com/protected
        → app's reverse proxy → forward-auth to authentik
        → :9000/outpost.goauthentik.io/auth/traefik
        │
        ▼
  internal/web/ router matches /outpost.goauthentik.io/ → embedded outpost
        │
        ▼
  internal/outpost/proxyv2 — check session, on miss start a flow

The embedded outpost shares process space with the server. When AUTHENTIK_OUTPOSTS__DISABLE_EMBEDDED_OUTPOST=true, the router 404s those paths and a separate authentik-proxy outpost is required.


6. The Flow Engine

The Flow engine is the single most important piece of code in authentik. It's implemented in pure Python and is general enough to express any sequence of user interactions, from a 1-stage password-only login to a 12-stage MFA-enrolment-with-consent-and-email-verification flow.

Model graph (authentik/flows/models.py)

Flow ─── 1:N ─── FlowStageBinding ─── N:1 ─── Stage
  │                    │                       │
  │                    │                       └─ (one of 22 concrete subclasses)
  │                    │
  │                    └─ PolicyBinding (N:1 to Policy)
  │
  ├─ designation: enum {authentication, invalidation, recovery,
  │                     unenrollment, enrollment, stage_configuration}
  ├─ authentication: enum {none, require_authenticated, require_unauthenticated,
  │                         require_superuser, require_redirect, require_outpost}
  ├─ policy_engine_mode: enum {any, all}
  ├─ denied_action: enum {message_continue, message, continue}
  └─ layout: enum {stacked, content_left, content_right, sidebar_left, sidebar_right}

FlowStageBinding extends PolicyBindingModel:
  ├─ order: int                # sort key
  ├─ re_evaluate_policies: bool # re-check on every stage entry
  ├─ invalid_response_action: enum {retry, restart, restart_with_context}

Stage (abstract base):
  └─ verbose_name + 22 concrete subclasses each in authentik/stages/<n>/models.py

The Planner (authentik/flows/planner.py)

class FlowPlan:
    flow_pk: UUID                 # which flow this plan belongs to
    bindings: list[FlowStageBinding]
    markers: list[StageMarker]    # parallel array; each marker can ReevaluateMarker, SkipMarker
    context: dict                 # arbitrary key/value; survives across stages

The FlowPlanner.plan(flow, request) method:

  1. Cache check — cache_key(flow, request.user) → lookup in django-postgres-cache (TTL: cache.timeout_flows, default 300 s). Cache hit returns a pre-computed FlowPlan; cache miss continues.
  2. Walk FlowStageBindings ordered by order.
  3. For each binding, evaluate the PolicyBindings attached:
    • If policy_engine_mode = all, all must pass; else any must pass.
    • If policies fail and the stage is marked negate=True, skip the stage.
  4. Build the FlowPlan, store it in the cache, return it.
  5. The FlowExecutorView pops bindings from the plan and renders them one at a time.

The cache_key includes flow.pk + user.pk (when user is known) so per-user policy decisions are remembered. The planner respects re_evaluate_policies=True on individual bindings — those skip the cache for that one stage.

Why this is powerful

  • Adding MFA to a flow = adding one FlowStageBinding to the existing Flow. No code change.
  • A/B-testing two stage types = bind both with mutually-exclusive policies.
  • Conditional consent = bind a Consent stage gated by an Expression policy that checks the requested scopes.
  • Multi-tenant differentiation = each Brand chooses different Flows for the same purpose.

Re-evaluation markers

Some stages mutate state mid-flow (e.g. user_write creates a user; afterwards group-membership policies need to be re-checked). Setting re_evaluate_policies=True on the downstream binding forces the planner to re-evaluate when the user re-enters that stage. The implementation is in flows/markers.py — ReevaluateMarker and SkipMarker are the two marker types tracked alongside the bindings list.

Stage extension model

Every stage is a self-contained Django app under authentik/stages/<name>/:

authentik/stages/password/
├── apps.py             # AuthentikStagePasswordConfig
├── models.py           # PasswordStage extends Stage
├── stage.py            # PasswordStageView extends ChallengeStageView
├── forms.py            # form schema if Django forms used
├── migrations/         # standard Django migrations
├── tests/              # pytest test cases
└── api/                # DRF ViewSet for /api/v3/stages/password/

To add a new stage type, create a new app following this layout, register it in authentik/root/settings.py, write the migration. The flow engine handles discovery via apps.get_app_configs() — no central registry.


7. Provider Implementations

Providers are protocol-specific outbound integrations. Each lives at authentik/providers/<name>/ (or authentik/enterprise/providers/<name>/ for EE).

Common pattern

File Purpose
models.py XxxProvider subclass of Provider. Adds protocol-specific fields.
views/ HTTP views (e.g. authorize.py for OAuth2, sso.py for SAML)
api/ DRF ViewSet for /api/v3/providers/<name>/
signals.py Django signals (e.g. clear cache on provider edit)
templates/ Server-rendered HTML (mostly SAML for AssertionConsumerService POST)
tests/ Per-provider test coverage
migrations/ Schema evolution

OAuth2 (authentik/providers/oauth2/)

The most-used provider. Implements OAuth2 + OIDC + extensions (PKCE, device code grant, RP-initiated logout, dynamic client registration, token introspection, revocation).

View Path Purpose
AuthorizationFlowInitView /application/o/authorize/ Starts the flow for authorization_code, implicit, hybrid
TokenView /application/o/token/ Exchanges code/refresh for tokens
UserInfoView /application/o/userinfo/ Returns claims for an access token
JWKSView /application/o/<slug>/jwks/ JSON Web Key Set for signature verification
ProviderInfoView /application/o/<slug>/.well-known/openid-configuration OIDC discovery
EndSessionView /application/o/<slug>/end-session/ RP-initiated logout
TokenRevokeView /application/o/revoke/ RFC 7009 revocation
TokenIntrospectionView /application/o/introspect/ RFC 7662 introspection
DeviceView /application/o/device/ RFC 8628 device code grant
DynamicClientRegistrationView /application/o/register/ RFC 7591

Token signing: each provider has a signing_key FK to CertificateKeyPair. Tokens are signed via jwcrypto (RS256/ES256/EdDSA). The extracted/class_hierarchy.tsv shows 1483 classes — many are JWT payload variants for the different grant types.

Token storage: access tokens are stored in authentik_providers_oauth2_accesstoken with TTL fields. Refresh tokens in authentik_providers_oauth2_refreshtoken. Authorization codes are in authentik_providers_oauth2_authorizationcode with 60s TTL. Cleanup runs every minute via a scheduled task.

Throttling: configured in default.yml:

throttle:
  providers:
    oauth2:
      device: 20/hour       # device-code-grant rate limit
  default: 1000/second

The throttle applies to the device-code endpoint specifically (the only one a non-authenticated client can hit at high rate).

SAML (authentik/providers/saml/)

File Purpose
processors/ request_parser.py, response_assertion.py, metadata.py — XML handling
views/sso.py Single Sign-On views (redirect + POST bindings)
views/slo.py Single Logout views
utils/encoder.py XMLSEC signing/encryption wrappers

Uses xmlsec (libxmlsec1 C library binding) + lxml. The defusedxml package is in the dep tree to prevent XXE attacks (verified via pyproject.toml).

NameID formats supported: persistent, transient, emailAddress, unspecified, windowsDomainQualifiedName, X509SubjectName.

Binding support: HTTP-Redirect, HTTP-POST. Artifact binding is not supported. ECP profile is not supported. Both IdP-initiated and SP-initiated flows work.

LDAP provider (authentik/providers/ldap/)

Unlike the other providers, LDAP runs in a Go outpost (internal/outpost/ldap/). The Python side is mostly metadata:

Python side Go side
Provider model: search/bind DNs, search group, MFA support, TLS settings bind/direct/, bind/memory/ handlers per auth method
Controller class (controllers/ldap.py) renders config for outpost search/direct/, search/memory/ for search ops
API for /api/v3/providers/ldap/ group/ for memberOf resolution
metrics/ for Prometheus

The outpost gets its config via WebSocket from the server (see internal/outpost/ak/).

Other providers (briefer)

Provider Highlights
RADIUS (authentik/providers/radius/) Goes through internal/outpost/radius/. Supports PAP, CHAP, MS-CHAPv2. Uses pyrad on Python side, native UDP on Go side.
Proxy (authentik/providers/proxy/) Three modes: forward_single, forward_domain, proxy. Outpost in internal/outpost/proxyv2/. Session stores: filesystem, postgres, in-memory.
RAC (authentik/providers/rac/, EE) Endpoints, ConnectionTokens. Go outpost speaks Guacamole protocol → translates to RDP/SSH/VNC.
SCIM (authentik/providers/scim/) Outbound user/group provisioning. clients/users.py, clients/groups.py, clients/schema.py. Uses scim2-filter-parser for compatibility.
MS Entra (authentik/enterprise/providers/microsoft_entra/, EE) Uses msgraph-sdk. Periodic full + delta sync.
Google Workspace (authentik/enterprise/providers/google_workspace/, EE) Uses google-api-python-client. Same pattern.
WS-Federation (authentik/enterprise/providers/ws_federation/, EE) Legacy SharePoint and old .NET. Generates wresult SAML tokens.
SSF (authentik/enterprise/providers/ssf/, EE) Shared Signals Framework — pushes events outbound to SaaS that consumes SSF.

8. Outpost Architecture

Outposts solve a fundamental problem: Django, even on uvicorn, can't economically handle raw-protocol LDAP/RADIUS/RDP connections. So authentik splits these into separate Go processes that own the wire protocol and talk to the Python server via REST + WebSocket for configuration.

Common bootstrap (internal/outpost/ak/entrypoint/)

Every outpost binary uses the same OutpostMain() helper:

// cmd/proxy/main.go
RunE: func(cmd *cobra.Command, args []string) error {
    err := entrypoint.OutpostMain("authentik.outpost.proxy", proxyv2.NewProxyServer)
    if err != nil {
        fmt.Println(helpMessage)
    }
    return err
}

OutpostMain does:

  1. Read AUTHENTIK_HOST, AUTHENTIK_TOKEN, AUTHENTIK_INSECURE from env.
  2. Build an APIController (internal/outpost/ak/api.go) — makes a REST call to /outposts/instances/?token=... to register, opens a WebSocket to /ws/outpost/<uuid>/ for config updates.
  3. Invoke the server factory (e.g. proxyv2.NewProxyServer) with the controller as the config source.
  4. Wait for an exit signal; on receipt, gracefully shut down all listeners.

Required env vars

Var Required Default Purpose
AUTHENTIK_HOST Yes — Where to reach authentik (e.g. https://authentik.example.com)
AUTHENTIK_TOKEN Yes — Outpost auth token (generated when outpost is created in UI)
AUTHENTIK_INSECURE No false Skip TLS verification (for self-signed certs in lab)
AUTHENTIK_HOST_BROWSER No = AUTHENTIK_HOST URL to send to browsers (when outpost is on a private network)
AUTHENTIK_LOG_LEVEL No info Log level

The proxy outpost's helpMessage documents these inline (cmd/proxy/main.go:18-28).

Healthcheck protocol

All outposts share a common healthcheck CLI: <binary> healthcheck <mode>. Modes:

Mode Checks
allinone / server http://localhost<path>-/health/live/ returns 200
worker http://localhost/-/health/live/

Used in Dockerfile as the HEALTHCHECK instruction. Returns exit code 0 (healthy) or 1.

Wire protocol — outpost ↔ server

Outposts speak two channels:

  1. REST — periodic state polling (/api/v3/outposts/instances/<id>/) and ad-hoc reads (provider config, user attributes for the embedded session lookup).
  2. WebSocket — /ws/outpost/<uuid>/ with JSON messages:
Message type Direction Payload
hello outpost → server version, build hash, hostname
ack server → outpost acknowledgement of hello
triggerUpdate server → outpost "config changed, refresh"
providerSpecific both per-provider data (e.g. LDAP user lookup, proxy session validation)

The websocket reconnects with exponential backoff on disconnect. If the server is unreachable for >5 minutes, the outpost continues serving with its last-known config (degraded mode).

Proxy outpost specifics (internal/outpost/proxyv2/)

Sub-packages:

Package Purpose
application/ Per-application RoutingApp instance
codecs/ HS256 signing for forward-auth signed redirects
filesystemstore/, postgresstore/, sessionstore/ Session storage backends (default: in-memory sessionstore)
metrics/ Prometheus metrics
templates/ Error pages
types/ Wire types
hs256/ HMAC-SHA256 helpers

The proxy outpost terminates the application's URL, decides if the request has a valid session, and either forwards (with injected headers) or redirects to the authentik flow.

LDAP outpost specifics (internal/outpost/ldap/)

Built on github.com/jimlambrt/gldap. Sub-packages:

Package Purpose
bind/direct/, bind/memory/ Two bind strategies: direct (query authentik on every bind) and memory (cache for performance, with TTL)
search/direct/, search/memory/ Same split for search ops
server/ Net listener and connection handler
group/ Group resolution + memberOf computation
flags/, constants/, utils/, handler/ Plumbing
metrics/ Prometheus

The memory backend caches users/groups in-process; the direct backend hits authentik on every operation. Memory is faster but uses RAM proportional to your directory size. Default is configurable per provider.

RADIUS outpost (internal/outpost/radius/)

Stateless. Each Access-Request triggers a flow execution synchronously. The flow runs to completion server-side; outpost returns Access-Accept or Access-Reject. MS-CHAPv2 is implemented; PAP/CHAP fall back to that.

RAC outpost (internal/outpost/rac/)

Hosts a WebSocket → Guacamole protocol bridge. The web client at web/src/rac/ connects via WebSocket; the outpost translates between Guacamole messages and the configured RDP/SSH/VNC endpoint. Connections short-lived; each new session re-validates with the server.


9. The Blueprint System

Blueprints are authentik's GitOps interface. Every object in the API can be expressed as YAML and applied declaratively. The implementation lives in authentik/blueprints/.

Loader (authentik/blueprints/v1/)

Module Purpose
common.py YAML loader registration with custom tags
importer.py Walks entries list, instantiates models, persists
exporter.py Inverse: dumps models to YAML preserving FK relationships
meta/ Self-describing metadata for the blueprint format itself

YAML tags

The loader registers a set of !Tag constructors that get evaluated at apply time:

Tag Implementation Use
!Env VAR_NAME Reads os.environ[VAR_NAME] Inject secrets from env into blueprints
!Find [model, [field, value]] Calls Model.objects.get(field=value) Reference existing objects by lookup
!KeyOf id_in_blueprint Cross-references identifiers.id within the same blueprint Wire blueprint-local FKs
!Format [template, *args] Python .format() String building
!If [condition, then, else] Conditional Dynamic blueprints
!Context key Reads from BlueprintInstance.context Apply-time variable injection

Apply flow

File watcher (authentik/blueprints/apps.py)
     │ on file create/modify
     ▼
Importer (v1/importer.py)
     │ parse YAML
     │ for each entry:
     │   - resolve identifiers → existing model? create_or_update
     │   - resolve attrs (eval tags)
     │   - validate via DRF serializer
     │   - save in transaction
     ▼
BlueprintInstance row updated with last_applied_hash
     │
     ▼
Emit Event(action="blueprint_apply")

Discovery

The apps.py registers two file watchers via watchdog:

  1. /blueprints/ (mount point, configurable via AUTHENTIK_BLUEPRINTS_DIR)
  2. The bundled blueprints/default/ and blueprints/system/

On change, the file is hashed and compared to BlueprintInstance.last_applied_hash in PostgreSQL. If different, re-apply. This means: change a blueprint, the worker picks it up within seconds and re-applies it — no restart needed.

Bundled blueprints

Directory Purpose
blueprints/default/ The default flows, stages, policies that ship with authentik. Re-applied on upgrade to recover from accidental deletions.
blueprints/example/ Reference blueprints — provider configs for Slack, Nextcloud, AWS, GitLab, etc. Not auto-applied.
blueprints/system/ Internal blueprints used by authentik itself (e.g. system-event configuration).
blueprints/testing/ CI-only blueprints used to seed test fixtures.
blueprints/migrations/ Schema evolution blueprints (when a Django migration alone can't fully express the change).

Apply via CLI

authentik/blueprints/management/commands/apply_blueprint.py:36:

docker compose exec worker ak apply_blueprint /path/to/file.yaml
docker compose exec worker ak apply_blueprint --export > all-current-state.yaml

The --export is the inverse: dumps the current DB state as a blueprint, useful for backing up a working configuration.


10. Configuration Reference

authentik/lib/default.yml is the canonical baseline. Every key is overridable via:

  1. Mounted local.env.yml (merged with default.yml via deep-merge — uses the deepmerge package).
  2. AUTHENTIK_* environment variables with __ separating nested keys.

Full keyspace (from authentik/lib/default.yml)

postgresql:
  host: localhost            # AUTHENTIK_POSTGRESQL__HOST
  name: authentik
  user: authentik
  port: 5432
  password: "env://POSTGRES_PASSWORD"   # special: read from env at runtime
  sslmode: disable           # disable | allow | prefer | require | verify-ca | verify-full
  conn_max_age: 0            # seconds; 0 = no persistent
  conn_health_checks: false
  use_pool: false
  test: { name: test_authentik }
  default_schema: public
  read_replicas: {}          # 0: { host: ..., port: ... }, 1: { ... }

listen:
  http:    ["[::]:9000"]
  https:   ["[::]:9443"]
  ldap:    ["[::]:3389"]
  ldaps:   ["[::]:6636"]
  radius:  ["[::]:1812"]
  metrics: ["[::]:9300"]
  debug:    0.0.0.0:9900
  debug_py: 0.0.0.0:9901
  debug_tokio: "[::]:6669"
  trusted_proxy_cidrs:
    - 127.0.0.0/8
    - 10.0.0.0/8
    - 172.16.0.0/12
    - 192.168.0.0/16
    - fe80::/10
    - ::1/128

http_timeout: 30

cache:
  # url: ""              # (default = django-postgres-cache; set to redis://… to use external)
  timeout: 300           # general cache TTL
  timeout_flows: 300     # FlowPlanner cache TTL
  timeout_policies: 300  # policy decision cache TTL

# channel:
#   url: ""              # (default = django-channels-postgres; set to redis:// to use external)

debug: false
debugger: false

log_level: info          # debug | info | warning | error | critical
log:
  http_headers:
    - User-Agent         # which incoming headers to log
  rust_log:              # per-module log levels for Rust frontend (when running src/)
    console_subscriber: info
    h2: info
    hyper_util: warn
    mio: info
    notify: info
    reqwest: info
    runtime: info
    rustls: info
    sqlx: info
    sqlx_postgres: info
    tokio: info
    tungstenite: info

sessions:
  unauthenticated_age: days=1

error_reporting:
  enabled: false
  sentry_dsn: https://[email removed]/...
  environment: customer
  send_pii: false
  sample_rate: 0.1

email:
  host: localhost
  port: 25
  username: ""
  password: ""
  use_tls: false
  use_ssl: false
  timeout: 10
  from: authentik@localhost
  template_dir: /templates

throttle:
  providers:
    oauth2:
      device: 20/hour
  default: 1000/second

outposts:
  container_image_base: ghcr.io/goauthentik/%(type)s:%(version)s
  discover: true
  disable_embedded_outpost: false

ldap:
  task_timeout_hours: 2
  page_size: 50
  tls: { ciphers: null }

sources:
  kerberos: { task_timeout_hours: 2 }

reputation:
  expiry: 86400          # seconds; reputation score lifetime

cookie_domain: null
disable_update_check: false
disable_startup_analytics: false

events:
  context_processors:
    geoip: "/geoip/GeoLite2-City.mmdb"
    asn: "/geoip/GeoLite2-ASN.mmdb"

compliance:
  fips:
    enabled: false       # set true → FIPS-only crypto

cert_discovery_dir: /certs

tenants:
  enabled: false         # django-tenants (schema-per-tenant); separate from Brands
  api_key: ""

blueprints_dir: /blueprints

web:
  workers: 2             # gunicorn worker processes
  threads: 4             # threads per worker
  path: /                # URL path prefix
  timeout_http_read_header: 5s
  timeout_http_read: 30s
  timeout_http_write: 60s
  timeout_http_idle: 120s

worker:
  processes: 1           # dramatiq worker processes
  threads: 2             # threads per process
  consumer_listen_timeout: seconds=30
  task_max_retries: 5
  task_default_time_limit: minutes=10
  task_purge_interval: days=1
  task_expiration: days=30
  scheduler_interval: seconds=60

storage:
  backend: file          # file | s3
  file:
    path: ./data
    url_expiry: minutes=15
  s3:
    # region: us-east-1
    # use_ssl: true
    # endpoint: https://s3.us-east-1.amazonaws.com
    # signature_version: s3v4
    # access_key: ""
    # secret_key: ""
    # bucket_name: authentik-data
    # custom_domain: null
    secure_urls: true
    url_expiry: minutes=15

skip_migrations: false

Env-var convention

Two-level nesting: AUTHENTIK_<TOP>__<SUB>=value. Three-level: AUTHENTIK_<TOP>__<SUB>__<SUB2>=value. Examples:

AUTHENTIK_POSTGRESQL__HOST=db.example.com
AUTHENTIK_POSTGRESQL__SSLMODE=require
AUTHENTIK_POSTGRESQL__READ_REPLICAS__0__HOST=replica1.example.com
AUTHENTIK_POSTGRESQL__READ_REPLICAS__0__PORT=5432
AUTHENTIK_EMAIL__USE_TLS=true
AUTHENTIK_OUTPOSTS__CONTAINER_IMAGE_BASE=internal.registry.example.com/authentik/%(type)s:%(version)s
AUTHENTIK_LOG__RUST_LOG__SQLX=warn

The parser is in authentik/lib/config.py (AttrEncoder, set_path_in_dict). The Go side has a twin at internal/config/config.go reading the same YAML.

Build-time env vars (extracted/config_keys.tsv)

These are read at container build / startup, not runtime config:

Var Used by Purpose
POSTGRES_PASSWORD default.yml env:// substitution Inject DB password without writing it to a config file
WORKSPACE_DIR packages/ak-common/src/config/mod.rs:33 Rust workspace root (build-time)
AUTHENTIK_ENV packages/ak-common/src/config/mod.rs:41 dev / production switch for Rust frontend
NODE_ENV esbuild production enables minification
AK_LOG_LEVEL web/logger/node.js:38 Build-time log level
AK_BUNDLER web/bundler/utils/node.js:30 Pick bundler (esbuild default)
AK_API_BASE_PATH web/bundler/utils/node.js:29 API base path injected into JS bundle
AK_DOCUSAURUS_ENV packages/docusaurus-config/lib/common.js:46 Docs build environment
GIT_BUILD_HASH internal/constants/constants.go:13 Embedded in Go binaries' version string
dramatiq_prom_host / dramatiq_prom_port packages/django-dramatiq-postgres/django_dramatiq_postgres/forks.py:14-15 Dramatiq Prometheus exporter bind

11. Database Schema and Migrations

Schema overview

Major tables (Django app prefix in parentheses):

Table App Purpose
core_user authentik_core Users (extends django.contrib.auth.models.AbstractUser)
core_group authentik_core Groups
core_application authentik_core Applications
core_source authentik_core Sources (abstract base)
core_token authentik_core API tokens + recovery tokens
core_authenticatedsession authentik_core Active sessions
core_propertymapping authentik_core Property mappings (abstract base)
flows_flow authentik_flows Flows
flows_stage authentik_flows Stages (abstract base)
flows_flowstagebinding authentik_flows Stage bindings
flows_flowtoken authentik_flows Continuation tokens
policies_policy authentik_policies Policies (abstract base)
policies_policybinding authentik_policies Polymorphic bindings (Flow ↔ Policy, Stage ↔ Policy, App ↔ Policy)
providers_oauth2_oauth2provider authentik_providers_oauth2 OAuth2 providers
providers_oauth2_accesstoken authentik_providers_oauth2 Access tokens
providers_oauth2_refreshtoken authentik_providers_oauth2 Refresh tokens
providers_oauth2_authorizationcode authentik_providers_oauth2 Auth codes (60s TTL)
providers_saml_samlprovider authentik_providers_saml SAML providers
(similar per provider/source/stage…)
events_event authentik_events Audit log
events_notification authentik_events Notifications
events_notificationrule authentik_events Notification rules
events_notificationtransport authentik_events Notification transports
outposts_outpost authentik_outposts Outpost instances
outposts_outpostserviceconnection authentik_outposts Docker/K8s connection configs
crypto_certificatekeypair authentik_crypto TLS certificates and keys
blueprints_blueprintinstance authentik_blueprints Applied blueprints (with hash)
brands_brand authentik_brands Multi-tenant brands
tenants_tenant authentik_tenants django-tenants tenants (if enabled)
rbac_role authentik_rbac RBAC roles
tasks_taskschedule authentik_tasks Cron-like task schedules

Polymorphic models

Several abstract bases use django-pgtrigger to enforce that subclass tables share a UUID with the parent table:

  • Stage → IdentificationStage, PasswordStage, …
  • Provider → OAuth2Provider, SAMLProvider, …
  • Source → LDAPSource, OAuthSource, …
  • Policy → ExpressionPolicy, EventMatcherPolicy, …
  • PropertyMapping → OAuth2ScopeMapping, SAMLPropertyMapping, …

SerializerModel (in authentik/lib/models.py) is a mixin that asserts every model has an associated DRF Serializer for the API.

Migrations

Where Purpose
authentik/*/migrations/*.py Standard Django migrations (auto-generated + hand-written)
blueprints/migrations/ Schema-evolution blueprints (when YAML changes are needed alongside Python migrations)
lifecycle/system_migrations/ Imperative one-shot scripts run on container start (e.g. fix a corrupted index)

Migrations are run in the worker container at startup via lifecycle/migrate.py:

# pseudocode
wait_for_db()           # lifecycle/wait_for_db.py
manage.migrate()        # standard Django
run_system_migrations() # lifecycle/system_migrations/
apply_default_blueprints()

Warning

Migrations are forward-only. Skipping minor releases can fail (see #22688, upgrade 2025.10.2 → 2026.5.0 broke migration authentik_core.0058_setup). The supported path is to upgrade through each minor in sequence.

Database performance considerations

  • The default conn_max_age: 0 opens a new PostgreSQL connection per request. Set to 60 for ~10× lower DB CPU at the cost of more idle connections. Set to -1 for "persistent until explicit close".
  • Set use_pool: true to enable the psycopg[pool] connection pool. Recommended for production at scale.
  • Read replicas are honored automatically for ViewSet list and retrieve actions (when read_replicas is configured).

12. Background Workers and Scheduling

Architecture

                 ┌─────────────────────┐
                 │     PostgreSQL      │
                 │  ┌───────────────┐  │
                 │  │ dramatiq_     │  │
                 │  │  postgres_    │  │
                 │  │  queue        │  │
                 │  │  (FIFO table) │  │
                 │  └───────┬───────┘  │
                 └──────────┼──────────┘
                            │ LISTEN/NOTIFY
                            ▼
        ┌───────────────────────────────────────┐
        │   worker container                     │
        │   ┌──────────────────────────────┐     │
        │   │ Scheduler (single instance)   │     │
        │   │  - reads tasks_taskschedule   │     │
        │   │  - enqueues at scheduled time │     │
        │   └──────────────────────────────┘     │
        │   ┌──────────────────────────────┐     │
        │   │ Worker processes (N)          │     │
        │   │  - each runs dramatiq         │     │
        │   │  - pulls from queue           │     │
        │   │  - runs task → result/error   │     │
        │   └──────────────────────────────┘     │
        └───────────────────────────────────────┘

Why dramatiq?

dramatiq was chosen over Celery for cleaner middleware, better retry semantics, and a simpler API. The PostgreSQL broker (packages/django-dramatiq-postgres/) was custom-built to eliminate the Redis dependency.

Task discovery

Each Django app can define tasks in <app>/tasks.py. The authentik/tasks/apps.py auto-discovers them via apps.get_app_configs() on startup. Example tasks:

  • authentik.providers.scim.tasks.scim_sync_all — periodic SCIM sync
  • authentik.events.tasks.event_notification_handler — dispatch notifications
  • authentik.events.tasks.notification_cleanup — purge old notifications
  • authentik.outposts.tasks.outpost_controller — reconcile outposts with Docker/K8s state
  • authentik.sources.ldap.tasks.ldap_sync_all — periodic LDAP sync per source
  • authentik.crypto.tasks.certificate_discovery — watch /certs/ for new files

Schedules

TaskSchedule rows in PostgreSQL define cron-like schedules. The scheduler (single-instance) polls every worker.scheduler_interval (default 60s) and enqueues due tasks. Worker processes pick them up.

Retry semantics

From default.yml:

worker:
  task_max_retries: 5
  task_default_time_limit: minutes=10
  task_purge_interval: days=1
  task_expiration: days=30

Failed tasks retry with exponential backoff up to 5 times, with a hard 10-minute limit per attempt. Completed/failed tasks are purged daily; the task history is kept for 30 days.

Pre-flight

lifecycle/wait_for_db.py blocks worker startup until PostgreSQL is ready. After DB is up, lifecycle/migrate.py runs migrations, then dramatiq starts.

Important

Worker startup can fail if migrations themselves fail (e.g. lock timeout, corrupt schema). If you see "worker not starting", check the worker logs for migrat keywords. The server can run without the worker (degraded mode — no background tasks fire), but the worker requires a healthy DB.


13. API Surface (/api/v3/)

The REST API is generated by drf-spectacular from DRF ViewSets across all Django apps. The OpenAPI spec is at schema.yml and rendered as Swagger UI at /api/v3/swagger/.

Versioning

The current version is v3 (path-versioned). Earlier v2 and v1 are retired. A future v4 would live at /api/v4/; both could coexist.

Authentication

Three methods, by header:

Method Header Use
Bearer token Authorization: Bearer <token> API-key access (user-scoped)
Session cookie (browser default) Admin UI calls
Outpost token Authorization: Bearer <token> Outpost ↔ server (the token scope is per-outpost, not user)

API tokens are generated under Admin → Directory → Tokens or via the API itself. Service-account users hold tokens with no associated interactive login.

Major endpoint groups

Path Resource
/api/v3/core/users/ User CRUD + impersonation, password set, MFA management
/api/v3/core/groups/ Group CRUD + membership
/api/v3/core/applications/ Application CRUD
/api/v3/core/sources/ Source listing (polymorphic)
/api/v3/flows/instances/ Flow CRUD
/api/v3/flows/bindings/ FlowStageBinding CRUD
/api/v3/flows/executor/<slug>/ Execute a flow programmatically
/api/v3/stages/<type>/ One ViewSet per stage type
/api/v3/providers/<type>/ One ViewSet per provider type
/api/v3/sources/<type>/ One ViewSet per source type
/api/v3/policies/<type>/ One ViewSet per policy type
/api/v3/policies/bindings/ PolicyBinding CRUD
/api/v3/outposts/instances/ Outpost CRUD
/api/v3/outposts/service_connections/<type>/ Docker / K8s connection configs
/api/v3/crypto/certificatekeypairs/ Cert CRUD + auto-generate + import
/api/v3/events/events/ Event read (write is internal)
/api/v3/events/notifications/ Notification CRUD
/api/v3/events/transports/ Transport CRUD
/api/v3/events/rules/ NotificationRule CRUD
/api/v3/managed/blueprints/ Blueprint upload + apply + export
/api/v3/admin/system/ System info, version, workers, settings
/api/v3/rbac/roles/ Role CRUD
/api/v3/rbac/permissions/ Permission listing
/api/v3/tenants/tenants/ Tenant CRUD (when tenants.enabled=true)
/api/v3/tenants/settings/ Tenant-scoped settings

Schema generation

docker compose exec worker ak build_schema --api-file schema.yml --blueprint-file blueprints/schema.json

Output: schema.yml (~3 MB, full OpenAPI 3.0) + blueprints/schema.json (JSON Schema for blueprint validation).

Generated SDKs

SDK Path Notes
TypeScript packages/client-ts/ Used by web/. Auto-generated, do not edit.
Go packages/client-go/ Used by outposts.
Rust packages/client-rust/ Used by Rust frontend (src/).

All three are regenerated via openapi-generator-cli on schema change. The generation is hooked into CI; manual regeneration is via make gen-client-* targets in the Makefile.

Standard DRF semantics: ?page=2, ?page_size=50, ?ordering=-created, ?search=foo, plus per-resource ?<field>=value filters via django-filter (declared in each ViewSet).

DjangoQL (djangoql package) is exposed as a ?q=… query on some endpoints — a more SQL-like search syntax. The ak-search-ql web component (web/src/components/ak-search-ql/) provides syntax highlighting + autocomplete.


14. Frontend Architecture (web/)

Stack

Layer Choice
Framework Lit (web components, ~6 KB runtime)
State Lit reactive controllers + URL state
Styling PatternFly 4 (server-hosted CSS) + custom theme
HTTP Generated @goauthentik/api client (fetch wrapper)
Routing web/paths/ — minimal hash router
Bundler esbuild (web/bundler/)
i18n @lit/localize — XLF files in web/xliff/, dictionaries in locale/<lang>/dictionaries/
Editor Monaco (lazy-loaded for blueprint editor)
Charts Chart.js wrappers in web/src/elements/charts/
Testing @web/test-runner (unit), @playwright/test (e2e in web/e2e/)

Entrypoint inventory

Entrypoint URL Bundle
admin/index.entrypoint.ts /if/admin/ Admin interface
user/index.entrypoint.ts /if/user/ User interface
flow/index.entrypoint.ts /if/flow/<slug>/ Flow executor
rac/index.entrypoint.ts /if/rac/<token>/ RAC client
standalone/api-browser/index.entrypoint.ts /if/api-browser/ Embedded Swagger UI
standalone/loading/index.entrypoint.ts (initial paint) Splash before route resolution

The Simplified Flow Executor (SFE)

web/packages/sfe/ is a separate, intentionally minimal bundle for IE-11 / MSEdge-18 (Office 365 / Teams). Per web/packages/sfe/README.md:

The Simplified Flow Executor is a limited fallback browser-side interpreter, written in TypeScript, for authentik's Flow language … It exists primarily to support late versions of Microsoft Office365 and Microsoft Teams, older software that still uses the MSEdge-18 and IE-11 Trident web engine.

Supports only 5 stages: identification, password, redirect, autosubmit, authenticator_validate. Other stage types return an error in SFE.

Built with Rollup (not esbuild) to fit the IE-11 module format. Pulls in:

  • core-js@3.49.0 for ES modern stdlib
  • formdata-polyfill
  • weakmap-polyfill
  • @webcomponents/template

The detection logic that routes a Trident UA to the SFE bundle lives in authentik/flows/views/executor.py (server-side UA sniff).

State management pattern

There's no global store (no Redux). Each route is its own Lit element with its own reactive controllers. Server state is the source of truth; on mutation, refetch. The AKBaseElement mixin (web/src/elements/Base.ts) provides locale + theme + global config.

Locale

locale/<lang>/LC_MESSAGES/*.po files are compiled to .xlf for @lit/localize. Adding a language:

  1. Add <lang> to web/scripts/build-locales.mjs.
  2. Run npm run extract-locales to seed.
  3. Translate.
  4. Commit.

Server-side Django translations use the same .po files (django-translatable).


15. Operations Runbooks

Daily checks

  1. Dashboard → All systems healthy, worker count ≥ 1, no failed system tasks.
  2. System → Tasks → No tasks stuck in "Running" for > 1 hour.
  3. Events → Filter by severity = warning or alert; review.
  4. Outposts → All Last seen < 5 minutes ago.

Weekly checks

  1. Backup PostgreSQL + /data/ volume.
  2. Check System → Sources sync results; investigate failures.
  3. Directory → Tokens — review and prune unused tokens.
  4. Customization → Certificates — note expiry dates of cert pairs in use.

Adding a node to a Kubernetes HA install

kubectl scale deployment authentik-server --replicas=3
kubectl scale deployment authentik-worker --replicas=2

Server is stateless (except for the embedded outpost — set outposts.disable_embedded_outpost=true in HA and use external proxy outposts). Worker shouldn't be scaled past 1-2 unless your task load demands it; the scheduler runs in only one of them (leader-elected via PostgreSQL advisory lock).

Rotating the secret key

NEW_KEY=$(openssl rand -base64 60 | tr -d '\n')

# 1. Set new key alongside old (comma-separated; first is used for signing, rest for verification)
AUTHENTIK_SECRET_KEY="${NEW_KEY},${OLD_KEY}" docker compose up -d

# 2. Wait for all sessions/tokens to expire (or revoke them)
# 3. Remove old key:
AUTHENTIK_SECRET_KEY="${NEW_KEY}" docker compose up -d

Migrating from bundled to external PostgreSQL

# 1. Backup
docker compose exec postgresql pg_dump -U authentik authentik > backup.sql

# 2. Restore to external DB
psql -h db.example.com -U authentik -d authentik < backup.sql

# 3. Update .env
AUTHENTIK_POSTGRESQL__HOST=db.example.com
AUTHENTIK_POSTGRESQL__PASSWORD=...

# 4. Edit compose.yml — remove postgresql service
docker compose up -d

Importing a TLS cert pair

docker compose exec worker ak import_certificate \
  --name "wildcard-example-com" \
  --certificate /certs/wildcard.crt \
  --private-key /certs/wildcard.key

Or drop them into /certs/ (mounted into the worker) — they're auto-discovered by the certificate_discovery task.

Tracing a specific user's login

  1. Events → Filter by user = [email removed] → see chronological events.
  2. System → Logs (if log_level: debug) for the worker — search for the user's email or session ID.
  3. Server logs: docker compose logs server | grep "session=alice".

Recovering a locked-out admin

docker compose exec worker ak create_recovery_key 60 akadmin

Token URL is printed; navigate to it to bypass auth and reset.

Disabling the embedded outpost in production

AUTHENTIK_OUTPOSTS__DISABLE_EMBEDDED_OUTPOST=true docker compose up -d

Then create a separate proxy outpost. This is the recommended config for HA installs (the embedded outpost can't coordinate across replicas; multiple separate outposts can).

Force-reapply default blueprints

If you accidentally deleted a default flow:

docker compose exec worker ak apply_blueprint default/flow-default-authentication.yaml

Or set AUTHENTIK_BLUEPRINTS__INTERVAL=10 to re-scan every 10 seconds (default is on change).


16. Security Analysis

Threat model

Asset Threats Mitigations
User credentials Phishing, credential stuffing, brute force MFA stages (TOTP/WebAuthn/Duo/etc.), reputation policy, captcha, GeoIP policy
Session cookies Theft via XSS, CSRF HttpOnly, Secure, SameSite=Lax; CSP headers; CSRF tokens on POST
API tokens Leak, replay Token scoping (RBAC), expiry, revocation, audit
Outpost ↔ server channel MITM TLS by default (set AUTHENTIK_INSECURE=true only for dev)
Database Exposure of password hashes (argon2id) Hash with argon2-cffi; reasonable cost parameters by default
SAML XML XXE, signature wrapping defusedxml, xmlsec signature validation
Blueprint apply Privilege escalation via crafted blueprint Restricted to admins or the file watcher (filesystem-mounted)

Authentication and authorization

  • Local password — argon2id (argon2-cffi==25.1.0). Configurable cost in policy.
  • MFA — TOTP (RFC 6238), WebAuthn (FIDO2, via fido2==2.2.0), Duo, SMS, email OTP.
  • Federated — SAML/OAuth/LDAP/Kerberos/SCIM/Plex/Telegram.
  • Cert-based — MTLS stage (EE) validates client X.509 cert against a configured trust chain.

Cryptography

  • TLS: stdlib OpenSSL by default; AWS-LC-RS with FIPS feature when compliance.fips.enabled=true.
  • Signing: cryptography==48.0.0 for X.509 ops, jwcrypto==1.5.7 for JWS/JWE, xmlsec==1.3.17 for SAML.
  • Hashing: argon2id for passwords, SHA-256 for token fingerprints.
  • Random: secrets (Python) for tokens, crypto/rand (Go) for outposts.

CSRF protection

Django's default CSRF middleware is enabled. The Flow executor POSTs include CSRF tokens. API endpoints called from browser sessions also enforce CSRF; token-authenticated requests skip CSRF (the token itself is the auth).

Audit trail

Every action emits an Event row:

  • Authentication: login, login_failed, logout
  • User mgmt: user_write, user_delete, password_set, email_sent
  • Configuration: model_created, model_updated, model_deleted, secret_view
  • Impersonation: impersonation_started, impersonation_ended (with both users recorded)
  • Source: source_linked, source_unlinked
  • Flow: flow_execution (records every flow run)
  • Policy: policy_execution (only when configured to log)
  • System: update_available, configuration_error

Events are stored in authentik_events_event indefinitely by default. Configure retention via a scheduled task or the EE audit log module.

Known security advisories

SECURITY.md references the CVE disclosure process. Recent advisories at docs.goauthentik.io/docs/security/cves. The project conducts external audits — see website/docs/security/audits-and-certs/.

Hardening checklist

  • [ ] AUTHENTIK_SECRET_KEY set to 60+ random chars, distinct per environment.
  • [ ] PostgreSQL on a private network; AUTHENTIK_POSTGRESQL__SSLMODE=require or higher.
  • [ ] Reverse proxy enforces TLS; never expose 9000/9443 directly.
  • [ ] AUTHENTIK_LISTEN__TRUSTED_PROXY_CIDRS correctly scoped (don't trust the world).
  • [ ] Default akadmin user disabled after creating a real admin.
  • [ ] Recovery key generated and stored offline.
  • [ ] MFA required for all admin users via flow policy.
  • [ ] Reputation policy enabled to throttle brute force.
  • [ ] GeoIP policy enabled if your user population is geographically constrained.
  • [ ] Notification rules for login_failed (≥ N in M minutes) routed to a SIEM or chat.
  • [ ] AUTHENTIK_ERROR_REPORTING__ENABLED=false if you don't want telemetry to Sentry.
  • [ ] AUTHENTIK_DISABLE_UPDATE_CHECK=true + AUTHENTIK_DISABLE_STARTUP_ANALYTICS=true for air-gapped.
  • [ ] Outpost tokens rotated periodically (regenerate in UI, redeploy outpost with new token).
  • [ ] Docker socket not mounted to worker, OR a socket-proxy is in place.
  • [ ] compliance.fips.enabled=true if FedRAMP/FIPS 140-2 required.

Caution

The internal debug endpoint at port 9900 exposes pprof profiles and dump_config — these reveal sensitive runtime state. The container does not expose this port publicly by default. Do not change that. Set AUTHENTIK_DEBUG=false (default) for production.


17. Performance Tuning

Server (Django + uvicorn)

Setting Default Tune to
web.workers (gunicorn processes) 2 2 × CPU cores
web.threads (threads per worker) 4 4-8 for CPU-bound, 16+ for I/O-bound
web.timeout_http_read 30 s 60 s if flows have slow stages (e.g. captcha provider)
web.timeout_http_idle 120 s 300 s for long-poll-style integrations
postgresql.conn_max_age 0 60 (persistent connections)
postgresql.use_pool false true (with psycopg[pool])
cache.timeout_flows 300 s 600 s (longer cache → less PostgreSQL load; trade off vs. policy freshness)

Worker

Setting Default Tune to
worker.processes 1 2-4 for heavy sync workloads (LDAP, SCIM)
worker.threads 2 4-8
worker.task_default_time_limit 10 m 30 m if your largest sync exceeds 10 m

PostgreSQL

-- Reasonable starting point for a medium install
max_connections = 200
shared_buffers = 2GB             -- 25% of RAM
effective_cache_size = 6GB       -- 75% of RAM
work_mem = 16MB
maintenance_work_mem = 256MB
random_page_cost = 1.1           -- if on SSD
wal_buffers = 16MB
synchronous_commit = on          -- never set off for an IdP

Index hot-spots: authentik_events_event.created, authentik_core_authenticatedsession.expires, authentik_providers_oauth2_accesstoken.expires.

Outpost scaling

  • LDAP outpost — the bind/memory/ backend dramatically reduces server load. Tune ldap.page_size (default 50) higher for large directories (500+ per page acceptable). Set ldap.task_timeout_hours to match your largest sync.
  • Proxy outpost — use postgresstore session backend for sticky-session-free load balancing across multiple proxy outposts.
  • RADIUS outpost — stateless and CPU-cheap; one outpost can handle thousands of req/s.

Frontend

  • The admin SPA's bundle is ~1 MB gzipped. The User and Flow bundles are smaller (~300 KB and ~150 KB).
  • All web assets are served with Cache-Control: max-age=31536000, immutable (filenames are hashed).
  • SFE bundle (for IE-11) is ~80 KB gzipped.

Caching impact

The FlowPlanner cache (cache.timeout_flows) is the single highest-leverage cache. A cache hit returns a pre-computed FlowPlan without re-evaluating policies — for high-traffic flows this can reduce per-request DB queries by 90%+.

The policy decision cache (cache.timeout_policies) caches individual policy results keyed on (policy_id, user_id, request_hash). Useful for expensive Expression policies; less useful for fast policies (the cache lookup may cost as much as the evaluation).


18. Code Quality and Patterns

Strengths

  • Consistent layering — every Django app has the same structure (models.py, api/, tests/, migrations/, often signals.py and tasks.py). Onboarding a new contributor is fast.
  • Clear abstractions — Stage, Provider, Source, Policy, PropertyMapping are well-defined base classes. Adding a new one is a recipe.
  • Type hints — Python 3.14 with extensive type annotation; pydantic for validation.
  • Test coverage — every stage, provider, source has its own tests/ directory. The briefing's confidence_gaps shows 47 modules without tests, mostly in web/ and template directories — Python coverage is high.
  • Separation of API and UI — the SPA consumes the same REST API as third-party clients. The schema is the contract.
  • Outposts as first-class citizens — running protocol handlers in their own process is the right call.
  • Blueprints — declarative config from day one means GitOps is supported without bolt-on tooling.

Rough edges

Issue Where Impact
cmd/server/server.go attemptProxyStart polls every 1s up to 100 times cmd/server/server.go:73-100 If the embedded outpost can't start, the server runs without it for ~100s before giving up. Could be event-driven.
authentik/lib/default.yml is the canonical config but every key needs a parallel entry in env-var parsing authentik/lib/config.py New config additions easy to forget on the env-var side.
Three runtimes (Python + Go + Rust) means three sets of CI, three sets of dependencies repo-wide Higher maintenance burden than a single-language codebase.
Migrations are forward-only and skip-version is unsupported per #22688 Users must upgrade through each minor.
Web bundle is ~1 MB for the admin SPA web/src/admin/ Heavy on first load. Code-splitting helps but not eliminated.
Rust frontend (src/) is in an in-between state repo-wide Two HTTP frontends in tree (Go cmd/server + Rust src/main.rs). Not yet clear which to use when.
Some EE features have non-obvious dependencies authentik/enterprise/ E.g. RAC requires a separate outpost binary, but the UI doesn't tell you until after creation.
cspell.config.jsonc shows the project maintains its own dictionary of accepted misspellings repo root A long list — implies some legacy decisions that didn't get cleaned.

Patterns worth borrowing

  • !Env, !Find, !KeyOf YAML tags — a clean blueprint language for declarative config. Many tools could learn from this.
  • Outpost-as-binary architecture — using Go for the protocol surface and Python for the business logic is the right split for an IdP.
  • PostgreSQL-only stack — the django-channels-postgres + django-postgres-cache + django-dramatiq-postgres combo eliminates Redis entirely. Powerful simplification.
  • Flow + Stage + Policy — extensible-by-composition for authentication workflows. Adopted by Keycloak (in the Authentication SPI) and others.

19. Testing Strategy

Python (pytest)

docker compose exec worker pytest authentik/

Tests cover models, ViewSets, flow executors, blueprint round-tripping, policy evaluation. Use pytest-django for DB fixtures, pytest-mock for mocking external calls.

Key test directories:

  • authentik/core/tests/
  • authentik/flows/tests/
  • authentik/blueprints/tests/
  • authentik/providers/<n>/tests/
  • authentik/sources/<n>/tests/
  • authentik/stages/<n>/tests/
  • authentik/policies/<n>/tests/

Go (go test)

go test ./...

Outposts have a smaller test footprint than the Python core, but each subpackage (internal/outpost/ldap/, internal/outpost/proxyv2/, etc.) has unit tests for handlers.

Rust (cargo test)

cargo test --workspace

Tests the axum extensions, PROXY-protocol parser, and config loader.

Integration (tests/integration/)

Python-driven tests that spin up the full stack via docker-compose, then exercise the API.

End-to-end (tests/e2e/ + web/e2e/)

Two layers:

  1. tests/e2e/ — docker-compose fixtures that test integrations with real LDAP servers, OAuth IdPs, Envoy/Nginx/Traefik forward-auth, etc.
  2. web/e2e/ — Playwright tests that drive a real browser through actual flows.

OIDC conformance

tests/openid_conformance/ runs the OpenID Connect Foundation conformance suite against a local authentik install. Run via make test-openid-conformance.

Continuous integration

GitHub Actions workflows live in .github/workflows/ (not in the briefing — see the repo). The README's badges show three pipelines: ci-main (core build/test), ci-outpost (Go binaries), ci-web (TS/web).

Test gaps (from metadata.json:1332-1346)

The briefing's confidence_gaps reported test_coverage for 47 directories. Spot-checking:

  • Several migrations/ directories — typically intentional (migrations aren't unit-tested).
  • A few smaller stages (account_lockdown, mtls) — possibly intentional for EE-only code under refactor.
  • Some web/src/elements/ subdirs — UI components without unit tests but covered by e2e.

This is a partial measurement, not a coverage report. For a true coverage map, use Codecov (the README badge links to the live report).


20. Suggested Improvements

In rough order of impact:

  1. Reduce admin SPA bundle size. Code-split per route more aggressively; consider lazy-loading Monaco only when the blueprint editor opens (it accounts for a large chunk of the bundle).
  2. Migrate from Go embedded outpost to Rust frontend (the partial work in src/). Once the Rust frontend ships, the embedded-outpost startup polling loop becomes unnecessary.
  3. Make migrations safe across minor versions. Either add forward-only migration squashing every N releases, or document a "minor-by-minor upgrade" tool that automates the staged upgrade.
  4. Fix the OAuth single logout regression (#22572). 14-comment thread, still open.
  5. Fix Safari admin crash (#19166). 8-comment thread, references the older #14322.
  6. Resolve the "2fa stage no identification data" regression (#19552). 43 comments — high user impact.
  7. Fix invitation creation when User has custom required attributes (#22637).
  8. Restore the email password-reset template grammar (#22757).
  9. Add a CLI for outpost local development — currently outpost contributors need a full authentik server running.
  10. Document the Rust frontend's role — src/main.rs exists but its current production-readiness vs. cmd/server/main.go isn't documented anywhere. Decide and document.
  11. Standardize the outpost healthcheck mode arguments — currently healthcheck allinone exists for the server binary but the other outpost binaries don't accept modes. Inconsistent.
  12. Reduce config drift between default.yml and internal/config/config.go — adding a key requires updating both. Consider generating one from the other.
  13. Add per-flow rate limiting — currently throttling is global or per-provider; per-flow would be useful for slowing brute force on a specific authentication flow.
  14. Expose more metrics — the Prometheus surface is good but adding metrics for "active flows", "policy evaluation duration", "blueprint apply duration" would help operators.
  15. Investigate the cspell.config.jsonc accepted-misspellings list — long enough to suggest some technical debt in naming.

21. Contributing

The project ships CONTRIBUTING.md and CODE_OF_CONDUCT.md at the repo root. Developer documentation at docs.goauthentik.io/docs/developer-docs/ covers:

  • Setting up a dev environment (uv for Python, node@24, go@1.22+, Rust stable, PostgreSQL).
  • Running the test suites.
  • Code style: ruff + black for Python, gofmt for Go, rustfmt for Rust, prettier+eslint for TS.
  • Documentation style: website/docs/developer-docs/docs/style-guide.mdx (referenced from README).
  • Translation: managed via Transifex.

Make targets (from Makefile)

make gen-dev-config           # Generate local dev config
make gen-client-ts            # Regenerate TypeScript SDK
make gen-client-go            # Regenerate Go SDK
make docs                     # Build docs site
make docs-watch               # Build + watch docs
make test                     # Run Python tests
make test-coverage            # Python + coverage report
make lint                     # Run all linters
make web                      # Build web bundle
make web-watch                # Build + watch web

PR workflow

  1. Fork on GitHub.
  2. Branch from main.
  3. Run make lint test locally.
  4. Push, open PR.
  5. CI runs (ci-main, ci-outpost, ci-web) — must be green.
  6. Maintainer review; typically one round of feedback.
  7. Squash-merge.

Issue tracker

github.com/goauthentik/authentik/issues. Bug reports should use the template; feature requests via the same tracker.

Community


22. Quick Reference

Service-port matrix

Container Port Default proto Purpose
server 9000 HTTP API + Web + embedded outpost
server 9443 HTTPS API + Web (TLS)
server 9300 HTTP Prometheus metrics
server 9900 HTTP Debug (pprof, dump_config) — bind 0.0.0.0 default, scope away
ldap outpost 3389 LDAP LDAP (StartTLS)
ldap outpost 6636 LDAPS LDAP over TLS
radius outpost 1812 RADIUS/UDP RADIUS
proxy outpost 9000 HTTP Forward-auth + reverse-proxy
proxy outpost 9443 HTTPS Forward-auth + reverse-proxy (TLS)
postgresql 5432 PostgreSQL DB

Container image matrix

ghcr.io/goauthentik/server:<version>       # main server + worker
ghcr.io/goauthentik/proxy:<version>        # proxy outpost
ghcr.io/goauthentik/ldap:<version>         # ldap outpost
ghcr.io/goauthentik/radius:<version>       # radius outpost
ghcr.io/goauthentik/rac:<version>          # rac outpost

Pattern is ghcr.io/goauthentik/%(type)s:%(version)s (configurable via AUTHENTIK_OUTPOSTS__CONTAINER_IMAGE_BASE).

Mount-point cheat sheet

Path Container Purpose
/etc/authentik/config.yml server, worker Merged effective config (read-only)
/blueprints/ server, worker Custom blueprints
/data/ server, worker Uploaded files
/certs/ server, worker Auto-discovered cert pairs
/templates/ worker Custom email templates
/geoip/ server, worker GeoLite2 databases
/var/run/docker.sock worker Optional, for outpost auto-deploy

Stage-type matrix

(Confirmed in authentik/stages/ tree)

Type Module License
identification authentik/stages/identification/ core
password authentik/stages/password/ core
user_login authentik/stages/user_login/ core
user_logout authentik/stages/user_logout/ core
user_write authentik/stages/user_write/ core
user_delete authentik/stages/user_delete/ core
prompt authentik/stages/prompt/ core
consent authentik/stages/consent/ core
email authentik/stages/email/ core
captcha authentik/stages/captcha/ core
invitation authentik/stages/invitation/ core
deny authentik/stages/deny/ core
redirect authentik/stages/redirect/ core
source authentik/stages/source/ (EE-extended) core/EE
dummy authentik/stages/dummy/ core (test-only)
authenticator_static authentik/stages/authenticator_static/ core
authenticator_totp authentik/stages/authenticator_totp/ core
authenticator_webauthn authentik/stages/authenticator_webauthn/ core
authenticator_duo authentik/stages/authenticator_duo/ core
authenticator_sms authentik/stages/authenticator_sms/ core
authenticator_email authentik/stages/authenticator_email/ core
authenticator_validate authentik/stages/authenticator_validate/ core
authenticator_endpoint_gdtc authentik/enterprise/stages/authenticator_endpoint_gdtc/ EE
account_lockdown authentik/enterprise/stages/account_lockdown/ EE
mtls authentik/enterprise/stages/mtls/ EE

Provider-type matrix

Type Module License Outpost?
OAuth2 / OIDC authentik/providers/oauth2/ core no
SAML authentik/providers/saml/ core no
LDAP authentik/providers/ldap/ core yes (ldap)
RADIUS authentik/providers/radius/ core yes (radius)
Proxy authentik/providers/proxy/ core yes (proxy or embedded)
SCIM authentik/providers/scim/ core no
RAC authentik/providers/rac/ EE yes (rac)
MS Entra authentik/enterprise/providers/microsoft_entra/ EE no
Google Workspace authentik/enterprise/providers/google_workspace/ EE no
WS-Federation authentik/enterprise/providers/ws_federation/ EE no
SSF authentik/enterprise/providers/ssf/ EE no

Source-type matrix

Type Module License
LDAP authentik/sources/ldap/ core
OAuth authentik/sources/oauth/ core
SAML authentik/sources/saml/ core
Kerberos authentik/sources/kerberos/ core
SCIM authentik/sources/scim/ core
Plex authentik/sources/plex/ core
Telegram authentik/sources/telegram/ core

Policy-type matrix

Type Module License
Expression authentik/policies/expression/ core
Event Matcher authentik/policies/event_matcher/ core
Password authentik/policies/password/ core
Expiry authentik/policies/expiry/ core
GeoIP authentik/policies/geoip/ core
Reputation authentik/policies/reputation/ core
Dummy authentik/policies/dummy/ core (test-only)
Unique Password authentik/enterprise/policies/unique_password/ EE

Useful one-liners

# Show the merged config in use
docker compose exec server cat /etc/authentik/config.yml

# Apply all blueprints in /blueprints/
docker compose exec worker ak apply_blueprint default/

# Export current state as a blueprint
docker compose exec worker ak apply_blueprint --export > snapshot.yaml

# Hash a password offline (for blueprint seeding)
docker compose exec worker ak hash_password 'correct horse battery staple'

# Smoke-test an LDAP source
docker compose exec worker ak ldap_check_connection my-ldap

# Trigger an on-demand LDAP sync
docker compose exec worker ak ldap_sync my-ldap

# Trigger an on-demand SCIM provisioning
docker compose exec worker ak scim_sync my-slack-provider

# Build a fresh OpenAPI schema
docker compose exec worker ak build_schema --api-file /tmp/schema.yml

# Generate a recovery key
docker compose exec worker ak create_recovery_key 60 akadmin

# Set a tenant feature flag
docker compose exec worker ak set_flag <flag_key> <flag_value>

# Convert a user to service account
docker compose exec worker ak change_user_type --type service_account alice

Where to learn more