mrwick/notebook

drag or arrow keys to move · scroll to zoom · esc to exit

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:

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