<?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>Oidc – Serverküche</title><link>https://serverkueche.de/en/tags/oidc/</link><description>Oidc – Neueste Beiträge von Serverküche</description><generator>Hugo</generator><language>en-US</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/en/tags/oidc/index.xml" rel="self" type="application/rss+xml"/><item><title>Authentik Part 2: Connecting Apps – Forward Auth &amp; OIDC</title><link>https://serverkueche.de/en/tutorials/authentik-connect-apps/</link><pubDate>Thu, 08 Oct 2026 00:00:00 +0000</pubDate><author>feedback@serverkueche.de (Serverküche)</author><guid>https://serverkueche.de/en/tutorials/authentik-connect-apps/</guid><description>Attach services to Authentik: forward auth with Traefik for apps without a login, and OpenID Connect for everything with its own SSO support – all verified.</description><content:encoded><![CDATA[<p>Authentik is running – now comes the part that justifies the effort: the services behind it. There are
exactly two routes, and the choice depends solely on whether the application understands SSO itself.</p>
<h2 id="what-are-we-building">What are we building?</h2>
<p>We attach two services, each the appropriate way:</p>
<ul>
<li><strong>Forward auth</strong> for an application <strong>without</strong> its own login. Traefik asks Authentik before every
request whether the visitor is signed in. If not, they land on the Authentik login page; if they are,
the application receives their identity <strong>as HTTP headers</strong>.</li>
<li><strong>OpenID Connect (OIDC)</strong> for applications <strong>with</strong> SSO support – Grafana, Nextcloud, Forgejo, Immich
and many others. There you enter client ID, secret and the endpoints, and the app does the rest.</li>
</ul>
<p>Everything below was carried out with <strong>Authentik 2026.8</strong> behind Traefik v3.7 on Debian 13: at the
end the test service shows the forwarded identity headers, and the OIDC discovery document returns real
endpoints.</p>
<h3 id="which-route-for-which-service">Which route for which service?</h3>
<p>The decision is almost always clear-cut:</p>
<table>
	<thead>
			<tr>
					<th>Service</th>
					<th>Supports OIDC?</th>
					<th>Route</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Nextcloud, Grafana, Forgejo, Immich, Paperless</td>
					<td>yes</td>
					<td><strong>OIDC</strong> – real accounts, roles from groups</td>
			</tr>
			<tr>
					<td>Uptime Kuma, Dockge, Netdata, a static dashboard</td>
					<td>no</td>
					<td><strong>forward auth</strong></td>
			</tr>
			<tr>
					<td>Metrics endpoints, <code>/actuator</code>, internal APIs</td>
					<td>irrelevant</td>
					<td>forward auth <strong>or</strong>, simpler: not public (VPN)</td>
			</tr>
			<tr>
					<td>Services with a mobile app (Immich, Nextcloud)</td>
					<td>yes</td>
					<td><strong>OIDC only</strong> – forward auth breaks the apps (see below)</td>
			</tr>
	</tbody>
</table>
<p>In short: <strong>OIDC where possible, forward auth where necessary.</strong> OIDC gives the application a real user
account including roles; forward auth only puts a door in front of it and passes a name through in a
header.</p>
<h2 id="prerequisites">Prerequisites</h2>
<ul>
<li>A <strong>running Authentik</strong> from <a href="/en/tutorials/authentik-single-sign-on/">part 1</a>, reachable at
<code>https://YOUR_DOMAIN</code></li>
<li>A <strong>Traefik v3</strong> with a <code>proxy</code> network (<a href="/en/tutorials/traefik-reverse-proxy/">guide</a>) – we work a
lot with middlewares here, the basics are in
<a href="/en/tutorials/traefik-middlewares-hardening/">hardening Traefik</a></li>
<li>A service to practise on. We use <code>traefik/whoami</code> because it echoes every header it receives – which
makes it immediately obvious whether the integration really works</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>One domain or two?
  </p>
  <div class="prose-kitchen text-sm">We show the setup <strong>on one domain</strong>: Authentik at <code>https://YOUR_DOMAIN/</code>, the protected service at
<code>https://YOUR_DOMAIN/whoami</code>. That is the frugal variant and exactly how it was tested. More common is
the split (<code>auth.YOUR_DOMAIN</code> for Authentik, <code>app.YOUR_DOMAIN</code> for the service) – the steps are
identical, only <code>external_host</code> and the Traefik rules then carry the other hostname.</div>
</div>
<h2 id="step-by-step">Step by step</h2>
<h3 id="1-access-for-automation-an-api-token">1. Access for automation: an API token</h3>
<p>Providers and applications can be created entirely through the API – handy, because it makes your work
reproducible instead of click-based. For that, Authentik needs a token.</p>
<p>Because you went through the setup wizard in part 1, <code>akadmin</code> already exists. So you create the
token <strong>in the interface</strong>: <strong>Directory → Tokens and App passwords → Create token</strong>. Use a
descriptive identifier (<code>api-setup</code>), set the intent to <strong>API</strong> and the user to <code>akadmin</code>. After
saving, copy the key via the copy icon – it is shown only once.</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>On a fresh installation it can be automatic
  </p>
  <div class="prose-kitchen text-sm"><p>If you set Authentik up from scratch, you can have the account and token created on the very
<strong>first</strong> start – no wizard needed. The variables belong on the <strong><code>worker</code></strong> service (not on
<code>server</code>) and are <strong>only read on the first startup</strong>; on an existing installation they have no
effect. Instead of the plaintext password, Authentik now recommends a hash. <code>hash_password</code> does <strong>not</strong>
take the password as an argument – <code>hash_password YOUR_PASSWORD</code> aborts with
<code>unrecognized arguments</code> – but via a prompt or standard input:</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>At the end you get a line starting with <code>pbkdf2_sha256$</code>. It goes into the <code>.env</code> as
<code>AK_BOOTSTRAP_PASSWORD_HASH</code>, the token as <code>AK_BOOTSTRAP_TOKEN</code> (for example from
<code>openssl rand -hex 32</code>). In <code>compose.yaml</code> the <code>worker</code> then looks like this:</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">you@YOUR_DOMAIN</span></span></span></code></pre></div>
</div>
<p>The hash contains <code>$</code> characters: wrap it in <strong>single</strong> quotes in the <code>.env</code>, and write each <code>$</code>
as <code>$$</code> when putting it directly into <code>compose.yaml</code>.</p>
</div>
</div>
<p>Verify the token – no matter which route produced it:</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 YOUR_LONG_RANDOM_TOKEN&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  https://YOUR_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>Warning
  </p>
  <div class="prose-kitchen text-sm">The bootstrap token is a <strong>full admin access without a second factor</strong>. It belongs in the <code>.env</code> with
<code>chmod 600</code>, not in a script in your repository – and if you only need it for setup, delete it
afterwards under <strong>Directory → Tokens</strong>.</div>
</div>
<h3 id="2-route-a-forward-auth-for-services-without-a-login">2. Route A: forward auth for services without a login</h3>
<p>The proxy provider is Authentik&rsquo;s answer to „this app has no user management at all&quot;. It comes in
several modes; we use <strong><code>forward_single</code></strong>: one application, one provider.</p>
<p>First the IDs of the two default flows (known from part 1):</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://YOUR_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 YOUR_LONG_RANDOM_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>Then the 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://YOUR_DOMAIN\&#34;
</span></span></span><span class="line"><span class="cl"><span class="s2">}&#34;</span></span></span></code></pre></div>
</div>
<p>And the <strong>application</strong> that uses it – it also creates the tile in the user library:</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 (protected)&#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>The <code>1</code> is the <code>pk</code> from the provider call&rsquo;s response. Both then appear in the overview:</p>
<p><figure class="my-6"><img src="/en/tutorials/authentik-connect-apps/anwendungen_hu_a190df419a72cb9a.webp" srcset="/en/tutorials/authentik-connect-apps/anwendungen_hu_9665cd701d1edb24.webp 480w, /en/tutorials/authentik-connect-apps/anwendungen_hu_a190df419a72cb9a.webp 768w, /en/tutorials/authentik-connect-apps/anwendungen_hu_ed38cdef55e354e4.webp 1200w, /en/tutorials/authentik-connect-apps/anwendungen_hu_662069adcbe5865e.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/en/tutorials/authentik-connect-apps/anwendungen_hu_3b3ccfcd2ace5d3e.webp"
    alt="The application list in Authentik with two entries: Grafana through an OAuth2/OpenID provider and whoami through a proxy provider" title="Two applications, two routes: OIDC provider and proxy provider"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Two applications, two routes: OIDC provider and proxy provider</figcaption></figure></p>
<h3 id="3-arm-the-outpost--and-the-most-common-trap">3. Arm the outpost – and the most common trap</h3>
<p>A proxy provider does not work alone: an <strong>outpost</strong> serves it. Authentik ships an embedded outpost that
runs inside the <code>server</code> container – you only have to assign the provider to it:</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>Without authentik_host your users land on localhost
  </p>
  <div class="prose-kitchen text-sm"><p>The embedded outpost does not know its public address by itself. In the test the <code>authentik_host</code> field
was <strong>empty</strong>, and an unauthenticated visitor was redirected to
<code>http://localhost/application/o/authorize/…</code> – a dead page for every user. Set it once and the redirect
is correct:</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://YOUR_DOMAIN/&#34;}}&#39;</span></span></span></code></pre></div>
</div>
<p>Afterwards the redirect points at <code>https://YOUR_DOMAIN/application/o/authorize/…</code> – measured before and
after. In the interface the same field lives under <strong>Applications → Outposts → Edit</strong>.</p>
</div>
</div>
<h3 id="4-traefik-the-forward-auth-middleware">4. Traefik: the forward auth middleware</h3>
<p>Now the connection is missing. Traefik asks the outpost before each request – that is what the
<code>forwardauth</code> middleware does. On the protected service:</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(`YOUR_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>The three decisive lines:</p>
<ul>
<li><strong><code>address</code></strong> points at the outpost – here through the service name <code>server</code> (Authentik reaches it under this alias on the Docker network) on the shared Docker
network, so <strong>without</strong> a detour over the internet. The path
<code>/outpost.goauthentik.io/auth/traefik</code> is the Traefik-specific check endpoint.</li>
<li><strong><code>trustForwardHeader=true</code></strong> passes the <code>X-Forwarded-*</code> headers along so Authentik knows the
originally requested address and can redirect back correctly.</li>
<li><strong><code>authResponseHeaders</code></strong> is the actual benefit: Traefik copies these headers from Authentik&rsquo;s response
into the request to your application. Without that line the service is protected but has no idea <strong>who</strong>
is there.</li>
</ul>
<h3 id="5-verify-that-it-really-protects">5. Verify that it really protects</h3>
<p>First without a login – <code>curl</code> is enough:</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://YOUR_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://YOUR_DOMAIN/application/o/authorize/?client_id=…&amp;redirect_uri=…%2Foutpost.goauthentik.io%2Fcallback…</span></span></code></pre></div>
</div>
<p>A <code>302</code> to the Authentik login is the desired result. In the browser the login page appears <strong>before</strong>
the service:</p>
<p><figure class="my-6"><img src="/en/tutorials/authentik-connect-apps/login-vor-whoami_hu_4e00c752777fda43.webp" srcset="/en/tutorials/authentik-connect-apps/login-vor-whoami_hu_308f3ef141065056.webp 480w, /en/tutorials/authentik-connect-apps/login-vor-whoami_hu_4e00c752777fda43.webp 768w, /en/tutorials/authentik-connect-apps/login-vor-whoami_hu_3ff6a74764fd1bf3.webp 1200w, /en/tutorials/authentik-connect-apps/login-vor-whoami_hu_b4def24d72c9718c.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/en/tutorials/authentik-connect-apps/login-vor-whoami_hu_ffdad5461985c78a.webp"
    alt="The Authentik login page shown when opening the protected service" title="Not signed in: Authentik asks before the service answers"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Not signed in: Authentik asks before the service answers</figcaption></figure></p>
<p>After logging in you are back at <code>/whoami</code> – and the service shows what it receives:</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: YOUR_DOMAIN
</span></span><span class="line"><span class="cl">X-Authentik-Email: you@YOUR_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>That is the whole trick: the application has not a single line of login code – and still knows who is
sitting in front of it. Services that support header authentication (Grafana with <code>auth.proxy</code>,
Paperless, Forgejo with reverse-proxy auth) can turn that straight into a session.</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>Headers are only as safe as their path
  </p>
  <div class="prose-kitchen text-sm">If an application trusts <code>X-Authentik-Username</code>, that header must <strong>never</strong> be able to come from
outside. Hence: the service is reachable exclusively through Traefik (no published port), and the
middleware is attached to <strong>every</strong> router leading to it. A second router without the middleware is an
open door.</div>
</div>
<h3 id="6-know-the-limits-of-forward-auth">6. Know the limits of forward auth</h3>
<p>Before you put everything behind forward auth: the mechanism assumes a <strong>browser</strong>. An unauthenticated
request gets a <code>302</code> redirect to a login form – and that is exactly what breaks anything that is not a
browser:</p>
<ul>
<li><strong>Mobile apps</strong> (Nextcloud, Immich, Audiobookshelf) cannot do anything with the redirect. They see
HTML instead of JSON and report „server unreachable&quot;. For such services <strong>OIDC</strong> is the only sensible
route – or you exempt the app&rsquo;s paths deliberately.</li>
<li><strong>API access and scripts</strong> (<code>curl</code>, backup jobs, webhooks) run into nothing. Wherever a service is
addressed by token, that path has to bypass the middleware – or you move the protection to an
<a href="/en/tutorials/traefik-middlewares-hardening/">IP allowlist</a>.</li>
<li><strong>WebDAV and sync clients</strong> behave like APIs, with the same outcome.</li>
</ul>
<p>For such exceptions you create a <strong>second router</strong> without the auth middleware that serves only the
necessary path – and protect it differently, for example with the application&rsquo;s own token. Example for
an API path:</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(`YOUR_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>Warning
  </p>
  <div class="prose-kitchen text-sm">Every such exception router is a deliberate hole in your protection. Keep the path as narrow as possible
(<code>/api</code>, not <code>/</code>), and write down why it exists – in six months nobody will remember otherwise.</div>
</div>
<p>For completeness, the third mode of the proxy provider: <strong><code>forward_domain</code></strong>. With it <strong>one</strong> outpost
serves all services of a domain (<code>*.YOUR_DOMAIN</code>), and users sign in once for all of them. That is the
more convenient setup from roughly three services onwards, but it requires Authentik and all
applications to live under the same parent domain so the session cookie can be shared. What is tested
here is <code>forward_single</code> – switching only affects the provider&rsquo;s mode and the cookie domain.</p>
<h3 id="7-route-b-openid-connect-for-services-with-their-own-sso">7. Route B: OpenID Connect for services with their own SSO</h3>
<p>If the application has an OIDC login, that is the better route: it then keeps real user accounts
including roles instead of just seeing a name in a header.</p>
<p>Create the provider – what matters is the <strong>redirect URI</strong>, which must match the application&rsquo;s callback
address exactly (Grafana as an example):</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>The response contains <code>client_id</code> and <code>client_secret</code> – you need both in the application. Then create an
<strong>application</strong> pointing at this provider, as above.</p>
<p>Whether everything lines up is revealed by the discovery document. Authentik publishes it per
application under its 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://YOUR_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://YOUR_DOMAIN/application/o/grafana/&#34;,
</span></span><span class="line"><span class="cl">&#34;authorization_endpoint&#34;: &#34;https://YOUR_DOMAIN/application/o/authorize/&#34;,
</span></span><span class="line"><span class="cl">&#34;token_endpoint&#34;: &#34;https://YOUR_DOMAIN/application/o/token/&#34;,
</span></span><span class="line"><span class="cl">&#34;userinfo_endpoint&#34;: &#34;https://YOUR_DOMAIN/application/o/userinfo/&#34;,</span></span></code></pre></div>
</div>
<p>Note: the <strong>issuer is application-specific</strong> (it contains the slug), while the remaining endpoints are the
same for all applications. Exactly these addresses – or the discovery URL itself if the app supports it –
go into the application. For Grafana, for instance:</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">YOUR_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">YOUR_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://YOUR_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://YOUR_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://YOUR_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>Groups as roles
  </p>
  <div class="prose-kitchen text-sm">Most applications can derive roles from a field in the token – Grafana for example via
<code>role_attribute_path</code>. That lets you steer admin rights centrally through Authentik groups instead of
maintaining them in every app. Which is exactly why part 1 advised attaching permissions to <strong>groups</strong>
rather than individual users.</div>
</div>
<h3 id="8-who-may-do-what-restricting-access">8. Who may do what? Restricting access</h3>
<p>As long as <strong>no</strong> policy is attached to an application, every signed-in user may use it. For the normal
case you bind a group to the application: <strong>Applications → your application → policy / group / user
bindings</strong>, then pick the group.</p>
<p>Only after that does the tile disappear from the user library for everyone else – and access is blocked.
The most honest way to verify is with a second test account that is <strong>not</strong> in the group: it should
neither see nor be able to open the application.</p>
<h2 id="when-things-go-wrong">When things go wrong</h2>
<div class="troubleshoot not-prose">
<p><strong>The script aborts while looking up the flows with <code>IndexError: list index out of range</code>.</strong> This
only happens right after a fresh installation: <code>/-/health/ready/</code> already reports <code>200</code> while the
<code>worker</code> is still importing the bundled flows. In our test there was only one flow at that moment,
all 15 were there 20 to 30 seconds later. Wait briefly and run it again.</p>
<p><strong>The request redirects to <code>http://localhost/application/o/authorize/…</code>.</strong> The embedded outpost does not
know its public address: the <code>authentik_host</code> field is empty. Set it to <code>https://YOUR_DOMAIN/</code> (step 3) –
before that every user lands on a dead page.</p>
<p><strong>Traefik answers with 500, or no redirect happens at all.</strong> Then the middleware cannot reach the
outpost. The <code>address</code> has to be resolvable from Traefik&rsquo;s point of view – so the <strong>container name</strong> on
the shared network (<code>http://server:9000/outpost.goauthentik.io/auth/traefik</code>), not <code>localhost</code>. From
the Traefik folder, check with <code>docker compose exec traefik wget -qO- http://server:9000/-/health/live/</code>
whether the connection works.</p>
<p><strong>The service is protected but does not know the user.</strong> <code>authResponseHeaders</code> is missing from the
middleware. Without that list Traefik discards the identity headers from Authentik&rsquo;s response – the login
works, but the application sees nothing.</p>
<p><strong>After signing in you end up in a redirect loop.</strong> Usually <code>external_host</code> does not match the address
actually being called (http instead of https, with or without a subdomain). It has to be exactly the URL
users open the service at.</p>
<p><strong>The OIDC login fails with „redirect_uri mismatch&quot;.</strong> Authentik checks the callback address strictly.
Enter exactly the URI the application sends – for Grafana e.g.
<code>https://GRAFANA_DOMAIN/login/generic_oauth</code>. A missing trailing slash is enough to break it.</p>
<p><strong>All signed-in users get in everywhere.</strong> Without a binding to a group an application is open to every
authenticated user. Attach a group binding to the application (step 8) and verify with an account that
is not in the group.</p>

</div>
<p class="mt-3 text-sm text-slate-500 dark:text-slate-400">
  Your problem is not listed?
  <a href="/en/errors/" class="font-medium text-paprika-700 hover:underline dark:text-paprika-400">Search all error messages →</a>
</p>

<h2 id="maintenance--backups">Maintenance &amp; backups</h2>
<p><strong>Spot-check after every Authentik update.</strong> An update can change flows and providers; the integrations
themselves are robust, but the quick test is worth it: <code>curl</code> on the protected path must still return
<code>302</code>, and the discovery document must show your endpoints. Two commands that keep a login from being
silently broken for weeks.</p>
<p><strong>Client secrets belong in the password manager.</strong> They live in the application (environment variables)
<strong>and</strong> in Authentik. If one is regenerated, the other side has to follow – note down which app uses
which secret when you create it, otherwise you will be searching in both systems later.</p>
<p><strong>The database matters even more now.</strong> Providers, applications and group bindings all live in
PostgreSQL. The <code>pg_dump</code> from <a href="/en/tutorials/authentik-single-sign-on/">part 1</a> is no longer a
nice-to-have: without it you rebuild every integration after an outage. Back it up daily with
<a href="/en/tutorials/restic-backups/">Restic</a>.</p>
<p><strong>Keep the emergency exit open.</strong> Every service behind forward auth is only as reachable as Authentik.
Keep a local account in important applications – and put <strong>no</strong> tools behind SSO that you need for
repairs; that applies especially to <a href="/en/tutorials/dockge-manage-compose/">Dockge</a> and similar management
interfaces. <a href="/en/tutorials/uptime-kuma-monitoring/">Monitoring</a> on <code>/-/health/ready/</code> notices the outage
before your users do.</p>
<p><strong>Tidy up tokens.</strong> The bootstrap token from step 1 is an admin access without a second factor. Once the
setup is in place, delete it under <strong>Directory → Tokens</strong> – and create a dedicated token with limited
rights for later automation.</p>
]]></content:encoded></item><item><title>Authentik: Single Sign-On for All Your Services (Part 1)</title><link>https://serverkueche.de/en/tutorials/authentik-single-sign-on/</link><pubDate>Tue, 06 Oct 2026 00:00:00 +0000</pubDate><author>feedback@serverkueche.de (Serverküche)</author><guid>https://serverkueche.de/en/tutorials/authentik-single-sign-on/</guid><description>Set up Authentik 2026.8 with Docker and Traefik: your own identity provider with Postgres instead of Redis – initial setup, flows and safe operation.</description><content:encoded><![CDATA[<p>With every new service the list of passwords grows: Nextcloud, Immich, Grafana, the wiki – each with
its own accounts. Authentik turns that around: <strong>one</strong> account, <strong>one</strong> login, centrally managed. This
first part builds the identity provider and explains its model. Part 2 then attaches the services.</p>
<h2 id="what-are-we-building">What are we building?</h2>
<p>At the end, <strong>Authentik 2026.8.3</strong> runs behind your Traefik at <code>https://YOUR_DOMAIN</code> – your own
identity provider with PostgreSQL on an internal network. You have an administrator account, you know
how flows, stages and providers fit together, and you can create users and groups.</p>
<p>Two things that many guides still show differently, and that will save you work:</p>
<ul>
<li><strong>Redis is no longer needed.</strong> Current Authentik versions keep cache and websocket channels in
PostgreSQL – visible in the startup log as the migrations <code>django_postgres_cache</code> and
<code>django_channels_postgres</code>. The stack now consists of <strong>PostgreSQL, server and worker</strong> only.</li>
<li><strong>Resource usage is modest.</strong> Measured while idle right after setup: server 312 MiB, worker 197 MiB,
PostgreSQL 42 MiB – around <strong>550 MiB RAM</strong> in total. The frequently quoted „at least 2 GB&quot; is a
recommendation with headroom, not a starting figure.</li>
</ul>
<h3 id="why-authentik--and-when-something-else">Why Authentik – and when something else?</h3>
<p>The three usual candidates differ mainly in weight:</p>
<ul>
<li><strong>Authentik</strong> ships a full web interface with user management, groups, an event log and protocols
from OIDC to LDAP. That is a lot of functionality – and the reason it needs a database and a worker.</li>
<li><strong>Authelia</strong> is considerably lighter, configured through YAML files, and has no user management in
the browser. If you want to protect two or three services and like keeping configuration in files,
it is the leaner choice.</li>
<li><strong>Keycloak</strong> is the heavyweight from the Java world, powerful in enterprise contexts, but with
matching resource and learning costs.</li>
</ul>
<p>For a self-operated stack with a handful of services and the wish to manage users in a browser,
Authentik is the best compromise. If you only have a handful of static users and no appetite for a
database, Authelia gets you there faster.</p>
<h2 id="prerequisites">Prerequisites</h2>
<ul>
<li>A server running Debian 13 with Docker (tested: netcup VPS, Docker 29.6.1, Compose v5.3.1)</li>
<li><strong>At least 2 GB RAM</strong> for the server overall – Authentik takes around 550 MiB, but database
maintenance and load spikes during many logins need headroom</li>
<li>A <strong>running Traefik</strong> with its <code>proxy</code> network (<a href="/en/tutorials/traefik-reverse-proxy/">guide</a>)</li>
<li>A dedicated subdomain, e.g. <code>auth.YOUR_DOMAIN</code>
(<a href="/en/tutorials/connect-domain-to-server/">connecting a domain</a>)</li>
<li>An understanding of <a href="/en/tutorials/understanding-docker-networks/">Docker networks</a> – we
deliberately separate the public and the internal network here</li>
</ul>
<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>Authentik becomes the master key
  </p>
  <div class="prose-kitchen text-sm">An identity provider concentrates risk: whoever takes it over takes over <strong>all</strong> attached services. So
this applies more strictly than elsewhere: a dedicated subdomain, TLS, a very strong administrator
password, a second factor – and timely updates. If you only run two services, SSO may be more risk than
gain; the benefit starts at roughly a handful of applications.</div>
</div>
<div class="not-prose my-6 overflow-hidden rounded-xl border border-paprika-200 bg-paprika-50 dark:border-paprika-800 dark:bg-paprika-900/20"
     data-track-content data-content-name="Affiliate-Box · /en/tutorials/authentik-single-sign-on/" data-content-piece="VPS 2000 G12.5">
  <div class="flex items-center justify-between border-b border-paprika-200 bg-paprika-100 px-4 py-1.5 text-xs font-semibold uppercase tracking-wide text-paprika-700 dark:border-paprika-800 dark:bg-paprika-900/40 dark:text-paprika-300">
    <span>🍳 Recommendation</span>
    <span title="Links marked with * are affiliate links.">Ad</span>
  </div>
  <div class="flex flex-col gap-4 p-4 sm:flex-row sm:items-center sm:justify-between">
    <div>
      <p class="text-lg font-bold text-slate-900 dark:text-white">VPS 2000 G12.5</p>
      <p class="mt-1 text-sm text-slate-600 dark:text-slate-300">8 vCores · 16 GB RAM · 256 GB SSD</p>
      <p class="mt-1 text-sm font-semibold text-paprika-700 dark:text-paprika-400">from €26.92/month</p>
      <p class="mt-2 text-sm text-slate-600 dark:text-slate-400">For Authentik plus the services behind it, the VPS 2000 is the more relaxed choice.</p>
    </div>
    <a href="https://www.netcup.com/en/server/vps/vps-2000-g12.5-iv-24m-eu?ref=44083#vps-2000-g12.5-iv-12m-eu" rel="sponsored noopener" target="_blank"
   data-track-event="Affiliate|netcup: Affiliate-Box|VPS 2000 G12.5 · {page}"
   class="inline-flex shrink-0 items-center justify-center rounded-lg bg-paprika-600 px-5 py-2.5 font-semibold text-white transition-colors hover:bg-paprika-700">
  Go to netcup →
</a>

  </div><div class="px-4 pb-4"><div class="not-prose my-3 rounded-lg border border-herb-500/40 bg-herb-50 px-3 py-2 text-sm text-slate-700 dark:bg-herb-900/20 dark:text-slate-200"
     data-voucher-api="vps-2000" data-voucher-msg-empty="No code is available for this plan right now – use the €5 code." data-voucher-msg-limit="No more codes today – use the €5 code or try again tomorrow." data-voucher-msg-error="The code couldn&#39;t be fetched right now – use the €5 code.">
  <p class="flex flex-wrap items-center gap-x-2 gap-y-1"><span>💶 <span class="font-semibold">1 month free</span>:</span>
    <button type="button" data-voucher-reveal data-copy-title="Click to copy"
            class="cursor-pointer rounded bg-white px-2 py-0.5 font-mono text-sm font-semibold text-herb-800 hover:ring-2 hover:ring-herb-500/40 dark:bg-slate-800 dark:text-herb-400">Show code</button>
    <button type="button" data-voucher-next hidden
            class="rounded border border-herb-500/40 px-2 py-0.5 text-xs text-herb-800 hover:bg-white dark:text-herb-400 dark:hover:bg-slate-800">another code</button>
  </p>
  <p class="mt-1 text-xs text-slate-500 dark:text-slate-400">
    Single use, valid for VPS 2000 G12.5. Redeem it in an empty cart – the code adds the server itself.
    <a href="https://www.netcup.com/en/checkout/cart?ref=44083" rel="sponsored noopener" target="_blank"
       data-track-event="Affiliate|netcup: Gutschein einlösen|VPS 2000 G12.5 · {page}"
       class="font-medium text-herb-800 underline underline-offset-2 hover:text-herb-900 dark:text-herb-400">Redeem in the cart →</a>
  </p><p class="mt-2 flex flex-wrap items-center gap-x-2 gap-y-1 border-t border-herb-500/30 pt-2">
    <span>💶 <span class="font-semibold">€5 voucher</span> for new netcup customers:</span>
    <button type="button" data-voucher-code data-track-voucher="36nc17844976032"
            title="Click to copy" class="cursor-pointer rounded bg-white px-2 py-0.5 font-mono text-sm font-semibold text-herb-800 hover:ring-2 hover:ring-herb-500/40 dark:bg-slate-800 dark:text-herb-400">36nc17844976032</button>
    <span class="text-xs text-slate-500 dark:text-slate-400">always valid · not for domains or VPS Lite</span>
  </p><p class="mt-1 text-xs"><a href="/en/netcup-voucher/" class="font-medium text-herb-800 underline underline-offset-2 hover:text-herb-900 dark:text-herb-400">All netcup vouchers →</a></p>
</div></div>
</div>

<h2 id="step-by-step">Step by step</h2>
<h3 id="1-create-directories-and-secrets">1. Create directories and secrets</h3>
<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">mkdir -p /opt/authentik/<span class="o">{</span>media,certs,custom-templates<span class="o">}</span> <span class="o">&amp;&amp;</span> <span class="nb">cd</span> /opt/authentik
</span></span><span class="line"><span class="cl">chmod <span class="m">755</span> media certs custom-templates</span></span></code></pre></div>
</div>
<p>That <code>chmod</code> is not cosmetic. If you have hardened your server with a stricter umask – such as
<code>UMASK 027</code> in <code>/etc/login.defs</code>, as the Lynis audit <span class="text-sm text-slate-500 dark:text-slate-400">(Tutorial expected in October)</span> recommends – <code>mkdir</code> creates <code>drwxr-x---</code>. The
Authentik container runs as user 1000 and cannot enter directories like that. With <code>chmod 755</code>
it works regardless of the umask:</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">without chmod (drwxr-x---)   /if/flow/initial-setup/  -&gt;  HTTP 500
</span></span><span class="line"><span class="cl">with    chmod (drwxr-xr-x)   /if/flow/initial-setup/  -&gt;  HTTP 200</span></span></code></pre></div>
</div>
<p>The nasty part: the health endpoint <code>/-/health/ready/</code> answers 200 in <strong>both</strong> cases. The
container counts as healthy while the interface serves nothing but errors.</p>
<p>Authentik needs two secrets: a database password and a <code>SECRET_KEY</code> that signs sessions and tokens.
Generate both randomly and put them into an <code>.env</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"><span class="o">{</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;PG_PASS=</span><span class="k">$(</span>openssl rand -base64 <span class="m">36</span> <span class="p">|</span> tr -d <span class="s1">&#39;\n=+/&#39;</span><span class="k">)</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;AUTHENTIK_SECRET_KEY=</span><span class="k">$(</span>openssl rand -base64 <span class="m">60</span> <span class="p">|</span> tr -d <span class="s1">&#39;\n&#39;</span><span class="k">)</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span> &gt; .env
</span></span><span class="line"><span class="cl">chmod <span class="m">600</span> .env</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>The SECRET_KEY must never change
  </p>
  <div class="prose-kitchen text-sm">All sessions and tokens are signed with <code>AUTHENTIK_SECRET_KEY</code>. If it is lost or replaced, every login
becomes invalid – in the worst case you cannot get into the interface any more. It belongs in your
backup and in your <a href="/en/tutorials/vaultwarden-password-manager/">password manager</a> <strong>before</strong> you carry
on.</div>
</div>
<h3 id="2-the-compose-file-with-separated-networks">2. The Compose file with separated networks</h3>
<p>The layout follows the pattern from
<a href="/en/tutorials/understanding-docker-networks/">understanding Docker networks</a>: only the <code>server</code> is
attached to the public <code>proxy</code> network, while the database and the <code>worker</code> live on an <strong>internal</strong>
network with no internet access. Create <code>/opt/authentik/compose.yaml</code> and replace <code>YOUR_DOMAIN</code>:</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="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">postgresql</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">postgres:16-alpine</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">healthcheck</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">test</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">&#34;CMD-SHELL&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}&#34;</span><span class="p">]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">interval</span><span class="p">:</span><span class="w"> </span><span class="l">30s</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">timeout</span><span class="p">:</span><span class="w"> </span><span class="l">5s</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">retries</span><span class="p">:</span><span class="w"> </span><span class="m">5</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">start_period</span><span class="p">:</span><span class="w"> </span><span class="l">20s</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">POSTGRES_DB</span><span class="p">:</span><span class="w"> </span><span class="l">authentik</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">POSTGRES_USER</span><span class="p">:</span><span class="w"> </span><span class="l">authentik</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">POSTGRES_PASSWORD</span><span class="p">:</span><span class="w"> </span><span class="l">${PG_PASS}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">database:/var/lib/postgresql/data</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">intern]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">server</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">ghcr.io/goauthentik/server:2026.8.3</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">command</span><span class="p">:</span><span class="w"> </span><span class="l">server</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">depends_on</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">postgresql</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">condition</span><span class="p">:</span><span class="w"> </span><span class="l">service_healthy</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_SECRET_KEY</span><span class="p">:</span><span class="w"> </span><span class="l">${AUTHENTIK_SECRET_KEY}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">AUTHENTIK_POSTGRESQL__HOST</span><span class="p">:</span><span class="w"> </span><span class="l">postgresql</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">AUTHENTIK_POSTGRESQL__NAME</span><span class="p">:</span><span class="w"> </span><span class="l">authentik</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">AUTHENTIK_POSTGRESQL__USER</span><span class="p">:</span><span class="w"> </span><span class="l">authentik</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">AUTHENTIK_POSTGRESQL__PASSWORD</span><span class="p">:</span><span class="w"> </span><span class="l">${PG_PASS}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">AUTHENTIK_ERROR_REPORTING__ENABLED</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;false&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">AUTHENTIK_LISTEN__TRUSTED_PROXY_CIDRS</span><span class="p">:</span><span class="w"> </span><span class="m">172.16.0.0</span><span class="l">/12</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">./media:/media</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">./custom-templates:/templates</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">shm_size</span><span class="p">:</span><span class="w"> </span><span class="l">512mb</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, intern]</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.authentik.rule=Host(`YOUR_DOMAIN`)&#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.authentik.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.authentik.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.services.authentik.loadbalancer.server.port=9000&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><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">image</span><span class="p">:</span><span class="w"> </span><span class="l">ghcr.io/goauthentik/server:2026.8.3</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">command</span><span class="p">:</span><span class="w"> </span><span class="l">worker</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">user</span><span class="p">:</span><span class="w"> </span><span class="l">root</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">depends_on</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">postgresql</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">condition</span><span class="p">:</span><span class="w"> </span><span class="l">service_healthy</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_SECRET_KEY</span><span class="p">:</span><span class="w"> </span><span class="l">${AUTHENTIK_SECRET_KEY}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">AUTHENTIK_POSTGRESQL__HOST</span><span class="p">:</span><span class="w"> </span><span class="l">postgresql</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">AUTHENTIK_POSTGRESQL__NAME</span><span class="p">:</span><span class="w"> </span><span class="l">authentik</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">AUTHENTIK_POSTGRESQL__USER</span><span class="p">:</span><span class="w"> </span><span class="l">authentik</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">AUTHENTIK_POSTGRESQL__PASSWORD</span><span class="p">:</span><span class="w"> </span><span class="l">${PG_PASS}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">AUTHENTIK_ERROR_REPORTING__ENABLED</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;false&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/var/run/docker.sock:/var/run/docker.sock</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">./media:/media</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">./certs:/certs</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">./custom-templates:/templates</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">shm_size</span><span class="p">:</span><span class="w"> </span><span class="l">512mb</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">intern]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">database</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">networks</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">proxy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">external</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">intern</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">internal</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span></span></span></code></pre></div>
</div>
<p>Four points that matter here:</p>
<ul>
<li>
<p><strong><code>loadbalancer.server.port=9000</code></strong> – internally Authentik listens on 9000 (HTTP). Its own HTTPS port
9443 stays unused: TLS is Traefik&rsquo;s job.</p>
</li>
<li>
<p><strong><code>shm_size: 512mb</code></strong> comes from the official reference and prevents memory errors on larger database
queries.</p>
</li>
<li>
<p><strong><code>user: root</code> plus the Docker socket on the worker</strong> are needed so Authentik can later manage its own
&ldquo;outposts&rdquo; as containers. The socket is deliberately <strong>not</strong> mounted <code>:ro</code> here: Authentik has to
create, kill and remove containers, and that only works with write access. If you don&rsquo;t need
outpost management, drop the socket.</p>
<p>Write access to the Docker socket is effectively root on the host, though. Authentik therefore
recommends putting a <strong>Docker socket proxy</strong> in front of it, allowing only the API calls actually
needed (create, read, kill and remove containers, query the version). On a server that does more
than run Authentik, that is worth the effort.</p>
</li>
<li>
<p><strong><code>AUTHENTIK_ERROR_REPORTING__ENABLED: &quot;false&quot;</code></strong> turns off error reporting to the project. It is
<strong>on</strong> by default; for privacy-friendly operation it belongs off.</p>
</li>
<li>
<p><strong><code>AUTHENTIK_LISTEN__TRUSTED_PROXY_CIDRS</code></strong> has been mandatory behind a reverse proxy since
<strong>2026.8</strong>. Authentik now only honours <code>X-Forwarded-Proto</code>, <code>X-Forwarded-Host</code> and
<code>X-Forwarded-For</code> when the connection comes from a network listed here – this prevents forged
proxy headers. Enter the Docker network Traefik connects from (Docker allocates from
<code>172.16.0.0/12</code> by default). Without it, Authentik builds redirects to <code>http://</code> instead of
<code>https://</code> and the interface hangs on an endless loading spinner. Find your Traefik&rsquo;s exact
address with <code>docker network inspect proxy</code>.</p>
</li>
</ul>
<h3 id="3-start-it-and-wait-for-the-migrations">3. Start it and wait for the migrations</h3>
<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="nb">cd</span> /opt/authentik <span class="o">&amp;&amp;</span> docker compose up -d</span></span></code></pre></div>
</div>
<p>On the <strong>first</strong> start Authentik creates its database schema – over 70 migrations, and that takes a
while. The health endpoint makes the progress observable:</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}\n&#34;</span> https://YOUR_DOMAIN/-/health/ready/</span></span></code></pre></div>
</div>
<p>While the migrations run it answers <code>404</code>; once Authentik is ready, <code>200</code>. On the VPS 1000 that most recently
took just under <strong>two minutes</strong>. The setup flow follows a few seconds later – the <code>worker</code> only
imports the bundled flows afterwards; in the test it was reachable 15 seconds after the first
<code>200</code>. In parallel, the container states:</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 ps --format <span class="s2">&#34;{{.Name}}\t{{.Status}}&#34;</span></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">authentik-postgresql-1   Up 27 seconds (healthy)
</span></span><span class="line"><span class="cl">authentik-server-1       Up 21 seconds (health: starting)
</span></span><span class="line"><span class="cl">authentik-worker-1       Up 21 seconds (healthy)</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>Tip
  </p>
  <div class="prose-kitchen text-sm"><code>docker logs -f authentik-server-1</code> shows the migrations one by one and then the web server starting.
Only after that is it worth looking at the browser – before that you only get error pages.</div>
</div>
<h3 id="4-create-the-administrator-account">4. Create the administrator account</h3>
<p>Now the step where many people stumble: open the <strong>root address</strong>, i.e. <code>https://YOUR_DOMAIN/</code>. From
there Authentik redirects you into the setup flow.</p>
<p>If you call the flow directly (<code>/if/flow/initial-setup/</code>), Authentik refuses with „request denied –
Access the authentik setup by navigating to https://…/&quot;. That is not a mistake in your configuration
but intentional.</p>
<p><figure class="my-6"><img src="/en/tutorials/authentik-single-sign-on/setup_hu_cf3c54ff84d6372b.webp" srcset="/en/tutorials/authentik-single-sign-on/setup_hu_ef965dde9c8d6b7a.webp 480w, /en/tutorials/authentik-single-sign-on/setup_hu_cf3c54ff84d6372b.webp 768w, /en/tutorials/authentik-single-sign-on/setup_hu_a82d1cd66d864546.webp 1200w, /en/tutorials/authentik-single-sign-on/setup_hu_febdebec62605359.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/en/tutorials/authentik-single-sign-on/setup_hu_c506cc8ef77a9021.webp"
    alt="The Authentik setup dialog with fields for the email address and password of the first administrator account" title="Initial setup: email and password for the administrator account"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Initial setup: email and password for the administrator account</figcaption></figure></p>
<p>Enter the email address and a <strong>very</strong> long password. Two things worth knowing:</p>
<ul>
<li>The username of the account created here is <strong><code>akadmin</code></strong> – not your email address.</li>
<li>The interface appears in your browser&rsquo;s language.</li>
</ul>
<p>After submitting you land in the <strong>user interface</strong> (<code>/if/user/</code>) – the view all your users will see
later: a tile library of the applications they are allowed to use.</p>
<h3 id="5-the-administrator-interface">5. The administrator interface</h3>
<p>Administration lives at <code>https://YOUR_DOMAIN/if/admin/</code> – or via the button in the top right:</p>
<p><figure class="my-6"><img src="/en/tutorials/authentik-single-sign-on/dashboard_hu_909686b69dd89f8e.webp" srcset="/en/tutorials/authentik-single-sign-on/dashboard_hu_4793c27e7aa5e960.webp 480w, /en/tutorials/authentik-single-sign-on/dashboard_hu_909686b69dd89f8e.webp 768w, /en/tutorials/authentik-single-sign-on/dashboard_hu_d8aad4621963b704.webp 1200w, /en/tutorials/authentik-single-sign-on/dashboard_hu_5e3fc4298cfabc8.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/en/tutorials/authentik-single-sign-on/dashboard_hu_2fe50f72eaed77e4.webp"
    alt="The Authentik administration overview with system status OK, the installed version, one worker, outpost status and the latest events" title="Administration overview: system state, version, workers and latest events"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Administration overview: system state, version, workers and latest events</figcaption></figure></p>
<p>Four tiles answer the typical operational questions:</p>
<ul>
<li><strong>System status</strong> – „everything is operational&quot; means: database reachable, no open warnings.</li>
<li><strong>Version</strong> – the running version. If it says the latest version is unknown, Authentik could not
perform the version check; with restricted outbound traffic that is normal.</li>
<li><strong>Workers</strong> – must be <strong>at least 1</strong>. If it says <code>0</code>, the <code>worker</code> is not running, and then a lot
silently does not happen: no emails, no certificate rotation, no scheduled tasks.</li>
<li><strong>Latest events</strong> – every login and change with user, time and client IP. This is your audit log and
the first place to look for „who changed that?&quot;.</li>
</ul>
<h3 id="6-understanding-the-model-flows-stages-providers-applications">6. Understanding the model: flows, stages, providers, applications</h3>
<p>Authentik is not a login form but a construction kit. Once you know these terms you find your way around
the interface immediately – and you understand the error messages.</p>
<p><figure class="my-6"><img src="/en/tutorials/authentik-single-sign-on/flows_hu_2918783a7ebf5b3b.webp" srcset="/en/tutorials/authentik-single-sign-on/flows_hu_c9c5e080789d69c5.webp 480w, /en/tutorials/authentik-single-sign-on/flows_hu_2918783a7ebf5b3b.webp 768w, /en/tutorials/authentik-single-sign-on/flows_hu_b89bd1b7d9863.webp 1200w, /en/tutorials/authentik-single-sign-on/flows_hu_b8b3eb9d7f5139ff.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/en/tutorials/authentik-single-sign-on/flows_hu_b655861c7914f42e.webp"
    alt="The flow overview in Authentik with the default flows for authentication, authorization, invalidation, enrollment and stage configuration" title="The bundled flows – each is a chain of stages"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">The bundled flows – each is a chain of stages</figcaption></figure></p>
<ul>
<li><strong>Stage</strong> – a single step: ask for a password, check a TOTP code, obtain consent, write a user. A
stage can do nothing on its own.</li>
<li><strong>Flow</strong> – a <strong>chain of stages</strong> for one purpose. Authentik ships ready-made ones, visible in the
list: <code>default-authentication-flow</code> (login, 4 stages), <code>default-invalidation-flow</code> (logout),
<code>default-source-enrollment</code> (enrollment through an external source), plus flows for setting up TOTP
and static recovery codes.</li>
<li><strong>Policy</strong> – a condition deciding whether a stage applies or access is allowed: „only members of this
group&quot;, „only from this network&quot;, „only with a confirmed email&quot;.</li>
<li><strong>Provider</strong> – the technical interface to <strong>one</strong> service: OAuth2/OpenID Connect, SAML, LDAP or the
<strong>proxy provider</strong> for applications without their own login.</li>
<li><strong>Application</strong> – the bracket around it: it connects a provider with permissions and creates the tile
in the user library.</li>
<li><strong>Outpost</strong> – an outsourced component Authentik operates itself (for the proxy provider, for
instance). That is exactly why the worker needs the Docker socket.</li>
</ul>
<p>For everyday use that means: you do <strong>not</strong> touch the default flows. You create one provider plus one
application per service – and that is precisely the content of
<a href="/en/tutorials/authentik-connect-apps/">part 2</a>.</p>
<p>What actually happens during a login can be read off the default flows. When a user opens a protected
service, the <code>default-authentication-flow</code> runs with its four stages:</p>
<ol>
<li><strong>Identification</strong> – who are you? (username or email, depending on the setting)</li>
<li><strong>Password</strong> – checked against the stored password</li>
<li><strong>Authenticator validation</strong> – if there is a second factor, it is demanded here</li>
<li><strong>User login</strong> – the session is created</li>
</ol>
<p>After that the <strong>authorization</strong> flow takes over: it decides whether this user may use this application
(<code>default-provider-authorization-explicit-consent</code> asks for consent once,
<code>…-implicit-consent</code> skips the question). On logout the <strong>invalidation</strong> flow applies.</p>
<p>If something goes wrong later, that order is your search aid: if the user gets stuck at the password, it
is a problem in the authentication flow. If they get through but don&rsquo;t see the application, it is
authorization – so permissions or policies.</p>
<h3 id="7-create-users-and-groups">7. Create users and groups</h3>
<p>Under <strong>Directory → Users</strong> you create accounts. The important distinction:</p>
<ul>
<li><strong>Users</strong> have a username, name, email and optionally attributes.</li>
<li><strong>Groups</strong> bundle users – and you attach the permissions for applications <strong>to groups</strong>. So create
groups right away (<code>family</code>, <code>admins</code>) instead of granting rights per user. Otherwise you maintain the
same list ten times over for ten services.</li>
</ul>
<p>You then use the <code>akadmin</code> account for administration only; for everyday use you create yourself a
normal account. That is the same separation as „don&rsquo;t work as root&quot;.</p>
<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>A second factor for the administrator
  </p>
  <div class="prose-kitchen text-sm">Authentik ships TOTP – the flow <code>default-authenticator-totp-setup</code> is in the list from step 6. Register a
second factor for <code>akadmin</code> through the user interface (<strong>Settings → MFA devices</strong>) before you attach any
services. An identity provider with plain password protection only does half its job. How TOTP works in
general is covered in <a href="/en/tutorials/ssh-2fa-totp/">SSH with 2FA</a>.</div>
</div>
<h3 id="8-harden-authentik-itself">8. Harden Authentik itself</h3>
<p>From now on the identity provider is the most rewarding target on your server. Four measures that cost
little together:</p>
<ul>
<li><strong>Put a rate limit and security headers in front of it.</strong> Authentik has its own reputation checks, but
the first brake belongs on the reverse proxy. Attach the middlewares from
<a href="/en/tutorials/traefik-middlewares-hardening/">hardening Traefik</a> to the router – a rate limit makes
password guessing unattractive before the request even reaches Authentik.</li>
<li><strong>Restrict the administration interface.</strong> You rarely need <code>/if/admin/</code>, and rarely while travelling.
An IP allowlist or access only through <a href="/en/tutorials/wireguard-vpn-setup/">WireGuard</a> is appropriate
here – without restricting the normal login path for your users.</li>
<li><strong>Make a second factor mandatory</strong>, at least for administrators. In Authentik that hangs off the
authenticator validation stage of the login flow.</li>
<li><strong>Leave error reporting off</strong> – already set in the Compose file above. By default Authentik sends
error reports to the project; for a service that sees every credential in your environment, that
belongs switched off.</li>
</ul>
<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>No SSO in front of your emergency exit
  </p>
  <div class="prose-kitchen text-sm">Tempting but dangerous: putting Authentik <strong>in front of</strong> the services you need in an emergency to repair
Authentik. A Dockge or Portainer behind forward auth is a lovely idea – until Authentik itself is broken
and you cannot log in anywhere. For tools like that, SSH or a local account stays your emergency exit.</div>
</div>
<h2 id="when-things-go-wrong">When things go wrong</h2>
<div class="troubleshoot not-prose">
<p><strong>The interface returns <code>Internal Server Error</code> while the container counts as healthy.</strong> Then
Authentik cannot read its template directories. Typical for servers with <code>UMASK 027</code>: <code>mkdir</code>
creates <code>drwxr-x---</code> there – and the container runs as user 1000, which cannot enter them. The log says <code>PermissionError: [Errno 13] Permission denied: '/templates/if/error.html'</code>. Fix it with <code>chmod 755 media certs custom-templates</code> in the
stack directory and restart.</p>
<p><strong>The page answers with 404 for minutes even though the containers are running.</strong> On the first start over
70 database migrations run; while they do, <code>/-/health/ready/</code> returns <code>404</code> as well. On the VPS 1000 that was
over after just under two minutes, the setup flow 15 seconds later. <code>docker logs -f authentik-server-1</code> shows the progress.</p>
<p><strong>Calling the setup flow gives „request denied&quot;.</strong> The setup flow cannot be opened directly via
<code>/if/flow/initial-setup/</code>. Go to the root address <code>https://YOUR_DOMAIN/</code>, from where Authentik redirects
correctly.</p>
<p><strong>The „workers&quot; tile shows 0.</strong> The <code>worker</code> container is not running or cannot reach the database.
Without it, emails, scheduled tasks and outpost management are left undone – with no error message in the
interface. Check <code>docker compose ps</code> and the worker&rsquo;s logs.</p>
<p><strong>Traefik answers with 404.</strong> Usually
<code>traefik.http.services.authentik.loadbalancer.server.port=9000</code> is missing. The container listens on
9000; port 9443 is Authentik&rsquo;s own HTTPS, which we deliberately do not use.</p>
<p><strong>After a rebuild all sessions are invalid or the login fails.</strong> Then the <code>AUTHENTIK_SECRET_KEY</code> changed –
for example because the <code>.env</code> was regenerated. The key has to stay the same for the lifetime of the
installation; restore it from your backup.</p>
<p><strong>The username doesn&rsquo;t match.</strong> The account created during setup is called <code>akadmin</code>, not your email
address. If you want to log in with the address, allow that in the identification stage of the login
flow.</p>

</div>
<p class="mt-3 text-sm text-slate-500 dark:text-slate-400">
  Your problem is not listed?
  <a href="/en/errors/" class="font-medium text-paprika-700 hover:underline dark:text-paprika-400">Search all error messages →</a>
</p>

<h2 id="maintenance--backups">Maintenance &amp; backups</h2>
<p><strong>Updates are mandatory here.</strong> The project releases frequently – the 2026.8 line saw several patch
releases within a few weeks. Because Authentik is the master key to every attached service, you want
security fixes promptly:</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="nb">cd</span> /opt/authentik <span class="o">&amp;&amp;</span> docker compose pull <span class="o">&amp;&amp;</span> docker compose up -d</span></span></code></pre></div>
</div>
<p><strong>Read first, then update.</strong> Authentik uses date-based versions (<code>2026.8.3</code>). Between <strong>minor</strong> versions
(2026.2 → 2026.5) there are regularly changes that need manual work; the release notes name them
explicitly. Never jump across several minor versions at once, and take a backup first: the database is
migrated on start, and that is not easily reversible.</p>
<p><strong>What belongs in the backup:</strong></p>
<ul>
<li>
<p><strong><code>.env</code></strong> – contains the <code>SECRET_KEY</code> and the database password. Without the key the rest is
worthless.</p>
</li>
<li>
<p><strong>The PostgreSQL database</strong> – it holds users, groups, flows, providers and applications. Back it up
<strong>not</strong> as a file copy of the volume but as a dump:</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 <span class="nb">exec</span> -T postgresql pg_dump -U authentik authentik <span class="se">\
</span></span></span><span class="line"><span class="cl">  <span class="p">|</span> gzip &gt; /opt/backup/authentik-<span class="k">$(</span>date +%F<span class="k">)</span>.sql.gz</span></span></code></pre></div>
</div>
</li>
<li>
<p><strong><code>media/</code></strong> – uploaded logos and icons. Small, but annoying to reconstruct.</p>
</li>
</ul>
<p><a href="/en/tutorials/restic-backups/">Restic</a> then picks up the dump. Why not a volume copy: you don&rsquo;t copy a
running database file by file – that produces inconsistent backups which only reveal themselves when you
try to restore.</p>
<p><strong>Plan for the outage.</strong> If Authentik is down, nobody gets into the attached services – that is the flip
side of single sign-on. Two precautions cost little: keep a <strong>local emergency account</strong> in every attached
service that works without SSO, and add Authentik to your
<a href="/en/tutorials/uptime-kuma-monitoring/">monitoring</a> – ideally with a check on <code>/-/health/ready/</code>, which
tests exactly what matters.</p>
<p><strong>Tidy up users and groups.</strong> An identity provider grows with you: guests, old accounts, test users. Take
a look at the user list twice a year and <strong>deactivate</strong> what is no longer needed – deactivating beats
deleting, because otherwise your event logs develop gaps.</p>
]]></content:encoded></item></channel></rss>