One Login for the Homelab: Authelia SSO with Traefik Forward-Auth
Every self-hosted service ships its own login page. Some (like Prometheus) ship none at all. This is how I put a single Authelia sign-on gate in front of my homelab — one login for everything — using Traefik forward-auth, all managed by GitOps (Flux). It covers the deployment, the two levels of "SSO," and every sharp edge I hit.

Two levels of "SSO" — know which you want
Authelia forward-auth is a gate in front of an app, not a replacement for its login. That distinction matters:
- Gate only — Authelia authenticates you, then the app still shows its own login. Good for apps with no login (Prometheus) or where you just want a wall. Downside: double login for apps that have their own auth.
- True SSO — the app trusts Authelia's
Remote-Userheader and logs you in automatically. No second prompt. Requires app support (Grafanaauth.proxy, Gitea reverse-proxy auth).
1. Deploy Authelia
File-based single-user, one-factor to start. Secrets come from Vault via External-Secrets; state is a small SQLite PVC. Config (trimmed):
# configuration.yml (ConfigMap)
theme: dark
server: { address: "tcp://0.0.0.0:9091/" }
authentication_backend:
file:
path: /config/users_database.yml
access_control:
default_policy: deny
rules:
- domain: [prometheus.leolab.pro, grafana.leolab.pro, git.leolab.pro]
policy: one_factor
session:
cookies:
- domain: leolab.pro # cookie shared across *.leolab.pro
authelia_url: https://auth.leolab.pro
storage:
local: { path: /data/db.sqlite3 }
notifier:
filesystem: { filename: /data/notification.txt }
Secrets (session, storage-encryption, jwt) inject via *_FILE env from an ExternalSecret; the user database (with an argon2id hash) mounts as a file:
# hash a password
docker run --rm authelia/authelia:4.38 \
authelia crypto hash generate argon2 --password 'your-password'# users_database.yml (whole file goes in the secret, not just the hash)
users:
leo:
displayname: "Admin"
password: "$argon2id$v=19$m=65536,t=3,p=4$...."
email: [email protected]
groups: [admins]
⚠️ Gotcha 1 — Kubernetes service-link env vars
Name the Service authelia and Kubernetes injects AUTHELIA_PORT, AUTHELIA_SERVICE_* env vars. Authelia reads any AUTHELIA_-prefixed env as configuration → it collides with server.address and the pod crash-loops. Fix:
spec:
template:
spec:
enableServiceLinks: false # stop the injected AUTHELIA_* env
⚠️ Gotcha 2 — the user database is a full YAML file
The users-database secret must contain the entire users_database.yml (the users: → <name> → password: structure), not just the bare hash. A bare hash gives users: non zero value required and Authelia won't start.
2. The Traefik forward-auth middleware
A Middleware that calls Authelia and copies the identity headers back:
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata: { name: authelia }
spec:
forwardAuth:
address: http://authelia.authelia.svc.cluster.local:9091/api/authz/forward-auth
trustForwardHeader: true
authResponseHeaders: [Remote-User, Remote-Groups, Remote-Email, Remote-Name]
Attach it to any app's Ingress with an annotation:
annotations:
traefik.ingress.kubernetes.io/router.middlewares: myns-authelia@kubernetescrd⚠️ Gotcha 3 — Traefik v3 blocks cross-namespace middleware
By default a v3 Ingress can't reference a Middleware in another namespace. Rather than enable allowCrossNamespace (and restart Traefik), just create the same Middleware object in each app's namespace — the forwardAuth.address still points at the one Authelia Service cluster-wide.
3. Whole-host vs path-scoped (Ghost)
Most apps: gate the whole host. But a public blog with an admin path (Ghost: public site + /ghost admin) must stay public while only the admin is gated. Solution — a second, path-scoped Ingress; PathPrefix(/ghost) outranks the blog's /:
# ghost-admin ingress: only /ghost, with the middleware
rules:
- host: ghost.leolab.pro
http: { paths: [{ path: /ghost, pathType: Prefix, backend: {...} }] }
annotations:
traefik.ingress.kubernetes.io/router.middlewares: ghost-authelia@kubernetescrd
The main blog ingress stays untouched and never depends on Authelia.
⚠️ Gotcha 4 — Gitea breaks the git CLI unless you bypass
Gate git.leolab.pro naively and git clone/push over HTTPS, the API, and the container registry all break — CLIs can't follow the browser login redirect. Bypass those paths in Authelia and gate only the web UI:
access_control:
rules:
- domain: git.leolab.pro
resources:
- "^/api($|/.*)"
- "^/v2($|/.*)" # container registry
- "^/[^/]+/[^/]+/(info/refs|git-upload-pack|git-receive-pack)($|/.*)"
policy: bypass
- domain: git.leolab.pro
policy: one_factor # web UI only
4. True SSO — trusting the header
For apps that support it, skip the second login entirely.
Grafana — auth.proxy trusts Remote-User and auto-logs-in:
[auth.proxy]
enabled = true
header_name = Remote-User
header_property = username
auto_sign_up = true
whitelist = 10.42.0.0/16 # pod CIDR — only Traefik reaches Grafana
[users]
auto_assign_org_role = Admin
Gitea — reverse-proxy auth auto-logs-in/registers from the header:
[service]
ENABLE_REVERSE_PROXY_AUTHENTICATION = true
ENABLE_REVERSE_PROXY_AUTO_REGISTRATION = true
[security]
REVERSE_PROXY_AUTHENTICATION_USER = Remote-User
REVERSE_PROXY_TRUSTED_PROXIES = 10.42.0.0/16
Security note: header-trust means the app blindly believes Remote-User. It's only safe because the apps are reachable solely through Traefik+Authelia — anything that could hit the pod directly could forge the header. Keep them off direct/NodePort exposure.
Who gets what
| Service | Mode |
|---|---|
| Grafana, Gitea | True SSO (header auto-login, no app password) |
| Prometheus, Excalidraw, Stirling-PDF | Gate only (no native login — Authelia is their auth) |
| Ghost admin (/ghost) | Gate only (Ghost keeps its own login behind it) |
| Public blog, Uptime-Kuma, CommaFeed | Untouched / native login |
Side effect: dashboards that call the API
Anything that scrapes a gated service's API from inside the cluster (a homepage dashboard, an exporter) will now get 401. Point those at the in-cluster service URL instead of the public hostname — service-to-service traffic never touches Traefik/Authelia:
# homepage widget
url: http://kube-prometheus-stack-prometheus.monitoring.svc.cluster.local:9090Roll it out carefully
- Deploy Authelia; confirm the portal loads and login works — before attaching any middleware (attaching while Authelia is down 500s the app).
- Canary on a low-risk app (Prometheus) to prove the redirect→login→back flow.
- Gate the rest, one at a time. Do Grafana last — if Authelia misbehaves you don't want to lock yourself out of the dashboards you'd debug it with.
- Keep a break-glass:
kubectl port-forward svc/grafana 3000:80reaches it directly, no gate.
End result: one Authelia login, shared across every *.leolab.pro service — auto-login where the app supports it, a clean gate where it doesn't, the public blog still public, and the git CLI still working.