Single Sign-On for Self-Hosted Services with Authelia
Self-hosting produces a pile of services, and a surprising number of them ship with weak authentication or none at all. Exposing ten separate login pages to the internet means ten separate things to keep patched and ten places a default credential might survive.
Forward authentication puts one login in front of all of them.
How it works
Your reverse proxy already sits in front of everything. Forward auth adds one step: before proxying a request, ask an authentication service whether it is allowed.
- Request arrives at the proxy.
- Proxy asks Authelia about it, forwarding the cookies and headers.
- Authelia returns 200 if authenticated, or 401.
- On 401 the proxy redirects to the login page.
- After login, the request proceeds and the backend sees it.
The protected application never receives unauthenticated traffic. An app with no login at all is now behind one, without modifying the app.
Configuration
# configuration.yml
theme: dark
default_redirection_url: https://home.example.com
server:
address: 'tcp://0.0.0.0:9091'
authentication_backend:
file:
path: /config/users_database.yml
password:
algorithm: argon2
argon2:
variant: argon2id
access_control:
default_policy: deny
rules:
- domain: 'public.example.com'
policy: bypass
- domain: 'jellyfin.example.com'
policy: one_factor
- domain: ['vault.example.com', 'admin.example.com']
policy: two_factor
- domain: 'grafana.example.com'
policy: two_factor
subject: ['group:admins']
session:
name: authelia_session
domain: example.com
expiration: 12h
inactivity: 45m
redis:
host: redis
port: 6379
regulation:
max_retries: 3
find_time: 2m
ban_time: 10m
notifier:
smtp:
address: 'submission://smtp.example.com:587'
sender: 'authelia@example.com'
Several things there are worth pointing at.
default_policy: deny is the right default. Everything is blocked unless a rule permits it, so forgetting to write a rule fails closed.
Per-service policies. A media server gets one factor; anything administrative gets two. That graduation is the practical value, because requiring a TOTP code to watch a film is friction people route around.
regulation is built-in rate limiting: three failures in two minutes earns a ten-minute ban. This is fail2ban for the login form, without configuring fail2ban.
Redis for sessions. Without it, restarting Authelia logs everyone out.
Wiring it to the proxy
With Caddy, which our web server comparison covers:
jellyfin.example.com {
forward_auth authelia:9091 {
uri /api/verify?rd=https://auth.example.com
copy_headers Remote-User Remote-Groups Remote-Email Remote-Name
}
reverse_proxy jellyfin:8096
}
auth.example.com {
reverse_proxy authelia:9091
}
With nginx it is auth_request, which needs a location block and header plumbing, and Caddy’s version is considerably shorter.
The copy_headers line passes identity to the backend. Applications supporting trusted header authentication use those to log the user in automatically, so there is no second login prompt.
That convenience has a cost worth understanding: the backend now trusts whatever headers arrive. If the application is reachable by any path that bypasses the proxy, someone can set those headers themselves and be whoever they like.
services:
jellyfin:
networks: [internal]
# deliberately no ports; only the proxy can reach it
networks:
internal:
internal: true
Our container security guide covers why a published port bypasses your firewall, which is exactly how that bypass happens accidentally.
The API problem
The one that catches everyone.
Forward auth redirects unauthenticated requests to a login page. An API client, a mobile app, or a curl script cannot follow that, so it receives HTML where it expected JSON and fails confusingly.
access_control:
rules:
- domain: 'jellyfin.example.com'
resources: ['^/api/.*', '^/socket']
policy: bypass
networks: ['10.0.0.0/8', '192.168.0.0/16']
- domain: 'jellyfin.example.com'
policy: one_factor
Bypass the API paths, restricted to your own networks, and require login for everything else. Rules are evaluated in order, so the specific one must come first.
The alternative is token authentication on the API, handled by the application itself.
Two-factor
Authelia supports TOTP, WebAuthn, and push notifications through Duo.
WebAuthn is the one to use where you can. A hardware key or platform authenticator is phishing-resistant in a way TOTP is not: a TOTP code can be typed into a convincing fake login page, and a WebAuthn assertion is bound to the origin and simply will not work there.
The same reasoning appears in our ssh-agent guide regarding hardware-backed SSH keys.
Authelia or Authentik
Authelia is a single Go binary, configured in YAML, focused on forward auth with OIDC support added. Light, fast, and the configuration is a file you can commit, which pairs with our sops and age guide for the secrets in it.
Authentik has a web admin interface, a database, user self-service, SAML, and a broader identity provider feature set. More capable and more to run.
For a homelab with a handful of users, Authelia. For something with real user management needs, Authentik.
What this does not do
Being clear about the boundary.
It does not patch the application. A vulnerability reachable after login is still reachable after login.
It does not protect against a compromise of Authelia or the proxy. They are now the thing guarding everything, which concentrates risk as well as reducing it.
It does not make exposure safe in general. It closes the largest hole, an unauthenticated service open to the internet, and the safer default remains not exposing anything.
Our self-hosting introduction argues for reaching services over WireGuard rather than publishing them, and that remains right for anything you do not need to share with people who will not install a VPN client.
SSO is for the cases where you do: a media server for family, a photo library for friends, a status page for a team. For those, one hardened login in front of everything is much better than ten.
Frequently Asked Questions
What is forward authentication?
The reverse proxy asks an authentication service about every request before passing it to the backend. If the service says the request is not authenticated the proxy redirects to a login page, so the protected application never sees unauthenticated traffic and needs no auth support of its own.
Does Authelia protect applications that have their own login?
It adds a layer in front, so an attacker must pass Authelia before reaching the application login at all. Most people then configure the application for trusted header authentication so users are not asked to log in twice, though that means trusting headers from the proxy.
What is the difference between Authelia and Authentik?
Authelia is lightweight, configured in YAML files, and focused on forward auth with OIDC support. Authentik is heavier, has a web administration interface and a database, and covers more identity provider features including SAML and user self-service. Authelia suits a homelab, Authentik suits something closer to an organisation.
Is putting SSO in front of a service enough to expose it to the internet?
It closes the largest hole, which is an unauthenticated application reachable from anywhere. It does not patch the application, protect against a vulnerability in Authelia or the proxy itself, or help if someone phishes a user. A VPN remains the stronger default for anything you do not need to share.
Why does the API path of my app break behind forward auth?
Because API clients cannot follow a browser redirect to a login page. The usual fix is to exclude API paths from the forward auth rule and authenticate them with a token instead, or to use a bypass policy for those paths with network restrictions.
Where should session and user data live?
Sessions in Redis and users in a file or LDAP backend. The file backend is fine for a handful of users and stores Argon2 password hashes. Using Redis rather than in-memory sessions means restarting Authelia does not log everyone out.