> ## Content Index
> Fetch the complete content index at: https://blog.cube-m.de/llms.txt
> Use this file to discover other available public pages before exploring further.

# 09. Authelia - Login-Schutz mit 2FA für persönliche Apps
- URL: https://blog.cube-m.de/09-authelia-login-schutz-mit-2fa-fur-personliche-apps/
- Published: 2026-05-27T11:48:17.000Z
- Updated: 2026-06-29T12:01:28.000Z
- Description: Caddy ist die einzige Eingangstür zu deinen Apps – aber er prüft nicht, wer reinkommt. Authelia übernimmt die Kontrolle: ein zentraler Login mit 2FA für alle persönlichen Apps.
- Author: Martin
- Tags: VPS, Infrastruktur, self-hosting, 2FA, Server-Sicherheit, docker, #Import 2026-09-06 19:52

Caddy bringt deine Apps öffentlich ans Netz, der Mail-Relay schickt Notifications raus. Was fehlt: ein Login davor. Für Apps wie Vaultwarden oder Immich willst du nicht den App-eigenen Login als einzige Hürde haben – du willst eine Schicht davor, die schon vor dem ersten App-Aufruf prüft, ob hier überhaupt jemand reinkommen darf. Und das nicht nur mit einem Passwort, sondern mit **2FA** (Zwei-Faktor-Authentifizierung): zusätzlich zum Passwort ein zweiter Faktor, typischerweise ein 6-stelliger Code, der sich alle 30 Sekunden ändert und nur auf deinem Handy lebt. Selbst wenn dein Passwort geleakt wird, kommt niemand ohne dein Handy rein.

Genau das macht Authelia. Eine kleine, ressourcenarme Anwendung, die zwischen Caddy und deinen persönlichen Apps sitzt und entscheidet: gültiger Login mit zweitem Faktor? → weiter zur App. Sonst → Login-Portal. Eine einzige Session deckt alle deine geschützten Apps ab – einmal eingeloggt, kommst du bis zum Cookie-Ablauf überall rein, ohne dich jedes Mal erneut anmelden zu müssen.

Wichtig vorab: Authelia ist der **Kontrolleur an der Tür**, kein Ersatz für den Login der App selbst. Caddy ist die einzige Eingangstür – aber er prüft nicht, wer reinkommt. Genau das macht Authelia: Du meldest dich an, Authelia entscheidet, ob du weiter darfst. Danach fragen Vaultwarden, Immich oder Paperless trotzdem nochmal nach ihrem eigenen Account. Klingt zunächst nach doppelter Arbeit, ist aber genau das Richtige: Die App-Logins bleiben App-Logins (mit ihren eigenen Rollen, Berechtigungen, Datenbanken), und Authelia stellt davor sicher, dass überhaupt nur angemeldete Nutzer die App zu sehen bekommen.

## Am Ende hast du

- Authelia + Redis laufen als UI-Defined Stack in Komodo
- Eine eigene Login-Seite unter `auth.deinedomain.de`, geschützt mit HTTPS
- Einen ersten Admin-User mit einem starken Passwort
- 2FA eingerichtet: Beim Login musst du zusätzlich einen 6-stelligen Code aus einer Authenticator-App eingeben
- Authelia kann Mails verschicken – z. B. für „Passwort vergessen"-Funktionen
- Einmal eingeloggt bleibst du eingeloggt, auch nach Updates oder Neustarts
- Authelia-Logs werden als Datei mitgeschrieben – die brauchen wir später für CrowdSec
- In Caddy reicht künftig **eine Zeile** pro App, um sie hinter den Login zu hängen
- Eine durchdachte Schutz-Strategie: Standard-Schutz für alle Apps, höchste Stufe für die wirklich sensiblen

## Voraussetzungen

- Artikel 02–08 abgeschlossen
- Caddy läuft (Artikel 07) – inkl. Wildcard-DNS, dadurch ist `auth.deinedomain.de` schon vorbereitet
- Mail-Relay läuft (Artikel 08) – Authelia verschickt Account-Setup-Mails darüber
- Komodo läuft, Server `vps-homelab` ist eingebunden
- Eine TOTP-App auf dem Smartphone (Empfehlung wie in Artikel 05: 2FAS Auth, Aegis oder Bitwarden Authenticator)

## 1\. Was Authelia für dieses Setup leistet

In Stichpunkten:

- **Ein Login für alles** – einmal an `auth.deine-domain.de` einloggen, danach gelten die Cookies für alle Subdomains von `deine-domain.de`. Vaultwarden, Immich, Paperless – alle bekommen denselben Login-Status.
- **2FA per Code aus der Authenticator-App** – zweiter Faktor zusätzlich zum Passwort. Pflicht oder optional je nach App.
- **Schutz vor automatischen Login-Versuchen** – nach mehreren Fehlversuchen wird der Account für eine konfigurierbare Zeit gesperrt. Das bremst Bots aus, die Passwörter durchprobieren.
- **Self-Service** – Passwort-Reset per E-Mail (über den Mail-Relay), 2FA-Setup im User-Portal.
- **Persistente Sessions** – durch Redis als Session-Store überleben Logins auch Stack-Updates und Container-Restarts.

Was Authelia **nicht** macht: den App-Login ersetzen. Wenn du dich bei Authelia eingeloggt hast und Vaultwarden öffnest, fragt Vaultwarden trotzdem nochmal nach seinem eigenen Master-Passwort. Authelia entscheidet nur: *Darf diese Person die App überhaupt sehen?* Was innerhalb der App passiert, regelt die App selbst.

Für die Apps in Phase 3 (Vaultwarden, Immich, Paperless) ist das genau das richtige Modell. Sie haben jeweils ihren eigenen Login; Authelia liefert die vorgelagerte Schutzschicht.

## 2\. So funktioniert Forward Auth

Wenn du `https://vault.deinedomain.de` aufrufst, passiert das hier:

```
1. Browser → Caddy:           GET https://vault.deinedomain.de/
2. Caddy → Authelia:          GET /api/authz/forward-auth (im internen Netz)
3. Authelia prüft die Cookies
4a. Cookie gültig + 2FA OK:   200 OK + User-Header
4b. Cookie fehlt/ungültig:    301 Redirect zu auth.deinedomain.de
5a. Caddy bei 200 OK:         reverse_proxy zur App, User-Header dran
5b. Caddy bei Redirect:       Browser landet auf Login-Portal
6. Nach Login:                Authelia setzt Cookie + Redirect zurück zur App

```

Das Schöne: Vaultwarden, Immich oder Paperless wissen nicht, dass Authelia existiert. Sie sehen normale HTTP-Anfragen, ggf. mit Headern wie `Remote-User` und `Remote-Email` zur Identifikation. Mehr nicht. Du kannst Authelia jederzeit vor jede beliebige Web-App schalten, solange Caddy davor sitzt.

Caddy spricht Authelia ausschließlich intern über das `homelab_net` an – `http://authelia:9091`. Authelia veröffentlicht keine Ports am Host, genau wie Mail-Relay und Komodo-Periphery.

## 3\. Access-Control-Strategie: pragmatisch statt paranoid

Authelia kennt vier Policy-Stufen pro Domain/Pfad:

| Policy      | Bedeutung                                   |
| ----------- | ------------------------------------------- |
| deny        | Zugriff komplett blockiert                  |
| bypass      | Authelia greift nicht ein, App-Login allein |
| one\_factor | Passwort allein reicht                      |
| two\_factor | Passwort + TOTP/WebAuthn                    |

### Die Strategie für diese Serie

```
default_policy: one_factor

→ two_factor + group:admins  für sensible Apps (Vaultwarden, Immich, Paperless, File Browser)
→ two_factor                 für persönliche Apps ohne erhöhte Anforderungen
→ bypass                     für die Auth-Subdomain selbst

```

**Warum `one_factor` als Default und nicht `deny`?**

Beide funktionieren grundsätzlich, aber `one_factor` ist als Sicherheitsnetz pragmatischer:

- Wenn du eine neue Subdomain in Caddy einbaust und `import auth` setzt, aber vergisst, eine `access_control`\-Regel zu ergänzen → mit `default_policy: deny` bekommst du 401 statt eines Login-Formulars. Du musst Authelia anfassen, bevor irgendwer reinkommt.
- Mit `default_policy: one_factor` ist die unbekannte Subdomain sofort mit Passwort-Login geschützt. Du kannst in Ruhe entscheiden, ob sie `two_factor` braucht – aber sie hängt nie ungeschützt im Netz.
- Wirklich öffentliche Subdomains (Ghost-Blog) haben in Caddy ohnehin **kein** `import auth` – die fragen Authelia nie. Die Default-Policy ist also nur das Fallback für Subdomains, die durch Authelia gehen.

**Zweiter Filter: `subject: ["group:admins"]`**

Für die wirklich sensiblen Apps reicht `two_factor` allein nicht aus. Wir verlangen zusätzlich, dass der eingeloggte User in der Gruppe `admins` ist:

```yaml
- domain: 'vault.deine-domain.de'
  policy: 'two_factor'
  subject: ['group:admins']

```

Was das bringt: Selbst wenn du später Family-Member oder Freunde als User in `users_database.yml` hinzufügst (z. B. für Outline oder Immich-Sharing), kommen die ohne `admins`\-Mitgliedschaft **nicht** auf Vaultwarden oder Paperless – auch nicht mit gültigem Passwort + 2FA.

> **Bei nur einem User**
> 
> Wenn du gerade Single-User unterwegs bist, ändert der `group:admins`\-Filter nichts an deinem Erlebnis – du bist ja in `admins`. Aber wenn du das Pattern von Anfang an mitnimmst, brauchst du beim ersten Zweit-User keine Konfigurations-Migration. Kostet jetzt nichts, schützt später konsequent.

### Empfohlene Vergabe in dieser Serie

| App                         | Policy                      | Begründung                                              |
| --------------------------- | --------------------------- | ------------------------------------------------------- |
| Vaultwarden (Art 12)        | two\_factor \+ group:admins | Master-Passwörter – nichts Sensibleres                  |
| Immich (Art 13)             | two\_factor \+ group:admins | Persönliche Fotos, oft auch Standortdaten               |
| Paperless-ngx (Art 14)      | two\_factor \+ group:admins | Ausweise, Verträge, Rechnungen                          |
| File Browser                | two\_factor \+ group:admins | Direkter Zugriff aufs Filesystem – hochsensibel         |
| Outline (Art 17)            | two\_factor                 | Auch für Nicht-Admins zugänglich (z. B. geteiltes Wiki) |
| Persönliche Bookmarks / RSS | two\_factor                 | Standard für persönliche Apps                           |

> **`bypass` vermeiden**
> 
> Versuchung: „Mein Ghost-Blog soll sowieso öffentlich sein – setze ich auf `bypass`." Falsch herum gedacht. Wenn eine Domain in Authelia auf `bypass` steht, geht jeder Request unverändert durch – also auch `/ghost/admin`. Den Admin-Bereich schützt man besser **gar nicht** über Authelia, sondern wie in Artikel 07 angedeutet per Caddy-Regel auf VPN-only. `bypass` brauchst du nur für die Authelia-Subdomain selbst.

## 4\. DNS-Vorbereitung

Wenn du Artikel 07 mit Wildcard-DNS abgeschlossen hast, ist hier nichts zu tun. Die Subdomain `auth.deinedomain.de` zeigt durch den `*`\-A-Record bereits auf deinen VPS.

**Quick-Check:**

```bash
dig +short auth.deinedomain.de

```

*Erwartung:* Deine VPS-IPv4 erscheint.

Wenn nicht: Du hast Wildcard möglicherweise nicht gesetzt – zurück zu Artikel 07, Abschnitt 3.

## 5\. Secrets generieren

> **Setup-Reihenfolge**
> 
> Wir bereiten zuerst alles auf dem Server vor (Sektionen 5–8: Secrets, Verzeichnisse, Konfigurationsdateien), bevor der Stack in Komodo angelegt wird (Sektionen 9–13). Dadurch ist beim ersten Deploy bereits alles vorhanden, was Authelia zum Starten braucht.

Authelia braucht vier zufällige Geheimnisse:

- **JWT Secret** – signiert die Tokens für den Identity-Validation-Flow (Passwort-Reset etc.)
- **Session Secret** – signiert die Session-Cookies, die nach dem Login an alle deine Subdomains gehen
- **Storage Encryption Key** – verschlüsselt die TOTP-Secrets und andere sensible Daten in der lokalen Datenbank
- **Redis Password** – schützt Redis intern, auch wenn es nur im internen Netz erreichbar ist (eine Schutzschicht mehr)

Auf dem Server generieren:

```bash
echo "JWT Secret:        $(openssl rand -hex 32)"
echo "Session Secret:    $(openssl rand -hex 32)"
echo "Storage Enc Key:   $(openssl rand -hex 32)"
echo "Redis Password:    $(openssl rand -base64 24)"

```

Alle Werte in deinen Passwort-Manager. Sobald sie in der `.env` stehen, brauchst du sie nur noch im Disaster-Recovery-Fall.

> ⚠️ **Der Storage Encryption Key ist nicht ersetzbar**
> 
> JWT und Session Secret kannst du im Notfall neu generieren – User müssen sich danach nur neu einloggen. Der Storage Encryption Key dagegen verschlüsselt die TOTP-Secrets in der SQLite-Datenbank. Verlierst du ihn, sind die TOTP-Setups deiner User unbrauchbar – jeder muss seine 2FA-App neu koppeln. Backup dieses Werts ist Pflicht, nicht Empfehlung.

## 6\. Verzeichnisse vorbereiten

Bevor wir die Datei schreiben, einmalig die komplette Verzeichnisstruktur vorbereiten. Komodo legt das Stack-Verzeichnis sonst erst beim Deploy an – aber dann sind wir zu spät dran, weil Authelia ohne Konfigurationsdatei nicht startet:

```bash
sudo mkdir -p /srv/docker/stacks/authelia/{config,data,log,redis-data}
sudo chown -R 1000:1000 /srv/docker/stacks/authelia/{data,log}

```

> **Hintergrund:** Authelia läuft im Container als User 1000\. Wenn das Host-Verzeichnis root gehört, scheitert SQLite beim Schreiben in `./data` und der Log-Writer in `./log`. Das `redis-data`\-Verzeichnis braucht keinen `chown`, weil der Redis-Container intern als User `redis` (UID 999) läuft und sich seine Rechte selbst setzt.

## 7\. configuration.yml

Das ist die zentrale Konfigurationsdatei von Authelia. Anders als `compose.yaml` und `.env` lässt sich diese Datei **nicht** sinnvoll über die Komodo-UI pflegen – wir legen sie direkt auf dem Server an.

**Per VS Codium** (falls du Remote-SSH in Artikel 07 eingerichtet hast):

Datei `/srv/docker/stacks/authelia/config/configuration.yml` neu anlegen und folgenden Inhalt einfügen.

**Per SSH:**

```bash
sudo nano /srv/docker/stacks/authelia/config/configuration.yml

```

In beiden Fällen ist der Inhalt:

```yaml
###############################################################
# Authelia - Hauptkonfiguration
###############################################################

theme: 'dark'

# Server
server:
  address: 'tcp://0.0.0.0:9091/'
  endpoints:
    authz:
      forward-auth:
        implementation: 'ForwardAuth'

# Logs in Datei – vorbereitet für CrowdSec in Artikel 10
log:
  level: 'info'
  format: 'text'
  file_path: '/log/authelia.log'
  keep_stdout: true

# TOTP-Setup (Issuer = was in der Authenticator-App angezeigt wird)
totp:
  issuer: '{{ env "AUTHELIA_TOTP_ISSUER" }}'
  period: 30
  skew: 1

# WebAuthn (Passkeys / YubiKey – optional, aber empfehlenswert)
webauthn:
  display_name: 'Authelia'
  attestation_conveyance_preference: 'indirect'
  timeout: '60 seconds'

# Auth-Backend: lokale YAML-Datei (gut für 1–10 User; bei mehr → Authentik-Bonus)
authentication_backend:
  file:
    path: '/config/users_database.yml'
    watch: true
    password:
      algorithm: 'argon2'

# Access Control – one_factor als Sicherheitsnetz, sensible Apps mit Admin-Filter
access_control:
  default_policy: 'one_factor'
  rules:
    # Auth-Subdomain selbst: muss bypass sein, sonst kein Login möglich
    - domain: 'auth.{{ env "AUTHELIA_DOMAIN" }}'
      policy: 'bypass'

    # Sensible Apps: 2FA + Admin-Mitgliedschaft erforderlich
    - domain:
        - 'vault.{{ env "AUTHELIA_DOMAIN" }}'
        - 'immich.{{ env "AUTHELIA_DOMAIN" }}'
        - 'docs.{{ env "AUTHELIA_DOMAIN" }}'
        - 'files.{{ env "AUTHELIA_DOMAIN" }}'
      policy: 'two_factor'
      subject: ['group:admins']

    # Persönliche Apps mit 2FA, aber ohne Admin-Filter (z. B. geteiltes Wiki)
    - domain: 'wiki.{{ env "AUTHELIA_DOMAIN" }}'
      policy: 'two_factor'

# Session – Redis als Store + Cookie auf Parent-Domain für echtes SSO
session:
  secret: '{{ env "AUTHELIA_SESSION_SECRET" }}'
  redis:
    host: 'redis'
    port: 6379
    password: '{{ env "AUTHELIA_REDIS_PASSWORD" }}'
    database_index: 0
  cookies:
    - name: 'authelia_session'
      domain: '{{ env "AUTHELIA_DOMAIN" }}'
      authelia_url: '{{ env "AUTHELIA_URL" }}'
      default_redirection_url: 'https://{{ env "AUTHELIA_DOMAIN" }}'
      expiration: '12 hours'
      inactivity: '45 minutes'
      remember_me: '1 month'

# Rate-Limiting bei Login-Versuchen
regulation:
  max_retries: 3
  find_time: '2 minutes'
  ban_time: '10 minutes'

# Lokale SQLite-DB für User-Daten, TOTP-Secrets etc.
storage:
  encryption_key: '{{ env "AUTHELIA_STORAGE_ENCRYPTION_KEY" }}'
  local:
    path: '/data/db.sqlite3'

# Notifier – nutzt den Mail-Relay aus Artikel 08
notifier:
  disable_startup_check: false
  smtp:
    address: 'smtp://mail-relay:2500'
    sender: '{{ env "AUTHELIA_SMTP_SENDER" }}'
    disable_require_tls: true
    disable_starttls: true

# Identity Validation – JWT-Secret für Passwort-Reset-Links
identity_validation:
  reset_password:
    jwt_secret: '{{ env "AUTHELIA_JWT_SECRET" }}'

```

### Was die wichtigsten Blöcke machen

| Block                                            | Was passiert                                                                                                                                                         |
| ------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| server.endpoints.authz.forward-auth              | Aktiviert genau den Endpoint, den Caddys forward\_auth gleich abfragt: /api/authz/forward-auth                                                                       |
| log.file\_path \+ keep\_stdout                   | Logs gehen zusätzlich in /log/authelia.log – CrowdSec liest die in Artikel 10\. keep\_stdout: true bedeutet: Komodo sieht weiterhin die Logs in der UI               |
| authentication\_backend.file                     | Lokale YAML als User-DB. watch: true heißt: Änderungen werden live übernommen, ohne Container-Restart                                                                |
| access\_control.default\_policy: one\_factor     | Sicherheitsnetz: unbekannte Subdomains bekommen mindestens Passwort-Schutz                                                                                           |
| subject: \['group:admins'\]                      | Zweiter Filter zusätzlich zu 2FA: der User muss auch in der admins\-Gruppe sein                                                                                      |
| session.redis                                    | Sessions liegen in Redis statt im Container-Speicher → überleben Restarts                                                                                            |
| session.cookies\[0\].domain                      | Parent-Domain. Cookie gilt für **alle** Subdomains → echtes SSO                                                                                                      |
| regulation.max\_retries: 3                       | Nach 3 Fehlversuchen ist der Account 10 Minuten gesperrt – einfache Brute-Force-Abwehr                                                                               |
| notifier.smtp                                    | Authelia verschickt Mails über mail-relay:2500 – ohne TLS, ohne Auth, da das Docker-Netz lokal ist (siehe Artikel 08)                                                |
| identity\_validation.reset\_password.jwt\_secret | Signiert die Token in Passwort-Reset-Links per Mail. Wichtig in 4.39+ – ältere Anleitungen zeigen oft noch eine Top-Level-jwt\_secret\-Variable, die ist abgekündigt |

> **Warum SQLite und nicht PostgreSQL?**
> 
> Die offiziellen Authelia-Docs raten beim File-Auth-Backend zu PostgreSQL und nennen SQLite „für Dev und Testing". Der Hintergrund ist Multi-Instance-Skalierung – mehrere Authelia-Container parallel hinter einem Load Balancer.
> 
> Im Self-Hosting für 1–3 User auf einem VPS ist das Szenario komplett irrelevant. SQLite läuft hier seit Jahren stabil, braucht null Setup und sichert sich mit dem `./data`\-Verzeichnis automatisch mit. Wer später echte Skalierung braucht, hat ohnehin schon den Sprung zu Authentik (Bonus-Artikel) gemacht.

> **Spätere Anpassungen**
> 
> Diese Datei wirst du im Laufe der Serie noch häufiger anfassen – jedes Mal, wenn eine neue App dazukommt, ergänzt du in `access_control.rules` einen Eintrag mit der neuen Subdomain. Den gleichen Weg nutzen wie jetzt: VS Codium öffnen und Datei bearbeiten, oder per SSH `sudo nano /srv/docker/stacks/authelia/config/configuration.yml`. Dank `watch: true` übernimmt Authelia Änderungen sofort, ohne dass du den Stack redeployen musst.

## 8\. Den ersten User anlegen

Authelia lädt User aus `users_database.yml`. Die Datei brauchen wir noch nicht beim ersten Start (Watch-Modus fängt sie später ab) – aber wir legen sie direkt mit an, damit du dich sofort einloggen kannst.

### 8.1 Passwort-Hash erzeugen

Authelia bringt einen Hash-Generator mit, den wir per Einmal-Container nutzen:

```bash
docker run --rm -it authelia/authelia:4.39 \
  authelia crypto hash generate argon2 \
  --password 'DEIN-STARKES-PASSWORT-HIER'

```

*Erwartung:* Eine Ausgabe wie

```
Digest: $argon2id$v=19$m=65536,t=3,p=4$Hjc8e7WYcBFcJmEDUOsS9A$ozM7RyZR1EyDR8cuyVpDDfmLrGPGFgo5E2NNqRumui4

```

Den kompletten String ab `$argon2id$…` kopieren – das ist der gehashte Wert, der gleich in die User-Datei kommt.

> **Wähle ein wirklich starkes Passwort**
> 
> Hier ist keine Master-Hygiene-Lektion nötig: 20+ Zeichen aus deinem Passwort-Manager, einmal generieren, fertig. Dieses Passwort schützt nachher den Zugriff auf Vaultwarden, Immich, Paperless und alles Weitere gleichzeitig – ein gutes Passwort hier ist sehr viel wertvoller als drei mittelmäßige in den Apps selbst.

### 8.2 users\_database.yml anlegen

Gleicher Weg wie bei der `configuration.yml` – per VS Codium oder per SSH:

```bash
sudo nano /srv/docker/stacks/authelia/config/users_database.yml

```

Inhalt:

```yaml
---
users:
  martin:
    disabled: false
    displayname: 'Martin'
    password: '$argon2id$v=19$m=65536,t=3,p=4$Hjc8e7WYcBFcJmEDUOsS9A$ozM7RyZR1EyDR8cuyVpDDfmLrGPGFgo5E2NNqRumui4'
    email: 'martin@deine-domain.de'
    groups:
      - 'admins'

```

Das `password`\-Feld bekommt den Hash aus Schritt 8.1 – **nicht** das Klartext-Passwort.

**Wichtig:** YAML ist whitespace-sensitiv. Nur Leerzeichen zum Einrücken, keine Tabs. Sonst quittiert Authelia beim Start mit:

```
could not parse the YAML database: yaml: line X: found character that cannot start any token

```

Die Gruppe `admins` ist genau die Gruppe, die in der `configuration.yml` per `subject: ['group:admins']` als Filter genutzt wird. Spätere User für Family/Freunde lässt du einfach weg aus dieser Gruppe – sie kommen dann durch Login + 2FA bis zu den Subdomains ohne Admin-Filter (z. B. das geteilte Wiki), aber nicht auf Vaultwarden oder Paperless.

## 9\. Stack in Komodo anlegen

Mit den Konfigurationsdateien auf dem Server geht es jetzt in Komodo weiter. Ab hier bis zum Deploy bleibt alles im Browser – kein Wechsel mehr.

In Komodo (`http://vps:9120` oder über die Tailscale-IP):

1. Links **Stacks** → oben rechts **\+ New Stack**
2. **Name:** `authelia`
3. **Server:** `vps-homelab`
4. **Stack Type:** `UI Defined`
5. **Create**

Im Stack-Editor pflegst du gleich `compose.yaml` (Sektion 10) und `.env` (Sektion 11). Beide Dateien greifen über `env_file: .env` und Volume-Mounts auf die in Sektion 6–8 vorbereiteten Verzeichnisse und Konfigurationsdateien zu.

## 10\. compose.yaml

Im Compose-Editor:

```yaml
name: authelia

services:
  authelia:
    image: authelia/authelia:latest
    container_name: authelia
    restart: unless-stopped
    env_file: .env
    volumes:
      - ./config:/config
      - ./data:/data
      - ./log:/log
    healthcheck:
      test: ["CMD", "authelia", "healthcheck"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 30s
    depends_on:
      redis:
        condition: service_healthy
    networks:
      - authelia_internal
      - homelab_net

  redis:
    image: redis:7-alpine
    container_name: authelia-redis
    restart: unless-stopped
    command: ["redis-server", "--requirepass", "${AUTHELIA_REDIS_PASSWORD}"]
    env_file: .env
    volumes:
      - ./redis-data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "-a", "${AUTHELIA_REDIS_PASSWORD}", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - authelia_internal

networks:
  authelia_internal:
  homelab_net:
    external: true

```

**Was passiert hier?**

| Block                         | Erklärung                                                                                                                                                                                |
| ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| image: authelia/authelia:4.39 | Pinning auf Minor-Version. Patch-Updates innerhalb 4.39.x kommen automatisch, ein potentieller Sprung auf 4.40 wäre eine bewusste Entscheidung – analog postgres:17 und caddy-crowdsec:2 |
| env\_file: .env               | Sämtliche Secrets und Domain-Werte lädt Authelia aus der .env, nicht hardcoded ins Image                                                                                                 |
| ./config:/config              | Hier liegen configuration.yml und users\_database.yml – die Konfigurations-Quelldateien                                                                                                  |
| ./data:/data                  | SQLite-Datenbank und Notification-State. Das ist das Verzeichnis, das du regelmäßig sicherst                                                                                             |
| ./log:/log                    | Authelia-Logs als Datei – vorbereitet für CrowdSec-Acquisition in Artikel 10                                                                                                             |
| authelia\_internal            | Internes Netz für Authelia ↔ Redis. Redis ist von außen unerreichbar, auch nicht von anderen Apps im homelab\_net                                                                        |
| homelab\_net                  | Nur Authelia hängt mit drin, damit Caddy es als authelia:9091 erreichen kann                                                                                                             |
| Redis requirepass             | Redis akzeptiert nur Verbindungen mit Passwort – auch im internen Netz                                                                                                                   |
| Kein ports:                   | Beide Container sind ausschließlich intern erreichbar                                                                                                                                    |

> **Warum Redis?**
> 
> Authelia kann grundsätzlich auch ohne Redis laufen – nutzt dann In-Memory-Session-Storage. Praktischer Effekt: Jedes Mal, wenn du den Authelia-Container neu startest (z. B. nach einem Update oder Stack-Redeploy), sind alle aktiven Sessions weg. Du wirst überall ausgeloggt und musst dich neu einloggen – Vaultwarden, Immich, Paperless, alles.
> 
> Bei monatlichen Updates ist das ein laufender, kleiner Ärger. Redis als Session-Store (ca. 10 MB Container) löst das: Sessions überleben Container-Restarts, „Remember me" funktioniert zuverlässig. Es ist auch die offizielle Empfehlung von Authelia für produktive Setups.

Pfade konsistent mit Artikel 07: alles unter `/srv/docker/stacks/authelia/`, von Komodo selbst angelegt.

## 11\. .env

Im `.env`\-Tab:

```
# Hauptdomain und Auth-Subdomain
AUTHELIA_DOMAIN=deine-domain.de
AUTHELIA_URL=https://auth.deine-domain.de

# TOTP-Issuer – erscheint später als App-Name in der Authenticator-App
AUTHELIA_TOTP_ISSUER=deinedomain.de

# Secrets aus Schritt 6
AUTHELIA_JWT_SECRET=<JWT-SECRET-AUS-OPENSSL>
AUTHELIA_SESSION_SECRET=<SESSION-SECRET-AUS-OPENSSL>
AUTHELIA_STORAGE_ENCRYPTION_KEY=<STORAGE-ENC-KEY-AUS-OPENSSL>
AUTHELIA_REDIS_PASSWORD=<REDIS-PASSWORD-AUS-OPENSSL>

# Mail-Versand: Sender-Adresse für Authelia-Mails
AUTHELIA_SMTP_SENDER="Authelia <noreply@deine-domain.de>"

# Zeitzone
TZ=Europe/Berlin

```

Authelia liest diese Werte über Template-Variablen in der `configuration.yml` (kommt im nächsten Schritt) – kein extra Setup nötig.

**Auszutauschen sind:**

- `deine-domain.de` → deine tatsächliche Hauptdomain (vier Stellen: `AUTHELIA_DOMAIN`, `AUTHELIA_URL`, `AUTHELIA_TOTP_ISSUER`, `AUTHELIA_SMTP_SENDER`)
- Die vier Secret-Platzhalter `<...-AUS-OPENSSL>` → die in Schritt 5 generierten Werte

Die Zeitzone passt für die meisten Self-Hoster, kann aber bei Bedarf angepasst werden.

## 12\. Update-Verhalten konfigurieren

Bevor du deployst, zwei Schalter im Stack-Editor:

- **Poll for Updates:** `on`
- **Auto Update:** `off`

Standard für alle Stacks in dieser Serie – Komodo zeigt dir an, wenn ein neues Image verfügbar ist, deployt aber nicht automatisch.

## 13\. Deploy und Logs prüfen

In Komodo im Stack-Overview: **Deploy** klicken.

Komodo schreibt alle Dateien auf den Host, zieht beide Images und startet die Container. In Komodo öffnen → Tab **Containers**:

- `authelia-redis` sollte nach \~10 Sekunden `Up (healthy)` zeigen
- `authelia` startet erst nach Redis (durch `depends_on`) und sollte nach \~30 Sekunden ebenfalls `Up (healthy)` zeigen

**Erwartete Schlüsselzeilen in den Authelia-Logs:**

```
level=info msg="Authelia v4.39.x is starting"
level=info msg="Startup checks completed successfully"
level=info msg="Listening for non-TLS connections on '0.0.0.0:9091'"
```

## 14\. Caddy einbinden: die Auth-Subdomain und das (auth)-Snippet

Jetzt der entscheidende Schritt: Authelia ans Caddy hängen. Dafür ergänzt du im Caddyfile (Stack `caddy`, Tab Additional Files → `Caddyfile`) zwei Sachen.

### 14.1 Das (auth)-Snippet definieren

Direkt nach dem `(public)`\-Snippet aus Artikel 07:

```caddyfile
# ============================================================
# Snippet: Authelia Forward-Auth für persönliche Apps
# Wird per `import auth` in jeder geschützten Site verwendet
# ============================================================
(auth) {
    forward_auth authelia:9091 {
        uri /api/authz/forward-auth?authelia_url=https://auth.deine-domain.de/
        copy_headers Remote-User Remote-Groups Remote-Email Remote-Name
    }
}

```

**Was passiert hier?**

- `forward_auth authelia:9091` – Caddy fragt vor jedem Request bei Authelia an
- `uri /api/authz/forward-auth` – genau der Endpoint, den wir in `configuration.yml` aktiviert haben (`server.endpoints.authz.forward-auth`)
- `?authelia_url=https://auth.deine-domain.de/` – sagt Authelia, wohin der Browser für den Login-Flow umgeleitet wird
- `copy_headers Remote-User Remote-Groups Remote-Email Remote-Name` – Wenn die Auth erfolgreich war, hängt Caddy diese Header an den Request, bevor er an die App geht. Apps wie Immich oder Paperless können das auswerten (z. B. um den User-Namen anzuzeigen)

### 14.2 Die Auth-Subdomain selbst

Direkt nach dem `test`\-Block den Login-Portal-Block ergänzen:

```caddyfile
# Authelia Login-Portal – muss öffentlich erreichbar sein
auth.deinedomain.de {
    import public
    reverse_proxy authelia:9091
}

```

Hier **kein** `import auth` – das wäre eine Endlosschleife, weil das Portal selbst nicht hinter sich selbst sitzen kann. Deshalb steht es in `access_control` auch auf `bypass`.

### 14.3 Caddy neu laden

Validieren und reloaden – Pattern aus Artikel 07:

```bash
docker exec caddy caddy validate --config /etc/caddy/Caddyfile
docker exec caddy caddy reload --config /etc/caddy/Caddyfile

```

Im Browser öffnen:

```
https://auth.deine-domain.de

```

*Erwartung:* Du siehst das Authelia-Login-Portal mit echtem Let's-Encrypt-Zertifikat. Schloss-Symbol klickbar → Aussteller `Let's Encrypt`.

## 15\. Erster Login und 2FA einrichten

### 15.1 Login

Auf `https://auth.deine-domain.de`:

- **Username:** `martin` (oder wie auch immer du den ersten User genannt hast)
- **Password:** das Klartext-Passwort, das du in Schritt 10.1 zum Hashen genutzt hast

Du landest auf dem User-Portal. Hier kannst du jetzt 2FA einrichten – bevor du es brauchst.

### 15.2 TOTP einrichten

Im User-Portal auf **Settings → Two-Factor Authentication → Set up One-Time Password**.

Authelia schickt dir jetzt eine Bestätigungs-Mail an die Adresse aus `users_database.yml` – das ist der Moment, an dem dein Mail-Relay aus Artikel 08 erstmals produktiv zum Einsatz kommt.

Im Mail-Postfach erscheint eine Mail mit einem Bestätigungslink. Klick drauf → zurück im Browser zeigt Authelia einen QR-Code.

QR-Code mit deiner Authenticator-App scannen. Der Code erscheint unter dem Namen `deine-domain.de` – das ist der `TOTP_ISSUER` aus deiner `.env`.

Die App zeigt dir einen 6-stelligen Code, der sich alle 30 Sekunden ändert. Diesen Code im Setup-Formular eingeben → fertig.

> **Mail kommt nicht an?**
> 
> Prüfe in dieser Reihenfolge:
> 
> 1. Mail-Relay-Container läuft? In Komodo: Container `mail-relay` → `Up`.
> 2. Logs des Mail-Relays in Komodo zeigen einen Versuch? Wenn nichts kommt, erreicht Authelia den Relay nicht – beide müssen im `homelab_net` sein.
> 3. Mail im Spam-Ordner? Bei der ersten Mail von einer neuen Absender-Adresse passiert das oft.
> 4. Bei wirklichem Versagen: `notifier.filesystem` als Alternative in `configuration.yml` – schreibt die Mails als Textdateien nach `/data/notification.txt`. Dann kannst du den Bestätigungslink dort rauskopieren. Aber das ist die Notlösung, nicht der Normalbetrieb.

### 15.3 Test: Login-Flow vollständig durchlaufen

Inkognito-Browser auf `https://auth.deine-domain.de`:

1. Username + Passwort → Login klappt
2. Bei einer geschützten App (z. B. später `vault.deine-domain.de`) wirst du zum 2FA-Schritt geleitet
3. TOTP-Code aus der Authenticator-App eingeben
4. → Weiterleitung zur App

### 15.4 Test: Session überlebt Container-Restart

Das ist der konkrete Mehrwert von Redis. Probier es einmal aus:

```bash
docker restart authelia

```

Anschließend im Browser eine geschützte Subdomain aufrufen – du bist **nicht** ausgeloggt. Ohne Redis wärst du an dieser Stelle wieder auf dem Login-Portal gelandet.

> **Optional: Passkeys statt 6-stelligem Code**
> 
> Neben TOTP unterstützt Authelia auch **WebAuthn** – das ist die Technik hinter Passkeys (z. B. im Apple-/Google-Konto), Hardware-Sticks wie YubiKey, oder Touch ID auf dem Mac. Statt einen Code abzutippen, bestätigst du den Login per Fingerabdruck, Face ID oder USB-Stick. Vorteil: niemand kann dich auf einer gefälschten Login-Seite austricksen, weil der Browser den echten Server überprüft. Im User-Portal unter **Set up WebAuthn** einrichten.
> 
> Empfehlung: TOTP als Backup behalten (für den Fall, dass der Hardware-Stick mal nicht zur Hand ist), Passkeys/WebAuthn als bequeme Standard-Methode hinzufügen.

## 16\. Wie das in den App-Artikeln aussehen wird

Damit du weißt, was du in Phase 3 erwartet: Jede geschützte App wird im Caddyfile in genau einem Block landen, mit zwei Imports:

```caddyfile
# Vaultwarden (Artikel 12)
vault.deine-domain.de {
    import public
    import auth
    reverse_proxy vaultwarden:80
}

# Immich (Artikel 13)
immich.deine-domain.de {
    import public
    import auth
    reverse_proxy immich-server:2283
}

# Paperless-ngx (Artikel 14)
docs.deine-domain.de {
    import public
    import auth
    reverse_proxy paperless:8000
}

```

Drei Dinge passieren mit `import auth`:

1. Vor jedem Request fragt Caddy bei Authelia an
2. Bei ungültigem Cookie → Redirect auf das Login-Portal
3. Bei gültigem Cookie + passender Policy → Request geht durch zur App, mit User-Headern

Plus: Die Domain muss in `access_control.rules` der `configuration.yml` stehen – sonst greift die `default_policy: one_factor`, was nur Passwort-Schutz bedeutet. Für sensible Apps trägst du die Subdomain mit `two_factor` \+ `subject: ['group:admins']` ein, und Authelia liest die Datei dank `watch: true` automatisch neu.

## 17\. Erweiterungs-Pfade

### Weitere User anlegen

Pro neuem User: Passwort-Hash erzeugen (Schritt 10.1), Eintrag in `users_database.yml` ergänzen – dank `watch: true` wird der Account sofort gültig. Wenn der User keine Admin-Rechte bekommen soll: Gruppe `admins` weglassen. Er kommt dann durch Login + 2FA bis zu den Subdomains ohne Admin-Filter, aber nicht auf die sensiblen Apps.

### Mehr Apps

Im `access_control.rules`\-Block die Subdomain einfügen, Policy und ggf. `subject` festlegen. Authelia liest die Datei live nach – kein Stack-Restart nötig.

### Wenn das nicht mehr reicht: Authentik (Bonus)

Drei Anzeichen, dass du über Authelia hinausgewachsen bist:

- Du willst Apps wie Komodo oder Immich per **OIDC-SSO** anmelden (Authelia ist nur Forward-Auth-Proxy, kein OIDC-Provider für die App-internen Logins).
- Du betreibst mehr als \~5 User und willst Rollen-/Gruppen-Verwaltung mit echter UI.
- Du brauchst LDAP/SAML-Integration für andere Tools.

Dann lohnt sich der Wechsel zu Authentik. Der Bonus-Artikel im Anschluss an diese Serie zeigt, wie du Authelia in-place gegen Authentik tauschst, ohne den Rest der Infrastruktur anzufassen – Caddy bleibt, der `(auth)`\-Snippet wird nur leicht umgeschrieben, der Rest läuft weiter wie zuvor.

Für 95 % der Self-Hosting-Setups ist Authelia ausreichend und bleibt es auch.

## Und jetzt?

Phase 2 ist damit für die Pflicht-Bausteine durch. Du hast eine Auth-Schicht mit 2FA, die in Phase 3 vor jede persönliche App gestellt wird – mit einer einzigen Zeile (`import auth`) pro Caddy-Block.

Wer den optionalen Sicherheitsblock noch dazunehmen will, macht jetzt Artikel 10 (CrowdSec) weiter. CrowdSec ergänzt Authelia gut: Authelia schützt das Login-Formular vor Brute Force auf Account-Ebene (über `regulation`), CrowdSec auf IP-Ebene über alle Apps hinweg. Die Authelia-Logs liegen dank `./log:/log` schon im richtigen Format vor – CrowdSec liest sie direkt mit. Das CrowdSec-Plugin steckt seit Artikel 07 schon im Caddy-Image, nur eingeschaltet wird es noch nicht.

Wer das überspringen will, geht direkt zu Artikel 11 (App-Baukasten) – dem Bindeglied zwischen Infrastruktur und den ersten echten Apps in Phase 3.

👉 **Nächster Artikel:** [10\. CrowdSec – Angriffserkennung für öffentliche Web-Dienste](#) *(optional)*  
👉 **Oder direkt:** [11\. App-Baukasten verstehen und nutzen](#)