SSD Nodes Learn Hosting plans →
Anleitungen Matt ConnorVon Matt Connor

authentik Forward Auth mit nginx einrichten

nginx fragt per auth_request bei authentik nach, bevor er deine App ausliefert. Proxy-Outpost, error_page 401, die X-authentik-Header und die Grenzen dieses Schutzes.

Was authentik Forward Auth mit nginx macht

authentik Forward Auth mit nginx bedeutet: nginx fragt vor jedem Request bei einem authentik Outpost nach, ob der Besucher angemeldet ist. Diese Nachfrage erledigt die Direktive auth_request. Antwortet der Outpost mit 200, reicht nginx den Request an deine App weiter. Antwortet er mit 401, schickt error_page 401 den Besucher zur Anmeldung bei authentik. Nach dem Login liefert nginx die Identität des Benutzers als HTTP-Header an die App, und die App braucht selbst keinen eigenen Login mehr.

Diese Anleitung setzt voraus, dass authentik bereits läuft und dass du nginx als Reverse Proxy betreibst. Läuft authentik noch nicht, richte zuerst authentik als eigenen Identity Provider auf einem VPS ein. Wer Traefik einsetzt, braucht die andere Variante: Forward Auth über die ForwardAuth-Middleware von Traefik. Das Prinzip ist in beiden Fällen dasselbe, die Konfiguration ist es nicht, und genau deshalb gibt es hier eine eigene Anleitung für nginx.

Alles unten ist gegen authentik 2026.8.3 geschrieben. Nachgesehen habe ich am 27. September 2026 in den Release Notes von 2026.8 und auf der nginx-Seite der authentik-Dokumentation. Die Namen der Identitäts-Header und der Image-Tag des Outposts gehören zu dieser Version. Vergleiche beides mit der Dokumentation deiner eigenen Version, bevor du etwas kopierst.

Was Forward Auth schützt und was nicht

auth_request ist ein Filter in einem location-Block von nginx. Geschützt ist damit genau der Weg durch diesen Block. Sonst nichts. Dieser Satz ist der wichtigste in der ganzen Anleitung, denn mehrere alltägliche Wege führen an nginx vorbei, und auf keinem davon sieht der Besucher jemals einen Login.

Ein veröffentlichter Container-Port. Steht in deiner Compose-Datei ports: - "8080:8080", dann lauscht die App auf allen Adressen des Hosts. Wer IP und Port kennt, redet direkt mit ihr. Eine ufw-Regel hilft dagegen nicht, weil Docker seine eigenen Regeln in eigene nftables-Ketten schreibt und diese Ketten vor den von ufw verwalteten Regeln greifen. Binde die Veröffentlichung deshalb an die Loopback-Adresse: ports: - "127.0.0.1:8080:8080". Dann erreicht nur nginx die App. Wie so ein vhost darüber aufgebaut ist, zeigt die Erklärung einer nginx-Reverse-Proxy-Konfiguration Zeile für Zeile.

Andere Container im gleichen Docker-Netz. Liegen App und Proxy im selben Netz, erreicht jeder weitere Container die App unter ihrem Servicenamen, also zum Beispiel http://app:8080. Der Request geht nicht durch nginx, also greift kein auth_request. Wenn das ein Problem ist, gib der App ein eigenes internes Netz, in dem nur nginx mit ihr liegt.

Die eigene API der App und ihre Tokens. Ein API-Schlüssel der App bleibt gültig, egal was authentik sagt, weil die App ihn selbst prüft. Dasselbe Problem gilt auch umgekehrt: schickst du API-Aufrufe durch den geschützten location-Block, bekommt der Client die 302 auf die Anmeldeseite von authentik und nicht seine JSON-Antwort. Forward Auth ist Schutz für Browser-Zugriffe. Für Clients mit Token brauchst du eine andere Lösung, dazu unten mehr.

Alles, was nicht über nginx läuft. SSH oder eine zweite Web-Oberfläche auf einem anderen Port zum Beispiel. Forward Auth sieht davon nichts.

Voraussetzung: kennt dein nginx die Direktive auth_request?

auth_request kommt aus dem Modul ngx_http_auth_request_module. Das Modul ist nicht in jedem nginx-Build enthalten, also prüfe es zuerst.

nginx -V 2>&1 | tr ' ' '\n' | grep auth_request

Die Ausgabe muss --with-http_auth_request_module enthalten. Kommt keine Zeile zurück, kennt dein nginx die Direktive nicht, und der nächste Reload bricht ab mit nginx: [emerg] unknown directive "auth_request". Die Pakete von Debian und Ubuntu enthalten das Modul. Bei selbst kompilierten Builds und bei sehr schlanken Container-Images fehlt es manchmal.

Außerdem brauchst du eine App, die schon über nginx mit gültigem TLS-Zertifikat (TLS steht für Transport Layer Security) erreichbar ist, und eine authentik-Instanz, die der nginx-Host über das Netz erreicht. Die Beispiele nutzen app.example.com für die geschützte App und authentik.example.com für authentik selbst.

Provider und Application in authentik anlegen

In der authentik-Oberfläche gehst du auf Applications, dann Providers, dann Create und wählst Proxy Provider. Wichtig sind zwei Felder. Als Authorization flow nimm den Flow mit impliziter Zustimmung, sonst bestätigt jeder Benutzer bei jedem Login eine Berechtigungsseite. Als Modus wähle Forward auth (single application). Der Modus Proxy würde authentik selbst zum Proxy machen, und das willst du hier nicht, weil nginx diese Rolle behält.

Als External host trage die vollständige öffentliche Adresse der App ein, also https://app.example.com. Dieses Feld ist nicht Kosmetik: der Outpost vergleicht damit den Host des eingehenden Requests und entscheidet, welche Application gemeint ist. Passt der Wert nicht zur Domain, die der Besucher aufruft, findet der Outpost keine passende Application und der Login endet im Kreis.

Danach legst du unter Applications eine Application an, gibst ihr einen Slug und verbindest sie mit dem gerade erstellten Provider. Zum Schluss öffnest du Applications, dann Outposts, und weist die Application einem Outpost zu. Ohne diese Zuweisung kennt der Outpost die Application nicht, auch wenn der Provider korrekt aussieht.

Eingebetteter Outpost oder eigener Container?

Jede authentik-Installation bringt einen eingebetteten Outpost mit. Er läuft im Core-Container und ist unter dessen Port 9000 auf dem Pfad /outpost.goauthentik.io erreichbar. Wenn nginx und authentik auf derselben Maschine laufen oder sich über ein Netz erreichen, brauchst du gar keinen zweiten Container. Setze proxy_pass dann einfach auf die authentik-Adresse mit Port 9000.

Einen eigenen Outpost-Container willst du, wenn nginx auf einem anderen Server steht als authentik und du den Auth-Request nicht über das öffentliche Netz schicken möchtest. Dann läuft der Outpost neben nginx.

services:
  authentik-proxy:
    image: ghcr.io/goauthentik/proxy:2026.8.3
    restart: unless-stopped
    ports:
      - "127.0.0.1:9000:9000"
    environment:
      AUTHENTIK_HOST: https://authentik.example.com
      AUTHENTIK_INSECURE: "false"
      AUTHENTIK_TOKEN: <Token aus der Outpost-Ansicht>

Den Token findest du in authentik in der Outpost-Ansicht unter View Deployment Info. Der Tag ist bewusst festgenagelt. Ein :latest zieht beim nächsten docker compose pull irgendeine neuere Version, und dann spricht ein Outpost von morgen mit einem Core von heute. Outpost und Core teilen eine interne API, deren Verträge sich zwischen Versionen ändern, also halte beide auf derselben Version und hebe sie gemeinsam an. Ob es geklappt hat, siehst du in der Outpost-Liste in authentik: dort steht der Outpost als gesund und mit seiner Versionsnummer, sobald er sich beim Core gemeldet hat.

Die nginx-Konfiguration Stück für Stück

Zwei map-Blöcke gehören in den http-Kontext, nicht in einen server-Block. Lege sie in eine eigene Datei unter /etc/nginx/conf.d/authentik-maps.conf, denn conf.d wird innerhalb von http eingebunden.

map $http_upgrade $connection_upgrade_keepalive {
    default upgrade;
    ''      '';
}

map $http_host $ak_http_host {
    default $http_host;
    ''      $host;
}

Der erste Block setzt den Connection-Header nur dann auf upgrade, wenn der Client wirklich ein Upgrade anfragt. Ein hart gesetztes Connection: upgrade bei jedem Request bricht normale Antworten. Der zweite Block liefert den Host, den der Browser geschickt hat, und fällt auf $host zurück, falls kein Host-Header ankommt.

Die beiden Blöcke, die zum Outpost gehören, brauchst du in jedem geschützten vhost. Schreibe sie einmal nach /etc/nginx/snippets/authentik-outpost.conf.

location /outpost.goauthentik.io {
    proxy_pass              http://127.0.0.1:9000/outpost.goauthentik.io;
    proxy_set_header        Host $ak_http_host;
    proxy_set_header        X-Original-URL $scheme://$ak_http_host$request_uri;
    add_header              Set-Cookie $auth_cookie;
    auth_request_set        $auth_cookie $upstream_http_set_cookie;
    proxy_pass_request_body off;
    proxy_set_header        Content-Length "";
}

location @goauthentik_proxy_signin {
    internal;
    add_header Set-Cookie $auth_cookie;
    return 302 /outpost.goauthentik.io/start?rd=$scheme://$ak_http_host$request_uri;
}

Der erste Block ist der Weg nach außen. Über ihn laufen der Auth-Request von nginx und der komplette Login im Browser, denn der Outpost bedient unter diesem Pfad auch /start und die Rückkehr-URL. X-Original-URL sagt dem Outpost, welche Adresse der Besucher eigentlich wollte. proxy_pass_request_body off zusammen mit dem leeren Content-Length verhindert, dass nginx den Body jedes Requests zusätzlich an den Outpost schickt, der ihn nicht braucht.

Der zweite Block ist der Ort, an den error_page 401 springt. internal bedeutet, dass ihn nur nginx selbst aufrufen kann und kein Besucher von außen. Die 302 zeigt auf /start mit der ursprünglichen Adresse im Parameter rd, damit der Benutzer nach dem Login dort ankommt, wo er hin wollte.

Jetzt der geschützte vhost.

server {
    listen 443 ssl http2;
    server_name app.example.com;

    ssl_certificate     /etc/letsencrypt/live/app.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/app.example.com/privkey.pem;

    proxy_buffers     8 16k;
    proxy_buffer_size 32k;

    include /etc/nginx/snippets/authentik-outpost.conf;

    location / {
        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;

        auth_request_set $authentik_username $upstream_http_x_authentik_username;
        auth_request_set $authentik_groups $upstream_http_x_authentik_groups;
        auth_request_set $authentik_entitlements $upstream_http_x_authentik_entitlements;
        auth_request_set $authentik_email $upstream_http_x_authentik_email;
        auth_request_set $authentik_name $upstream_http_x_authentik_name;
        auth_request_set $authentik_uid $upstream_http_x_authentik_uid;

        proxy_set_header X-authentik-username $authentik_username;
        proxy_set_header X-authentik-groups $authentik_groups;
        proxy_set_header X-authentik-entitlements $authentik_entitlements;
        proxy_set_header X-authentik-email $authentik_email;
        proxy_set_header X-authentik-name $authentik_name;
        proxy_set_header X-authentik-uid $authentik_uid;

        proxy_pass http://127.0.0.1:8080;
        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_set_header Upgrade           $http_upgrade;
        proxy_set_header Connection        $connection_upgrade_keepalive;
    }
}

Ein Hinweis zur ersten Zeile: nginx 1.24, die Version in Ubuntu 24.04, erwartet listen 443 ssl http2;. Ab nginx 1.25.1 schreibst du stattdessen listen 443 ssl; und eine eigene Zeile http2 on;. Die alte Form gibt dort eine Warnung aus.

auth_request /outpost.goauthentik.io/auth/nginx ist der Endpunkt speziell für nginx. Er antwortet mit 200 oder 401 und ohne Body, damit error_page die Umleitung übernehmen kann. Die anderen Endpunkte unter /auth/ heißen nach ihrem Proxy, etwa /auth/traefik oder /auth/envoy, und antworten anders.

Die Zeile add_header Set-Cookie $auth_cookie; sieht überflüssig aus und ist der häufigste Grund für eine Endlosschleife. nginx verwirft die Antwort-Header eines Sub-Requests, und der Auth-Request ist ein Sub-Request. Ohne auth_request_set und dieses add_header erreicht das Session-Cookie von authentik den Browser nie. Der Besucher meldet sich an, kommt zurück, hat kein Cookie, wird wieder zur Anmeldung geschickt.

Die größeren Puffer sind ebenfalls Pflicht und nicht Geschmackssache. Die Antwort des Outposts trägt ein Cookie und mehrere Header, und das passt oft nicht in den Standardpuffer von 4k. Dann steht im error.log die Zeile upstream sent too big header while reading response header from upstream und der Besucher sieht eine 502.

Prüfen und neu laden:

sudo nginx -t && sudo systemctl reload nginx

Danach ein Test ohne Cookie, direkt von der Kommandozeile:

curl -sI https://app.example.com/ | head -n 5

Richtig sieht das so aus: Statuszeile HTTP/2 302 und ein location-Header, der auf /outpost.goauthentik.io/start?rd=https://app.example.com/ zeigt. Kommt stattdessen eine 200 mit dem Inhalt deiner App zurück, greift auth_request in diesem location-Block nicht. Kommt eine 500, hat der Auth-Request selbst ein Problem, und die Ursache steht im error.log.

Identitäts-Header an die App weitergeben

Nach einem erfolgreichen Login setzt der Outpost diese Header in seiner Antwort, und die Konfiguration oben schreibt sie an die App weiter: X-authentik-username, X-authentik-email, X-authentik-name, X-authentik-uid, X-authentik-groups und X-authentik-entitlements. X-authentik-groups enthält die Gruppen des Benutzers, getrennt durch |. Die Namen stammen aus der nginx-Seite der authentik-Dokumentation zu 2026.8 und haben sich in früheren Versionen schon geändert, also prüfe sie gegen deine Version.

Der Umweg über auth_request_set ist nötig, weil die Header aus einem Sub-Request kommen. $upstream_http_x_authentik_username liest den Header aus der Antwort des Outposts in eine nginx-Variable, und erst proxy_set_header gibt ihn an die App weiter. Lässt du eine der beiden Zeilen weg, sieht die App einen leeren Wert.

Eine App, die einem Header glaubt, glaubt ihm immer. Sie kann nicht unterscheiden, ob nginx ihn gesetzt hat oder ein Angreifer. Genau deshalb steht der Abschnitt zum Bedrohungsmodell weiter oben: Trusted-Header-Login und ein öffentlich erreichbarer App-Port ergeben zusammen einen offenen Zugang, bei dem ein einziger selbst gesetzter Header genügt. Die App darf nur über nginx erreichbar sein.

Grafana zum Beispiel liest den Benutzernamen aus einem konfigurierbaren Header, wenn du [auth.proxy] aktivierst und header_name auf X-authentik-username setzt. Andere Apps nennen dasselbe Verfahren Remote User oder Trusted Header SSO, wobei SSO für Single Sign-on steht. Paperless-ngx und Forgejo können es, jeweils mit eigenen Einstellungsnamen. Suche in der Dokumentation der App nach diesen Begriffen und übernimm den exakten Namen von dort, statt ihn zu raten.

Welche Apps passen zu Forward Auth und welche brauchen OIDC?

Forward Auth passt gut zu Anwendungen ohne eigenen Login. Prometheus, Alertmanager, ein Verzeichnislisting, eine Statusseite, ein selbst gebautes Dashboard: hier ersetzt authentik den fehlenden Login vollständig. Ebenso passt es zu Apps mit dokumentiertem Trusted-Header-Login, weil sie den Benutzer aus dem Header übernehmen und ihre eigene Rechteverwaltung darauf aufbauen.

Forward Auth passt schlecht, sobald ein Client kein Browser ist. Die Desktop- und Mobil-Clients von Nextcloud oder die Jellyfin-Apps auf dem Fernseher können keine Anmeldeseite ausfüllen. Sie bekommen die 302 zu authentik und melden einen Fehler. Für solche Anwendungen brauchst du OIDC (OpenID Connect) in der App selbst, also einen OAuth2/OpenID-Provider in authentik statt eines Proxy Providers. Dasselbe gilt für einen Container, der die API einer anderen App mit einem Token abfragt.

Der Zwischenweg ist ein zweiter location-Block ohne auth_request, zum Beispiel für /api/. Er funktioniert, verlagert den Schutz dieses Pfades aber komplett in die App. Nimm ihn nur, wenn die App dort eine eigene Authentifizierung hat, die du kennst.

Dass manche Projekte OIDC ausschließlich in der Bezahlversion anbieten, ist ein eigenes Thema: die SSO-Tax bei selbst gehosteten Apps erklärt, wo diese Grenze verläuft und warum Forward Auth dort oft der einzige bezahlbare Ausweg ist. Wenn du noch entscheidest, welcher Identity Provider überhaupt bei dir laufen soll, vergleicht Keycloak, authentik und Zitadel die üblichen Kandidaten. Und wer nur eine einzige App absichern will, ohne einen Outpost zu betreiben, kommt mit oauth2-proxy vor der App ans gleiche Ziel, ebenfalls über auth_request.

Domain Level Forward Auth für mehrere Apps

Für jede App einen eigenen Provider anzulegen wird mühsam, sobald es mehr als eine Handvoll sind. authentik kennt dafür den Modus Forward auth (domain level). Ein Provider deckt dann alle Hosts unter einer Domain ab, zum Beispiel alles unter *.example.com, und pro App legst du nur noch eine Application an. Das Cookie gilt für die ganze Domain, also genügt ein Login für alle Apps darunter.

In nginx ändert sich dabei eine Zeile. Die Umleitung im @goauthentik_proxy_signin-Block zeigt nicht mehr auf den lokalen Pfad, sondern auf den authentik-Host, weil die Session dort zentral entsteht.

location @goauthentik_proxy_signin {
    internal;
    add_header Set-Cookie $auth_cookie;
    return 302 https://authentik.example.com/outpost.goauthentik.io/start?rd=$scheme://$ak_http_host$request_uri;
}

Der Preis dieser Bequemlichkeit: eine Session gilt für alle Apps unter der Domain. Wer Zugriff auf eine bekommt, hat die Session für alle, und die Trennung machst du dann allein über die Bindings der Applications in authentik.

Fehlersuche mit den Meldungen, die im Log stehen

nginx: [emerg] unknown directive "auth_request" beim Reload. Dein nginx hat das Modul nicht. Prüfe es mit dem nginx -V-Befehl von oben und wechsle notfalls auf das Distributionspaket.

Endlosschleife zwischen App und Anmeldeseite. Der Browser läuft zwischen app.example.com und authentik hin und her, ohne je anzukommen. Zwei Ursachen sind häufig. Entweder fehlt add_header Set-Cookie $auth_cookie; in einem der Blöcke, dann erreicht das Session-Cookie den Browser nicht. Oder der External host im Provider passt nicht genau zur aufgerufenen Adresse, dann findet der Outpost keine passende Application. Vergleiche die Adresse zeichenweise, inklusive Schema und Port.

Der Besucher sieht 500, im error.log steht auth request unexpected status: 404. Das Modul behandelt nur 2xx, 401 und 403 als gültige Antworten. Alles andere wird zur 500 für den Besucher, und die tatsächliche Zahl steht in dieser Logzeile. Eine 404 heißt meistens, dass der Pfad im proxy_pass des Outpost-Blocks nicht stimmt: er muss auf /outpost.goauthentik.io enden.

502, und im error.log steht connect() failed (111: Connection refused) while connecting to upstream. nginx erreicht den Outpost nicht. Prüfe mit curl -sI http://127.0.0.1:9000/outpost.goauthentik.io/start, ob der Port aus Sicht des nginx-Hosts überhaupt antwortet. Läuft nginx selbst im Container, zeigt 127.0.0.1 auf den nginx-Container und nicht auf den Host.

502, und im error.log steht upstream sent too big header while reading response header from upstream. Die Puffer sind zu klein. Setze proxy_buffers 8 16k; und proxy_buffer_size 32k; im server-Block.

Die App zeigt einen anonymen Benutzer, obwohl der Login klappt. Entweder greift ein anderer location-Block. auth_request und die Header-Zeilen wirken nur in dem Block, der den Request bearbeitet, und ein spezifischerer location ohne diese Zeilen erbt sie nicht von einem Nachbarblock. Oder die App ist nicht auf Trusted-Header-Login eingestellt und ignoriert die Header deshalb.

WebSockets brechen nach kurzer Zeit ab. Die Zeilen für Upgrade und Connection fehlen im geschützten Block, oder Connection ist hart auf upgrade gesetzt. Nutze die Variable aus dem map-Block.

Nach dem Login landet der Benutzer auf der Startseite statt auf der ursprünglichen Seite. Der Parameter rd in der 302 fehlt oder ist unvollständig. Er muss die vollständige Adresse enthalten, also Schema plus Host plus $request_uri.

FAQ

Brauche ich einen separaten Outpost-Container oder reicht der eingebettete?

Der eingebettete Outpost reicht in den meisten Fällen. Er läuft im authentik-Core und antwortet auf Port 9000 unter /outpost.goauthentik.io. Setze proxy_pass im Outpost-Block auf diese Adresse und weise die Application in authentik dem eingebetteten Outpost zu. Einen eigenen Container brauchst du erst, wenn nginx auf einem anderen Server steht und der Auth-Request nicht über das öffentliche Netz laufen soll.

Warum landet der Login in einer Endlosschleife?

Meistens fehlt add_header Set-Cookie $auth_cookie;. nginx verwirft die Header eines Sub-Requests, und der Auth-Request ist einer. Ohne diese Zeile erreicht das Session-Cookie von authentik den Browser nie, und der Benutzer wird nach jedem Login sofort wieder zur Anmeldung geschickt. Die zweite häufige Ursache ist ein External host im Provider, der nicht exakt der aufgerufenen Adresse entspricht, inklusive Schema und Port.

Kann ich mit Forward Auth eine API oder eine Mobile-App schützen?

Nein, jedenfalls nicht sinnvoll. Der geschützte Block antwortet unangemeldeten Clients mit einer 302 auf eine HTML-Anmeldeseite, und ein API-Client kann damit nichts anfangen. Für Clients mit Token oder für Mobile-Apps brauchst du OIDC in der App selbst, also einen OAuth2/OpenID-Provider in authentik. Als Zwischenlösung kannst du einen Pfad wie /api/ in einem eigenen location-Block ohne auth_request führen, dann schützt ihn aber nur noch die App.

Ist es sicher, dass die App dem Header X-authentik-username glaubt?

Nur solange die App ausschließlich über nginx erreichbar ist. Der Header ist ein normaler HTTP-Header, und wer die App direkt erreicht, kann ihn selbst setzen und sich damit als jeden Benutzer ausgeben. Binde deshalb den Container-Port an 127.0.0.1 und halte die App aus Netzen fern, in denen fremde Container liegen. In nginx setzt du im geschützten Block ohnehin alle X-authentik--Header aus deinen eigenen Variablen neu, und das überschreibt Werte, die ein Client von außen mitgeschickt hat.

Muss der Outpost dieselbe Version wie authentik haben?

Ja, halte beide auf derselben Version. Outpost und Core sprechen über eine interne API, deren Verträge sich zwischen Versionen ändern, und ein Versionsunterschied zeigt sich als Outpost, der sich in der Oberfläche nicht als gesund meldet. Genau deshalb steht im Compose-Beispiel ein fester Tag wie 2026.8.3 und kein :latest: bei :latest verschiebt ein beiläufiges docker compose pull den Outpost, ohne dass du den Core anfasst.