One VPS, one login
My VPS runs more than a dozen web apps. Each app had its own login page, its own password, and its own idea of security. I wanted one login for all of them.
The fix is Authelia behind Caddy. Caddy asks Authelia about every request. If the visitor has no session, Authelia shows its login page. After one login, a cookie on the parent domain covers every app.
The Caddy side
One snippet holds the check. Every private site imports it.
(sso) {
forward_auth authelia:9091 {
uri /api/authz/forward-auth
copy_headers Remote-User Remote-Groups Remote-Email Remote-Name
}
}
auth.example.com {
reverse_proxy authelia:9091
}
status.example.com {
import sso
reverse_proxy gatus:8080
}
A new service costs two lines: import sso and a reverse_proxy.
The Authelia side
The access rules start from deny. A hostname that nobody lists gets a 403,
not an open door.
access_control:
default_policy: 'deny'
rules:
- domain: 'auth.example.com'
policy: 'bypass'
- domain: ['status.example.com', 'rss.example.com']
policy: 'one_factor'
session:
cookies:
- domain: 'example.com'
authelia_url: 'https://auth.example.com'
Apps that can read the user
copy_headers passes Remote-User and Remote-Email to the app. Some apps
trust these headers and skip their own login. FreshRSS reads Remote-User.
Firefly III reads Remote-Email. The result is one login, not two.
The trap: header login that trusts anyone
One app offered a header-login mode. In the version I ran, the app
accepted a forged Remote-User header from any source. Its “trusted proxies”
setting did not limit that. Anybody who could reach the app directly could log
in as anybody.
I left that mode off. The app also speaks OpenID Connect, and Authelia can act as an OIDC provider. The app now shows a “Continue with SSO” button. The login goes through Authelia, and no header is trusted.
One more detail: the app read the username from the ID token, not from the userinfo endpoint. By default Authelia puts only the minimum claims in the ID token, so the app named the new user after an opaque ID. A claims policy fixed it:
claims_policies:
id_token_profile:
id_token: ['email', 'preferred_username', 'name', 'groups']
What stays outside
Not every path can sit behind a login page. A phone app cannot click through a web login. So these paths stay open, each with its own authentication:
- The DNS-over-HTTPS endpoint, because DNS clients do not log in
- The password manager API, because its master password encrypts the vault
- The RSS API, which phone readers use with an app password
Caddy handles this with a handle block per path. The open path comes first,
and everything else falls through to import sso.
What I learned
- Start from
default_policy: deny. A forgotten hostname then fails closed. - Test header login with a forged header before you trust it.
- Authelia keeps sessions in memory by default. A restart logs everyone out.