LUNAROPS · OPERATIONAL UPLINK 100% UPTIME 1,247d POSTS 893 JEFF.MOON@LUNAROPS.DEV UTC --:--:--

Authentik: A Self-Hosted Identity Provider for Everything

authentikssoidentityoidcsamlldaphomelabsecurityself-hosted

Every self-hosted stack eventually grows to the point where managing separate logins for Grafana, Gitea, Nextcloud, Proxmox, Portainer, Jellyfin, and a dozen other services becomes painful. Password reuse, no MFA enforcement, and no central audit log of who logged in to what. The obvious fix is Single Sign-On — one login, one MFA prompt, everything else flows through.

The enterprise answer is Okta or Azure AD. The self-hosted answer is Authentik: a full-featured identity provider that speaks OIDC, OAuth2, SAML, LDAP, SCIM, and RADIUS. It’s mature (used in production at thousands of organizations), actively developed, and runs comfortably on the same homelab hardware as the rest of your stack.

This guide walks through deploying Authentik, connecting your first few applications, setting up an LDAP outpost for legacy services, and hardening the installation for production use.

What Authentik Provides

  • SSO via OIDC/OAuth2: Any modern app with OAuth2 support (Grafana, Gitea, Nextcloud, ArgoCD, Portainer, VS Code Server, etc.) can authenticate through Authentik
  • SAML 2.0: For enterprise apps and anything that speaks SAML rather than OIDC
  • LDAP Outpost: Expose a virtual LDAP directory so legacy apps that only understand LDAP can authenticate against Authentik’s user store
  • RADIUS Outpost: For network devices like VPNs and switches that authenticate via RADIUS
  • Proxy Outpost: Put Authentik in front of apps that have no auth at all — Authentik handles the login and passes verified headers
  • MFA enforcement: TOTP, WebAuthn (hardware keys, passkeys, biometrics), SMS
  • User enrollment flows: Invite-based registration, email verification, self-service password reset
  • Audit log: Every authentication event, policy evaluation, and admin action is logged
  • Social login: Forward authentication to Google, GitHub, Discord, and others while still managing users centrally

Architecture Overview

Authentik runs as two containers: a server (handles web UI, admin, and authentication flows) and a worker (handles background tasks, policy evaluation, and outpost communication). Both share a PostgreSQL database and Redis.

Browser / Client App
        │
        ▼
   Reverse Proxy (Traefik / nginx)
        │
        ▼
   Authentik Server (:9000 / :9443)
        │
   ┌────┴────────────────────┐
   │                         │
PostgreSQL              Redis
(user store,         (session cache,
 flow state,          rate limiting,
 audit log)           task queue)
        │
   Authentik Worker
(background jobs,
 LDAP/Proxy outpost)

Deploying with Docker Compose

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
# docker-compose.yml
version: "3.8"

services:
  postgresql:
    image: postgres:16-alpine
    container_name: authentik-db
    restart: unless-stopped
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}"]
      interval: 30s
      timeout: 5s
      retries: 5
    volumes:
      - authentik-db:/var/lib/postgresql/data
    environment:
      POSTGRES_DB: ${PG_DB:-authentik}
      POSTGRES_USER: ${PG_USER:-authentik}
      POSTGRES_PASSWORD: ${PG_PASS}
    networks:
      - authentik-internal

  redis:
    image: redis:alpine
    container_name: authentik-redis
    restart: unless-stopped
    command: --save 60 1 --loglevel warning
    healthcheck:
      test: ["CMD-SHELL", "redis-cli ping | grep PONG"]
      interval: 30s
      timeout: 3s
      retries: 5
    volumes:
      - authentik-redis:/data
    networks:
      - authentik-internal

  server:
    image: ghcr.io/goauthentik/server:2024.12.3
    container_name: authentik-server
    restart: unless-stopped
    command: server
    environment:
      AUTHENTIK_REDIS__HOST: redis
      AUTHENTIK_POSTGRESQL__HOST: postgresql
      AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik}
      AUTHENTIK_POSTGRESQL__NAME: ${PG_DB:-authentik}
      AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS}
      AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY}
      AUTHENTIK_ERROR_REPORTING__ENABLED: "false"
      # Email settings for password reset and enrollment
      AUTHENTIK_EMAIL__HOST: ${EMAIL_HOST:-localhost}
      AUTHENTIK_EMAIL__PORT: ${EMAIL_PORT:-25}
      AUTHENTIK_EMAIL__USERNAME: ${EMAIL_USERNAME:-""}
      AUTHENTIK_EMAIL__PASSWORD: ${EMAIL_PASSWORD:-""}
      AUTHENTIK_EMAIL__USE_TLS: ${EMAIL_USE_TLS:-false}
      AUTHENTIK_EMAIL__USE_SSL: ${EMAIL_USE_SSL:-false}
      AUTHENTIK_EMAIL__FROM: ${EMAIL_FROM:-authentik@yourdomain.com}
    volumes:
      - authentik-media:/media
      - authentik-custom-templates:/templates
    ports:
      - "9000:9000"   # HTTP
      - "9443:9443"   # HTTPS
    depends_on:
      postgresql:
        condition: service_healthy
      redis:
        condition: service_healthy
    networks:
      - authentik-internal
      - proxy   # Shared with your reverse proxy

  worker:
    image: ghcr.io/goauthentik/server:2024.12.3
    container_name: authentik-worker
    restart: unless-stopped
    command: worker
    environment:
      AUTHENTIK_REDIS__HOST: redis
      AUTHENTIK_POSTGRESQL__HOST: postgresql
      AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik}
      AUTHENTIK_POSTGRESQL__NAME: ${PG_DB:-authentik}
      AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS}
      AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY}
    # Worker needs Docker socket for outpost management
    user: root
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - authentik-media:/media
      - authentik-custom-templates:/templates
      - authentik-certs:/certs
    depends_on:
      postgresql:
        condition: service_healthy
      redis:
        condition: service_healthy
    networks:
      - authentik-internal

volumes:
  authentik-db:
  authentik-redis:
  authentik-media:
  authentik-custom-templates:
  authentik-certs:

networks:
  authentik-internal:
    internal: true
  proxy:
    external: true

Generate the required secrets before starting:

1
2
3
4
# Generate a secure secret key
python3 -c "from django.utils.crypto import get_random_string; print(get_random_string(50))"
# Or with openssl:
openssl rand -hex 32
1
2
3
4
5
6
7
8
9
# .env
PG_PASS=your_strong_postgres_password
AUTHENTIK_SECRET_KEY=your_50_char_secret_key_here
EMAIL_HOST=smtp.yourprovider.com
EMAIL_PORT=587
EMAIL_USERNAME=authentik@yourdomain.com
EMAIL_PASSWORD=your_smtp_password
EMAIL_USE_TLS=true
EMAIL_FROM=authentik@yourdomain.com
1
2
3
4
5
6
7
8
# Start the stack
docker compose up -d

# Run initial database migrations
docker compose exec worker ak migrate

# Create the initial admin user
docker compose exec server ak create_initial_superuser

Access the admin UI at http://localhost:9000/if/admin/ and the user-facing flows at http://localhost:9000/.

Traefik Integration

Most homelab setups route through Traefik. Configure Authentik behind Traefik with HTTPS:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Add to your main docker-compose.yml or traefik labels on the server container
labels:
  - "traefik.enable=true"
  - "traefik.http.routers.authentik.rule=Host(`auth.yourdomain.com`)"
  - "traefik.http.routers.authentik.entrypoints=websecure"
  - "traefik.http.routers.authentik.tls.certresolver=letsencrypt"
  - "traefik.http.services.authentik.loadbalancer.server.port=9000"
  # For the outpost forward auth middleware (used later)
  - "traefik.http.middlewares.authentik.forwardauth.address=http://authentik-server:9000/outpost.goauthentik.io/auth/traefik"
  - "traefik.http.middlewares.authentik.forwardauth.trustForwardHeader=true"
  - "traefik.http.middlewares.authentik.forwardauth.authResponseHeaders=X-authentik-username,X-authentik-groups,X-authentik-email,X-authentik-name,X-authentik-uid,X-authentik-jwt,X-authentik-meta-jwks,X-authentik-meta-outpost,X-authentik-meta-provider,X-authentik-meta-app,X-authentik-meta-version"

Core Concepts

Before wiring up apps, understand Authentik’s building blocks:

Providers: Define how an application authenticates — OIDC/OAuth2, SAML, LDAP, Proxy, RADIUS. Each provider implements a protocol.

Applications: The entry in Authentik’s catalog that ties a provider to a name, icon, and access policy. Users see Applications in their dashboard.

Outposts: Deployed services that handle specific protocols — the Proxy outpost intercepts requests before they hit your app, the LDAP outpost presents a virtual LDAP server.

Flows: Directed graphs of stages that define the authentication process. The default login flow goes: Identification → Password → MFA (if enrolled). You can customize these extensively.

Policies: Rules that gate access to applications. Bind a policy to an application to restrict access to specific groups, IP ranges, or evaluation results.

Blueprints: YAML-defined infrastructure-as-code for Authentik’s configuration — providers, applications, flows, and policies expressed as code rather than clicks.

Connecting Applications via OIDC

Grafana

In the Authentik Admin UI:

  1. Create a Provider: Admin → Providers → Create → OAuth2/OIDC Provider

    • Name: Grafana
    • Client type: Confidential
    • Redirect URIs: https://grafana.yourdomain.com/login/generic_oauth
    • Copy the Client ID and Client Secret
  2. Create an Application: Admin → Applications → Create

    • Name: Grafana
    • Slug: grafana
    • Provider: Grafana (the one just created)

Then configure Grafana to use it:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# grafana.ini
[auth.generic_oauth]
enabled = true
name = Authentik
allow_sign_up = true
client_id = YOUR_CLIENT_ID
client_secret = YOUR_CLIENT_SECRET
scopes = openid email profile
auth_url = https://auth.yourdomain.com/application/o/authorize/
token_url = https://auth.yourdomain.com/application/o/token/
api_url = https://auth.yourdomain.com/application/o/userinfo/
# Map Authentik groups to Grafana roles
role_attribute_path = contains(groups, 'grafana-admins') && 'Admin' || contains(groups, 'grafana-editors') && 'Editor' || 'Viewer'

Or via environment variables in Docker Compose:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
environment:
  GF_AUTH_GENERIC_OAUTH_ENABLED: "true"
  GF_AUTH_GENERIC_OAUTH_NAME: "Authentik"
  GF_AUTH_GENERIC_OAUTH_CLIENT_ID: "${GRAFANA_CLIENT_ID}"
  GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET: "${GRAFANA_CLIENT_SECRET}"
  GF_AUTH_GENERIC_OAUTH_SCOPES: "openid email profile"
  GF_AUTH_GENERIC_OAUTH_AUTH_URL: "https://auth.yourdomain.com/application/o/authorize/"
  GF_AUTH_GENERIC_OAUTH_TOKEN_URL: "https://auth.yourdomain.com/application/o/token/"
  GF_AUTH_GENERIC_OAUTH_API_URL: "https://auth.yourdomain.com/application/o/userinfo/"
  GF_AUTH_GENERIC_OAUTH_ROLE_ATTRIBUTE_PATH: "contains(groups, 'grafana-admins') && 'Admin' || 'Viewer'"
  GF_AUTH_DISABLE_LOGIN_FORM: "true"   # Force SSO, hide password form
  GF_AUTH_OAUTH_AUTO_LOGIN: "true"     # Redirect to SSO immediately

Gitea / Forgejo

In Authentik:

  1. Create an OIDC Provider with redirect URI: https://gitea.yourdomain.com/user/oauth2/authentik/callback
  2. Create an Application linked to that provider

In Gitea’s admin panel, Site Administration → Authentication Sources → Add Authentication Source:

  • Type: OAuth2
  • Name: authentik
  • OAuth2 Provider: OpenID Connect
  • Client ID/Secret: from Authentik
  • OpenID Connect Auto Discovery URL: https://auth.yourdomain.com/application/o/gitea/.well-known/openid-configuration

Portainer

Redirect URI: https://portainer.yourdomain.com

In Portainer: Settings → Authentication → OAuth → Custom

  • Client ID/Secret: from Authentik
  • Authorization URL: https://auth.yourdomain.com/application/o/authorize/
  • Access Token URL: https://auth.yourdomain.com/application/o/token/
  • Resource URL: https://auth.yourdomain.com/application/o/userinfo/
  • Redirect URL: https://portainer.yourdomain.com
  • User Identifier: preferred_username
  • Scopes: openid email profile

ArgoCD

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# argocd-cm ConfigMap
data:
  oidc.config: |
    name: Authentik
    issuer: https://auth.yourdomain.com/application/o/argocd/
    clientID: YOUR_CLIENT_ID
    clientSecret: $oidc.authentik.clientSecret
    requestedScopes: ["openid", "profile", "email", "groups"]
    requestedIDTokenClaims:
      groups:
        essential: true
1
2
3
4
5
6
# argocd-rbac-cm ConfigMap
data:
  policy.csv: |
    g, argocd-admins, role:admin
    g, argocd-viewers, role:readonly
  scopes: '[groups]'

The Proxy Outpost: SSO for Apps Without Native Auth

Many self-hosted apps have no authentication at all — or have auth you can’t integrate with OIDC. The Authentik proxy outpost sits in front of these apps and handles authentication before the request reaches the app.

Setting Up the Proxy Outpost

  1. In Authentik Admin: Providers → Create → Proxy Provider

    • Name: Heimdall Proxy (or whatever app)
    • Mode: Forward auth (single application)
    • External host: https://heimdall.yourdomain.com
  2. Create an Application for it

  3. Admin → Outposts → Create

    • Type: Proxy
    • Integrate with the providers you just created
  4. Authentik automatically deploys a container via the Docker socket. Or deploy it manually:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# outpost deployment (Authentik manages this automatically,
# but you can also deploy manually for more control)
services:
  authentik-proxy:
    image: ghcr.io/goauthentik/proxy:2024.12.3
    ports:
      - "9000"
    environment:
      AUTHENTIK_HOST: https://auth.yourdomain.com
      AUTHENTIK_INSECURE: "false"
      AUTHENTIK_TOKEN: YOUR_OUTPOST_TOKEN
    networks:
      - proxy
  1. Add the Traefik middleware to any app that should be protected:
1
2
3
4
5
6
7
8
9
# Any app you want to protect — just add the middleware label
services:
  heimdall:
    image: lscr.io/linuxserver/heimdall:latest
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.heimdall.rule=Host(`heimdall.yourdomain.com`)"
      - "traefik.http.routers.heimdall.middlewares=authentik@docker"  # <-- this line
      - "traefik.http.routers.heimdall.tls.certresolver=letsencrypt"

Now visiting heimdall.yourdomain.com redirects to Authentik’s login page. After authentication, the proxy forwards the request with headers like X-authentik-username and X-authentik-email so the app knows who’s logged in.

LDAP Outpost: Legacy App Support

Some apps — particularly self-hosted ones from the late 2000s or early 2010s — only understand LDAP for authentication. Gitea (optional), Nextcloud (optional), and many network devices fall into this category. Authentik’s LDAP outpost presents a virtual LDAP directory backed by Authentik’s user store.

Deploy the LDAP Outpost

  1. Admin → Outposts → Create

    • Type: LDAP
    • Bind as: cn=ldapservice,ou=serviceaccounts,dc=ldap,dc=goauthentik,dc=io
  2. Deploy the outpost container:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
services:
  authentik-ldap:
    image: ghcr.io/goauthentik/ldap:2024.12.3
    ports:
      - "389:3389"    # LDAP
      - "636:6636"    # LDAPS
    environment:
      AUTHENTIK_HOST: https://auth.yourdomain.com
      AUTHENTIK_INSECURE: "false"
      AUTHENTIK_TOKEN: YOUR_LDAP_OUTPOST_TOKEN
    networks:
      - proxy
    restart: unless-stopped

Configure a Service to Use LDAP

Example — Nextcloud LDAP configuration:

LDAP server: authentik-ldap  (or the container IP/hostname)
Port: 3389
DN: ou=users,dc=ldap,dc=goauthentik,dc=io
Bind DN: cn=ldapservice,ou=serviceaccounts,dc=ldap,dc=goauthentik,dc=io
Bind password: <your service account password in Authentik>
User search base: ou=users,dc=ldap,dc=goauthentik,dc=io
User filter: (objectClass=user)

The virtual LDAP tree Authentik exposes:

dc=ldap,dc=goauthentik,dc=io
├── ou=users
│   ├── cn=alice            # Regular users
│   ├── cn=bob
│   └── cn=carol
├── ou=groups
│   ├── cn=nextcloud-users
│   └── cn=nextcloud-admins
└── ou=serviceaccounts
    └── cn=ldapservice      # Bind account for services

Policies and Access Control

Policies control who can access which applications. They’re evaluated at login time and can check group membership, IP address, user attributes, and more.

Group-Based Access Policy

  1. Admin → Groups → Create groups: nextcloud-users, grafana-admins, homelab-users

  2. Add users to groups via Admin → Users → select user → Groups tab

  3. Bind a policy to an Application:

    • Admin → Applications → select app → Policy Bindings
    • Create Binding → Group → nextcloud-users
    • Users not in this group see “Permission denied” instead of a login form

Expression Policies (Python)

For more complex rules, write a Python expression policy:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# Policy: Allow access only from home IP range or with valid MFA
# Admin → Policies → Expression Policy

from ipaddress import ip_address, ip_network

# Get the client IP
client_ip = ip_address(request.http_request.META.get("REMOTE_ADDR", "0.0.0.0"))

home_network = ip_network("192.168.1.0/24")
vpn_network = ip_network("10.8.0.0/24")

# Allow from home or VPN regardless
if client_ip in home_network or client_ip in vpn_network:
    return True

# From external: require that MFA is enrolled and was used this session
if not request.context.get("mfa_devices"):
    ak_message("MFA is required for external access")
    return False

return True
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# Policy: Time-based access (only during business hours)
from datetime import datetime, timezone

now = datetime.now()
hour = now.hour
weekday = now.weekday()  # 0=Monday, 6=Sunday

# Block access on weekends and outside 8am-6pm
if weekday >= 5:
    ak_message("Access restricted on weekends")
    return False

if not (8 <= hour < 18):
    ak_message("Access restricted outside business hours")
    return False

return True

Authentik’s flows are fully customizable. The default enrollment flow does email verification, but you can build invite-only enrollment with single-use tokens:

  1. Admin → Flows → Create

    • Designation: Enrollment
    • Name: Invite-Only Enrollment
  2. Add stages:

    • Invitation Stage: validate a token from the invite link
    • User Write Stage: create the user account
    • Email Verification Stage: send and verify email
    • Prompt Stage: collect name, password
    • User Login Stage: log them in after enrollment
  3. Admin → Invitations → Create Invitation → share the link

The invite link: https://auth.yourdomain.com/if/flow/invite-only-enrollment/?itoken=abc123

MFA Configuration

TOTP (Google Authenticator, Authy)

  1. Admin → Stages → Create → Authenticator TOTP Stage

    • Digits: 6
    • Configure stage name: totp-setup
  2. Add to your authentication flow after the password stage

    • Add TOTP Validation Stage to the login flow (after password)
    • With a TOTP Setup Stage as the fallback (if not enrolled, prompt to enroll)

WebAuthn (Passkeys, YubiKey)

  1. Admin → Stages → Create → Authenticator WebAuthn Stage

    • User verification: Required (enforces biometric/PIN)
    • Resident key requirement: Preferred (enables passkey mode)
  2. Users enroll their device at https://auth.yourdomain.com/user/ → MFA Devices

Enforcing MFA for Specific Applications

Bind an MFA policy to sensitive applications:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# Expression policy: require MFA for admin apps
# Bind this to Portainer, ArgoCD, Proxmox, etc.

mfa_devices = request.context.get("mfa_devices")
if not mfa_devices:
    ak_message("This application requires MFA enrollment.")
    return False

# Also require that MFA was actually used in this session
if not request.context.get("is_mfa_authenticated"):
    ak_message("Please authenticate with MFA to access this application.")
    return False

return True

Blueprints: Configuration as Code

Managing Authentik via the UI is fine for initial setup. For production, use blueprints to version-control your configuration:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# blueprints/grafana-oidc.yaml
version: 1
metadata:
  name: Grafana OIDC Provider
entries:
  - model: authentik_providers_oauth2.oauth2provider
    state: present
    identifiers:
      name: grafana-oidc
    attrs:
      name: grafana-oidc
      authorization_flow: !Find [authentik_flows.flow, [slug, default-provider-authorization-implicit-consent]]
      client_type: confidential
      client_id: !Env GRAFANA_CLIENT_ID
      client_secret: !Env GRAFANA_CLIENT_SECRET
      redirect_uris: "https://grafana.yourdomain.com/login/generic_oauth"
      signing_key: !Find [authentik_crypto.certificatekeypair, [name, authentik Self-signed Certificate]]
      property_mappings:
        - !Find [authentik_providers_oauth2.scopemapping, [scope_name, openid]]
        - !Find [authentik_providers_oauth2.scopemapping, [scope_name, email]]
        - !Find [authentik_providers_oauth2.scopemapping, [scope_name, profile]]
        - !Find [authentik_providers_oauth2.scopemapping, [scope_name, goauthentik.io/providers/oauth2/scope-groups]]

  - model: authentik_core.application
    state: present
    identifiers:
      slug: grafana
    attrs:
      name: Grafana
      slug: grafana
      provider: !Find [authentik_providers_oauth2.oauth2provider, [name, grafana-oidc]]
      meta_icon: https://grafana.com/static/img/menu/grafana2.svg
      meta_description: Metrics and dashboards
      group: Observability

Apply blueprints:

1
2
3
# Apply via the UI: Admin → Blueprints → Import
# Or via the CLI:
docker compose exec worker ak apply_blueprint /blueprints/grafana-oidc.yaml

Store all your blueprints in a Git repository and apply them in CI to keep Authentik configuration in sync with your infrastructure code.

Social Login: Delegate to GitHub or Google

Let users log in with their GitHub or Google account, while still managing them centrally in Authentik:

  1. Create a GitHub OAuth App at github.com/settings/developers

    • Callback URL: https://auth.yourdomain.com/source/oauth/callback/github/
  2. Add the Source in Authentik: Admin → Federation & Social login → GitHub

    • Client ID/Secret: from GitHub
    • Consumer Key Name: github
    • Enrollment flow: your invite-only enrollment flow (so GitHub users go through the same verification)

Now the login page shows a “Sign in with GitHub” button. The GitHub identity is linked to an Authentik account, and all your group-based access policies still apply.

Monitoring and Audit

Application Log

Every authentication event is logged. View them at Admin → Events → Logs:

  • Filter by user, application, or event type
  • Export to CSV for compliance reporting
  • Set up event notifications for suspicious activity

Prometheus Metrics

Authentik exposes metrics at /-/metrics:

1
2
3
4
5
6
7
# prometheus.yml
scrape_configs:
  - job_name: authentik
    static_configs:
      - targets: ['authentik-server:9300']
    metrics_path: /-/metrics
    bearer_token: YOUR_AUTHENTIK_API_TOKEN

Key metrics:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Login success rate
sum(rate(authentik_flows_plan_timing_sum{flow_slug="default-authentication-flow"}[5m]))

# Active sessions
authentik_sessions_active_count

# Failed login attempts (potential brute force)
sum(rate(authentik_events_total{action="login_failed"}[5m]))

# Policy evaluation time
histogram_quantile(0.99, rate(authentik_policies_execution_time_bucket[5m]))

Alerting on Suspicious Activity

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# In Authentik Admin → Events → Notification Rules
# Create a rule: alert on repeated login failures from the same IP

# Or wire to Prometheus alerting:
groups:
  - name: authentik
    rules:
      - alert: AuthentikBruteForce
        expr: |
          sum by (client_ip) (
            rate(authentik_events_total{action="login_failed"}[5m])
          ) > 5
        for: 2m
        annotations:
          summary: "Possible brute force from {{ $labels.client_ip }}"

Production Hardening

Secret Key Rotation

If you need to rotate the secret key (e.g., after suspected compromise):

1
2
3
4
5
6
7
# Generate new key
openssl rand -hex 32

# Update .env, then restart
docker compose up -d server worker

# Note: existing sessions will be invalidated

Rate Limiting

Authentik includes built-in rate limiting, but add nginx/Traefik rate limits in front for additional protection:

1
2
3
4
5
6
# Traefik rate limit middleware
labels:
  - "traefik.http.middlewares.auth-ratelimit.ratelimit.average=100"
  - "traefik.http.middlewares.auth-ratelimit.ratelimit.burst=50"
  - "traefik.http.middlewares.auth-ratelimit.ratelimit.period=1m"
  - "traefik.http.routers.authentik.middlewares=auth-ratelimit"

Backup Strategy

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#!/usr/bin/env bash
# scripts/backup-authentik.sh
set -euo pipefail

BACKUP_DIR="/backups/authentik/$(date +%Y%m%d_%H%M%S)"
mkdir -p "$BACKUP_DIR"

# Dump PostgreSQL
docker compose exec -T postgresql pg_dump \
  -U authentik authentik | gzip > "$BACKUP_DIR/authentik.sql.gz"

# Backup media files (custom icons, uploaded files)
docker compose cp server:/media "$BACKUP_DIR/media"

# Backup blueprints if using file-based blueprints
cp -r ./blueprints "$BACKUP_DIR/blueprints"

echo "Backup complete: $BACKUP_DIR"

# Retain last 30 days
find /backups/authentik -maxdepth 1 -mtime +30 -exec rm -rf {} +

Restrict Admin UI Access

The admin interface at /if/admin/ should not be publicly accessible. Restrict it to your home/VPN network:

1
2
3
4
5
# Traefik IP whitelist middleware for admin paths
labels:
  - "traefik.http.middlewares.admin-whitelist.ipallowlist.sourcerange=192.168.1.0/24,10.8.0.0/24"
  - "traefik.http.routers.authentik-admin.rule=Host(`auth.yourdomain.com`) && PathPrefix(`/if/admin/`)"
  - "traefik.http.routers.authentik-admin.middlewares=admin-whitelist"

Quick Integration Reference

Application Protocol Notes
Grafana OIDC Full role mapping via role_attribute_path
Gitea / Forgejo OIDC OAuth2 source in admin panel
Nextcloud OIDC or LDAP Social Login app for OIDC
ArgoCD OIDC Group-based RBAC
Portainer OIDC OAuth settings in UI
Proxmox OIDC Realm configuration
Jellyfin OIDC SSO Plugin required
Vaultwarden OIDC + SSO Plugin Needs Vaultwarden config
VS Code Server Proxy Outpost No native auth needed
Heimdall Proxy Outpost Dashboard has no auth
Netdata Proxy Outpost Protect metrics
Paperless-ngx OIDC Native OIDC support
Synology DSM OIDC OIDC SSO app in DSM
pfSense / OPNsense RADIUS Outpost Network auth

Conclusion

Authentik transforms a sprawling collection of individually-authenticated services into a unified platform with one login, one MFA policy, one audit log, and one place to revoke access. When a team member leaves or a device is compromised, you disable one account in Authentik rather than hunting down passwords across a dozen applications.

The proxy outpost is a particularly powerful feature — apps that have no authentication at all suddenly get SSO without any code changes. The LDAP outpost bridges the gap to legacy applications that can’t speak OIDC. And the blueprint system ensures your identity infrastructure is version-controlled and reproducible.

Start with Docker Compose, wire up two or three apps via OIDC, and feel the difference of a unified login experience. The rest of the integrations follow naturally.

Comments