Gino Eising
Gino Eising
Nerd by Nature
Apr 24, 2026 7 min read

Authentik Proxy Outpost on Kubernetes: The Parts Nobody Documents

thumbnail for this post

April 2026 — Every Authentik tutorial covers OIDC. Here’s what to do when the app has no OIDC support at all.

I run Authentik as my SSO provider on a self-hosted RKE2 cluster. Most apps I deploy support OIDC natively — you register an application in Authentik, it hands out tokens, life is good. But some apps don’t speak OIDC. n8n’s community edition is one. So is anything where you want to bolt authentication onto a bare internal tool without touching its config.

Authentik has a second auth mode for this: the proxy outpost with forward auth. Nginx intercepts every request, asks Authentik “is this user authenticated?”, and either passes the request through or redirects to login. The app never sees an auth challenge. It sounds straightforward. It is not, and the documentation leaves out the parts that will actually burn your time.

This post is those parts.

The two auth models — why this matters

When an app supports OIDC natively, the flow is: user hits app → app redirects to Authentik → Authentik issues a token → app validates the token. Authentik is the identity provider; the app is the relying party. You configure one thing in Authentik (an OAuth2/OIDC provider) and one thing in the app (client ID, secret, callback URL).

The proxy outpost model is different. Authentik sits in front of the app. Nginx uses its auth_request directive to call Authentik on every incoming request. Authentik checks whether the request has a valid session cookie. If yes, it returns 200 and sets X-authentik-* headers. If no, it returns 401 and nginx redirects the user to login. The app receives only authenticated requests, plus those headers identifying the user.

The app never participates in authentication. It doesn’t need to know about Authentik at all.

The embedded outpost trap

Here’s the first thing that will waste your afternoon.

Authentik ships with an embedded outpost — a proxy outpost that runs inside the Authentik server pod itself, always on. You don’t need to deploy anything extra. Sounds great. The catch: the embedded outpost has no providers assigned to it by default. When nginx calls https://app.djieno.com/outpost.goauthentik.io/auth/nginx and that request routes to the embedded outpost, the embedded outpost has no idea which application you’re talking about. It returns 404. Nginx converts 404 from an auth endpoint to 500 for the original request. Your app is just broken.

There are two ways out: assign your proxy provider to the embedded outpost in Authentik’s admin UI, or deploy a separate external proxy-outpost pod and route to that instead. I went with the external outpost — it’s cleaner for multi-app setups and avoids side-loading configuration onto the embedded outpost that you’ll forget about later.

The callback URL mismatch

If you try to use the embedded outpost anyway (or you accidentally configure your nginx annotations to point at authentik.djieno.com instead of your app domain), you’ll hit a second bug that’s harder to diagnose.

The proxy provider has an external_host field. This must match the hostname where your app lives — https://app.djieno.com. When a user logs in, Authentik sends them to https://app.djieno.com/outpost.goauthentik.io/callback to complete the flow. If the embedded outpost handles that callback but the callback goes to authentik.djieno.com, the OAuth2 state won’t match the provider’s external_host, and the user lands on the Authentik home page instead of the app. No error. Just a confusing redirect.

The fix: the /outpost.goauthentik.io path must be handled at the app’s hostname, not the Authentik hostname. That means the app’s ingress (or a second ingress on the same hostname, in the same namespace) has to route that path to the outpost.

Why ExternalName services exist for this

nginx ingress can only route to services in the same namespace as the Ingress resource. The app runs in n8n-prd. The external proxy-outpost pod runs in authentik-prd. You can’t directly reference a service across namespaces in an Ingress backend.

The solution is a Kubernetes ExternalName service. This is a service with no selector that resolves to a DNS name — it’s just an alias. You create one in n8n-prd that points at the outpost’s cluster-internal DNS name in authentik-prd. Then you create an Ingress in n8n-prd that routes /outpost.goauthentik.io to this alias service.

ExternalName service (outpost-svc.yaml in n8n-prd):

apiVersion: v1
kind: Service
metadata:
  name: authentik-proxy-outpost
  namespace: n8n-prd
spec:
  type: ExternalName
  externalName: ak-outpost-proxy-outpost.authentik-prd.svc.cluster.local
  ports:
    - port: 9000
      targetPort: 9000

The ak-outpost-proxy-outpost name comes from whatever you named the outpost in Authentik. Check kubectl get svc -n authentik-prd to confirm.

The TLS cert problem you’ll hit if you route wrong

There’s a tempting shortcut: add the app hostname to the ingress in authentik-prd and handle everything there. Don’t. The TLS secret for app.djieno.com lives in n8n-prd (or gets issued by cert-manager into that namespace). If nginx tries to serve app.djieno.com from the authentik-prd ingress, it falls back to the default self-signed cert. Your browser will complain. Your auth flow will break in confusing ways.

Keep every ingress for app.djieno.com in the namespace where the TLS secret lives. That’s the app namespace.

Server-snippets are disabled

If you’re running the hardened nginx ingress that ships with RKE2, the following annotations are disabled:

  • nginx.ingress.kubernetes.io/server-snippet
  • nginx.ingress.kubernetes.io/configuration-snippet
  • nginx.ingress.kubernetes.io/auth-snippet

A lot of older guides and Stack Overflow answers use these to set up forward auth. They won’t work. The approach I’m describing here — auth-url and auth-signin annotations combined with a separate ingress for the outpost path — doesn’t need them.

The full working setup

Authentik side

  1. Create a Proxy Provider (type: Forward single application)
  2. Set external_host to https://app.djieno.com
  3. Create an Application and assign the provider
  4. Create an access policy (e.g. bind an admin group if this is internal-only)
  5. Go to Outposts, find your external proxy-outpost, edit it, and add the new provider. Do not use the embedded outpost.

Kubernetes side

Three resources in the app namespace, plus annotations on the existing app ingress.

ExternalName service:

apiVersion: v1
kind: Service
metadata:
  name: authentik-proxy-outpost
  namespace: n8n-prd
spec:
  type: ExternalName
  externalName: ak-outpost-proxy-outpost.authentik-prd.svc.cluster.local
  ports:
    - port: 9000
      targetPort: 9000

Outpost path ingress — handles /outpost.goauthentik.io/* on the app hostname. No TLS section needed here; the primary app ingress already handles TLS for this hostname and nginx merges the rules:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: n8n-outpost-path
  namespace: n8n-prd
spec:
  ingressClassName: nginx
  rules:
    - host: "app.djieno.com"
      http:
        paths:
          - path: /outpost.goauthentik.io
            pathType: Prefix
            backend:
              service:
                name: authentik-proxy-outpost
                port:
                  number: 9000

App ingress annotations — on the primary app ingress, add:

nginx.ingress.kubernetes.io/auth-url: "https://app.djieno.com/outpost.goauthentik.io/auth/nginx"
nginx.ingress.kubernetes.io/auth-signin: "https://app.djieno.com/outpost.goauthentik.io/start?rd=$scheme://$best_http_host$request_uri"
nginx.ingress.kubernetes.io/auth-response-headers: "Set-Cookie,X-authentik-username,X-authentik-groups,X-authentik-email,X-authentik-name,X-authentik-uid"

The auth-url and auth-signin both point at the app hostname, not authentik.djieno.com. This is the thing that routes correctly through the second ingress to the proxy-outpost via ExternalName.

What the app receives

After a successful auth check, nginx forwards the request to the app with these headers set by the outpost:

  • X-authentik-username
  • X-authentik-email
  • X-authentik-groups
  • X-authentik-name
  • X-authentik-uid

The app can use these if it wants, or ignore them entirely. The authentication gate is fully enforced at the nginx layer regardless. Unauthenticated requests never reach the app pod.

One note: if the app has its own login page (n8n does), users will still see it after passing the SSO gate. That’s a separate UX issue — the nginx auth gate and the app’s internal auth are independent. The nginx gate ensures only authenticated users can reach the app at all; what the app does with those users after that is up to it.

The request flow

Browser → https://app.djieno.com/
  → nginx ingress (n8n-prd namespace)
    → auth_request to /outpost.goauthentik.io/auth/nginx
      → matched by outpost-path ingress
        → ExternalName service → ak-outpost-proxy-outpost.authentik-prd.svc.cluster.local:9000
    → 401: redirect to /outpost.goauthentik.io/start?rd=...
    → 200: forward to app pod with X-authentik-* headers

Summary

The working pattern:

  • Deploy an external proxy-outpost in Authentik. Assign your proxy provider to it, not to the embedded outpost.
  • Set external_host on the provider to match the app URL, not the Authentik URL.
  • In the app namespace, create an ExternalName service pointing at the outpost’s ClusterDNS address.
  • In the app namespace, create a second Ingress routing /outpost.goauthentik.io on the app hostname to that ExternalName service.
  • Add auth-url and auth-signin annotations to the primary app ingress, both pointing at the app hostname.
  • Keep TLS termination on the primary app ingress in the app namespace. Don’t touch the Authentik namespace.

Everything in the app namespace, no server-snippets, no cross-namespace ingress hacks. Once it clicks, it’s replicable across every app that needs SSO without native OIDC support.