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
- Executive Summary
- System Architecture
- Repository Structure
- Module Inventory
- Request Lifecycle
- The Flow Engine
- Provider Implementations
- Outpost Architecture
- The Blueprint System
- Configuration Reference
- Database Schema and Migrations
- Background Workers and Scheduling
- API Surface (
/api/v3/) - Frontend Architecture (
web/) - Operations Runbooks
- Security Analysis
- Performance Tuning
- Code Quality and Patterns
- Testing Strategy
- Suggested Improvements
- Contributing
- 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 usingLISTEN/NOTIFYfor pub/sub, eliminating the need for a Redis channel layer.django-dramatiq-postgres(packages/django-dramatiq-postgres/) — adramatiqbroker 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 Rustsrc/main.rsis destined to replacecmd/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 remainscmd/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:
- Cache check —
cache_key(flow, request.user)→ lookup indjango-postgres-cache(TTL:cache.timeout_flows, default 300 s). Cache hit returns a pre-computed FlowPlan; cache miss continues. - Walk FlowStageBindings ordered by
order. - For each binding, evaluate the PolicyBindings attached:
- If
policy_engine_mode = all, all must pass; elseanymust pass. - If policies fail and the stage is marked
negate=True, skip the stage.
- If
- Build the FlowPlan, store it in the cache, return it.
- The
FlowExecutorViewpops 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:
- Read
AUTHENTIK_HOST,AUTHENTIK_TOKEN,AUTHENTIK_INSECUREfrom env. - 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. - Invoke the server factory (e.g.
proxyv2.NewProxyServer) with the controller as the config source. - 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:
- REST — periodic state polling (
/api/v3/outposts/instances/<id>/) and ad-hoc reads (provider config, user attributes for the embedded session lookup). - 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:
/blueprints/(mount point, configurable viaAUTHENTIK_BLUEPRINTS_DIR)- The bundled
blueprints/default/andblueprints/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:
- Mounted
local.env.yml(merged withdefault.ymlvia deep-merge — uses thedeepmergepackage). 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.0broke migrationauthentik_core.0058_setup). The supported path is to upgrade through each minor in sequence.
Database performance considerations
- The default
conn_max_age: 0opens a new PostgreSQL connection per request. Set to60for ~10× lower DB CPU at the cost of more idle connections. Set to-1for "persistent until explicit close". - Set
use_pool: trueto enable thepsycopg[pool]connection pool. Recommended for production at scale. - Read replicas are honored automatically for ViewSet
listandretrieveactions (whenread_replicasis 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 syncauthentik.events.tasks.event_notification_handler— dispatch notificationsauthentik.events.tasks.notification_cleanup— purge old notificationsauthentik.outposts.tasks.outpost_controller— reconcile outposts with Docker/K8s stateauthentik.sources.ldap.tasks.ldap_sync_all— periodic LDAP sync per sourceauthentik.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
migratkeywords. 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.
Filtering, pagination, search
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.0for ES modern stdlibformdata-polyfillweakmap-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:
- Add
<lang>toweb/scripts/build-locales.mjs. - Run
npm run extract-localesto seed. - Translate.
- Commit.
Server-side Django translations use the same .po files (django-translatable).
15. Operations Runbooks
Daily checks
- Dashboard → All systems healthy, worker count ≥ 1, no failed system tasks.
- System → Tasks → No tasks stuck in "Running" for > 1 hour.
- Events → Filter by severity =
warningoralert; review. - Outposts → All Last seen < 5 minutes ago.
Weekly checks
- Backup PostgreSQL +
/data/volume. - Check System → Sources sync results; investigate failures.
- Directory → Tokens — review and prune unused tokens.
- 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
- Events → Filter by user =
[email removed]→ see chronological events. - System → Logs (if
log_level: debug) for the worker — search for the user's email or session ID. - 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.0for X.509 ops,jwcrypto==1.5.7for JWS/JWE,xmlsec==1.3.17for 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_KEYset to 60+ random chars, distinct per environment. - [ ] PostgreSQL on a private network;
AUTHENTIK_POSTGRESQL__SSLMODE=requireor higher. - [ ] Reverse proxy enforces TLS; never expose 9000/9443 directly.
- [ ]
AUTHENTIK_LISTEN__TRUSTED_PROXY_CIDRScorrectly scoped (don't trust the world). - [ ] Default
akadminuser 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=falseif you don't want telemetry to Sentry. - [ ]
AUTHENTIK_DISABLE_UPDATE_CHECK=true+AUTHENTIK_DISABLE_STARTUP_ANALYTICS=truefor 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=trueif FedRAMP/FIPS 140-2 required.
Caution
The internal debug endpoint at port 9900 exposes
pprofprofiles anddump_config— these reveal sensitive runtime state. The container does not expose this port publicly by default. Do not change that. SetAUTHENTIK_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. Tuneldap.page_size(default 50) higher for large directories (500+ per page acceptable). Setldap.task_timeout_hoursto match your largest sync. - Proxy outpost — use
postgresstoresession 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/, oftensignals.pyandtasks.py). Onboarding a new contributor is fast. - Clear abstractions —
Stage,Provider,Source,Policy,PropertyMappingare 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'sconfidence_gapsshows 47 modules without tests, mostly inweb/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,!KeyOfYAML 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:
tests/e2e/— docker-compose fixtures that test integrations with real LDAP servers, OAuth IdPs, Envoy/Nginx/Traefik forward-auth, etc.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:
- 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).
- 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. - 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.
- Fix the OAuth single logout regression (#22572). 14-comment thread, still open.
- Fix Safari admin crash (#19166). 8-comment thread, references the older #14322.
- Resolve the "2fa stage no identification data" regression (#19552). 43 comments — high user impact.
- Fix invitation creation when User has custom required attributes (#22637).
- Restore the email password-reset template grammar (#22757).
- Add a CLI for outpost local development — currently outpost contributors need a full authentik server running.
- Document the Rust frontend's role —
src/main.rsexists but its current production-readiness vs.cmd/server/main.goisn't documented anywhere. Decide and document. - Standardize the outpost healthcheck mode arguments — currently
healthcheck allinoneexists for the server binary but the other outpost binaries don't accept modes. Inconsistent. - Reduce config drift between
default.ymlandinternal/config/config.go— adding a key requires updating both. Consider generating one from the other. - 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.
- Expose more metrics — the Prometheus surface is good but adding metrics for "active flows", "policy evaluation duration", "blueprint apply duration" would help operators.
- Investigate the
cspell.config.jsoncaccepted-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 (
uvfor 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
- Fork on GitHub.
- Branch from
main. - Run
make lint testlocally. - Push, open PR.
- CI runs (
ci-main,ci-outpost,ci-web) — must be green. - Maintainer review; typically one round of feedback.
- Squash-merge.
Issue tracker
github.com/goauthentik/authentik/issues. Bug reports should use the template; feature requests via the same tracker.
Community
- Discord — primary real-time channel.
- Discussions on GitHub.
- @goauthentik on Mastodon for announcements.
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 |
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
- Official docs: docs.goauthentik.io
- Architecture page: docs.goauthentik.io/docs/core/architecture
- Developer docs: docs.goauthentik.io/docs/developer-docs
- Style guide: website/docs/developer-docs/docs/style-guide.mdx
- Helm chart: github.com/goauthentik/helm
- SECURITY.md: github.com/goauthentik/authentik/blob/main/SECURITY.md
- Releases: docs.goauthentik.io/docs/releases
- Discord: goauthentik.io/discord
- Sponsor / Enterprise: goauthentik.io/pricing