Authentik: SSO for Your On-Prem Cloud
Putting every on-prem cloud service behind single sign-on with Authentik — the forward auth pattern, the auth-url hostname gotcha, and native OIDC for Grafana.
Most on-prem cloud apps have their own login page. That means a separate username and password for ArgoCD, another for Grafana, another for MinIO, another for Kafka UI. It's fine for a while, and then one day you've got fifteen services and you're maintaining fifteen credential sets.
Authentik solves this. It's a self-hosted identity provider — you log in once, and every app trusts Authentik's assertion that you're you.
This post covers how I run Authentik on my cluster, the two patterns I use (forward auth and native OIDC), and the non-obvious parts that the docs gloss over.
What Authentik Actually Does
Authentik is an identity provider (IdP). Apps delegate authentication to it: instead of checking your password themselves, they ask Authentik "is this user who they say they are?" and trust the answer.
For apps that natively support OIDC (Grafana, for example), this is straightforward — you configure the app with Authentik's OIDC endpoint and it handles the auth flow.
For apps that don't support any auth standard (Kafka UI, Redis Insight, Headlamp), you use forward auth: the ingress controller intercepts every request and asks Authentik whether the user is logged in before forwarding it to the app. The app itself never handles authentication.
The forward auth pattern is the more interesting one, so that's most of what this post covers.
Forward Auth: How It Works
The flow looks like this:
- User visits
kafka-ui.yanatech.co.uk - ingress-nginx intercepts the request and sends a sub-request to Authentik's outpost asking "is this user authenticated?"
- If yes, Authentik responds with user info headers and nginx forwards the original request to the Kafka UI pod
- If no, Authentik redirects the user to the login page, then redirects back after login
The key component is the outpost — a lightweight proxy pod that Authentik deploys automatically when you create a forward auth application. The outpost handles the auth check sub-requests from nginx.
Setting Up Forward Auth: Step by Step
1. Create an Authentik Provider
In the Authentik UI (authentik.yanatech.co.uk), create a Proxy Provider in Forward auth mode (single application). Give it:
- Name:
kafka-ui - External host:
https://kafka-ui.yanatech.co.uk - Mode: Forward auth (single application)
The "single application" mode means the outpost only protects one hostname. There's also a domain-level mode that protects all subdomains, but per-app mode is simpler to reason about.
2. Create an Authentik Application
Create an Application that references the provider. This is what appears in the Authentik user dashboard. The application name is what users see when they click through from the Authentik portal.
When you assign the application to an outpost (next step), Authentik automatically deploys the outpost pod.
3. Assign to an Outpost
Authentik auto-creates an ak-outpost-<name> Deployment in the authentik namespace. This outpost pod is what nginx actually communicates with during the auth check.
You don't manage this pod — Authentik creates and updates it based on the application configuration.
4. ExternalName Service in the App Namespace
The nginx auth-url annotation needs to reach the outpost, but the outpost is in the authentik namespace. You could reference it by its full DNS name, but a cleaner pattern is an ExternalName Service in the app's namespace:
apiVersion: v1
kind: Service
metadata:
name: ak-outpost-kafka-ui
namespace: kafka
spec:
type: ExternalName
externalName: ak-outpost-kafka-ui.authentik.svc.cluster.local
ports:
- port: 9000
targetPort: 9000
This lets the nginx annotations reference ak-outpost-kafka-ui.kafka.svc.cluster.local instead of the cross-namespace name.
5. Outpost Ingress in the App Namespace
The outpost needs to receive requests at the app's hostname on the /outpost.goauthentik.io path. Add a second Ingress in the app namespace:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: kafka-ui-outpost
namespace: kafka
spec:
ingressClassName: nginx
tls:
- hosts: [kafka-ui.yanatech.co.uk]
secretName: wildcard-yanatech-tls
rules:
- host: kafka-ui.yanatech.co.uk
http:
paths:
- path: /outpost.goauthentik.io
pathType: Prefix
backend:
service:
name: ak-outpost-kafka-ui
port:
number: 9000
6. Auth Annotations on the Main Ingress
Finally, add the auth annotations to the app's main Ingress:
annotations:
nginx.ingress.kubernetes.io/auth-url: "https://kafka-ui.yanatech.co.uk/outpost.goauthentik.io/auth/nginx"
nginx.ingress.kubernetes.io/auth-signin: "https://kafka-ui.yanatech.co.uk/outpost.goauthentik.io/start?rd=$escaped_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"
nginx.ingress.kubernetes.io/auth-snippet: |
proxy_set_header X-Original-URL $scheme://$http_host$request_uri;
The auth-url Hostname Gotcha
The auth-url annotation must use the external hostname, not the internal Kubernetes service address.
This is the mistake that trips everyone up:
# Wrong — outpost won't recognise this host
auth-url: "http://ak-outpost-kafka-ui.kafka.svc.cluster.local:9000/outpost.goauthentik.io/auth/nginx"
# Right — must match the external hostname configured in the Authentik Provider
auth-url: "https://kafka-ui.yanatech.co.uk/outpost.goauthentik.io/auth/nginx"
The reason: the outpost matches incoming auth check requests against the list of external hosts configured in its Authentik application. If the host header on the auth check doesn't match, the outpost rejects it. An internal Kubernetes service URL will never match an external hostname.
When this is misconfigured, every request returns 401 immediately — the auth check fails silently and nginx denies access. The outpost logs will show no application found for host.
The auth-snippet Annotation
The auth-snippet annotation adds proxy_set_header X-Original-URL to the nginx sub-request. This tells the outpost what the user was originally trying to access, so it can redirect them back to the right URL after login.
Without it, users always get redirected to the app's root after logging in, regardless of what they were trying to access. For apps where you frequently deep-link (Grafana dashboards, ArgoCD application views), this is noticeably annoying.
auth-snippet requires two additional ingress-nginx settings — without them, nginx rejects the annotation:
# ingress-nginx Helm values
controller:
config:
allow-snippet-annotations: "true"
annotations-risk-level: "Critical"
The Critical level is a deliberate gate — snippet annotations can inject arbitrary nginx config, so the ingress-nginx maintainers made you opt in explicitly. For a single-tenant on-prem cloud cluster with no untrusted users creating Ingresses, this is fine.
Native OIDC: Grafana
For apps that support OIDC natively, the setup is simpler and more robust. Grafana is a good example.
In Authentik, create an OAuth2/OIDC Provider with:
- Redirect URI:
https://grafana.yanatech.co.uk/login/generic_oauth - Scopes:
openid,email,profile
Then configure Grafana to use it:
# Grafana Helm values (grafana.ini section)
grafana.ini:
auth.generic_oauth:
enabled: true
name: Authentik
client_id: <client-id-from-authentik>
client_secret: <client-secret-from-authentik>
scopes: openid email profile
auth_url: https://authentik.yanatech.co.uk/application/o/authorize/
token_url: https://authentik.yanatech.co.uk/application/o/token/
api_url: https://authentik.yanatech.co.uk/application/o/userinfo/
role_attribute_path: "contains(groups[*], 'admins') && 'Admin' || 'Viewer'"
auth:
oauth_auto_login: true
disable_login_form: true
oauth_auto_login: true means visiting Grafana immediately redirects to Authentik — there's no Grafana login form to fill out. disable_login_form: true removes the native form entirely, so Authentik is the only path in.
The role_attribute_path JMESPath expression maps Authentik group membership to Grafana roles — Authentik admins group members get Grafana Admin, everyone else gets Viewer.
The Headlamp Exception
Not every app's SSO integration works. Headlamp (the Kubernetes web UI) has a documented upstream bug where Authentik OIDC breaks the token exchange. Until it's fixed, the workaround is a long-lived ServiceAccount token:
kubectl create token headlamp -n headlamp --duration=8760h
8760 hours is one year. Not elegant, but functional. The token is stored in Vaultwarden and rotated annually.
This is a useful reminder that "all apps behind SSO" is an aspiration, not a guarantee. Some apps have broken OIDC implementations, some don't support any auth delegation at all. Accept the exceptions, document them, and move on.
Network Policy Considerations
With Cilium's default-deny-all NetworkPolicies, the forward auth traffic needs explicit egress rules. ingress-nginx pods make outbound requests to the outpost pods on port 9000 — by default, this is blocked.
Add an egress rule on the ingress-nginx namespace allowing traffic to the authentik namespace on port 9000, and a corresponding ingress rule on authentik allowing it from ingress-nginx. The exact policy depends on your namespace labelling, but the traffic path is always the same: ingress-nginx → outpost pod on port 9000.
Running Authentik for a year on a cluster with ~20 services behind it, the operational feel is good. Single login, the Authentik portal shows all your apps in one place, and audit logs show every login attempt. The forward auth pattern adds a couple of minutes of setup per app — a worthwhile trade for not maintaining fifteen separate credential sets.
The gotchas (external hostname in auth-url, annotations-risk-level: Critical, the outpost pod model) are things you hit once and remember. After the first two or three apps, the pattern becomes muscle memory.