docs: deepen security/hardening batch to real deep-dive standard
Names real technology throughout (Vault, Authentik, pfSense, firewalld, Wazuh) instead of staying generic, adds a 'why this approach vs the alternative' section to every doc, cross-links heavily between docs instead of treating each system in isolation.
This commit is contained in:
8 files changed
+117
-100
No files matched your search
+22
-15
@@ -1,7 +1,18 @@
|
||||
# 🪪 SSO & Identity
|
||||
|
||||
> [!NOTE]
|
||||
> Every app that needs single sign-on registers against the identity provider once per reverse-proxy zone it's exposed on — and a real platform limitation shapes how superuser accounts authenticate.
|
||||
> 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`](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`](dns-naming.md) only works if the identity provider itself lives inside that same trust boundary.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| **Software** | Authentik, self-hosted |
|
||||
| **Pattern** | Provider + application pair registered once per reverse-proxy zone an app is exposed on |
|
||||
| **Native OIDC/SAML apps** | ~12 apps, where the upstream software supports it directly |
|
||||
| **Everything else** | Proxy-based forward-auth — SSO without the app needing to implement OIDC itself |
|
||||
|
||||
## Why proxy-based forward-auth, not just native OIDC everywhere
|
||||
|
||||
Most self-hosted apps don't implement OIDC or SAML themselves — a media manager or a bookmarking tool typically wasn't built with enterprise SSO in mind. Requiring native OIDC support fleet-wide would have meant leaving most apps without SSO at all. Authentik's proxy-provider forward-auth pattern sits in front of any app regardless of whether it understands OIDC, checking authentication at the reverse-proxy layer before a request ever reaches the app itself. The ~12 apps that *do* support native OIDC still use it — it's a better experience where available (no separate login redirect through the proxy) — but forward-auth is what makes SSO actually fleet-wide rather than limited to whichever apps happen to support it.
|
||||
|
||||
## Login flow — same shape, every app
|
||||
|
||||
@@ -9,40 +20,36 @@
|
||||
sequenceDiagram
|
||||
participant U as User
|
||||
participant App as App (browser-facing zone)
|
||||
participant IdP as Identity Provider (internal zone)
|
||||
participant IdP as Authentik (internal zone)
|
||||
|
||||
U->>App: Opens app
|
||||
App->>U: Redirect to identity provider
|
||||
App->>U: Redirect to Authentik
|
||||
U->>IdP: OAuth authorize/token exchange
|
||||
Note over IdP: Always happens on the internal zone,<br/>even though the app itself lives<br/>on the browser-facing zone
|
||||
IdP->>U: Success
|
||||
U->>App: Lands back on the app
|
||||
```
|
||||
|
||||
Every app-specific setting beyond this shape (token lifetime, scopes, flow type) is deliberately **not** generalized across apps — each app's own OIDC documentation gets read fresh rather than copying another app's provider config, since the details genuinely differ per vendor.
|
||||
This split matters more than it looks: the browser-facing zone enforces mutual TLS (see [`docs/dns-naming.md`](dns-naming.md)) and the app itself might live there, but the actual authentication handshake always happens against Authentik's internal-zone address. Getting this backwards — pointing the OIDC discovery/authority setting at the wrong zone — is a real, easy mistake when onboarding a new app, since the app's own login page is on the *other* zone and it's natural to assume the auth endpoint matches.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| **Registration pattern** | Provider + application pair per zone (up to 2 per app) |
|
||||
| **Native OIDC/SAML apps** | ~12 apps, where the upstream supports it directly |
|
||||
| **Everything else** | Proxy-based forward-auth |
|
||||
Every app-specific setting beyond this shape (token lifetime, scopes, flow type) is deliberately **not** generalized across apps — each app's own OIDC documentation gets read fresh rather than copying another app's provider config, since the details genuinely differ per vendor and copying blind is how subtle auth bugs get introduced.
|
||||
|
||||
## A real limitation, documented rather than hidden
|
||||
## A real platform limitation, worked around rather than ignored
|
||||
|
||||
> [!WARNING]
|
||||
> LDAP-based login through the identity provider's own outpost **isn't viable for the primary superuser account** — the outpost hides superuser accounts from LDAP directory search, so any app requiring a search-then-bind flow can never authenticate that account through LDAP. The two workarounds — a second non-admin account, or demoting the primary account from superuser — were both rejected as worse than the problem. Header-based forward-auth is used instead for apps in that situation.
|
||||
> Authentik's own LDAP outpost **hides superuser accounts from LDAP directory search** — so any app requiring a search-then-bind LDAP flow (a media server's user-management plugin, for instance) can never authenticate the fleet's primary superuser account through LDAP, full stop. Two workarounds were on the table: create a second, non-admin account just for LDAP-dependent apps, or demote the primary account from superuser entirely. Both were rejected — a second account fragments identity across the fleet in a way that undermines the whole point of centralized SSO, and demoting the primary account breaks unrelated admin workflows that depend on superuser rights. **Header-based forward-auth is used instead** for any app in this situation: Authentik's proxy layer authenticates the user and passes identity via HTTP headers, sidestepping the LDAP search-then-bind requirement entirely rather than working around Authentik's own limitation inside LDAP itself.
|
||||
|
||||
## Git access: least privilege, not the admin account
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
CI["CI / Automation"] -->|scoped write| Bot["Dedicated bot account<br/>non-admin"]
|
||||
Bot --> Repo["Git server"]
|
||||
CI["CI / Orchestration engine<br/>see docs/orchestration.md"] -->|scoped write| Bot["Dedicated bot account<br/>non-admin"]
|
||||
Bot --> Repo["Gitea"]
|
||||
Other["Other hosts"] -->|read-only| Deploy["Deploy keys<br/>per host"]
|
||||
Deploy --> Repo
|
||||
```
|
||||
|
||||
CI and automation pushes 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. A compromised automation credential is never an admin credential.
|
||||
This is a smaller decision than the SSO model above, but the same principle: automation should never run with more privilege than it needs. CI and orchestration pushes (the flows described in [`docs/orchestration.md`](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."
|
||||
|
||||
---
|
||||
Related stacks: [`stacks/identity-and-security/authentik/`](../stacks/identity-and-security/authentik/), [`stacks/platform/gitea/`](../stacks/platform/gitea/)
|
||||
Related: [`docs/dns-naming.md`](dns-naming.md), [`docs/orchestration.md`](orchestration.md), [`docs/secrets-and-pki.md`](secrets-and-pki.md) · Stacks: [`stacks/identity-and-security/authentik/`](../stacks/identity-and-security/authentik/), [`stacks/platform/gitea/`](../stacks/platform/gitea/)
|
||||
Reference in new issue
Block a user