630 lines
27 KiB
Markdown
630 lines
27 KiB
Markdown
# 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](#-executive-summary-quick-answers)
|
||
2. [Method 1: Manual Web UI & Invitation Links](#-method-1-manual-web-ui--invitation-links)
|
||
3. [Method 2: User Provisioning via Terraform](#-method-2-user-provisioning-via-terraform)
|
||
4. [Method 3: Bulk Import from Excel / CSV (Python Script)](#-method-3-bulk-import-from-excel--csv-python-script)
|
||
5. [Customizing Registration & Onboarding Emails](#-customizing-registration--onboarding-emails)
|
||
6. [Access Control & Authorization Policies (Limiting App Access)](#-access-control--authorization-policies-limiting-app-access)
|
||
7. [Custom Attributes & OIDC Claim Mappings](#-custom-attributes--oidc-claim-mappings)
|
||
8. [Production Checklist & Best Practices](#-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. |
|
||
|
||
---
|
||
|
||
## 👤 Method 1: Manual Web UI & Invitation Links
|
||
|
||
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)**:
|
||
```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`:
|
||
|
||
```mermaid
|
||
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)**:
|
||
```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
|
||
```hcl
|
||
# 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`):
|
||
|
||
```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`:
|
||
```hcl
|
||
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`):
|
||
|
||
```python
|
||
#!/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
|
||
```html
|
||
<!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. • <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.
|
||
|
||
```mermaid
|
||
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`:
|
||
|
||
```hcl
|
||
# 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:
|
||
|
||
```hcl
|
||
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`.
|
||
|
||
### 1. Recommended Attribute Schema for Haumdaucher
|
||
```json
|
||
{
|
||
"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**:
|
||
|
||
```hcl
|
||
# 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**.
|