{"id":1953,"date":"2026-03-07T14:28:38","date_gmt":"2026-03-07T12:28:38","guid":{"rendered":"https:\/\/cln.io\/blog\/?p=1953"},"modified":"2026-03-10T20:19:51","modified_gmt":"2026-03-10T18:19:51","slug":"traefik-authentik-forward-auth-protect-any-app","status":"publish","type":"post","link":"https:\/\/cln.io\/blog\/traefik-authentik-forward-auth-protect-any-app\/","title":{"rendered":"How to Protect Any App with Authentik and Traefik Forward Auth"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">You have a web app. You want to put it behind authentication without touching the app&#8217;s code. <a href=\"https:\/\/doc.traefik.io\/traefik\/\">Traefik<\/a> and <a href=\"https:\/\/goauthentik.io\/\">Authentik<\/a> make this possible with <strong>forward authentication<\/strong> &#8211; Traefik asks Authentik &#8220;is this user allowed?&#8221; before forwarding the request to your app.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This guide walks through the full setup with a simple <code>whoami<\/code> container as the demo app. The examples use Docker Swarm, but a Docker Compose equivalent is included for each step &#8211; the only difference is <code>providers.swarm<\/code> vs <code>providers.docker<\/code> and <code>@swarm<\/code> vs <code>@docker<\/code>. By the end, unauthenticated requests get redirected to Authentik&#8217;s login page, and authenticated requests arrive at your app with identity headers like <code>X-Authentik-Username<\/code> and <code>X-Authentik-Groups<\/code> injected automatically.<\/p>\n\n\n\n<nav aria-label=\"Table of Contents\" class=\"wp-block-table-of-contents\"><ol><li><a class=\"wp-block-table-of-contents__entry\" href=\"https:\/\/cln.io\/blog\/traefik-authentik-forward-auth-protect-any-app\/#the-end-result\">The End Result<\/a><\/li><li><a class=\"wp-block-table-of-contents__entry\" href=\"https:\/\/cln.io\/blog\/traefik-authentik-forward-auth-protect-any-app\/#how-forward-auth-works\">How Forward Auth Works<\/a><\/li><li><a class=\"wp-block-table-of-contents__entry\" href=\"https:\/\/cln.io\/blog\/traefik-authentik-forward-auth-protect-any-app\/#what-you-need\">What You Need<\/a><\/li><li><a class=\"wp-block-table-of-contents__entry\" href=\"https:\/\/cln.io\/blog\/traefik-authentik-forward-auth-protect-any-app\/#part-1-traefik-setup\">Part 1: Traefik Setup<\/a><\/li><li><a class=\"wp-block-table-of-contents__entry\" href=\"https:\/\/cln.io\/blog\/traefik-authentik-forward-auth-protect-any-app\/#part-2-authentik-configuration\">Part 2: Authentik Configuration<\/a><ol><li><a class=\"wp-block-table-of-contents__entry\" href=\"https:\/\/cln.io\/blog\/traefik-authentik-forward-auth-protect-any-app\/#create-a-proxy-provider-in-authentik\">Create a Proxy Provider in Authentik<\/a><\/li><li><a class=\"wp-block-table-of-contents__entry\" href=\"https:\/\/cln.io\/blog\/traefik-authentik-forward-auth-protect-any-app\/#configure-the-embedded-outpost\">Configure the Embedded Outpost<\/a><\/li><\/ol><\/li><li><a class=\"wp-block-table-of-contents__entry\" href=\"https:\/\/cln.io\/blog\/traefik-authentik-forward-auth-protect-any-app\/#part-3-your-app-the-easy-part\">Part 3: Your App (The Easy Part)<\/a><\/li><li><a class=\"wp-block-table-of-contents__entry\" href=\"https:\/\/cln.io\/blog\/traefik-authentik-forward-auth-protect-any-app\/#reading-user-identity-in-your-app\">Reading User Identity in Your App<\/a><\/li><li><a class=\"wp-block-table-of-contents__entry\" href=\"https:\/\/cln.io\/blog\/traefik-authentik-forward-auth-protect-any-app\/#protecting-multiple-apps\">Protecting Multiple Apps<\/a><\/li><li><a class=\"wp-block-table-of-contents__entry\" href=\"https:\/\/cln.io\/blog\/traefik-authentik-forward-auth-protect-any-app\/#gotchas\">Gotchas<\/a><ol><li><a class=\"wp-block-table-of-contents__entry\" href=\"https:\/\/cln.io\/blog\/traefik-authentik-forward-auth-protect-any-app\/#the-outpost-route-is-required\">The outpost route is required<\/a><\/li><li><a class=\"wp-block-table-of-contents__entry\" href=\"https:\/\/cln.io\/blog\/traefik-authentik-forward-auth-protect-any-app\/#network-connectivity\">Network connectivity<\/a><\/li><li><a class=\"wp-block-table-of-contents__entry\" href=\"https:\/\/cln.io\/blog\/traefik-authentik-forward-auth-protect-any-app\/#header-trust\">Header trust<\/a><\/li><li><a class=\"wp-block-table-of-contents__entry\" href=\"https:\/\/cln.io\/blog\/traefik-authentik-forward-auth-protect-any-app\/#self-signed-certs\">Self-signed certs<\/a><\/li><\/ol><\/li><\/ol><\/nav>\n\n\n\n<h2 id=\"the-end-result\" class=\"wp-block-heading\">The End Result<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Visit <code>https:\/\/app.localhost<\/code> &#8211; Traefik intercepts the request, sees no session, and redirects to Authentik:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" src=\"https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/03\/forward-auth-01-login-redirect-1-2.png\" alt=\"Authentik login page after forward auth redirect\" class=\"wp-image-2080\"\/><figcaption class=\"wp-element-caption\">Unauthenticated request to app.localhost redirects to Authentik automatically.<\/figcaption><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">After logging in, Traefik forwards the request to the app with Authentik&#8217;s identity headers injected:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" src=\"https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/03\/forward-auth-04-whoami-2.png\" alt=\"whoami output showing X-Authentik headers\" class=\"wp-image-2081\"\/><figcaption class=\"wp-element-caption\">The whoami container shows all headers &#8211; including X-Authentik-Username, X-Authentik-Email, X-Authentik-Groups, and X-Authentik-Name.<\/figcaption><\/figure>\n\n\n\n<h2 id=\"how-forward-auth-works\" class=\"wp-block-heading\">How Forward Auth Works<\/h2>\n\n\n\n<ol class=\"wp-block-list\">\n<li>User requests <code>https:\/\/app.localhost<\/code><\/li>\n\n\n\n<li>Traefik&#8217;s <code>forwardauth<\/code> middleware sends a sub-request to Authentik&#8217;s outpost: <code>http:\/\/authentik_server:9000\/outpost.goauthentik.io\/auth\/traefik<\/code><\/li>\n\n\n\n<li>If the user has no valid session, Authentik returns <strong>401<\/strong> &#8211; Traefik redirects the user to Authentik&#8217;s login page<\/li>\n\n\n\n<li>User authenticates on Authentik, gets a session cookie<\/li>\n\n\n\n<li>On the next request, Authentik&#8217;s outpost returns <strong>200<\/strong> with identity headers (<code>X-Authentik-Username<\/code>, etc.)<\/li>\n\n\n\n<li>Traefik copies those headers to the request and forwards it to the app<\/li>\n\n\n\n<li>Your app sees the request with user identity injected &#8211; no auth code needed in the app itself<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">The key insight: <strong>your app never handles authentication<\/strong>. It just reads headers.<\/p>\n\n\n\n<h2 id=\"what-you-need\" class=\"wp-block-heading\">What You Need<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Docker with Compose (or Swarm)<\/li>\n\n\n\n<li>Traefik as reverse proxy<\/li>\n\n\n\n<li>Authentik as identity provider<\/li>\n\n\n\n<li>Your app (we&#8217;ll use <code>traefik\/whoami<\/code> as the example)<\/li>\n<\/ul>\n\n\n\n<h2 id=\"part-1-traefik-setup\" class=\"wp-block-heading\">Part 1: Traefik Setup<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Traefik needs to be running with the Swarm provider enabled. Here&#8217;s the relevant config (this example uses Swarm mode with Traefik v3&#8217;s dedicated <code>providers.swarm<\/code> &#8211; for regular Docker Compose, use <code>providers.docker<\/code> instead and drop the <code>deploy<\/code> blocks):<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"yaml\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">services:\n  traefik:\n    image: traefik:v3.6\n    command:\n      - \"--providers.swarm.endpoint=unix:\/\/\/var\/run\/docker.sock\"\n      - \"--providers.swarm.exposedByDefault=false\"\n      - \"--providers.swarm.network=shared_proxy\"\n      - \"--entrypoints.web.address=:80\"\n      - \"--entrypoints.web.http.redirections.entryPoint.to=websecure\"\n      - \"--entrypoints.websecure.address=:443\"\n    ports:\n      - \"80:80\"\n      - \"443:443\"\n    volumes:\n      - \/var\/run\/docker.sock:\/var\/run\/docker.sock:ro\n    networks:\n      - proxy\n    deploy:\n      placement:\n        constraints:\n          - node.role == manager\n\nnetworks:\n  proxy:\n    external: true\n    name: shared_proxy<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The important bits: <code>exposedByDefault=false<\/code> (we opt-in per service with <code>traefik.enable=true<\/code>), and a shared network that all services connect to. In Traefik v3, Swarm has its own dedicated provider (<code>providers.swarm<\/code>) instead of the v2 <code>providers.docker.swarmMode=true<\/code> flag.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Not using Swarm?<\/strong> If you&#8217;re running plain Docker Compose, swap <code>providers.swarm<\/code> for <code>providers.docker<\/code> and drop the <code>deploy<\/code> blocks. The Traefik config becomes:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"yaml\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">services:\n  traefik:\n    image: traefik:v3.6\n    command:\n      - \"--providers.docker.endpoint=unix:\/\/\/var\/run\/docker.sock\"\n      - \"--providers.docker.exposedByDefault=false\"\n      - \"--providers.docker.network=shared_proxy\"\n      - \"--entrypoints.web.address=:80\"\n      - \"--entrypoints.web.http.redirections.entryPoint.to=websecure\"\n      - \"--entrypoints.websecure.address=:443\"\n    ports:\n      - \"80:80\"\n      - \"443:443\"\n    volumes:\n      - \/var\/run\/docker.sock:\/var\/run\/docker.sock:ro\n    networks:\n      - proxy\n\nnetworks:\n  proxy:\n    external: true\n    name: shared_proxy<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">And all middleware references use <code>@docker<\/code> instead of <code>@swarm<\/code> (e.g. <code>authentik-auth@docker<\/code>). The rest of the setup is identical.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1280\" height=\"800\" src=\"https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/03\/forward-auth-10-routers-3.png\" alt=\"Traefik dashboard showing HTTP routers\" class=\"wp-image-2079\" srcset=\"https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/03\/forward-auth-10-routers-3.png 1280w, https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/03\/forward-auth-10-routers-3-300x188.png 300w, https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/03\/forward-auth-10-routers-3-1024x640.png 1024w, https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/03\/forward-auth-10-routers-3-768x480.png 768w\" sizes=\"auto, (max-width: 1280px) 100vw, 1280px\" \/><figcaption class=\"wp-element-caption\">The Traefik dashboard showing all routers &#8211; including the demo-app router on app.localhost.<\/figcaption><\/figure>\n\n\n\n<h2 id=\"part-2-authentik-configuration\" class=\"wp-block-heading\">Part 2: Authentik Configuration<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The Authentik server container defines the ForwardAuth middleware that Traefik uses. This goes in Authentik&#8217;s deploy labels:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"yaml\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\"># On the Authentik server service:\ndeploy:\n  labels:\n    - \"traefik.enable=true\"\n    # Authentik UI\/API\n    - \"traefik.http.routers.authentik.rule=Host(`auth.localhost`)\"\n    - \"traefik.http.routers.authentik.entrypoints=websecure\"\n    - \"traefik.http.routers.authentik.tls=true\"\n    - \"traefik.http.services.authentik.loadbalancer.server.port=9000\"\n    # Embedded outpost: handles \/outpost.goauthentik.io on app.localhost\n    - \"traefik.http.routers.authentik-outpost.rule=Host(`app.localhost`) &amp;&amp; PathPrefix(`\/outpost.goauthentik.io`)\"\n    - \"traefik.http.routers.authentik-outpost.entrypoints=websecure\"\n    - \"traefik.http.routers.authentik-outpost.tls=true\"\n    - \"traefik.http.routers.authentik-outpost.service=authentik\"\n    # ForwardAuth middleware (reusable by any app)\n    - \"traefik.http.middlewares.authentik-auth.forwardauth.address=http:\/\/authentik_server:9000\/outpost.goauthentik.io\/auth\/traefik\"\n    - \"traefik.http.middlewares.authentik-auth.forwardauth.trustForwardHeader=true\"\n    - \"traefik.http.middlewares.authentik-auth.forwardauth.authResponseHeaders=X-authentik-username,X-authentik-groups,X-authentik-email,X-authentik-name,X-authentik-uid\"<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This does three things:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Routes <code>auth.localhost<\/code><\/strong> to Authentik&#8217;s UI on port 9000<\/li>\n\n\n\n<li><strong>Routes <code>\/outpost.goauthentik.io<\/code> on app.localhost<\/strong> to Authentik &#8211; needed for the OAuth callback<\/li>\n\n\n\n<li><strong>Defines a reusable <code>authentik-auth<\/code> middleware<\/strong> &#8211; any service can reference <code>authentik-auth@swarm<\/code> to require authentication<\/li>\n<\/ul>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1280\" height=\"800\" src=\"https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/03\/forward-auth-09-middleware-2.png\" alt=\"Traefik middleware list showing authentik-auth forwardauth\" class=\"wp-image-2072\" srcset=\"https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/03\/forward-auth-09-middleware-2.png 1280w, https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/03\/forward-auth-09-middleware-2-300x188.png 300w, https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/03\/forward-auth-09-middleware-2-1024x640.png 1024w, https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/03\/forward-auth-09-middleware-2-768x480.png 768w\" sizes=\"auto, (max-width: 1280px) 100vw, 1280px\" \/><figcaption class=\"wp-element-caption\">The Traefik middleware list showing the authentik-auth forwardauth middleware.<\/figcaption><\/figure>\n\n\n\n<h3 id=\"create-a-proxy-provider-in-authentik\" class=\"wp-block-heading\">Create a Proxy Provider in Authentik<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">In the Authentik admin UI, create a <strong>Proxy Provider<\/strong> with these settings:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Setting<\/th><th>Value<\/th><\/tr><\/thead><tbody><tr><td><strong>Name<\/strong><\/td><td><code>Demo App Provider<\/code><\/td><\/tr><tr><td><strong>Authorization flow<\/strong><\/td><td><code>default-provider-authorization-implicit-consent<\/code><\/td><\/tr><tr><td><strong>External host<\/strong><\/td><td><code>https:\/\/app.localhost<\/code><\/td><\/tr><tr><td><strong>Mode<\/strong><\/td><td>Forward auth (single application)<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" src=\"https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/03\/forward-auth-07-provider-2.png\" alt=\"Authentik proxy provider detail for Demo App\" class=\"wp-image-2083\"\/><figcaption class=\"wp-element-caption\">The Demo App Provider showing Forward auth (single application) mode and external host.<\/figcaption><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Then create an <strong>Application<\/strong> linked to this provider:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Setting<\/th><th>Value<\/th><\/tr><\/thead><tbody><tr><td><strong>Name<\/strong><\/td><td><code>Demo App<\/code><\/td><\/tr><tr><td><strong>Slug<\/strong><\/td><td><code>demo-app<\/code><\/td><\/tr><tr><td><strong>Provider<\/strong><\/td><td><code>Demo App Provider<\/code><\/td><\/tr><tr><td><strong>Launch URL<\/strong><\/td><td><code>https:\/\/app.localhost<\/code><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" src=\"https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/03\/forward-auth-05-applications-1-2.png\" alt=\"Authentik applications list showing Demo App\" class=\"wp-image-2082\"\/><figcaption class=\"wp-element-caption\">The Demo App application in the Authentik applications list.<\/figcaption><\/figure>\n\n\n\n<h3 id=\"configure-the-embedded-outpost\" class=\"wp-block-heading\">Configure the Embedded Outpost<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Go to <strong>Applications \u2192 Outposts<\/strong> and edit the <strong>authentik Embedded Outpost<\/strong>. Add the Demo App Provider to its provider list, and set:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Setting<\/th><th>Value<\/th><\/tr><\/thead><tbody><tr><td><strong>authentik_host<\/strong><\/td><td><code>https:\/\/auth.localhost<\/code><\/td><\/tr><tr><td><strong>authentik_host_browser<\/strong><\/td><td><code>https:\/\/auth.localhost<\/code><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" src=\"https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/03\/forward-auth-08-outposts-2-2.png\" alt=\"Authentik outposts list showing embedded outpost with Demo App Provider\" class=\"wp-image-2084\"\/><figcaption class=\"wp-element-caption\">The embedded outpost linked to the Demo App Provider.<\/figcaption><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\"><code>authentik_host_browser<\/code> is what the user&#8217;s browser uses. If your internal hostname differs from the public one, set both separately.<\/p>\n\n\n\n<h2 id=\"part-3-your-app-the-easy-part\" class=\"wp-block-heading\">Part 3: Your App (The Easy Part)<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Here&#8217;s the complete Docker Compose for a protected app. This is all you need:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"yaml\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">services:\n  demo-app:\n    image: traefik\/whoami:latest\n    networks:\n      - proxy\n    deploy:\n      replicas: 1\n      labels:\n        - \"traefik.enable=true\"\n        - \"traefik.http.routers.demo-app.rule=Host(`app.localhost`)\"\n        - \"traefik.http.routers.demo-app.entrypoints=websecure\"\n        - \"traefik.http.routers.demo-app.tls=true\"\n        - \"traefik.http.routers.demo-app.middlewares=authentik-auth@swarm\"\n        - \"traefik.http.services.demo-app.loadbalancer.server.port=80\"\n\nnetworks:\n  proxy:\n    external: true\n    name: shared_proxy<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The magic is one line:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"yaml\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">traefik.http.routers.demo-app.middlewares=authentik-auth@swarm<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">That&#8217;s it. This tells Traefik to run the <code>authentik-auth<\/code> ForwardAuth middleware (defined on the Authentik server) before forwarding requests to your app. No code changes, no auth libraries, no session management.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Replace <code>traefik\/whoami<\/code> with any container &#8211; a Node.js app, a Python Flask server, a static site, anything. As long as it&#8217;s on the <code>shared_proxy<\/code> network and has that middleware label, it&#8217;s protected.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Docker Compose version:<\/strong> Move the <code>labels<\/code> from <code>deploy<\/code> directly onto the service, and use <code>authentik-auth@docker<\/code>:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"yaml\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">services:\n  demo-app:\n    image: traefik\/whoami:latest\n    labels:\n      - \"traefik.enable=true\"\n      - \"traefik.http.routers.demo-app.rule=Host(`app.localhost`)\"\n      - \"traefik.http.routers.demo-app.entrypoints=websecure\"\n      - \"traefik.http.routers.demo-app.tls=true\"\n      - \"traefik.http.routers.demo-app.middlewares=authentik-auth@docker\"\n      - \"traefik.http.services.demo-app.loadbalancer.server.port=80\"\n    networks:\n      - proxy\n\nnetworks:\n  proxy:\n    external: true\n    name: shared_proxy<\/pre>\n\n\n\n<h2 id=\"reading-user-identity-in-your-app\" class=\"wp-block-heading\">Reading User Identity in Your App<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">After authentication, Traefik injects these headers into every request to your app:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Header<\/th><th>Example Value<\/th><\/tr><\/thead><tbody><tr><td><code>X-Authentik-Username<\/code><\/td><td><code>alice<\/code><\/td><\/tr><tr><td><code>X-Authentik-Email<\/code><\/td><td><code>alice@demo.example.com<\/code><\/td><\/tr><tr><td><code>X-Authentik-Name<\/code><\/td><td><code>Alice Johnson<\/code><\/td><\/tr><tr><td><code>X-Authentik-Groups<\/code><\/td><td><code>developers|admins|pee<\/code><\/td><\/tr><tr><td><code>X-Authentik-Uid<\/code><\/td><td><code>7ed9cf8ff2620824d2ac...<\/code><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Your app can read these headers to know who the user is and what groups they belong to &#8211; no need to implement OAuth, OIDC, or any auth flow. Here&#8217;s a quick example in Python:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"python\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">from flask import Flask, request\n\napp = Flask(__name__)\n\n@app.route(\"\/\")\ndef hello():\n    user = request.headers.get(\"X-Authentik-Username\", \"anonymous\")\n    groups = request.headers.get(\"X-Authentik-Groups\", \"\").split(\"|\")\n    return f\"Hello {user}! Groups: {groups}\"\n<\/pre>\n\n\n\n<h2 id=\"protecting-multiple-apps\" class=\"wp-block-heading\">Protecting Multiple Apps<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>authentik-auth@swarm<\/code> middleware is reusable. To protect another app, just add the middleware label &#8211; no extra Authentik configuration needed:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"yaml\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">services:\n  another-app:\n    image: your-app:latest\n    networks:\n      - proxy\n    deploy:\n      labels:\n        - \"traefik.enable=true\"\n        - \"traefik.http.routers.another-app.rule=Host(`another.localhost`)\"\n        - \"traefik.http.routers.another-app.entrypoints=websecure\"\n        - \"traefik.http.routers.another-app.tls=true\"\n        - \"traefik.http.routers.another-app.middlewares=authentik-auth@swarm\"\n        - \"traefik.http.services.another-app.loadbalancer.server.port=8080\"<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Note:<\/strong> If you use <code>forward_single<\/code> mode (as we did), you need a separate Proxy Provider and Application in Authentik for each hostname. If you have many apps, consider using <code>forward_domain<\/code> mode instead, which protects all subdomains under a single provider.<\/p>\n\n\n\n<h2 id=\"gotchas\" class=\"wp-block-heading\">Gotchas<\/h2>\n\n\n\n<h3 id=\"the-outpost-route-is-required\" class=\"wp-block-heading\">The outpost route is required<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">You <strong>must<\/strong> route <code>\/outpost.goauthentik.io<\/code> on your app&#8217;s hostname to the Authentik server. Without it, the OAuth callback after login will fail. This is the <code>authentik-outpost<\/code> router in the Authentik labels above.<\/p>\n\n\n\n<h3 id=\"network-connectivity\" class=\"wp-block-heading\">Network connectivity<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The ForwardAuth middleware uses an internal URL (<code>http:\/\/authentik_server:9000\/...<\/code>) &#8211; this means Traefik must be able to reach the Authentik server container directly. Both must be on the same Docker network (in our case, <code>shared_proxy<\/code>).<\/p>\n\n\n\n<h3 id=\"header-trust\" class=\"wp-block-heading\">Header trust<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>X-Authentik-*<\/code> headers are injected by Traefik, not by the client. But if your app is somehow accessible without going through Traefik, a client could forge these headers. Always ensure your app is <strong>only reachable through Traefik<\/strong> &#8211; in Docker, this means keeping it on an internal network without exposed ports.<\/p>\n\n\n\n<h3 id=\"self-signed-certs\" class=\"wp-block-heading\">Self-signed certs<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">If using self-signed certificates (like in this demo), you&#8217;ll need to accept the cert warning on <code>auth.localhost<\/code> in your browser <em>before<\/em> visiting <code>app.localhost<\/code>. Otherwise the redirect to Authentik will fail silently.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p class=\"wp-block-paragraph\"><em>Tested with Traefik v3.6 and Authentik 2025.2.1. Examples use Docker Swarm with Traefik v3&#8217;s dedicated <code>providers.swarm<\/code>. For Docker Compose (non-Swarm), use <code>providers.docker<\/code> and reference middlewares as <code>@docker<\/code> instead of <code>@swarm<\/code>. The whoami container is just for demonstration &#8211; replace it with any web application.<\/em><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><em>This post was written by <a href=\"https:\/\/claude.ai\">Claude<\/a> (claude-opus-4-6), Anthropic&#8217;s AI assistant, with human direction and review.<\/em><\/p>\n\n","protected":false},"excerpt":{"rendered":"<p>You have a web app. You want to put it behind authentication without touching the app&#8217;s code. Traefik and Authentik make this possible with forward authentication &#8211; Traefik asks Authentik &#8220;is this user allowed?&#8221; before [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":2054,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[63,48,26],"tags":[],"class_list":["post-1953","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-authentication","category-security","category-it"],"_links":{"self":[{"href":"https:\/\/cln.io\/blog\/wp-json\/wp\/v2\/posts\/1953","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/cln.io\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/cln.io\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/cln.io\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/cln.io\/blog\/wp-json\/wp\/v2\/comments?post=1953"}],"version-history":[{"count":7,"href":"https:\/\/cln.io\/blog\/wp-json\/wp\/v2\/posts\/1953\/revisions"}],"predecessor-version":[{"id":2087,"href":"https:\/\/cln.io\/blog\/wp-json\/wp\/v2\/posts\/1953\/revisions\/2087"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/cln.io\/blog\/wp-json\/wp\/v2\/media\/2054"}],"wp:attachment":[{"href":"https:\/\/cln.io\/blog\/wp-json\/wp\/v2\/media?parent=1953"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/cln.io\/blog\/wp-json\/wp\/v2\/categories?post=1953"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/cln.io\/blog\/wp-json\/wp\/v2\/tags?post=1953"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}