<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Forward-Auth – Serverküche</title><link>https://serverkueche.de/tags/forward-auth/</link><description>Forward-Auth – Neueste Beiträge von Serverküche</description><generator>Hugo</generator><language>de-DE</language><managingEditor>feedback@serverkueche.de (Serverküche)</managingEditor><webMaster>feedback@serverkueche.de (Serverküche)</webMaster><copyright>2026 Serverküche</copyright><lastBuildDate>Thu, 08 Oct 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://serverkueche.de/tags/forward-auth/index.xml" rel="self" type="application/rss+xml"/><item><title>Authentik Teil 2: Apps anbinden – Forward-Auth &amp; OIDC</title><link>https://serverkueche.de/tutorials/authentik-apps-anbinden/</link><pubDate>Thu, 08 Oct 2026 00:00:00 +0000</pubDate><author>feedback@serverkueche.de (Serverküche)</author><guid>https://serverkueche.de/tutorials/authentik-apps-anbinden/</guid><description>Dienste an Authentik anbinden: Forward-Auth mit Traefik für Apps ohne Login und OpenID Connect für alles mit eigener SSO-Unterstützung – jeder Schritt geprüft.</description><content:encoded><![CDATA[<p>Authentik läuft – jetzt kommt der Teil, der den Aufwand rechtfertigt: die Dienste dahinter. Es gibt
genau zwei Wege, und die Wahl hängt allein daran, ob die Anwendung selbst SSO versteht.</p>
<h2 id="was-bauen-wir">Was bauen wir?</h2>
<p>Wir binden zwei Dienste an, jeweils auf dem passenden Weg:</p>
<ul>
<li><strong>Forward-Auth</strong> für eine Anwendung <strong>ohne</strong> eigene Anmeldung. Traefik fragt vor jeder Anfrage bei
Authentik nach, ob der Besucher angemeldet ist. Ist er es nicht, landet er auf der
Authentik-Anmeldeseite; ist er es, bekommt die Anwendung seine Identität <strong>als HTTP-Header</strong>.</li>
<li><strong>OpenID Connect (OIDC)</strong> für Anwendungen <strong>mit</strong> SSO-Unterstützung – Grafana, Nextcloud, Forgejo,
Immich und viele andere. Dort trägst du Client-ID, Secret und die Endpunkte ein, den Rest macht die
App selbst.</li>
</ul>
<p>Alles unten wurde mit <strong>Authentik 2026.8</strong> hinter Traefik v3.7 auf Debian 13 durchgespielt: Am Ende
zeigt der Testdienst die durchgereichten Identitäts-Header, und das OIDC-Discovery-Dokument liefert
echte Endpunkte.</p>
<h3 id="welcher-weg-für-welchen-dienst">Welcher Weg für welchen Dienst?</h3>
<p>Die Entscheidung ist fast immer eindeutig:</p>
<table>
	<thead>
			<tr>
					<th>Dienst</th>
					<th>Unterstützt OIDC?</th>
					<th>Weg</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Nextcloud, Grafana, Forgejo, Immich, Paperless</td>
					<td>ja</td>
					<td><strong>OIDC</strong> – echte Konten, Rollen aus Gruppen</td>
			</tr>
			<tr>
					<td>Uptime Kuma, Dockge, Netdata, ein statisches Dashboard</td>
					<td>nein</td>
					<td><strong>Forward-Auth</strong></td>
			</tr>
			<tr>
					<td>Metrik-Endpunkte, <code>/actuator</code>, interne APIs</td>
					<td>irrelevant</td>
					<td>Forward-Auth <strong>oder</strong> einfacher: nicht öffentlich (VPN)</td>
			</tr>
			<tr>
					<td>Dienste mit Mobil-App (Immich, Nextcloud)</td>
					<td>ja</td>
					<td><strong>nur OIDC</strong> – Forward-Auth bricht die Apps (siehe unten)</td>
			</tr>
	</tbody>
</table>
<p>Kurz: <strong>OIDC, wo möglich; Forward-Auth, wo nötig.</strong> OIDC gibt der Anwendung ein echtes Benutzerkonto
inklusive Rollen; Forward-Auth stellt nur eine Tür davor und schiebt einen Namen im Header durch.</p>
<h2 id="voraussetzungen">Voraussetzungen</h2>
<ul>
<li>Ein <strong>laufendes Authentik</strong> aus <a href="/tutorials/authentik-single-sign-on/">Teil 1</a>, erreichbar unter
<code>https://DEINE_DOMAIN</code></li>
<li>Ein <strong>Traefik v3</strong> mit <code>proxy</code>-Netz (<a href="/tutorials/reverse-proxy-traefik/">Anleitung</a>) – wir arbeiten
hier viel mit Middlewares, die Grundlagen dazu stehen in
<a href="/tutorials/traefik-middlewares-absichern/">Traefik härten</a></li>
<li>Ein Dienst zum Üben. Wir nehmen <code>traefik/whoami</code>, weil er alle empfangenen Header ausgibt – damit
ist sofort sichtbar, ob die Anbindung wirklich greift</li>
</ul>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-herb-400 bg-herb-50 dark:border-herb-700 dark:bg-herb-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">🧑‍🍳</span>Ein oder zwei Domains?
  </p>
  <div class="prose-kitchen text-sm">Wir zeigen den Aufbau <strong>auf einer Domain</strong>: Authentik unter <code>https://DEINE_DOMAIN/</code>, der geschützte
Dienst unter <code>https://DEINE_DOMAIN/whoami</code>. Das ist die sparsame Variante und genau so getestet.
Üblicher ist die Trennung (<code>auth.DEINE_DOMAIN</code> für Authentik, <code>app.DEINE_DOMAIN</code> für den Dienst) – die
Schritte sind identisch, nur <code>external_host</code> und die Traefik-Regeln tragen dann den anderen Hostnamen.</div>
</div>
<h2 id="schritt-für-schritt">Schritt für Schritt</h2>
<h3 id="1-zugang-für-die-automatisierung-ein-api-token">1. Zugang für die Automatisierung: ein API-Token</h3>
<p>Provider und Anwendungen lassen sich vollständig über die API anlegen – praktisch, weil du damit
reproduzierbar arbeitest statt zu klicken. Dafür braucht Authentik einen Token.</p>
<p>Weil du in Teil 1 den Einrichtungsassistenten durchlaufen hast, existiert <code>akadmin</code> bereits. Den
Token legst du deshalb <strong>in der Oberfläche</strong> an: <strong>Verzeichnis → Tokens und App-Passwörter → Token
erstellen</strong>. Als Bezeichner nimm etwas Sprechendes (<code>api-setup</code>), als Absicht <strong>API</strong>, als Benutzer
<code>akadmin</code>. Nach dem Speichern kopierst du den Schlüssel über das Kopiersymbol – er wird nur einmal
angezeigt.</p>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-sky-300 bg-sky-50 dark:border-sky-800 dark:bg-sky-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">ℹ️</span>Bei einer Neuinstallation geht es auch automatisch
  </p>
  <div class="prose-kitchen text-sm"><p>Setzt du Authentik neu auf, kannst du Konto und Token beim <strong>allerersten</strong> Start erzeugen lassen –
ohne Assistenten. Die Variablen gehören an den <strong><code>worker</code></strong>-Dienst (nicht an <code>server</code>) und werden
<strong>nur beim ersten Start gelesen</strong>; bei einer bestehenden Installation bleiben sie wirkungslos.
Statt des Klartext-Passworts empfiehlt Authentik inzwischen einen Hash. Das Passwort nimmt
<code>hash_password</code> <strong>nicht als Argument</strong> – <code>hash_password DEIN_PASSWORT</code> bricht mit
<code>unrecognized arguments</code> ab –, sondern über eine Eingabeaufforderung oder die
Standardeingabe:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker compose run --rm server hash_password</span></span></code></pre></div>
</div>
<p>Am Ende steht eine Zeile, die mit <code>pbkdf2_sha256$</code> beginnt. Sie kommt als
<code>AK_BOOTSTRAP_PASSWORD_HASH</code> in die <code>.env</code>, der Token als <code>AK_BOOTSTRAP_TOKEN</code> (zum Beispiel aus
<code>openssl rand -hex 32</code>). In der <code>compose.yaml</code> sieht der <code>worker</code> dann so aus:</p>
<div class="sk-code">
  <span class="sk-code-head">YAML</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="w">  </span><span class="nt">worker</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">AUTHENTIK_BOOTSTRAP_PASSWORD_HASH</span><span class="p">:</span><span class="w"> </span><span class="l">${AK_BOOTSTRAP_PASSWORD_HASH}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">AUTHENTIK_BOOTSTRAP_TOKEN</span><span class="p">:</span><span class="w"> </span><span class="l">${AK_BOOTSTRAP_TOKEN}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">AUTHENTIK_BOOTSTRAP_EMAIL</span><span class="p">:</span><span class="w"> </span><span class="l">du@DEINE_DOMAIN</span></span></span></code></pre></div>
</div>
<p>Der Hash enthält <code>$</code>-Zeichen: in der <code>.env</code> in <strong>einfache</strong> Anführungszeichen setzen, direkt in der
<code>compose.yaml</code> jedes <code>$</code> als <code>$$</code> schreiben.</p>
</div>
</div>
<p>Prüfe den Token – egal auf welchem Weg er entstanden ist:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -s -H <span class="s2">&#34;Authorization: Bearer DEIN_LANGER_ZUFALLS_TOKEN&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  https://DEINE_DOMAIN/api/v3/core/users/me/</span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">{&#34;user&#34;:{&#34;username&#34;:&#34;akadmin&#34;, … &#34;is_superuser&#34;:true, …}}</span></span></code></pre></div>
</div>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-amber-400 bg-amber-50 dark:border-amber-700 dark:bg-amber-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">⚠️</span>Achtung
  </p>
  <div class="prose-kitchen text-sm">Der Bootstrap-Token ist ein <strong>vollwertiger Admin-Zugang ohne zweiten Faktor</strong>. Er gehört in die <code>.env</code>
mit <code>chmod 600</code>, nicht in ein Skript im Repository – und wenn du ihn nur zum Einrichten brauchst,
lösch ihn danach unter <strong>Verzeichnis → Tokens</strong>.</div>
</div>
<h3 id="2-weg-a-forward-auth-für-dienste-ohne-login">2. Weg A: Forward-Auth für Dienste ohne Login</h3>
<p>Der Proxy-Provider ist Authentiks Antwort auf „diese App hat gar keine Benutzerverwaltung&quot;. Er kommt
in mehreren Betriebsarten; wir nehmen <strong><code>forward_single</code></strong>: eine Anwendung, ein Provider.</p>
<p>Zuerst die IDs der beiden Standard-Flows (aus Teil 1 bekannt):</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nv">API</span><span class="o">=</span>https://DEINE_DOMAIN/api/v3
</span></span><span class="line"><span class="cl"><span class="nv">TOK</span><span class="o">=</span><span class="s2">&#34;Authorization: Bearer DEIN_LANGER_ZUFALLS_TOKEN&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">AUTHZ</span><span class="o">=</span><span class="k">$(</span>curl -s -H <span class="s2">&#34;</span><span class="nv">$TOK</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="nv">$API</span><span class="s2">/flows/instances/?slug=default-provider-authorization-implicit-consent&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  <span class="p">|</span> python3 -c <span class="s2">&#34;import json,sys; print(json.load(sys.stdin)[&#39;results&#39;][0][&#39;pk&#39;])&#34;</span><span class="k">)</span>
</span></span><span class="line"><span class="cl"><span class="nv">INVAL</span><span class="o">=</span><span class="k">$(</span>curl -s -H <span class="s2">&#34;</span><span class="nv">$TOK</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="nv">$API</span><span class="s2">/flows/instances/?slug=default-provider-invalidation-flow&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  <span class="p">|</span> python3 -c <span class="s2">&#34;import json,sys; print(json.load(sys.stdin)[&#39;results&#39;][0][&#39;pk&#39;])&#34;</span><span class="k">)</span></span></span></code></pre></div>
</div>
<p>Dann den Provider:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -s -X POST -H <span class="s2">&#34;</span><span class="nv">$TOK</span><span class="s2">&#34;</span> -H <span class="s2">&#34;content-type: application/json&#34;</span> <span class="s2">&#34;</span><span class="nv">$API</span><span class="s2">/providers/proxy/&#34;</span> -d <span class="s2">&#34;{
</span></span></span><span class="line"><span class="cl"><span class="s2">  \&#34;name\&#34;: \&#34;whoami-forwardauth\&#34;,
</span></span></span><span class="line"><span class="cl"><span class="s2">  \&#34;authorization_flow\&#34;: \&#34;</span><span class="nv">$AUTHZ</span><span class="s2">\&#34;,
</span></span></span><span class="line"><span class="cl"><span class="s2">  \&#34;invalidation_flow\&#34;: \&#34;</span><span class="nv">$INVAL</span><span class="s2">\&#34;,
</span></span></span><span class="line"><span class="cl"><span class="s2">  \&#34;mode\&#34;: \&#34;forward_single\&#34;,
</span></span></span><span class="line"><span class="cl"><span class="s2">  \&#34;external_host\&#34;: \&#34;https://DEINE_DOMAIN\&#34;
</span></span></span><span class="line"><span class="cl"><span class="s2">}&#34;</span></span></span></code></pre></div>
</div>
<p>Und die <strong>Anwendung</strong>, die ihn benutzt – sie erzeugt auch die Kachel in der Benutzer-Bibliothek:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -s -X POST -H <span class="s2">&#34;</span><span class="nv">$TOK</span><span class="s2">&#34;</span> -H <span class="s2">&#34;content-type: application/json&#34;</span> <span class="s2">&#34;</span><span class="nv">$API</span><span class="s2">/core/applications/&#34;</span> -d <span class="s1">&#39;{
</span></span></span><span class="line"><span class="cl"><span class="s1">  &#34;name&#34;: &#34;whoami (geschützt)&#34;, &#34;slug&#34;: &#34;whoami&#34;, &#34;provider&#34;: 1
</span></span></span><span class="line"><span class="cl"><span class="s1">}&#39;</span></span></span></code></pre></div>
</div>
<p>Die <code>1</code> ist die <code>pk</code> aus der Antwort des Provider-Aufrufs. Danach stehen beide in der Übersicht:</p>
<p><figure class="my-6"><img src="/tutorials/authentik-apps-anbinden/anwendungen_hu_caea8955d7f4d689.webp" srcset="/tutorials/authentik-apps-anbinden/anwendungen_hu_1a744ca96188ab75.webp 480w, /tutorials/authentik-apps-anbinden/anwendungen_hu_caea8955d7f4d689.webp 768w, /tutorials/authentik-apps-anbinden/anwendungen_hu_84772322bbc9e7d7.webp 1200w, /tutorials/authentik-apps-anbinden/anwendungen_hu_658009001869930.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/tutorials/authentik-apps-anbinden/anwendungen_hu_48ed49d0fa62dc94.webp"
    alt="Die Anwendungsliste in Authentik mit zwei Einträgen: Grafana über einen OAuth2/OpenID-Anbieter und whoami über einen Proxy Provider" title="Zwei Anwendungen, zwei Wege: OIDC-Anbieter und Proxy Provider"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Zwei Anwendungen, zwei Wege: OIDC-Anbieter und Proxy Provider</figcaption></figure></p>
<h3 id="3-den-outpost-scharf-schalten--und-die-häufigste-falle">3. Den Outpost scharf schalten – und die häufigste Falle</h3>
<p>Ein Proxy-Provider arbeitet nicht allein: Ein <strong>Outpost</strong> liefert ihn aus. Authentik bringt einen
eingebetteten Outpost mit, der im <code>server</code>-Container läuft – du musst ihm den Provider nur zuweisen:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nv">OUT</span><span class="o">=</span><span class="k">$(</span>curl -s -H <span class="s2">&#34;</span><span class="nv">$TOK</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="nv">$API</span><span class="s2">/outposts/instances/&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  <span class="p">|</span> python3 -c <span class="s2">&#34;import json,sys; print(json.load(sys.stdin)[&#39;results&#39;][0][&#39;pk&#39;])&#34;</span><span class="k">)</span>
</span></span><span class="line"><span class="cl">curl -s -X PATCH -H <span class="s2">&#34;</span><span class="nv">$TOK</span><span class="s2">&#34;</span> -H <span class="s2">&#34;content-type: application/json&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  <span class="s2">&#34;</span><span class="nv">$API</span><span class="s2">/outposts/instances/</span><span class="nv">$OUT</span><span class="s2">/&#34;</span> -d <span class="s1">&#39;{&#34;providers&#34;: [1]}&#39;</span></span></span></code></pre></div>
</div>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-paprika-400 bg-paprika-50 dark:border-paprika-700 dark:bg-paprika-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">🔥</span>Ohne authentik_host landen deine Nutzer auf localhost
  </p>
  <div class="prose-kitchen text-sm"><p>Der eingebettete Outpost kennt seine öffentliche Adresse nicht von selbst. Im Test war das Feld
<code>authentik_host</code> <strong>leer</strong>, und die Weiterleitung eines nicht angemeldeten Besuchers ging auf
<code>http://localhost/application/o/authorize/…</code> – für jeden Nutzer eine tote Seite. Einmal setzen, dann
stimmt sie:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -s -X PATCH -H <span class="s2">&#34;</span><span class="nv">$TOK</span><span class="s2">&#34;</span> -H <span class="s2">&#34;content-type: application/json&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  <span class="s2">&#34;</span><span class="nv">$API</span><span class="s2">/outposts/instances/</span><span class="nv">$OUT</span><span class="s2">/&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  -d <span class="s1">&#39;{&#34;config&#34;: {&#34;authentik_host&#34;: &#34;https://DEINE_DOMAIN/&#34;}}&#39;</span></span></span></code></pre></div>
</div>
<p>Danach zeigt die Weiterleitung auf <code>https://DEINE_DOMAIN/application/o/authorize/…</code> – vorher und
nachher nachgemessen. In der Oberfläche liegt dasselbe Feld unter
<strong>Anwendungen → Outposts → Bearbeiten</strong>.</p>
</div>
</div>
<h3 id="4-traefik-die-forward-auth-middleware">4. Traefik: die Forward-Auth-Middleware</h3>
<p>Jetzt fehlt die Verbindung. Traefik fragt vor jeder Anfrage beim Outpost nach – das macht die
Middleware <code>forwardauth</code>. Am geschützten Dienst:</p>
<div class="sk-code">
  <span class="sk-code-head">YAML</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="w">  </span><span class="nt">whoami</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">traefik/whoami:v1.12</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">networks</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">proxy]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">labels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.enable=true&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.routers.whoami.rule=Host(`DEINE_DOMAIN`) &amp;&amp; PathPrefix(`/whoami`)&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.routers.whoami.entrypoints=websecure&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.routers.whoami.tls.certresolver=le&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.routers.whoami.middlewares=ak-auth&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.services.whoami.loadbalancer.server.port=80&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.middlewares.ak-auth.forwardauth.address=http://server:9000/outpost.goauthentik.io/auth/traefik&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.middlewares.ak-auth.forwardauth.trustForwardHeader=true&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.middlewares.ak-auth.forwardauth.authResponseHeaders=X-authentik-username,X-authentik-groups,X-authentik-email,X-authentik-name,X-authentik-uid&#34;</span></span></span></code></pre></div>
</div>
<p>Die drei entscheidenden Zeilen:</p>
<ul>
<li><strong><code>address</code></strong> zeigt auf den Outpost – hier über den Container-Namen <code>server</code> im gemeinsamen
Docker-Netz, also <strong>ohne</strong> Umweg über das Internet. Der Pfad <code>/outpost.goauthentik.io/auth/traefik</code>
ist der Traefik-spezifische Prüf-Endpunkt.</li>
<li><strong><code>trustForwardHeader=true</code></strong> gibt die <code>X-Forwarded-*</code>-Header weiter, damit Authentik die
ursprünglich aufgerufene Adresse kennt und korrekt zurückleiten kann.</li>
<li><strong><code>authResponseHeaders</code></strong> ist der eigentliche Nutzen: Diese Header kopiert Traefik aus der Antwort
von Authentik in die Anfrage an deine Anwendung. Fehlt die Zeile, ist der Dienst geschützt, weiß
aber nicht, <strong>wer</strong> da ist.</li>
</ul>
<h3 id="5-prüfen-dass-es-wirklich-schützt">5. Prüfen, dass es wirklich schützt</h3>
<p>Erst ohne Anmeldung – dafür reicht <code>curl</code>:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -s -o /dev/null -w <span class="s2">&#34;%{http_code} → %{redirect_url}\n&#34;</span> https://DEINE_DOMAIN/whoami</span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">302 → https://DEINE_DOMAIN/application/o/authorize/?client_id=…&amp;redirect_uri=…%2Foutpost.goauthentik.io%2Fcallback…</span></span></code></pre></div>
</div>
<p>Ein <code>302</code> auf die Authentik-Anmeldung ist das gewünschte Ergebnis. Im Browser erscheint die
Anmeldeseite <strong>vor</strong> dem Dienst:</p>
<p><figure class="my-6"><img src="/tutorials/authentik-apps-anbinden/login-vor-whoami_hu_64711354cb18f01c.webp" srcset="/tutorials/authentik-apps-anbinden/login-vor-whoami_hu_537022e50054a3fb.webp 480w, /tutorials/authentik-apps-anbinden/login-vor-whoami_hu_64711354cb18f01c.webp 768w, /tutorials/authentik-apps-anbinden/login-vor-whoami_hu_3308fea323cb2426.webp 1200w, /tutorials/authentik-apps-anbinden/login-vor-whoami_hu_98e313df300b25bc.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/tutorials/authentik-apps-anbinden/login-vor-whoami_hu_2d8883223024e9c6.webp"
    alt="Die Authentik-Anmeldeseite, die beim Aufruf des geschützten Dienstes erscheint" title="Nicht angemeldet: Authentik fragt, bevor der Dienst antwortet"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Nicht angemeldet: Authentik fragt, bevor der Dienst antwortet</figcaption></figure></p>
<p>Nach der Anmeldung landest du wieder auf <code>/whoami</code> – und der Dienst zeigt, was bei ihm ankommt:</p>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">GET /whoami HTTP/1.1
</span></span><span class="line"><span class="cl">Host: DEINE_DOMAIN
</span></span><span class="line"><span class="cl">X-Authentik-Email: du@DEINE_DOMAIN
</span></span><span class="line"><span class="cl">X-Authentik-Groups: authentik Admins
</span></span><span class="line"><span class="cl">X-Authentik-Name: authentik Default Admin
</span></span><span class="line"><span class="cl">X-Authentik-Uid: d216e191a6f972c39931a005a7998a68…
</span></span><span class="line"><span class="cl">X-Authentik-Username: akadmin
</span></span><span class="line"><span class="cl">X-Forwarded-Proto: https</span></span></code></pre></div>
</div>
<p>Das ist der ganze Trick: Die Anwendung hat keine Zeile Anmeldecode – und weiß trotzdem, wer vor ihr
sitzt. Dienste, die Header-Authentifizierung unterstützen (Grafana mit <code>auth.proxy</code>, Paperless,
Forgejo mit Reverse-Proxy-Auth), können daraus direkt eine Sitzung machen.</p>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-amber-400 bg-amber-50 dark:border-amber-700 dark:bg-amber-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">⚠️</span>Header sind nur so sicher wie ihr Weg
  </p>
  <div class="prose-kitchen text-sm">Wenn eine Anwendung <code>X-Authentik-Username</code> vertraut, darf dieser Header <strong>niemals</strong> von außen kommen
können. Deshalb: Der Dienst ist ausschließlich über Traefik erreichbar (kein veröffentlichter Port),
und die Middleware hängt an <strong>jedem</strong> Router, der zu ihm führt. Ein zweiter Router ohne Middleware ist
eine offene Tür.</div>
</div>
<h3 id="6-die-grenzen-von-forward-auth-kennen">6. Die Grenzen von Forward-Auth kennen</h3>
<p>Bevor du alles hinter Forward-Auth hängst: Der Mechanismus setzt einen <strong>Browser</strong> voraus. Eine nicht
angemeldete Anfrage bekommt eine <code>302</code>-Weiterleitung auf ein Anmeldeformular – und genau daran
scheitert alles, was kein Browser ist:</p>
<ul>
<li><strong>Mobil-Apps</strong> (Nextcloud, Immich, Audiobookshelf) können mit der Weiterleitung nichts anfangen. Sie
sehen HTML statt JSON und melden „Server nicht erreichbar&quot;. Für solche Dienste ist <strong>OIDC</strong> der
einzige sinnvolle Weg – oder du nimmst die App-Pfade gezielt aus.</li>
<li><strong>API-Zugriffe und Skripte</strong> (<code>curl</code>, Backup-Jobs, Webhooks) laufen ins Leere. Wo ein Dienst per
Token angesprochen wird, muss dieser Pfad an der Middleware vorbei – oder du verlagerst den Schutz
auf eine <a href="/tutorials/traefik-middlewares-absichern/">IP-Allowlist</a>.</li>
<li><strong>WebDAV und Sync-Clients</strong> verhalten sich wie APIs, mit demselben Ergebnis.</li>
</ul>
<p>Für solche Ausnahmen legst du einen <strong>zweiten Router</strong> ohne die Auth-Middleware an, der nur den nötigen
Pfad bedient – und schützt ihn anders, etwa mit dem Token der Anwendung selbst. Beispiel für einen
API-Pfad:</p>
<div class="sk-code">
  <span class="sk-code-head">YAML</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.routers.app-api.rule=Host(`DEINE_DOMAIN`) &amp;&amp; PathPrefix(`/api`)&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.routers.app-api.entrypoints=websecure&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.routers.app-api.tls.certresolver=le&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.routers.app-api.middlewares=rate@file&#34;</span></span></span></code></pre></div>
</div>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-amber-400 bg-amber-50 dark:border-amber-700 dark:bg-amber-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">⚠️</span>Achtung
  </p>
  <div class="prose-kitchen text-sm">Jeder solche Ausnahme-Router ist eine bewusste Lücke in deiner Absicherung. Halte den Pfad so eng wie
möglich (<code>/api</code>, nicht <code>/</code>), und schreib dir auf, warum er existiert – in einem halben Jahr weiß das
sonst niemand mehr.</div>
</div>
<p>Zur Vollständigkeit die dritte Betriebsart des Proxy-Providers: <strong><code>forward_domain</code></strong>. Damit bedient
<strong>ein</strong> Outpost alle Dienste einer Domain (<code>*.DEINE_DOMAIN</code>), und Nutzer melden sich einmal für alle an.
Das ist der bequemere Aufbau ab etwa drei Diensten, verlangt aber, dass Authentik und alle Anwendungen
unter derselben übergeordneten Domain liegen, damit das Sitzungs-Cookie geteilt werden kann. Hier
getestet ist <code>forward_single</code> – die Umstellung betrifft nur den Modus im Provider und die
Cookie-Domain.</p>
<h3 id="7-weg-b-openid-connect-für-dienste-mit-eigenem-sso">7. Weg B: OpenID Connect für Dienste mit eigenem SSO</h3>
<p>Hat die Anwendung eine OIDC-Anmeldung, ist das der bessere Weg: Sie führt dann echte Benutzerkonten
inklusive Rollen, statt nur einen Namen im Header zu sehen.</p>
<p>Provider anlegen – wichtig ist die <strong>Redirect-URI</strong>, die genau der Rückadresse der Anwendung
entsprechen muss (Beispiel Grafana):</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -s -X POST -H <span class="s2">&#34;</span><span class="nv">$TOK</span><span class="s2">&#34;</span> -H <span class="s2">&#34;content-type: application/json&#34;</span> <span class="s2">&#34;</span><span class="nv">$API</span><span class="s2">/providers/oauth2/&#34;</span> -d <span class="s2">&#34;{
</span></span></span><span class="line"><span class="cl"><span class="s2">  \&#34;name\&#34;: \&#34;grafana-oidc\&#34;,
</span></span></span><span class="line"><span class="cl"><span class="s2">  \&#34;authorization_flow\&#34;: \&#34;</span><span class="nv">$AUTHZ</span><span class="s2">\&#34;,
</span></span></span><span class="line"><span class="cl"><span class="s2">  \&#34;invalidation_flow\&#34;: \&#34;</span><span class="nv">$INVAL</span><span class="s2">\&#34;,
</span></span></span><span class="line"><span class="cl"><span class="s2">  \&#34;client_type\&#34;: \&#34;confidential\&#34;,
</span></span></span><span class="line"><span class="cl"><span class="s2">  \&#34;redirect_uris\&#34;: [{\&#34;matching_mode\&#34;: \&#34;strict\&#34;, \&#34;url\&#34;: \&#34;https://GRAFANA_DOMAIN/login/generic_oauth\&#34;}]
</span></span></span><span class="line"><span class="cl"><span class="s2">}&#34;</span></span></span></code></pre></div>
</div>
<p>Die Antwort enthält <code>client_id</code> und <code>client_secret</code> – beide brauchst du in der Anwendung. Danach wie
oben eine <strong>Anwendung</strong> anlegen, die auf diesen Provider zeigt.</p>
<p>Ob alles sitzt, verrät das Discovery-Dokument. Authentik veröffentlicht es pro Anwendung unter deren
Slug:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -s https://DEINE_DOMAIN/application/o/grafana/.well-known/openid-configuration <span class="se">\
</span></span></span><span class="line"><span class="cl">  <span class="p">|</span> python3 -m json.tool <span class="p">|</span> head -12</span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">&#34;issuer&#34;: &#34;https://DEINE_DOMAIN/application/o/grafana/&#34;,
</span></span><span class="line"><span class="cl">&#34;authorization_endpoint&#34;: &#34;https://DEINE_DOMAIN/application/o/authorize/&#34;,
</span></span><span class="line"><span class="cl">&#34;token_endpoint&#34;: &#34;https://DEINE_DOMAIN/application/o/token/&#34;,
</span></span><span class="line"><span class="cl">&#34;userinfo_endpoint&#34;: &#34;https://DEINE_DOMAIN/application/o/userinfo/&#34;,</span></span></code></pre></div>
</div>
<p>Beachte: Der <strong>Issuer ist anwendungsspezifisch</strong> (er enthält den Slug), die übrigen Endpunkte sind für
alle Anwendungen gleich. Genau diese Adressen – oder die Discovery-URL selbst, wenn die App sie
unterstützt – trägst du in der Anwendung ein. Für Grafana etwa:</p>
<div class="sk-code">
  <span class="sk-code-head">INI</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="na">GF_AUTH_GENERIC_OAUTH_ENABLED</span><span class="o">=</span><span class="s">true</span>
</span></span><span class="line"><span class="cl"><span class="na">GF_AUTH_GENERIC_OAUTH_NAME</span><span class="o">=</span><span class="s">Authentik</span>
</span></span><span class="line"><span class="cl"><span class="na">GF_AUTH_GENERIC_OAUTH_CLIENT_ID</span><span class="o">=</span><span class="s">DEINE_CLIENT_ID</span>
</span></span><span class="line"><span class="cl"><span class="na">GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET</span><span class="o">=</span><span class="s">DEIN_CLIENT_SECRET</span>
</span></span><span class="line"><span class="cl"><span class="na">GF_AUTH_GENERIC_OAUTH_SCOPES</span><span class="o">=</span><span class="s">openid profile email</span>
</span></span><span class="line"><span class="cl"><span class="na">GF_AUTH_GENERIC_OAUTH_AUTH_URL</span><span class="o">=</span><span class="s">https://DEINE_DOMAIN/application/o/authorize/</span>
</span></span><span class="line"><span class="cl"><span class="na">GF_AUTH_GENERIC_OAUTH_TOKEN_URL</span><span class="o">=</span><span class="s">https://DEINE_DOMAIN/application/o/token/</span>
</span></span><span class="line"><span class="cl"><span class="na">GF_AUTH_GENERIC_OAUTH_API_URL</span><span class="o">=</span><span class="s">https://DEINE_DOMAIN/application/o/userinfo/</span></span></span></code></pre></div>
</div>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-herb-400 bg-herb-50 dark:border-herb-700 dark:bg-herb-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">🧑‍🍳</span>Gruppen als Rollen
  </p>
  <div class="prose-kitchen text-sm">Die meisten Anwendungen können aus einem Feld im Token Rollen ableiten – Grafana etwa über
<code>role_attribute_path</code>. Damit steuerst du Admin-Rechte zentral über Authentik-Gruppen, statt sie in
jeder App zu pflegen. Genau deshalb der Rat aus Teil 1: Berechtigungen an <strong>Gruppen</strong> hängen, nicht an
einzelne Benutzer.</div>
</div>
<h3 id="8-wer-darf-was-zugriff-einschränken">8. Wer darf was? Zugriff einschränken</h3>
<p>Solange an einer Anwendung <strong>keine</strong> Richtlinie hängt, darf sie jeder angemeldete Benutzer nutzen. Für
den Regelfall bindest du eine Gruppe an die Anwendung: <strong>Anwendungen → deine Anwendung →
Richtlinien / Gruppen / Benutzer-Bindungen</strong>, dort die Gruppe auswählen.</p>
<p>Erst danach verschwindet die Kachel in der Benutzer-Bibliothek für alle anderen – und der Zugriff ist
gesperrt. Prüfen lässt sich das am ehrlichsten mit einem zweiten Testkonto, das <strong>nicht</strong> in der
Gruppe ist: Es soll die Anwendung weder sehen noch aufrufen können.</p>
<h2 id="wenn-es-nicht-funktioniert">Wenn es nicht funktioniert</h2>
<div class="troubleshoot not-prose">
<p><strong>Das Skript bricht beim Ermitteln der Flows mit <code>IndexError: list index out of range</code> ab.</strong> Das
passiert nur direkt nach einer frischen Installation: <code>/-/health/ready/</code> meldet schon <code>200</code>, während
der <code>worker</code> die mitgelieferten Abläufe noch einspielt. Im Test gab es in diesem Moment erst einen
Flow, 20 bis 30 Sekunden später alle 15. Kurz warten und neu starten.</p>
<p><strong>Der Aufruf leitet auf <code>http://localhost/application/o/authorize/…</code> weiter.</strong> Der eingebettete
Outpost kennt seine öffentliche Adresse nicht: Das Feld <code>authentik_host</code> ist leer. Setz es auf
<code>https://DEINE_DOMAIN/</code> (Schritt 3) – vorher landet jeder Nutzer auf einer toten Seite.</p>
<p><strong>Traefik antwortet mit 500 oder es kommt gar keine Weiterleitung.</strong> Dann erreicht die Middleware den
Outpost nicht. Die <code>address</code> muss aus Traefiks Sicht auflösbar sein – also der <strong>Container-Name</strong> im
gemeinsamen Netz (<code>http://server:9000/outpost.goauthentik.io/auth/traefik</code>), nicht <code>localhost</code>. Prüfe
im Traefik-Ordner mit <code>docker compose exec traefik wget -qO- http://server:9000/-/health/live/</code>, ob
die Verbindung steht.</p>
<p><strong>Der Dienst ist geschützt, kennt aber den Benutzer nicht.</strong> Es fehlt <code>authResponseHeaders</code> an der
Middleware. Ohne diese Liste verwirft Traefik die Identitäts-Header aus Authentiks Antwort – die
Anmeldung funktioniert, die Anwendung sieht aber nichts.</p>
<p><strong>Nach der Anmeldung landet man in einer Weiterleitungsschleife.</strong> Meist passt <code>external_host</code> nicht
zur tatsächlich aufgerufenen Adresse (http statt https, mit oder ohne Subdomain). Er muss exakt die
URL sein, unter der Nutzer den Dienst aufrufen.</p>
<p><strong>Die OIDC-Anmeldung bricht mit „redirect_uri mismatch&quot; ab.</strong> Authentik prüft die Rückadresse strikt.
Trag im Provider genau die URI ein, die die Anwendung schickt – bei Grafana etwa
<code>https://GRAFANA_DOMAIN/login/generic_oauth</code>. Ein fehlender Schrägstrich reicht zum Scheitern.</p>
<p><strong>Alle angemeldeten Nutzer kommen überall rein.</strong> Ohne Bindung an eine Gruppe ist eine Anwendung für
jeden angemeldeten Benutzer offen. Häng eine Gruppen-Bindung an die Anwendung (Schritt 8) und prüfe
mit einem Konto, das nicht in der Gruppe ist.</p>

</div>
<p class="mt-3 text-sm text-slate-500 dark:text-slate-400">
  Dein Problem ist nicht dabei?
  <a href="/fehler/" class="font-medium text-paprika-700 hover:underline dark:text-paprika-400">Alle Fehlermeldungen durchsuchen →</a>
</p>

<h2 id="wartung--backups">Wartung &amp; Backups</h2>
<p><strong>Nach jedem Authentik-Update stichprobenartig prüfen.</strong> Ein Update kann Flows und Provider
verändern; die Anbindungen selbst sind robust, aber der schnelle Test lohnt: <code>curl</code> auf den
geschützten Pfad muss weiterhin <code>302</code> liefern, und das Discovery-Dokument muss deine Endpunkte
zeigen. Zwei Kommandos, die verhindern, dass eine Anmeldung wochenlang unbemerkt kaputt ist.</p>
<p><strong>Client-Secrets gehören in den Passwortmanager.</strong> Sie stehen in der Anwendung
(Umgebungsvariablen) <strong>und</strong> in Authentik. Wird eines neu erzeugt, muss die andere Seite mitgezogen
werden – notiere beim Anlegen, welche App welches Secret nutzt, sonst suchst du später in beiden
Systemen.</p>
<p><strong>Die Datenbank ist jetzt noch wichtiger.</strong> Provider, Anwendungen und Gruppen-Bindungen liegen alle in
PostgreSQL. Der <code>pg_dump</code> aus <a href="/tutorials/authentik-single-sign-on/">Teil 1</a> ist damit kein
Nice-to-have mehr: Ohne ihn baust du nach einem Ausfall jede Anbindung neu. Mit
<a href="/tutorials/backups-mit-restic/">Restic</a> täglich wegsichern.</p>
<p><strong>Den Notausgang offen halten.</strong> Jeder Dienst hinter Forward-Auth ist nur so erreichbar wie Authentik.
Behalte in wichtigen Anwendungen ein lokales Konto – und häng <strong>keine</strong> Werkzeuge hinter SSO, die du
zur Reparatur brauchst; das gilt besonders für <a href="/tutorials/dockge-compose-verwalten/">Dockge</a> und
ähnliche Verwaltungsoberflächen. Ein <a href="/tutorials/uptime-kuma-monitoring/">Monitoring</a> auf
<code>/-/health/ready/</code> merkt den Ausfall vor deinen Nutzern.</p>
<p><strong>Tokens aufräumen.</strong> Der Bootstrap-Token aus Schritt 1 ist ein Admin-Zugang ohne zweiten Faktor. Wenn
die Einrichtung steht, lösch ihn unter <strong>Verzeichnis → Tokens</strong> – und leg für spätere Automatisierung
einen eigenen Token mit begrenzten Rechten an.</p>
]]></content:encoded></item></channel></rss>