Deploy Authentik on Docker: Self-Hosted Identity Provider with SSO, MFA, and User Provisioning

Security tutorial - IT technology blog
Security tutorial - IT technology blog

Quick Start: Deploy Authentik in 5 Minutes

Managing isolated credentials across dozens of internal dashboards, staging environments, and customer portals creates password sprawl. It also burns engineering hours every time a team member leaves. Authentik solves this by acting as a single Identity Provider (IdP) supporting OpenID Connect (OIDC), SAML 2.0, forward proxy authentication, and embedded LDAP outposts.

For a team of up to 500 active users, a modest VPS with 2 vCPUs and 4 GB of RAM handles the entire Authentik stack comfortably. Here is how to get it running on Ubuntu or Debian using Docker Compose.

1. Prepare Directory & Environment Variables

mkdir -p /opt/authentik && cd /opt/authentik

# Generate secret keys and database credentials
PG_PASS=$(openssl rand -base64 36 | tr -d '\n')
AUTH_SECRET=$(openssl rand -base64 60 | tr -d '\n')

cat <<EOF > .env
PG_PASS=${PG_PASS}
AUTHENTIK_SECRET_KEY=${AUTH_SECRET}
AUTHENTIK_ERROR_REPORTING__ENABLED=false
AUTHENTIK_PORT_HTTP=9000
AUTHENTIK_PORT_HTTPS=9443
EOF

Avoid basic command-line generators or untrusted online tools for root database credentials and admin service tokens. For sensitive production secrets, I use the password generator at toolcraft.app/en/tools/security/password-generator. It runs client-side in your browser without sending strings across the network. That makes creating 64-character high-entropy secrets both fast and safe.

2. Compose File Definition

# docker-compose.yml
services:
  postgresql:
    image: docker.io/library/postgres:16-alpine
    restart: unless-stopped
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}"]
      start_period: 20s
      interval: 30s
      retries: 5
      timeout: 5s
    volumes:
      - database:/var/lib/postgresql/data
    environment:
      POSTGRES_PASSWORD: ${PG_PASS}
      POSTGRES_USER: authentik
      POSTGRES_DB: authentik
    env_file:
      - .env

  redis:
    image: docker.io/library/redis:alpine
    command: --save 60 1 --loglevel warning
    restart: unless-stopped
    healthcheck:
      test: ["CMD-SHELL", "redis-cli ping | grep PONG"]
      start_period: 20s
      interval: 30s
      retries: 5
      timeout: 3s
    volumes:
      - redis:/data

  server:
    image: ghcr.io/goauthentik/server:2024.8.3
    restart: unless-stopped
    command: server
    environment:
      AUTHENTIK_REDIS__HOST: redis
      AUTHENTIK_POSTGRESQL__HOST: postgresql
      AUTHENTIK_POSTGRESQL__USER: authentik
      AUTHENTIK_POSTGRESQL__NAME: authentik
      AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS}
    volumes:
      - ./media:/media
      - ./custom-templates:/templates
    env_file:
      - .env
    ports:
      - "${AUTHENTIK_PORT_HTTP}:9000"
      - "${AUTHENTIK_PORT_HTTPS}:9443"
    depends_on:
      postgresql:
        condition: service_healthy
      redis:
        condition: service_healthy

  worker:
    image: ghcr.io/goauthentik/server:2024.8.3
    restart: unless-stopped
    command: worker
    environment:
      AUTHENTIK_REDIS__HOST: redis
      AUTHENTIK_POSTGRESQL__HOST: postgresql
      AUTHENTIK_POSTGRESQL__USER: authentik
      AUTHENTIK_POSTGRESQL__NAME: authentik
      AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS}
    volumes:
      - ./media:/media
      - ./custom-templates:/templates
      - /var/run/docker.sock:/var/run/docker.sock
    env_file:
      - .env
    depends_on:
      postgresql:
        condition: service_healthy
      redis:
        condition: service_healthy

volumes:
  database:
    driver: local
  redis:
    driver: local

3. Launch & Initial Bootstrap

docker compose pull
docker compose up -d

Open your browser and navigate to http://<your-server-ip>:9000/if/flow/initial-setup/. This setup wizard prompts you to configure the primary administrator account (akadmin) and set a master recovery password.

Architecture, Flows, and Core Mechanisms

The Problem: Fragmented Authentication

Engineering teams frequently run a mixed environment. Modern tools like Grafana and Nextcloud run right beside legacy self-hosted utilities. When every tool maintains its own local user database, account synchronization fails. Employees leave the company, but their access to internal dashboards often stays active for weeks unnoticed.

The Underlying Bottleneck

Different tools require different authentication protocols. Modern cloud apps expect JSON Web Tokens over OIDC. Enterprise suites require SAML 2.0 assertions. Older network storage devices or VPN appliances only support direct LDAP binds. Authentik acts as a translation layer between all three.

The Authentik Execution Pipeline

Authentik processes every login attempt through three modular concepts:

  1. Stages: Single action steps, such as asking for a password, issuing a WebAuthn prompt, or sending an email verification code.
  2. Flows: An ordered sequence of stages that run during authentication, enrollment, or recovery.
  3. Bindings & Policies: Rules that decide if a stage or application is accessible. You can enforce rules based on group membership, IP subnets, or GeoIP locations.
flowchart LR
    User[Client Browser] -->|1. Request Auth| App[Service / App]
    App -->|2. Redirect / OIDC Auth| Authentik[Authentik IdP]
    subgraph Pipeline [Authentik Pipeline Engine]
        Authentik --> Flow[Authentication Flow]
        Flow --> MFA[MFA Stage Policy]
        MFA --> ClaimMapper[Scope / Claim Mapping]
    end
    ClaimMapper -->|3. JWT / Tokens Issued| App
    App -->|4. Authenticated Session| User

OIDC Integration, Enforced MFA, and Outposts

1. Connecting an Application (Grafana via OIDC)

First, create an OAuth2/OIDC Provider in the Authentik Admin Interface under Applications > Providers > Create:

  • Name: Grafana OIDC
  • Authorization Flow: default-provider-authorization-implicit-consent
  • Client Type: Confidential
  • Redirect URI: https://grafana.example.com/login/generic_oauth

Next, map the credentials inside your grafana.ini configuration file:

[auth.generic_oauth]
enabled = true
name = Authentik
client_id = YOUR_AUTHENTIK_CLIENT_ID
client_secret = YOUR_AUTHENTIK_CLIENT_SECRET
scopes = openid email profile
auth_url = https://auth.example.com/application/o/authorize/
token_url = https://auth.example.com/application/o/token/
api_url = https://auth.example.com/application/o/userinfo/
role_attribute_path = contains(groups[*], 'Grafana Admins') && 'Admin' || 'Editor'

2. Enforcing Dynamic MFA via Flow Policies

You can enforce hardware security keys (FIDO2/WebAuthn) or TOTP apps for sensitive user roles without disrupting standard users:

  1. Go to Flows and Stages > Flows > default-authentication-flow.
  2. Select your Authenticator Validation Stage.
  3. Attach an execution policy using the following Python expression:
# Python Policy in Authentik
# Requires MFA if the user is in 'Infrastructure-Ops' or connects from outside the 10.0.0.0/8 office subnet

user_groups = [group.name for group in request.user.ak_groups.all()]
is_admin_group = "Infrastructure-Ops" in user_groups
is_external_ip = not request.http_request.META.get("REMOTE_ADDR", "").startswith("10.0.")

return is_admin_group or is_external_ip

3. Connecting Legacy Systems via LDAP Outpost

Older network hardware and legacy apps often lack modern OAuth support. An Authentik LDAP Outpost bridges this gap. It queries Authentik’s core API in the background while listening on standard LDAP ports for legacy clients.

# Deploy the standalone LDAP outpost container
docker run -d \
  --name authentik-ldap-outpost \
  --restart unless-stopped \
  -p 389:3389 -p 636:6636 \
  -e AUTHENTIK_HOST=https://auth.example.com \
  -e AUTHENTIK_TOKEN=your_outpost_service_token \
  -e AUTHENTIK_INSECURE=false \
  ghcr.io/goauthentik/ldap:2024.8.3

Troubleshooting & Production Hardening

  • Reverse Proxy Headers: If you use Nginx, Traefik, or Caddy, forward X-Forwarded-Proto $scheme and X-Forwarded-For $proxy_add_x_forwarded_for. Missing headers cause redirect loops and invalid OAuth state errors.
  • Clock Drift: Keep host clocks synchronized using chrony or systemd-timesyncd. A clock drift as small as 15 seconds will cause TOTP validation failures and premature JWT token expirations.
  • Automated Database Snapshots: Authentik stores all flow definitions and tokens in Postgres. Back up your database every 24 hours with a simple cron task:
# Production database backup snapshot
docker compose exec -T postgresql pg_dump -U authentik authentik | gzip > /opt/backups/authentik_$(date +%F_%T).sql.gz
Share: