Files
public/docs/sso-and-identity.md
T
thesabear 3e91e426d1 sso-and-identity: verify against live-checked KB, expand real depth, add evidence
Source Trilium notes (Authentik/SSO Integration, Gitea Access Model)
live-verified and stamped this session - authentik_api_token confirmed
in vault.yml, automation-bot confirmed as a real non-admin Gitea account
via the API.

Cut the redundant Mermaid diagram (prose right below it said the same
thing). Restored the LDAP/superuser-outpost exception from the source
note as a compact callout rather than a full section, proportionate to
its actual weight. Added the real reasoning for centralizing auth and
the proxy-layer defense-in-depth argument, a table of the ~9 apps
running real native OIDC/SAML on top of the universal Proxy Provider
baseline (corrected after initially misstating it as either/or), the
provider-removal discipline, and two screenshots proving the Write/
Read-only access split instead of just asserting it.
2026-08-26 11:54:29 +03:00

6.5 KiB

🪪 SSO & Identity

Note

Authentik is the identity provider for this fleet — self-hosted rather than an external IdP, for the same reason Vault is self-hosted in docs/secrets-and-pki.md: internal-only apps shouldn't need to trust a third party just to authenticate a login, and the two-zone reverse-proxy model in docs/dns-naming.md only works if the identity provider itself lives inside that same trust boundary.

Why centralize authentication at all

A homelab with dozens of self-hosted apps means dozens of different auth implementations if every app handles its own login — wildly inconsistent quality, no shared MFA, and revoking access means hunting down credentials app by app instead of doing it in one place. Centralizing on one identity provider fixes all three: every app's login gets the same enforcement regardless of how good or bad that app's own native auth actually is, and killing access anywhere starts with one action, not a checklist.

The proxy layer earns its own keep on top of that, though — it protects apps that don't have a login of their own at all. Not every self-hosted tool ships with real authentication; some are built assuming they'll only ever be reached from an already-trusted network and skip it entirely. Registering a Proxy Provider in front of an app like that puts Authentik's forward-auth check at the reverse-proxy hop, before the request ever reaches the app itself — so an app with zero security of its own still sits behind a real login, enforced by something other than the app. For apps that already have native OIDC, the proxy layer isn't doing the authenticating (the app does its own token exchange) — but for everything else, it's the only auth check that exists, period.

Every app gets the Proxy Provider registration described above — no exceptions, that's the universal baseline. A handful of apps get a second, additional layer on top of it: real native OIDC/SAML support for their own login page, used alongside the Proxy Provider rather than replacing it:

App Stack
Gitea stacks/platform/gitea/
Immich stacks/media/immich/
Nextcloud stacks/productivity/nextcloud/
Paperless-ngx stacks/productivity/paperless-ngx/
Grafana stacks/platform/grafana-stack/
Audiobookshelf stacks/media/audiobookshelf/
Dockhand stacks/platform/dockhand/
Proxmox / PBS hypervisor-level, not a docker stack

Every other app runs on the Proxy Provider alone — which, per the point above, is never a weaker path: it's the same forward-auth check either way, just without a second, app-native login layered on top of it.

Tip

Removal is a real, deliberate step, not an afterthought. Decommissioning an app means deleting the Application (by slug), then the Proxy Provider, then detaching that provider from the outpost's own provider list, then removing its icon file — in that order. Skipping the outpost-detach step is the easy mistake: the Application and Provider both look gone, but the outpost still references a provider that no longer resolves to anything, and that orphaned reference doesn't surface as an error anywhere obvious.

Dual-zone registration

Every new app gets a Proxy Provider + Application pair registered per reverse-proxy zone it's actually exposed on — up to two registrations for the same app, one per zone from docs/dns-naming.md, driven by actual need rather than registering every app on both zones by default.

OIDC login: the redirect crosses zones, every time

For an app using native OIDC, the login always follows the same path:

  1. User opens the app on the browser-facing zone.
  2. The app redirects the browser to Authentik to do the actual OAuth authorize/token exchange.
  3. That exchange happens against Authentik's own address on the service-to-service zone — reachable from a browser for this one hop specifically, even though that zone doesn't get the mutual-TLS treatment the browser-facing zone does.
  4. On success, the browser lands back on the app, still on the browser-facing zone.

So an app's OIDC discovery/authority setting always points at the service-to-service zone, even though the app itself is normally reached on the browser-facing zone — the two zones are defined in docs/dns-naming.md. This is confirmed against one real app's actual login flow, not assumed to hold for every app without checking. Every other app-specific setting (token lifetime, scopes, flow type) doesn't generalize the same way — each app's own OIDC documentation gets read fresh rather than copying another app's provider config, since those details genuinely differ per vendor.

Note

Not every app can take the OIDC path above. Some — Jellyfin's auth plugin among them — only support LDAP: search the directory, then bind as whatever matched. Authentik's own LDAP outpost hides superuser accounts from that search by design, so the account that matters most can't authenticate this way for an app that requires it. The fix wasn't a second account or demoting the superuser — it was skipping LDAP for that app and reusing the same header-based forward-auth pattern the proxy-provider apps already use.

Git access: least privilege, not the admin account

Automation should never run with more privilege than it needs. CI and orchestration pushes (the flows described in docs/orchestration.md) run through a dedicated, non-admin bot account with scoped write access — never through an administrator identity. Hosts that only need to read get separate read-only deploy keys instead of any shared credential. The practical payoff: if an automation credential ever leaks, the blast radius is "can push to specific repos," not "has full admin control of the Git server."

Gitea repo collaborators: automation-bot with Write access, not an admin account

Gitea repo deploy keys: a host-specific key scoped to Read only


Related: docs/dns-naming.md, docs/orchestration.md, docs/secrets-and-pki.md · Stacks: stacks/identity-and-security/authentik/, stacks/platform/gitea/