Authentik

We document the setup of Authentik, the identity provider used to protect the private services at piribauer.ch. Authentik runs in Docker and exposes its server on 127.0.0.1:9000. All protected apps delegate authentication to it via nginx forward-auth — they never implement any login logic themselves.

We assume Docker and nginx are already installed, and that a wildcard or per-subdomain TLS certificate is in place (we use Certbot).

Docker Compose

We create the working directory and place the compose file there:

$ mkdir -p /home/julian/authentik
$ cd /home/julian/authentik

The docker-compose.yml runs four services: PostgreSQL, Redis, and the Authentik server and worker. The server is bound to 127.0.0.1:9000 so it is only reachable locally.

services:
  postgresql:
    image: docker.io/library/postgres:16-alpine
    restart: unless-stopped
    volumes:
      - database:/var/lib/postgresql/data
    environment:
      POSTGRES_PASSWORD: ${PG_PASS}
      POSTGRES_USER: ${PG_USER:-authentik}
      POSTGRES_DB: ${PG_DB:-authentik}

  redis:
    image: docker.io/library/redis:alpine
    command: --save 60 1 --loglevel warning
    restart: unless-stopped
    volumes:
      - redis:/data

  server:
    image: ghcr.io/goauthentik/server:2024.12.3
    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"
    volumes:
      - ./media:/media
      - ./custom-templates:/templates
    ports:
      - "127.0.0.1:9000:9000"
      - "127.0.0.1:9443:9443"

  worker:
    image: ghcr.io/goauthentik/server:2024.12.3
    restart: unless-stopped
    command: worker
    user: root
    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"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - ./media:/media
      - ./certs:/certs
      - ./custom-templates:/templates

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

Environment file

We create /home/julian/authentik/.env with the database credentials and a secret key. The secret key should be a long random string — it is used to sign sessions and must not change after the first start.

PG_PASS=<random password>
PG_USER=authentik
PG_DB=authentik
AUTHENTIK_SECRET_KEY=<long random string>
AUTHENTIK_ERROR_REPORTING__ENABLED=false

Starting Authentik

$ cd /home/julian/authentik
$ docker compose up -d

Since docker.service is enabled in systemd and all containers have restart: unless-stopped, Authentik comes back up automatically after a reboot.

On first start, we navigate to https://auth.piribauer.ch/if/flow/initial-setup/ to set the admin password.

Nginx — Authentik itself

We proxy auth.piribauer.ch to the local server:

server {
    server_name auth.piribauer.ch;

    location / {
        proxy_pass         http://127.0.0.1:9000;
        proxy_http_version 1.1;
        proxy_set_header   Upgrade $http_upgrade;
        proxy_set_header   Connection "upgrade";
        proxy_set_header   Host $host;
        proxy_set_header   X-Real-IP $remote_addr;
        proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header   X-Forwarded-Proto $scheme;
        proxy_buffering    off;
    }

    listen 443 ssl;
    ssl_certificate     /etc/letsencrypt/live/piribauer.ch/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/piribauer.ch/privkey.pem;
    include             /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam         /etc/letsencrypt/ssl-dhparams.pem;
}

Creating a protected application

For each service we want to protect, we create an application and a proxy provider in the Authentik admin UI at https://auth.piribauer.ch/if/admin/.

Under Providers, we create a new Proxy Provider in forward-auth mode for the application’s domain (e.g. https://hub.piribauer.ch). We then create an Application linked to that provider. Finally, under Outposts, we add the application to the embedded outpost so the outpost begins serving the forward-auth endpoint for it.

Nginx — forward-auth for protected apps

Each protected app follows the same nginx pattern. We add two locations and an auth_request directive to the main location:

# Authentik outpost endpoint
location /outpost.goauthentik.io {
    proxy_pass              http://127.0.0.1:9000/outpost.goauthentik.io;
    proxy_set_header        Host $host;
    proxy_set_header        X-Original-URL $scheme://$host$request_uri;
    proxy_set_header        X-Real-IP $remote_addr;
    proxy_set_header        X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header        X-Forwarded-Proto $scheme;
    proxy_pass_request_body off;
    proxy_set_header        Content-Length "";
    add_header              Set-Cookie $auth_cookie;
    auth_request_set        $auth_cookie $upstream_http_set_cookie;
}

# Redirect unauthenticated requests to the login page
location @goauthentik_proxy_signin {
    internal;
    add_header Set-Cookie $auth_cookie;
    return 302 /outpost.goauthentik.io/start?rd=$scheme://$http_host$request_uri;
}

location / {
    # ... proxy_pass or root directive ...

    auth_request      /outpost.goauthentik.io/auth/nginx;
    error_page 401  = @goauthentik_proxy_signin;

    auth_request_set $auth_cookie           $upstream_http_set_cookie;
    add_header       Set-Cookie $auth_cookie;

    # Optional: expose user attributes to the backend
    auth_request_set $authentik_username  $upstream_http_x_authentik_username;
    auth_request_set $authentik_email     $upstream_http_x_authentik_email;
    proxy_set_header X-Authentik-Username $authentik_username;
    proxy_set_header X-Authentik-Email    $authentik_email;
}

On each request, nginx calls the outpost’s /auth/nginx endpoint. If the user has a valid session, the outpost returns 200 and nginx proceeds, injecting the username and email as headers. If not, nginx returns 401 which triggers the redirect to the login flow.