Authentik¶
Authentik is the identity provider for the homelab, available at auth.hdhomelab.com. It handles SSO for all services via OIDC and LDAP, and is the source of truth for users, groups, and access policies.
Deployment¶
Authentik runs in Kubernetes (infra namespace) via the official Helm chart, managed by Flux CD. It uses an external PostgreSQL database on the NAS and sends email via Gmail SMTP.
graph LR
User --> Traefik
Traefik --> authentik-server
authentik-server --> PostgreSQL["PostgreSQL (NAS)"]
authentik-server --> Gmail["Gmail SMTP"]
authentik-worker --> PostgreSQL["PostgreSQL (NAS)"]
-
URL
-
Namespace
infra -
Database
PostgreSQL on NAS (
192.168.68.7) -
Email
Gmail SMTP (
smtp.gmail.com:587) -
Helm chart
charts.goauthentik.io/authentik -
Config
flux/infrastructure/base/authentik/
Authentication Protocols¶
Used by apps that support OAuth2/OpenID Connect — e.g. Grafana, Headlamp, Gitea, Jellyseerr.
Each OIDC app gets a Provider + Application pair managed by the authentik-oidc OpenTofu module. The module handles client credentials, redirect URIs, group-based access policies, and optional custom scope mappings.
Used by apps that only support LDAP — e.g. Jellyfin, Emby.
Each LDAP app gets a dedicated provider with a unique, app-scoped Base DN to avoid outpost routing conflicts.
Use unique Base DNs per app
Sharing a Base DN across LDAP providers causes non-deterministic outpost routing. Each app must have its own scoped DN:
The service account bind DN then becomes:
Configuration¶
All Authentik resources — users, groups, applications, providers, flows, policies — are managed as code via OpenTofu in tofu/tf-deploy/authentik/.
tofu/tf-deploy/authentik/
├── locals.tf # All applications, users, and group memberships
├── main.tf # Module calls for OIDC and LDAP apps
├── avatars.tf # Avatar upload support + system settings
├── mfa_email.tf # Email OTP authenticator stage
├── recovery.tf # Password recovery flow
├── enrollment.tf # Invitation-based enrollment flow + library flag groups
└── meta.tf # Provider config (Vault token, Authentik URL)
Adding an Application¶
All apps are declared in locals.tf under the applications map. Add an entry and run tofu apply:
cd tofu/tf-deploy/authentik
tofu init -backend-config=backend.pg.tfbackend
tofu plan -out plan.out
tofu apply plan.out
User Onboarding¶
New users are onboarded via an invitation-based enrollment flow. The admin's only action is creating an invitation — the user self-registers and is automatically assigned to the correct groups.
Flow¶
graph LR
A[Admin creates invitation] --> B[Shares link with user]
B --> C[Invitation validated]
C --> D[User fills in details]
D --> E[Account created]
E --> F[Groups assigned]
F --> G[Auto login]
style G fill:#27ae60,stroke:#1e8449,color:#fff
| Order | Stage | Purpose |
|---|---|---|
| 1 | Invitation | Validates token; blocks anonymous access |
| 2 | Prompt | Collects username, name, email, password |
| 3 | User write | Creates user as internal, active |
| 4 | User login | Assigns groups from invitation, then logs in |
Creating an Invitation¶
Go to Authentik Admin → Directory → Invitations → Create.
| Field | Value |
|---|---|
| Flow | invitation-enrollment |
| Attributes | JSON groups to assign (see below) |
Set Attributes based on what the user needs access to:
The enrollment flow assigns the listed groups at login. On first Jellyfin login, jellyfin-librarian reads jellyfin_libraries from the user's attributes and sets the correct library folders automatically.
Implementation¶
Defined in tofu/tf-deploy/authentik/enrollment.tf:
| Resource | Purpose |
|---|---|
authentik_flow.enrollment |
Flow with enrollment designation, slug invitation-enrollment |
authentik_stage_invitation.enrollment |
Validates token; rejects anonymous access |
authentik_stage_prompt.enrollment |
Collects username, name, email, password |
authentik_stage_user_write.enrollment |
Creates internal, active users |
authentik_stage_user_login.enrollment |
Logs in after creation |
authentik_policy_expression.enrollment_group_assignment |
Reads prompt_data["groups"] and assigns user to those groups |
Policy evaluation timing
The group assignment policy is bound to the login stage binding with evaluate_on_plan = false and re_evaluate_policies = true. This is required so the policy runs after the invitation stage has populated the flow context — not during the initial flow planning phase.
Password Recovery¶
Users can reset their password via the Forgot password? link on the login page. The flow is fully managed in tofu/tf-deploy/authentik/recovery.tf and sends a reset link via email.
Flow Stages¶
graph LR
A[Forgot password?] --> B[Enter email or username]
B --> C[Reset email sent]
C --> D[Set new password]
D --> E[Logged in]
style E fill:#27ae60,stroke:#1e8449,color:#fff
| Order | Stage | Purpose |
|---|---|---|
| 1 | Identification | Accept email or username; always pretends user exists to prevent enumeration |
| 2 | Send password_reset.html with a 30-minute token |
|
| 3 | Password prompt | Collect new password + confirmation |
| 4 | User write | Write new password; never creates new users |
| 5 | User login | Automatically log in after successful reset |
Login page integration
The recovery flow is linked to the built-in default-authentication-identification stage (imported via import {} block). This makes the Forgot password? link appear on the login page for all apps using the default authentication flow.
Gmail SMTP¶
Authentik sends email (password resets, MFA codes) via Gmail SMTP. The configuration lives in the HelmRelease at flux/infrastructure/base/authentik/helmrelease.yaml.
| Setting | Value |
|---|---|
| Host | smtp.gmail.com |
| Port | 587 |
| TLS | Enabled (STARTTLS) |
| SSL | Disabled |
| Credentials | authentik-smtp-credentials secret (from Vault) |
App password required
Gmail requires an App Password — not your regular account password. Generate one under Google Account → Security → 2-Step Verification → App passwords and store it in Vault at infra/authentik/smtp.
Avatar Pattern¶
Authentik's default settings page doesn't include avatar upload. This pattern adds a file-upload field and a delete checkbox to the user settings flow, storing the image as a base64 data URI directly in the user's attributes.
How It Works¶
graph TD
A[User submits settings form] --> B{avatar_reset checked?}
B -- yes --> C[Clear user.attributes.avatar]
B -- no --> D{File uploaded?}
D -- no --> E[Leave avatar unchanged]
D -- yes --> F{MIME type valid?}
F -- no --> G[Return error]
F -- yes --> H[Store data URI in user.attributes.avatar]
style C fill:#e74c3c,stroke:#c0392b,color:#fff
style H fill:#27ae60,stroke:#1e8449,color:#fff
style G fill:#e67e22,stroke:#d35400,color:#fff
The avatar chain in Authentik system settings resolves left to right:
| Priority | Source | Condition |
|---|---|---|
| 1 | user.attributes["avatar"] |
Set by expression policy on upload |
| 2 | Gravatar | Matched by email hash |
| 3 | Initials | Always available as final fallback |
Key Design Decisions¶
Field key prefix rule
Authentik's UserWriteStage writes any prompt_data key prefixed with attributes. directly to user.attributes. Fields without this prefix are ignored.
The avatar fields use unprefixed keys (avatar_upload, avatar_reset) so the expression policy is the sole writer to user.attributes["avatar"] — preventing an empty file submission from silently overwriting an existing avatar.
No filesystem storage
Authentik 2026.x stores base64 data URIs from file prompt fields natively in the database. No NFS volume or media directory is needed for avatar data.
Expression Policy Behaviour¶
| Condition | Action |
|---|---|
avatar_reset checked |
Remove user.attributes["avatar"]; fallback chain takes over |
| No file selected | Leave existing avatar untouched |
| File selected, invalid MIME | Return error message to user |
| File selected, valid MIME | Store data URI in user.attributes["avatar"] |
Implementation¶
Defined in tofu/tf-deploy/authentik/avatars.tf:
| Resource | Purpose |
|---|---|
authentik_system_settings |
Sets avatar chain to attributes.avatar,gravatar,initials |
authentik_stage_prompt_field.avatar_upload |
File input (type = "file", field_key = "avatar_upload") |
authentik_stage_prompt_field.avatar_reset |
Delete checkbox (type = "checkbox", field_key = "avatar_reset") |
authentik_policy_expression.avatar |
Expression policy — sole writer to user.attributes["avatar"] |
authentik_stage_prompt.user_settings |
Built-in settings stage, imported and extended with new fields |
Note
The built-in default-user-settings prompt stage is imported into Tofu state using an import {} block, allowing it to be managed without recreating it.