Skip to content

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)"]
Hold "Alt" / "Option" to enable pan & zoom
  • URL


    auth.hdhomelab.com

  • 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:

ou=<app>,DC=ldap,DC=goauthentik,DC=io

The service account bind DN then becomes:

cn=<app>_service,ou=goauthentik.io/service-accounts,ou=<app>,DC=ldap,DC=goauthentik,DC=io


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:

myapp = {
  name = "My App"
  type = "oidc"
  groups = {
    myapp_user = {
      user_names = ["hd", "datui"]
      bind_order = 10
    }
  }
  redirect_uris = [{
    matching_mode = "strict"
    url           = "https://myapp.hdhomelab.com/callback"
  }]
}
myapp = {
  name            = "My App"
  type            = "ldap"
  base_dn         = "ou=myapp,DC=ldap,DC=goauthentik,DC=io"
  meta_launch_url = "https://myapp.hdhomelab.com"
  meta_publisher  = "My App"
  groups = {
    myapp_users = {
      user_names = ["hd", "datui"]
      bind_order = 0
    }
  }
}
Apply changes
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
Hold "Alt" / "Option" to enable pan & zoom
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:

groups:
  - jellyfin_user
  - miniflux_user
attributes:
  jellyfin_libraries: cn
groups:
  - jellyfin_user
  - miniflux_user
attributes:
  jellyfin_libraries: en
groups:
  - jellyfin_user
  - audiobookshelf_user
  - gitea_user
  - headlamp_viewer
  - grafana_viewer
  - jenkins_user
  - kavita_download
  - miniflux_user
  - synology_user
  - vikunja_user
  - shelfarr_user
attributes:
  jellyfin_libraries: cn
groups:
  - jellyfin_user
  - audiobookshelf_user
  - gitea_user
  - headlamp_viewer
  - grafana_viewer
  - jenkins_user
  - kavita_download
  - miniflux_user
  - synology_user
  - vikunja_user
  - shelfarr_user
attributes:
  jellyfin_libraries: en
groups:
  - jellyfin_user
  - gitea_user
  - headlamp_viewer
  - grafana_viewer
  - jenkins_user
  - miniflux_user
attributes:
  jellyfin_libraries: cn
groups:
  - jellyfin_user
  - gitea_user
  - headlamp_viewer
  - grafana_viewer
  - jenkins_user
  - miniflux_user
attributes:
  jellyfin_libraries: en

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
Hold "Alt" / "Option" to enable pan & zoom
Order Stage Purpose
1 Identification Accept email or username; always pretends user exists to prevent enumeration
2 Email 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
Hold "Alt" / "Option" to enable pan & zoom

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.