Files
infrapuzzle/k8s/auth/USER_MANAGEMENT.md
T

27 KiB
Raw Blame History

Authentik User Management: Complete Roundup & Operations Guide

This guide covers everything you need to know about managing users in Authentik for the Haumdaucher infrastructure (https://auth.haumdaucher.de): from manual creation and invitation workflows to Terraform IaC, custom email branding, fine-grained access control, handling custom attributes (e.g., Vereins-Mitgliedsnummer, second email), and bulk-importing users from an Excel / CSV spreadsheet.


📑 Table of Contents

  1. Executive Summary: Quick Answers
  2. Method 1: Manual Web UI & Invitation Links
  3. Method 2: User Provisioning via Terraform
  4. Method 3: Bulk Import from Excel / CSV (Python Script)
  5. Customizing Registration & Onboarding Emails
  6. Access Control & Authorization Policies (Limiting App Access)
  7. Custom Attributes & OIDC Claim Mappings
  8. Production Checklist & Best Practices

⚡ Executive Summary: Quick Answers

Question Answer & Key Mechanism
How to add new users? 4 Ways: (1) Admin Web UI (Directory > Users), (2) Invitations flow (Directory > Invitations - recommended for self-service), (3) Terraform (authentik_user resource), or (4) REST API / Python script for bulk imports.
Can this be done in Terraform? Yes. Use resource "authentik_user" in terraform/authentik.tf. Supports custom attributes via jsonencode(), group assignments, and active status.
Can I change the email sent to the user? Yes. Configured in Flows & Stages > Stages > Email Stage. You can customize the subject line, token expiration, and HTML template (written in Django template syntax with access to user fields and custom attributes).
Can I limit what a user has access to? Yes. Through Policy Bindings attached to Applications: Group Membership Policies (e.g., infra-admins vs users), IP/Geo Policies, or Python Expression Policies checking custom attributes (e.g., only active club members).
How to handle Excel tables with custom attributes? Authentik stores arbitrary key-value pairs in user.attributes (JSON). You can import an Excel/CSV file with a Python script or Terraform csvdecode(), map fields like haumdaucher_mitgliedsnummer or second_email, and expose them to downstream apps (Nextcloud, Grafana, Gitea) via OIDC Scope Mappings.

Authentik offers two UI methods to onboard users:

Option A: Direct User Creation (Admin sets password or triggers reset)

  1. Go to https://auth.haumdaucher.de and log in with your admin account.
  2. Navigate to Directory > Users and click Create.
  3. Fill in:
    • Username: max.mustermann
    • Name: Max Mustermann
    • Email: max@example.com
    • Type: Internal
    • Groups: Select groups (e.g., users, verein-mitglieder).
    • Attributes (YAML/JSON):
      {
        "second_email": "max.privat@gmail.com",
        "haumdaucher_mitgliedsnummer": "HM-2024-042",
        "phone": "+49 170 1234567"
      }
      
  4. Click Create.
  5. To set credentials: Click on the user -> Actions -> Set Password (or Explicitly trigger password reset flow so the user receives a link via email).


⚠️ Troubleshooting: "Flow does not apply to current user"

If you encounter this screen when opening an invite link:

"Flow does not apply to current user."

Why this happens:

  1. Wrong Flow Selected (default-source-enrollment): In your URL (/if/flow/default-source-enrollment/?itoken=...), the flow used was default-source-enrollment. That flow is strictly designed for external identity providers (OAuth/SAML like Google/GitHub) and binds a policy that checks for an active external source in the context. When accessed directly with an ?itoken=..., Authentik evaluates this policy, fails, and displays this error.
  2. Missing Invitation Stage: Standard enrollment flows reject direct token access unless an Invitation Stage is bound to the beginning of the flow to validate ?itoken=....
  3. Active Admin Session: If an administrator is currently logged in to the browser and clicks the enrollment link, flows with an "unauthenticated-only" policy will reject the session. Always test invitation URLs in an Incognito / Private window.

Solution: The Haumdaucher Custom Invitation Flow (haumdaucher-invitation-flow)

We provision a dedicated, streamlined flow in terraform/authentik.tf:

graph LR
    Link[Invite Link with ?itoken=...] --> Stage1[1. Invitation Stage<br/>Validates Token]
    Stage1 --> Stage2[2. Prompt Stage<br/>User inputs Username, Name, Password]
    Stage2 --> Stage3[3. User Write Stage<br/>Writes to DB & assigns Groups]
    Stage3 --> Stage4[4. User Login Stage<br/>Logs user into session]
    Stage4 --> Stage5[5. Redirect Stage<br/>Redirects to haumdaucher.de]

Flow Behavior (Tailored to your setup):

  • User Inputs: Prompts the user to choose their preferred Username, Full Name, and Password (with confirmation).
  • Email & Custom Attributes: Pre-filled from the admin invitation (e.g. haumdaucher_mitgliedsnummer, second_email).
  • MFA / 2FA: Optional. The user registers immediately with their password and can enroll Passkeys/TOTP anytime in their profile settings.
  • Automatic Redirect: Upon completing registration, the user is automatically logged in and redirected to https://haumdaucher.de.

How to Create an Invitation using the Custom Flow

  1. Go to https://auth.haumdaucher.de > Directory > Invitations.
  2. Click Create (or New Invitation).
  3. Set the fields:
    • Name: Invite - Max Mustermann
    • Expires: Set expiration (e.g., 7 days).
    • Flow: Select Haumdaucher Invitation Flow (haumdaucher-invitation-flow).
    • Single use: Checked (true).
    • Fixed Data / Attributes (JSON):
      {
        "email": "max@example.com",
        "haumdaucher_mitgliedsnummer": "HM-2024-042",
        "second_email": "max.privat@gmail.com"
      }
      
  4. Click Create.
  5. Copy the generated URL:
    https://auth.haumdaucher.de/if/flow/haumdaucher-invitation-flow/?itoken=<generated-token>
    
  6. Send the link to the user. When they open it, they will see the branded Haumdaucher registration form asking for their Username, Name, and Password!

🏗️ Method 2: User Provisioning via Terraform

You can manage users, groups, and permissions directly in terraform/authentik.tf.

1. Single User Definition

# Group for club members
resource "authentik_group" "verein_mitglieder" {
  name         = "verein-mitglieder"
  is_superuser = false
}

# User with custom attributes
resource "authentik_user" "max_mustermann" {
  username   = "max.mustermann"
  name       = "Max Mustermann"
  email      = "max@example.com"
  type       = "internal"
  is_active  = true
  groups     = [
    authentik_group.users.id,
    authentik_group.verein_mitglieder.id
  ]
  attributes = jsonencode({
    "second_email"                 = "max.privat@gmail.com",
    "haumdaucher_mitgliedsnummer" = "HM-2024-042",
    "eintrittsdatum"               = "2024-01-15",
    "status"                       = "aktiv"
  })
}

2. Batch Provisioning from CSV in Terraform

If you maintain users in a CSV file (terraform/users.csv):

username,name,email,second_email,mitgliedsnummer,is_admin
max.mustermann,Max Mustermann,max@example.com,max.privat@gmail.com,HM-2024-042,false
anna.schmidt,Anna Schmidt,anna@example.com,anna.work@company.de,HM-2024-043,true

In terraform/authentik.tf:

locals {
  csv_users = csvdecode(file("${path.module}/users.csv"))
}

resource "authentik_user" "csv_members" {
  for_each = { for u in local.csv_users : u.username => u }

  username   = each.value.username
  name       = each.value.name
  email      = each.value.email
  type       = "internal"
  is_active  = true
  groups = compact([
    authentik_group.users.id,
    authentik_group.verein_mitglieder.id,
    each.value.is_admin == "true" ? authentik_group.infra_admins.id : null
  ])
  attributes = jsonencode({
    "second_email"                 = each.value.second_email,
    "haumdaucher_mitgliedsnummer" = each.value.mitgliedsnummer
  })
}

⚖️ Trade-offs: When to use Terraform for Users

  • Pros: Version-controlled in Git, declarative, reproducible, ideal for admins and service accounts (ai_assistant).
  • Cons / Considerations: Terraform cannot trigger interactive password onboarding emails out of the box; passwords must be set via recovery flow or first login reset.

📊 Method 3: Bulk Import from Excel / CSV (Python Script)

If you have an Excel file (mitglieder.xlsx) or CSV, using Authentik's REST API is the cleanest approach because it can create users, attach custom attributes, and automatically email them an invitation link to set their own password.

Ready-to-Run Python Import Script

Save this script (e.g. k8s/auth/scripts/import_users_excel.py):

#!/usr/bin/env python3
"""
Bulk User Importer for Authentik (auth.haumdaucher.de)
Reads an Excel (.xlsx) or CSV file and creates users with custom attributes and invitation links.
"""

import sys
import os
import requests
import pandas as pd

# Configuration
AUTHENTIK_URL = os.getenv("AUTHENTIK_URL", "https://auth.haumdaucher.de")
AUTHENTIK_TOKEN = os.getenv("AUTHENTIK_TOKEN")  # API Token (e.g. ai_assistant_token)

if not AUTHENTIK_TOKEN:
    print("❌ Error: AUTHENTIK_TOKEN environment variable is required.")
    sys.exit(1)

HEADERS = {
    "Authorization": f"Bearer {AUTHENTIK_TOKEN}",
    "Content-Type": "application/json",
    "Accept": "application/json"
}

def get_or_create_group(group_name: str) -> str:
    """Fetch group ID or create if it doesn't exist."""
    res = requests.get(f"{AUTHENTIK_URL}/api/v3/core/groups/?name={group_name}", headers=HEADERS)
    res.raise_for_status()
    results = res.json().get("results", [])
    if results:
        return results[0]["pk"]
    
    # Create group
    create_res = requests.post(f"{AUTHENTIK_URL}/api/v3/core/groups/", headers=HEADERS, json={"name": group_name})
    create_res.raise_for_status()
    return create_res.json()["pk"]

def import_users_from_file(file_path: str, send_invitation: bool = True):
    # Read Excel or CSV
    if file_path.endswith((".xlsx", ".xls")):
        df = pd.read_excel(file_path)
    else:
        df = pd.read_csv(file_path)

    # Standardize column names (lowercase & stripped)
    df.columns = [c.strip().lower() for c in df.columns]

    print(f"📋 Found {len(df)} rows to process.")
    
    users_group_pk = get_or_create_group("users")
    verein_group_pk = get_or_create_group("verein-mitglieder")

    for idx, row in df.iterrows():
        name = str(row.get("name", "")).strip()
        email = str(row.get("email", "")).strip()
        username = str(row.get("username", "")).strip() or email.split("@")[0].lower()
        second_email = str(row.get("second_email", row.get("zweite_email", ""))).strip()
        mitgliedsnummer = str(row.get("mitgliedsnummer", row.get("verein_mitgliedsnummer", ""))).strip()

        if not email or email == "nan":
            print(f"⚠️ Skipping row {idx+1}: missing email")
            continue

        # Prepare user payload with custom attributes
        user_payload = {
            "username": username,
            "name": name if name != "nan" else username,
            "email": email,
            "type": "internal",
            "is_active": True,
            "groups": [users_group_pk, verein_group_pk],
            "attributes": {
                "second_email": second_email if second_email != "nan" else "",
                "haumdaucher_mitgliedsnummer": mitgliedsnummer if mitgliedsnummer != "nan" else "",
                "source": "excel_import"
            }
        }

        # 1. Create or Update User in Authentik
        res = requests.get(f"{AUTHENTIK_URL}/api/v3/core/users/?username={username}", headers=HEADERS)
        res.raise_for_status()
        existing = res.json().get("results", [])

        if existing:
            user_pk = existing[0]["pk"]
            patch_res = requests.patch(f"{AUTHENTIK_URL}/api/v3/core/users/{user_pk}/", headers=HEADERS, json=user_payload)
            patch_res.raise_for_status()
            print(f"🔄 Updated existing user: {username} ({email})")
        else:
            create_res = requests.post(f"{AUTHENTIK_URL}/api/v3/core/users/", headers=HEADERS, json=user_payload)
            if not create_res.ok:
                print(f"❌ Failed to create {username}: {create_res.text}")
                continue
            user_pk = create_res.json()["pk"]
            print(f"✅ Created user: {username} ({email}) [ID: {user_pk}]")

        # 2. Generate Invitation / Password Reset Link
        if send_invitation:
            invitation_payload = {
                "name": f"Onboarding {username}",
                "single_use": True,
                "fixed_data": {
                    "email": email,
                    "username": username
                }
            }
            inv_res = requests.post(f"{AUTHENTIK_URL}/api/v3/stages/invitation/invitations/", headers=HEADERS, json=invitation_payload)
            if inv_res.ok:
                inv_key = inv_res.json()["pk"]
                link = f"{AUTHENTIK_URL}/if/flow/default-enrollment-flow/?itoken={inv_key}"
                print(f"   ✉️ Invitation Link: {link}")

if __name__ == "__main__":
    if len(sys.argv) < 2:
        print("Usage: python3 import_users_excel.py <path-to-excel-or-csv>")
        sys.exit(1)
    import_users_from_file(sys.argv[1])

✉️ Customizing Registration & Onboarding Emails

When an admin creates a user or triggers an enrollment/password recovery, Authentik sends transactional emails. You can customize their sender address, subject line, and full HTML appearance.

1. SMTP & Sender Settings

Authentik sends emails using the SMTP configuration defined in k8s/auth/authentik.secret.values.yaml or global environment variables:

  • AUTHENTIK_EMAIL__HOST: smtp.gmail.com (or Google Workspace / OVH relay)
  • AUTHENTIK_EMAIL__PORT: 587
  • AUTHENTIK_EMAIL__USE_TLS: true
  • AUTHENTIK_EMAIL__USERNAME: service@haumdaucher.de
  • AUTHENTIK_EMAIL__PASSWORD: <app-password>
  • AUTHENTIK_EMAIL__FROM: HAUMDAUCHER Identity <service@haumdaucher.de>

2. Customizing Email Stages in Authentik

Authentik controls outgoing emails via Email Stages:

  1. Open Admin UI: Flows & Stages > Stages.
  2. Locate the relevant email stage:
    • default-password-reset-email (for password recovery & manual user activation)
    • default-enrollment-email / invitation-email (for new user invitations)
    • default-user-settings-password-reset-email
  3. Click Edit on the stage:
    • Subject: Willkommen bei Haumdaucher e.V. – Aktiviere deinen Zugang
    • Token Expiry: Minutes=1440 (24 hours) or Minutes=10080 (7 days)
    • Template: Select standard template or your custom template path.

3. Customizing the HTML Email Template (Django Syntax)

Authentik email templates use Django template syntax. Available variables in email templates include:

  • {{ user.name }} / {{ user.username }} / {{ user.email }}
  • {{ user.attributes.haumdaucher_mitgliedsnummer }}
  • {{ url }} (The activation / password reset link)
  • {{ token_expires }} (Expiry timestamp)
  • {{ brand.branding_title }} (HAUMDAUCHER)

Example Custom Haumdaucher Welcome Template

<!DOCTYPE html>
<html>
<head>
  <style>
    body { font-family: 'Outfit', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background-color: #0d0d12; color: #f0f0f5; margin: 0; padding: 40px; }
    .card { background: #181824; border: 1px solid rgba(212, 175, 55, 0.3); border-radius: 16px; max-width: 540px; margin: 0 auto; padding: 32px; box-shadow: 0 12px 30px rgba(0,0,0,0.5); }
    .header { text-align: center; margin-bottom: 24px; }
    .logo { height: 60px; }
    h1 { color: #d4af37; font-size: 22px; margin-top: 12px; }
    p { color: #c0c0d0; line-height: 1.6; font-size: 15px; }
    .btn { display: block; text-align: center; background: linear-gradient(135deg, #d4af37, #aa8520); color: #000; font-weight: bold; text-decoration: none; padding: 14px 24px; border-radius: 10px; margin: 28px 0; }
    .meta { background: rgba(255,255,255,0.05); border-radius: 8px; padding: 12px 16px; font-size: 13px; color: #a0a0b0; }
    .footer { text-align: center; font-size: 12px; color: #606070; margin-top: 30px; }
  </style>
</head>
<body>
  <div class="card">
    <div class="header">
      <img class="logo" src="https://haumdaucher.de/images/logo_classic.png" alt="Haumdaucher Logo" />
      <h1>Willkommen beim Haumdaucher e.V.</h1>
    </div>
    
    <p>Hallo <strong>{{ user.name }}</strong>,</p>
    <p>dein zentraler Zugang für die Haumdaucher Cloud & Services wurde eingerichtet.</p>
    
    {% if user.attributes.haumdaucher_mitgliedsnummer %}
    <div class="meta">
      <strong>Mitgliedsnummer:</strong> {{ user.attributes.haumdaucher_mitgliedsnummer }}<br/>
      <strong>Benutzername:</strong> {{ user.username }}
    </div>
    {% endif %}

    <p>Klicke auf den Button unten, um dein Passwort festzulegen und deinen Zugang zu aktivieren:</p>
    
    <a class="btn" href="{{ url }}">Zugang aktivieren & Passwort setzen</a>
    
    <p style="font-size: 12px; color: #888;">Dieser Link ist 7 Tage lang gültig. Falls du die Registrierung nicht angefordert hast, kannst du diese E-Mail ignorieren.</p>
    
    <div class="footer">
      HAUMDAUCHER e.V. &bull; <a href="https://haumdaucher.de" style="color: #d4af37;">haumdaucher.de</a>
    </div>
  </div>
</body>
</html>

🔒 Access Control & Authorization Policies (Limiting App Access)

By default, an application without an attached policy allows any authenticated user to access it. Authentik provides fine-grained Access Control (RBAC & PBAC) to restrict access per application.

graph TD
    subgraph "Authentik Directory Groups"
        Admin["👑 <b>infra-admins</b><br/>(Moritz & Wife / Full Admin)"]
        Verein["🍺 <b>verein-mitglieder</b><br/>(Club Members)"]
        Normal["👤 <b>users</b><br/>(Normal Authenticated Users)"]
    end

    subgraph "Tier 1: Admin & Ops Tools (ForwardAuth / OIDC)"
        Z2M["Zigbee2MQTT"]
        Prom["Prometheus & Alertmanager"]
        PMA["phpMyAdmin (Nextcloud & Gitea)"]
        N8N["n8n Automation"]
        MinIO["MinIO Console"]
        Coder["Coder v2"]
        OpenClaw["OpenClaw Engine"]
    end

    subgraph "Tier 2: Daily Shared Services (OIDC)"
        HASS["Home Assistant"]
        Grafana["Grafana Dashboards"]
        Nextcloud["Nextcloud Cloud"]
        Gitea["Gitea Git Platform"]
        ClubWeb["haumdaucher.de"]
    end

    %% Admin Links (Full Superadmin access everywhere)
    Admin ==>|Full Access| Z2M
    Admin ==>|Full Access| OpenClaw
    Admin ==>|Admin| HASS
    Admin ==>|Admin| Grafana
    Admin ==>|Admin| Nextcloud
    Admin ==>|Admin| Gitea
    Admin ==>|Admin| ClubWeb

    %% Normal User Links (Restricted)
    Normal -->|Viewer (Read-Only)| Grafana
    Normal -->|Dashboard View| HASS
    Normal -->|Standard User| Nextcloud
    Normal -->|Developer| Gitea

    %% Club Members Links
    Verein -->|Member Portal| ClubWeb
    Verein -->|Club Shared Folder| Nextcloud
    Verein -->|Viewer (Read-Only)| Grafana

Master Permission Matrix

Application Domain Auth Type infra-admins (Moritz & Wife) verein-mitglieder (Club) users (Normal Users)
OpenClaw openclaw.haumdaucher.de ForwardAuth Full Access ✅ ❌ Denied ❌ Denied
Zigbee2MQTT zigbee2mqtt.moritzgraf.de ForwardAuth Full Access ✅ ❌ Denied ❌ Denied
Prometheus / Alertmanager prometheus.haumdaucher.de ForwardAuth Full Access ✅ ❌ Denied ❌ Denied
phpMyAdmin (All DBs) *.phpmyadmin.haumdaucher.de ForwardAuth Full Access ✅ ❌ Denied ❌ Denied
n8n n8n.moritzgraf.de ForwardAuth Full Access ✅ ❌ Denied ❌ Denied
MinIO Console minio-console.haumdaucher.de Native OIDC consoleAdmin ✅ ❌ Denied ❌ Denied
Coder v2 coder.moritzgraf.de Native OIDC Owner / Admin ✅ ❌ Denied ❌ Denied
Home Assistant hass.moritzgraf.de Native OIDC Administrator ✅ ❌ Denied Standard User
Grafana grafana.haumdaucher.de Native OIDC Admin ✅ Viewer (Read-Only) Viewer (Read-Only)
Nextcloud cloud.haumdaucher.de Native OIDC Admin ✅ User (Club Folder) User (Standard)
Gitea git.moritzgraf.de Native OIDC Admin ✅ ❌ / Member Developer
haumdaucher.de haumdaucher.de ForwardAuth / OIDC Admin ✅ Member Access ✅ ❌ Denied

Policy Bindings in Terraform

All policy bindings are enforced directly in terraform/authentik.tf:

# 1. Admin-Only Tools (Restricted strictly to infra-admins)
resource "authentik_policy_binding" "openclaw_admin_only" {
  target = authentik_application.openclaw.uuid
  group  = authentik_group.infra_admins.id
  order  = 0
}

resource "authentik_policy_binding" "zigbee2mqtt_admin_only" {
  target = authentik_application.zigbee2mqtt.uuid
  group  = authentik_group.infra_admins.id
  order  = 0
}

# 2. Shared & Member Applications (Accessible to users group)
resource "authentik_policy_binding" "grafana_users" {
  target = authentik_application.grafana.uuid
  group  = authentik_group.users.id
  order  = 0
}

resource "authentik_policy_binding" "nextcloud_users" {
  target = authentik_application.nextcloud.uuid
  group  = authentik_group.users.id
  order  = 0
}

2. Attribute-Based Policy (Python Expression Policy)

You can write Python expressions that evaluate custom user attributes dynamically:

resource "authentik_policy_expression" "require_active_membership" {
  name       = "require-active-haumdaucher-membership"
  expression = <<EOF
# Must have a valid Mitgliedsnummer and active status
mitgliedsnummer = request.user.attributes.get('haumdaucher_mitgliedsnummer')
status = request.user.attributes.get('status', 'aktiv')

if not mitgliedsnummer or not mitgliedsnummer.startswith('HM-'):
    ak_message("Keine gültige Haumdaucher Mitgliedsnummer hinterlegt.")
    return False

if status != 'aktiv':
    ak_message("Deine Mitgliedschaft ist derzeit nicht aktiv.")
    return False

return True
EOF
}

# Attach policy to Nextcloud application
resource "authentik_policy_binding" "nextcloud_active_members" {
  target = authentik_application.nextcloud.uuid
  policy = authentik_policy_expression.require_active_membership.id
  order  = 1
}

🏷️ Custom Attributes & OIDC Claim Mappings

Authentik allows storing any arbitrary JSON object inside user.attributes.

{
  "haumdaucher_mitgliedsnummer": "HM-2024-042",
  "second_email": "max.privat@gmail.com",
  "eintrittsdatum": "2024-01-15",
  "verein_rolle": "Vorstand / Kassenwart / Mitglied",
  "phone": "+49 170 1234567",
  "telegram_handle": "@max_mustermann"
}

2. Exposing Custom Attributes via OIDC Claims (Terraform)

To pass haumdaucher_mitgliedsnummer or second_email to downstream applications like Nextcloud, Grafana, or Gitea:

# Define a custom Scope Mapping in Terraform
resource "authentik_property_mapping_provider_scope" "haumdaucher_profile" {
  name        = "Haumdaucher Extended Profile"
  scope_name  = "haumdaucher_profile"
  description = "Provides Vereins-Mitgliedsnummer and secondary email to OIDC clients"
  expression  = <<EOF
return {
    "haumdaucher_mitgliedsnummer": request.user.attributes.get("haumdaucher_mitgliedsnummer", ""),
    "second_email": request.user.attributes.get("second_email", ""),
    "verein_rolle": request.user.attributes.get("verein_rolle", "Mitglied")
}
EOF
}

# Add this mapping to the OAuth2 provider (e.g. Nextcloud or Grafana)
resource "authentik_provider_oauth2" "nextcloud" {
  name               = "Nextcloud"
  client_id          = "nextcloud"
  client_secret      = random_password.nextcloud_oauth_client_secret.result
  client_type        = "confidential"
  authorization_flow = data.authentik_flow.default_authorization_flow.id
  invalidation_flow  = data.authentik_flow.default_invalidation_flow.id
  signing_key        = data.authentik_certificate_key_pair.generated.id
  
  property_mappings  = [
    data.authentik_property_mapping_provider_scope.openid.id,
    data.authentik_property_mapping_provider_scope.email.id,
    data.authentik_property_mapping_provider_scope.profile.id,
    authentik_property_mapping_provider_scope.haumdaucher_profile.id, # <-- Custom scope
  ]
}

🛡️ Production Checklist & Best Practices

When managing users in production, keep the following considerations in mind:

1. Passwordless & Multi-Factor Authentication (MFA / 2FA)

  • Enforce WebAuthn / Passkeys (FIDO2) or TOTP (Authenticator App) for users in the infra-admins group.
  • In Authentik, this is done by binding an Authenticator Validation Stage to the default authentication flow or creating an Admin Authorization Flow.

2. User Self-Service & Attribute Editing

  • Authentik provides a self-service portal at https://auth.haumdaucher.de/if/user/#/settings.
  • Users can change their password, register Passkeys, and edit user profile fields.
  • Security Rule: Keep administrative attributes (like haumdaucher_mitgliedsnummer) read-only by not exposing them to the user edit flow, while allowing users to freely edit personal contact details like second_email or phone.

3. Offboarding & Member Deactivation

  • If a member leaves the club:
    1. Set is_active = false (or remove from verein-mitglieder group).
    2. Authentik immediately terminates active sessions and revokes ForwardAuth access and OIDC refresh tokens.
    3. No need to manually reset passwords on Nextcloud, Grafana, or Gitea—SSO handles deprovisioning instantly.

4. GDPR / DSGVO & Privacy

  • Store only necessary data in user attributes.
  • If a member requests account deletion, deleting the user in Authentik cascades identity termination across all connected services.

5. Backups & Audit Logs

  • User database changes are stored in the PostgreSQL cluster (authentik-db) and backed up with daily Velero snapshot schedules.
  • Authentik logs all login attempts, failed logins, and administrative modifications under Events > Logs.