Authentifizierung vor Zugriff: Ein vorgeschalteter Proxy für Keycloak-geschützte Anwendungen

Wenn Unternehmen ihre Anwendungen über Keycloak absichern, läuft das in der Praxis meistens so: Die Anwendung wird aus dem Internet erreichbar gemacht, und beim Zugriff delegiert sie die Anmeldung per OIDC an Keycloak. Klingt vernünftig – ist es im Kern auch. Aber schau dir das mal genauer an: Damit die Anwendung überhaupt zu Keycloak weiterleiten kann, muss sie erstmal selbst unauthentifiziert per HTTP aus dem Internet erreichbar sein. Natürlich steht davor in der Regel ohnehin schon irgendeine Art von Edge-Infrastruktur, die grobe Dinge wie offene Admin-Ports oder interne Management-Schnittstellen fernhält – das ist nicht der Punkt. Der Punkt ist grundsätzlicher: Die Anwendung selbst, mit ihrer eigenen Web- und Applikationslogik, nimmt Anfragen entgegen und verarbeitet sie, bevor überhaupt feststeht, wer da anfragt. Und damit ist sie all den Risiken ausgesetzt, denen jede im Netz erreichbare, unauthentifizierte Anwendung ausgesetzt ist.

Sicherheitstechnisch wäre es deutlich eleganter, das Verhältnis umzudrehen: Erst die Authentifizierung, dann der Zugriff. Ein Nutzer soll die Anwendung selbst gar nicht erst zu Gesicht bekommen, bevor er sich nicht gegenüber Keycloak ausgewiesen hat. Genau das ist die Idee hinter einem vorgeschalteten, authentifizierenden Reverse Proxy: Er nimmt sämtliche Anfragen von außen entgegen, prüft, ob der Nutzer bereits eine gültige Sitzung hat, und lässt nur authentifizierte Requests überhaupt in Richtung Anwendung durch.

Dieses Setup haben wir bei diversen Kunden bereits mit Netscaler umgesetzt. In diesem Beitrag zeigen wir die gleiche Idee bewusst mit Open-Source-Werkzeugen: nginx in Kombination mit oauth2-proxy. Es geht uns dabei weniger um die eine richtige technische Umsetzung – die variiert je nach Produkt und Umgebung – sondern um das dahinterliegende Prinzip. Und das ist, wenn man einmal genauer hinschaut, nicht ganz so trivial, wie es auf den ersten Blick wirkt.

Die Idee in Kürze: zwei OIDC-Verbindungen

Bevor wir ins Detail gehen, hier der Ablauf auf einer Flughöhe, auf der man ihn sich gut merken kann. Ein Nutzer versucht, auf die Anwendung zuzugreifen. Dieser Zugriff geht immer zuerst beim Proxy ein. Der Proxy prüft, ob eine gültige Sitzung vorliegt – wenn nicht, baut er eine OIDC-Verbindung zu Keycloak auf, um den Nutzer zu authentifizieren. Erst danach reicht er den Request an die Anwendung weiter.

Jetzt kommt der Teil, der beim ersten Nachdenken überrascht: Die Anwendung kennt die Sitzung des Proxys nicht. Sie hat ihre eigene, unabhängige Authentifizierung gegen Keycloak – und baut deshalb selbst noch einmal eine zweite OIDC-Verbindung zu Keycloak auf. Das klingt nach unnötiger Doppelarbeit, läuft in der Praxis aber komplett automatisch im Hintergrund ab: Weil der Nutzer bei Keycloak bereits eine aktive Sitzung hat (Single Sign-On), bekommt er dort kein Login-Formular mehr zu sehen. Er merkt von dieser zweiten Runde schlicht nichts.

Als Bild dafür ein stark vereinfachtes Sequenzdiagramm der vier beteiligten Komponenten:

Vereinfachtes Sequenzdiagramm eines authentifizierenden Reverse Proxy vor Keycloak: zwei OIDC-Verbindungen zwischen Nutzer, Proxy, Keycloak und Anwendung

Zwei OIDC-Verbindungen, ein Nutzer, der davon fast nichts mitbekommt. Klingt einfach – ist es im Kern auch. Aber unter der Haube passiert an einigen Stellen mehr, als man zunächst vermutet. Schauen wir uns das im Detail an.

Der Ablauf im Detail

Nehmen wir an, eine Anwendung liegt im internen Netz und soll aus dem Internet erreichbar gemacht werden. Sämtlicher Zugriff von außen läuft über den Proxy

  • Phase 1 – Erstzugriff ohne Sitzung. Der Nutzer ruft die Anwendung auf. Der Proxy stellt fest: Es liegt kein gültiges Session-Cookie vor. Statt den Request auch nur in die Nähe der Anwendung zu lassen, schickt er dem Browser einen Redirect zu Keycloak.
  • Phase 2 – OIDC-Runde 1: Anmeldung am Proxy. Der Browser folgt dem Redirect und landet beim Login-Formular von Keycloak. Der Nutzer meldet sich an. Keycloak legt daraufhin eine eigene SSO-Sitzung an (das berühmte KEYCLOAK_SESSION-Cookie) und schickt den Browser mit einem Autorisierungscode zurück zur Callback-URL des Proxys. Wichtig dabei: Das ist keine simple Weiterleitung, sondern eine vollständige OIDC-Föderation – der Proxy tauscht den Code im Hintergrund (server-seitig, ohne Umweg über den Browser) gegen ein ID-Token ein. Erst jetzt legt der Proxy seine eigene Sitzung an und setzt sein eigenes Cookie.
  • Phase 3 – Zugriff erreicht die Anwendung, aber ohne Sitzung. Der Browser ruft die ursprünglich gewünschte URL erneut auf – diesmal mit gültigem Proxy-Cookie. Der Proxy lässt den Request jetzt durch und reicht ihn an die Anwendung weiter. Die Anwendung kennt den Nutzer aber nicht: Das Cookie, das im Rahmen der Proxy-Anmeldung entstanden ist, gilt nicht für sie. Sie hat ihre eigene, unabhängige Session-Verwaltung. Also tut die Anwendung das Naheliegende und schickt den Nutzer selbst zu Keycloak – die zweite OIDC-Föderation beginnt.
  • Phase 4 – OIDC-Runde 2: Silent SSO an der Anwendung. Und hier liegt der eigentliche Clou des ganzen Flows: Weil der Nutzer bei Keycloak schon eine aktive SSO-Sitzung hat, erkennt Keycloak ihn sofort wieder. Es gibt kein erneutes Login-Formular, keine Eingabe von Zugangsdaten – Keycloak reicht direkt einen neuen Autorisierungscode zurück an die Anwendung. Die Anwendung tauscht diesen Code – wieder im Hintergrund – gegen ihre eigenen Tokens ein und legt ihre eigene Session an. Der Nutzer sieht von alldem nur ein kurzes Aufflackern der Adresszeile, wenn überhaupt.
  • Phase 5 – Zugriff gewährt. Der nächste Request läuft glatt durch: gültiges Proxy-Cookie, gültige App-Session. Die Anwendung liefert ihren Inhalt aus.

Der komplette Ablauf noch einmal im Detail, inklusive aller Redirects, des Backchannel-Token-Tauschs und der Stelle, an der SSO greift:

Detailliertes Sequenzdiagramm: kompletter Ablauf vom Erstzugriff über die Proxy-Anmeldung bis zum Silent SSO an der Keycloak-geschützten Anwendung

Technische Umsetzung mit nginx und oauth2-proxy

Kommen wir zur konkreten Umsetzung. Ein Hinweis vorweg: Wir dokumentieren hier bewusst nur die Einstellungen, die für das Verständnis des Patterns wesentlich sind – kein komplettes nginx-Tutorial, kein Grundlagenwissen zu TLS-Terminierung, Puffergrößen oder Upstream-Tuning. Das Setup besteht aus drei nginx-Server-Blöcken und einem Keycloak-Client.

1. Keycloak selbst veröffentlichen

Keycloak läuft intern und wird über nginx nach außen freigegeben:

upstream keycloak_backend {
  server keycloak-backend.internal:8443;
  keepalive 64;
}

server {
  listen 443 ssl;
  server_name auth.example.com;

  location / {
    proxy_pass https://keycloak_backend;
    proxy_http_version 1.1;

    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-Host  $host;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Forwarded-Port  $server_port;
  }
}

Die Quintessenz hier sind die X-Forwarded-*-Header. Keycloak muss wissen, unter welchem Host, Schema und Port es von außen tatsächlich erreichbar ist, um korrekte Redirect- und Issuer-URLs in seinen Tokens und im OIDC-Discovery-Dokument zu bauen.

2. Die zentrale Login-Domain für oauth2-proxy

Diese Domain macht ausschließlich die Endpunkte von oauth2-proxy erreichbar (/oauth2/start, /oauth2/callback, /oauth2/auth, /oauth2/sign_out) – nichts sonst:

server {
  listen 443 ssl;
  server_name proxy.example.com;

  location /oauth2/ {
    proxy_pass https://oauth2-proxy-backend:4180;
    proxy_http_version 1.1;

    proxy_set_header Host             $host;
    proxy_set_header X-Forwarded-Host proxy.example.com;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Forwarded-For  $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Uri  $request_uri;
  }

  location / {
    return 404;
  }
}

Zwei Dinge sind hier bemerkenswert. Erstens: Diese Domain ist genau die Callback-URL, die wir gleich im Keycloak-Client hinterlegen – ein zentraler oauth2-proxy, eine zentrale Login-Domain. Zweitens: Alles, was nicht /oauth2/ ist, liefert bewusst 404. Diese Domain hat schlicht keine weitere Angriffsfläche.

Weil ein zentraler oauth2-proxy hier verwendet wird, können sich mehrere geschützte Anwendungen diese eine Login-Domain teilen – jede Anwendung bekommt ihren eigenen server_name-Block, aber die Authentifizierungslogik liegt an einer Stelle.

3. Die geschützte Anwendung

Das ist der eigentliche Kern des Patterns – hier passiert die Durchsetzung „erst authentifizieren, dann zugreifen“:

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

  error_page 401 = @oauth2_signin;
  location @oauth2_signin {
    return 302 https://proxy.example.com/oauth2/start?rd=$scheme://$host$request_uri;
  }

  location = /oauth2/auth {
    internal;
    proxy_pass https://oauth2-proxy-backend:4180/oauth2/auth;

    proxy_pass_request_body off;
    proxy_set_header Content-Length "";

    proxy_set_header X-Original-URI    $request_uri;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Forwarded-Host  proxy.example.com;
  }

  location / {
    auth_request /oauth2/auth;
    auth_request_set $user        $upstream_http_x_auth_request_user;
    auth_request_set $email       $upstream_http_x_auth_request_email;
    auth_request_set $auth_cookie $upstream_http_set_cookie;
    add_header Set-Cookie $auth_cookie;

    proxy_set_header X-Forwarded-User  $user;
    proxy_set_header X-Forwarded-Email $email;

    proxy_set_header Host $host;
    proxy_pass https://app-backend.internal;
  }
}

auth_request ist der Mechanismus, der das ganze Pattern trägt: Bei jedem einzelnen Request an die Anwendung schickt nginx zuerst eine interne Subrequest an oauth2-proxy und fragt „ist dieser Nutzer authentifiziert?“. Nur bei einer positiven Antwort geht der eigentliche Request überhaupt in Richtung Anwendung weiter. Kommt stattdessen ein 401 zurück, greift error_page 401, und der Nutzer wird – inklusive rd-Parameter für die ursprünglich gewünschte URL – zur zentralen Login-Domain geschickt. Die Location für /oauth2/auth ist mit internal; markiert und von außen gar nicht direkt aufrufbar; sie existiert nur für diese interne Weiche.

Wichtig für das Verständnis im Zusammenspiel mit den Diagrammen aus Kapitel 3: Dieser nginx-host sitzt vor der eigentlichen Anwendung und bildet ausschließlich die erste OIDC-Verbindung ab – die zwischen Proxy und Keycloak (Phase 1 bis 3 im Sequenzdiagramm). Lässt auth_request den Request passieren, wird er unverändert an app-backend.internal weitergereicht. Was dort passiert, ist bei uns bewusst so gebaut, dass die Anwendung die durchgereichten Header (X-Forwarded-User– und X-Forwarded-Email-Header) zwar bekommt, diese aber nicht auswertet: Sie ist selbst als eigener OIDC-Client in Keycloak registriert und baut beim Empfang des Requests ihre eigene, zweite OIDC-Verbindung auf (Phase 3 bis 4 im Diagramm) – inklusive eigenem Session-Cookie. Diese zweite Föderation läuft vollständig innerhalb des Anwendungs-Backends ab und taucht deshalb in dieser nginx-Konfiguration nicht mehr auf; sie beginnt genau da, wo proxy_pass https://app-backend.internal; endet.

4. Der Keycloak-Client für den Proxy

Zuletzt braucht oauth2-proxy einen eigenen OIDC-Client in Keycloak. Die wesentlichen Einstellungen dafür:

EinstellungWertWarum
Client authenticationOnVertraulicher Client mit Secret – nötig für den serverseitigen Code-Tausch im Backchannel
Authentication flowStandard FlowAuthorization Code Flow – der Standardweg für serverseitige Clients wie oauth2-proxy
Valid redirect URIshttps://proxy.example.com/oauth2/callbackExakt auf die eine Callback-URL des Proxys begrenzt, keine Wildcards

Dazu kommen zwei Protocol-Mapper, die an diesem Client hängen. Der Group-Membership-Mapper (groups) schreibt die Gruppenmitgliedschaften des Nutzers in ID-Token, Access-Token, Userinfo-Endpoint und Token-Introspection – praktisch, sobald man später Zugriffsentscheidungen anhand von Gruppen treffen will, sei es im Proxy oder in der Anwendung. Der Audience-Mapper (aud-mapper) setzt den aud-Claim explizit auf proxy-client, sodass bei jeder Token-Validierung eindeutig ist, für welchen Client das Token überhaupt ausgestellt wurde. proxy-client ist dabei schlicht die Client-ID, die beim Anlegen des Clients in Keycloak vergeben wurde und die oauth2-proxy in seiner eigenen Konfiguration (--client-id=proxy-client) identisch hinterlegt bekommt – der Proxy „kennt“ seinen Namen also nicht von sich aus, sondern beide Stellen müssen von Hand übereinstimmend gepflegt werden.

Fazit

Das Prinzip ist am Ende simpel zu formulieren: Authentifizierung vor Zugriff, nicht danach. Die Umsetzung mit nginx und oauth2-proxy zeigt aber gut, dass hinter dieser einfachen Formulierung zwei vollständige, unabhängige OIDC-Föderationen stecken, die nur dank Single Sign-On bei Keycloak für den Nutzer wie eine einzige, nahtlose Anmeldung wirken. Ob man das mit Open-Source-Werkzeugen wie hier gezeigt oder mit einer kommerziellen Lösung wie Netscaler umsetzt, ändert am Grundprinzip nichts – nur an den Stellschrauben, an denen man drehen muss.

Ein Wort zu KI in diesem Beitrag

Dieser Beitrag ist mit Unterstützung von KI entstanden – beim Formulieren der Texte und bei den Sequenzdiagrammen. Das verstecken wir nicht.
Was die KI uns nicht abgenommen hat: das Muster stammt aus echten Kundenprojekten, jede technische Behauptung haben wir selbst gegengeprüft, und beide Diagramme brauchten mehrere Korrekturrunden, bis Ablauf und Lesbarkeit gestimmt haben. Unterm Strich stecken trotzdem gut 2 Arbeitstage drin – nicht fürs Tippen, sondern fürs Prüfen und Nachschärfen. KI beschleunigt die Ausformulierung, die fachliche Verantwortung bleibt bei uns

Nach oben scrollen