Compare commits

...

3 commits

Author SHA1 Message Date
20025814e8 chore(agent): update briefings to reflect M7.3 doc mirror
Root Agent.md and stacks/forgejo/Agent.md had stale references to
"docs live in iCloud, will be mirrored in M7.3". Now M7.3 is done,
so the briefings point to docs/adr/ and docs/runbooks/ directly
with relative links that render as clickable in the Forgejo web UI.

The iCloud folder stays around for loose notes and credentials
that cannot go into a public repo, but the primary source of truth
for ADRs / runbooks / guides is now this repo.

Refs OP#1118
2026-04-11 22:26:24 +02:00
6570e81850 docs(runbooks,guides): mirror runbooks and Authentik OIDC guide
Two Forgejo runbooks plus the Authentik OAuth2 provider guide,
mirrored from the iCloud folder into the versioned repo.

Runbooks:
- forgejo-admin-recovery.md — fallback login when Authentik is down
  using the local admin-local user (prohibit_login reset via SQL).
- forgejo-backup-restore.md — backup format, restore scenarios
  (full / DB-only / single file), disaster recovery on new host.

Guides:
- authentik-oauth2-provider.md — reusable template for adding native
  OIDC integrations in Authentik. First applied for Forgejo, ready
  to reuse for OpenProject, Nextcloud, Grafana. Includes the
  important launch-URL gotcha from ADR-0006.

Each category folder has a README.md with format conventions and
an index of the current documents.

Refs OP#1118
2026-04-11 22:26:24 +02:00
88c541c9ed docs(adr): mirror 6 ADRs from M7.1 into repo
Adds the Architecture Decision Records that were written during the
Forgejo deployment (M7.1) as part of moving docs from the iCloud folder
into this versioned repository.

Includes:
- ADR-0001: Forgejo vs Gitea (non-profit stewardship)
- ADR-0002: ai-apps placement (no separate VM)
- ADR-0003: Native OIDC, not ForwardAuth
- ADR-0004: Subdomain code.sdda.eu
- ADR-0005: Volume mount on /data (lesson learned)
- ADR-0006: Silent SSO via OAuth2 launch URL (lesson learned)

Plus a docs/adr/README.md that explains the ADR format, lists the
current ADRs, and provides a template for future entries.

Refs OP#1118
2026-04-11 22:26:05 +02:00
15 changed files with 979 additions and 9 deletions

View file

@ -89,11 +89,14 @@ Das bedeutet:
- M7: Operations & Documentation Foundation - M7: Operations & Documentation Foundation
- M7.1: Forgejo Deployment (#1119) — abgeschlossen - M7.1: Forgejo Deployment (#1119) — abgeschlossen
- M7.2: Erstes Infra-Repo (dieses Repo!) - M7.2: Erstes Infra-Repo (dieses Repo!)
- M7.3+: ADRs/Runbooks spiegeln, Mirror zu GitHub, etc. - M7.3: ADRs/Runbooks/Guides gespiegelt (dieser Commit)
- M7.4+: Mirror zu GitHub, rclone backup, weitere Apps
## Related External Docs ## Related Docs (im Repo)
Außerhalb dieses Repos lebt Doku in: Ab M7.3 lebt die Doku primär in diesem Repo:
- `/Users/benjaminweinlich/Library/Mobile Documents/com~apple~CloudDocs/AI/IT & Infrastruktur/` - [`docs/adr/`](docs/adr/) — Architecture Decision Records (6 ADRs aus M7.1)
- `Forgejo/` — ADRs + Runbooks für den Forgejo-Stack (werden in M7.3 hier hin gespiegelt) - [`docs/runbooks/`](docs/runbooks/) — Ops-Runbooks
- `Authentik/howto-oauth2-provider.md` — Template für OIDC-Integrationen - [`docs/guides/`](docs/guides/) — Wiederverwendbare Setup-Anleitungen
- `Hetzner/ai-apps-stacks.md` — Cloud-Inventory - [`docs/architecture/`](docs/architecture/) — Topologie + Stack-Inventories
Der lokale iCloud-Ordner `AI/IT & Infrastruktur/` ist ab jetzt sekundär — dort leben nur noch Credentials-Hinweise und Lose-Notizen, die nicht in ein public Repo gehören.

56
docs/README.md Normal file
View file

@ -0,0 +1,56 @@
# docs/
Alle Dokumentation zur Electric-Horses-Infrastruktur. Jeder Unterordner hat seine eigene `README.md` mit Konventionen und Index.
## Struktur
```
docs/
├── README.md # diese Datei
├── architecture/ # Topologie, Stack-Inventories, Diagramme
│ └── ai-apps-stacks.md # Stack-Snapshot auf ai-apps
├── adr/ # Architecture Decision Records
│ ├── README.md # Format-Guide + Index
│ └── 0001-0006-*.md # 6 ADRs aus M7.1
├── runbooks/ # Schritt-für-Schritt Anleitungen für Ops
│ ├── README.md # Format + Index
│ ├── forgejo-admin-recovery.md
│ └── forgejo-backup-restore.md
└── guides/ # Wiederverwendbare Setup-Anleitungen
├── README.md # Format + Index
└── authentik-oauth2-provider.md
```
## Philosophie
**Docs-as-Code:** Dokumentation lebt neben dem Code, im selben Git-Repo, mit derselben Change-History. Keine separaten Wikis (driften), keine Confluence (teuer, closed), keine Notion (lock-in).
**Mermaid für Diagramme:** Forgejo rendert Mermaid-Blöcke im Web-UI direkt. Beispiel:
~~~mermaid
graph LR
Browser --> Nginx
Nginx --> Directus
Nginx --> Astro
~~~
**Markdown für alles andere:** Keine Word-Docs, keine PDFs (außer Anhänge). Alles in Plain-Text lesbar + commitbar + diffbar.
## Was hierher gehört
- Architektur-Beschreibungen, Topologie-Diagramme
- ADRs (warum haben wir X gebaut wie wir es gebaut haben)
- Runbooks (wie debuggt / repariert man konkret)
- Guides (wie richtet man etwas Wiederkehrendes ein)
- How-Tos für Team-Onboarding (später, wenn das Team wächst)
## Was NICHT hierher gehört
- Secrets / Credentials (niemals, Repo ist public!)
- Kundendaten / personenbezogene Daten (DSGVO)
- Marketing-Texte (gehört auf die Website)
- Lange Design-Specs in Rohform (lieber als ADR destillieren)
## Navigation-Tipp
Im Forgejo-Web-UI kannst du die `README.md`-Dateien durchklicken — jede erklärt ihren Unterordner und verlinkt die einzelnen Inhalte. Das ersetzt ein Wiki-Interface.

View file

@ -0,0 +1,39 @@
# ADR-0001: Forgejo statt Gitea
**Status:** Accepted
**Datum:** 2026-04-11
**Entscheider:** Benjamin Weinlich
**Phase:** M7.1 — Forgejo Deployment
## Kontext
Wir brauchen ein selbst-gehostetes Git-Hosting für Code, Configs und Dokumentation. GitHub ist kommerziell und bei Microsoft; das passt nicht zum Souveränitäts-Prinzip unseres Stacks. Die naheliegende Open-Source-Alternative ist die Gitea-Familie.
Gitea war lange die populärste FOSS-Option. Im August 2022 wurde die Entwicklung von einer kommerziellen Firma (Gitea Ltd.) übernommen. Das hat die Community gespalten: Ein Teil der ursprünglichen Maintainer gründete **Forgejo** als Community-Fork, gesteuert vom non-profit Verein **Codeberg e.V.** (Berlin).
## Entscheidung
Wir nutzen **Forgejo** (nicht Gitea).
## Konsequenzen
### Positiv
- **Non-profit Stewardship:** Codeberg e.V. ist ein eingetragener Verein ohne kommerzielle Interessen. Keine Gefahr von rug-pulls oder "free tier" Beschränkungen.
- **Schnellere Security-Updates:** Forgejo hat einen dedizierten Security-Track.
- **API-kompatibel mit Gitea:** Migration zwischen Gitea und Forgejo in beide Richtungen ist trivial. Keine Lock-in.
- **Aktive Entwicklung:** Forgejo hat ein festes Release-Rhythmus und transparente Roadmap.
- **Passt zu unserem Prinzip:** Open Source > proprietär, Souveränität > Bequemlichkeit.
### Negativ / Trade-off
- Kleinere Community als Gitea (noch). Dritt-Dokumentation findet sich öfter für Gitea — aber da APIs identisch sind, ist das selten ein Blocker.
- Integrations (z.B. mit CI-Tools) sind oft als "Gitea" beworben aber funktionieren meist auch für Forgejo.
## Alternativen
- **Gitea:** Verworfen wegen kommerzieller Stewardship-Sorgen.
- **GitLab CE:** Zu schwergewichtig (>4 GB RAM, lange Installation), würde ai-apps sprengen.
- **Gogs:** Vorgänger von Gitea, Entwicklung eingeschlafen. Keine Zukunft.
- **SourceHut:** Minimalistisch, aber ungewöhnliches UI, kleine Community.
- **GitHub private:** Verworfen wegen Souveränitäts-Prinzip.
## Referenzen
- [Forgejo Projekt](https://forgejo.org/)
- [Codeberg e.V.](https://join.codeberg.org/)
- [Warum Forgejo gegründet wurde](https://forgejo.org/2022-12-15-hello-forgejo/)

View file

@ -0,0 +1,56 @@
# ADR-0002: Forgejo auf ai-apps, nicht auf separater VM
**Status:** Accepted
**Datum:** 2026-04-11
**Entscheider:** Benjamin Weinlich
**Phase:** M7.1 — Forgejo Deployment
## Kontext
Forgejo kann überall laufen — auf einem eigenen Hetzner-Server, neben bestehenden Services, oder sogar on-prem. Wir müssen uns festlegen.
Optionen geprüft:
1. **Auf ai-apps (Hetzner cx22)** neben n8n, eh-search, locosoft, etc.
2. **Auf Pegasus (Hetzner cx33)** neben dem Electric-Horses Stack
3. **Auf einem neuen separaten Hetzner cx22** nur für Forgejo
4. **On-premises im Autohaus** auf Hardware dort
## Entscheidung
**Auf ai-apps.** Kein separater Server, kein Pegasus, kein on-prem.
## Begründung
### Warum ai-apps
- **Traefik 3.6.2 läuft schon** dort mit Let's Encrypt Resolver. Keine neue TLS-Infrastruktur nötig.
- **Stack-Pattern etabliert** (`/opt/ai-apps/<name>/docker-compose.yml`) — Forgejo reiht sich nahtlos ein.
- **Private-Network-Kommunikation** mit anderen Services verfügbar (Mailcow, Authentik, etc.).
- **Ressourcen reichen:** 7.6 GB RAM total, nach Verbrauch der bestehenden Services ~5.7 GB verfügbar (inkl. Cache). Forgejo + Postgres peak ~800 MB.
- **Operations-Pattern bekannt:** Docker Compose, Portainer, Cron-Backups — alles schon da.
### Warum nicht Pegasus
Pegasus hostet den öffentlichen Webseiten-Stack (Astro, Directus, MariaDB, Nginx, Qdrant). Das ist kunden-facing Production mit hohen Verfügbarkeitsanforderungen. Wir wollen keine zusätzliche Last oder Neuerungen dort. Pegasus hat außerdem kein public IPv4 — wir müssten über Webmin proxyn, was den Pfad unnötig verkompliziert.
### Warum nicht separater Server
Ein neuer cx22 würde ~4.50 €/Monat kosten und zusätzlich eigenen Traefik, eigene Let's Encrypt Config, eigene Backup-Infra, eigene Monitoring-Integration erfordern. Für einen einzelnen Service mit <1 GB RAM-Bedarf ist der Ops-Overhead nicht gerechtfertigt. **Re-evaluation:** Sobald Forgejo Actions (CI/CD runners) aktiviert werden, wird der Ressourcenbedarf steigen und ein Upgrade auf cx32 oder ein separater Runner-Host Sinn machen.
### Warum nicht on-prem
Git-Hosting muss 24/7 von außen erreichbar sein (Home-Office, Mobil, von überall). Das Autohaus-LAN ist hinter Consumer-Internet und hat keinen stabilen Public-Pfad. Außerdem steht der on-prem Stack (Loco-Soft-Server, eh-sync Bridge) unter anderen Verfügbarkeitsbedingungen.
## Konsequenzen
### Positiv
- Schnelles Deployment (keine neue Infrastruktur)
- Keine Extra-Kosten
- Integration mit Authentik via Private Network (10.0.0.x)
- Konsolidiertes Monitoring (alle Stacks via Portainer sichtbar)
### Negativ / Risiko
- **RAM-Druck:** Bei ~250 MB wirklich freiem RAM ist der Spielraum knapp. Forgejo + Postgres peak ~800 MB kommt nah an die Obergrenze. Mitigation: Monitoring, ggf. Upgrade auf cx32 (16 GB, +4 €/Monat) wenn nötig.
- **Single-Point-of-Failure ai-apps:** Wenn ai-apps ausfällt, sind n8n, eh-search, locosoft und Forgejo gleichzeitig weg. Mitigation: Hetzner Cloud Snapshots, Docker Volume Backups.
- **Keine CI-Runner möglich:** Forgejo Actions würde ai-apps sprengen. Wenn CI dazu kommt, separater Runner-Host (z.B. Woodpecker auf cx22).
## Re-evaluation-Trigger
- RAM-Nutzung auf ai-apps dauerhaft > 85%
- Wunsch nach Forgejo Actions / CI-Runnern
- Mehrere gleichzeitige große Git-Pushes verlangsamen andere Services spürbar
In diesen Fällen: Separater Hetzner-Server für Forgejo (oder cx32-Upgrade von ai-apps).

View file

@ -0,0 +1,48 @@
# ADR-0003: Natives OIDC statt Authentik ForwardAuth
**Status:** Accepted
**Datum:** 2026-04-11
**Entscheider:** Benjamin Weinlich
**Phase:** M7.1 — Forgejo Deployment
## Kontext
Authentik bietet zwei Integrations-Patterns:
1. **ForwardAuth / Proxy-Provider:** Der Authentik Embedded Outpost macht Auth vor der App. Traefik leitet Requests zuerst an den Outpost, der User-Claims als HTTP-Header injectet. Die App weiß nichts von OIDC, sieht nur "authenticated" Requests. Unsere bestehenden Apps (n8n, locosoft-hilfe-system) nutzen dieses Pattern.
2. **Nativer OIDC Client:** Die App spricht selbst OIDC mit Authentik. Sie macht den Authorization-Code-Flow, holt Tokens, parsed id_token und Userinfo. Kein Outpost als Middleware.
Für Forgejo müssen wir wählen.
## Entscheidung
**Nativer OIDC Client** in Forgejo. Forgejo macht den OAuth2-Handshake direkt mit Authentik.
## Begründung
### Pro natives OIDC
- **Forgejo ist ein echter OIDC-Client**. Es unterstützt OAuth2/OIDC als First-Class-Feature mit vollem Flow, Token-Exchange, Userinfo-Retrieval, Auto-Registration, Account-Linking via Email.
- **User-Identität kommt voll an:** Forgejo weiß wer der User ist (Name, Email, Avatar), kann ihn lokal anlegen und persistieren. Bei ForwardAuth sieht es nur "authenticated" plus Header — was für Gruppen-Zuordnung und persistente User-Records umständlich wäre.
- **Git-Commits signiert:** Forgejo kann Commits im Namen des Users signieren, was mit ForwardAuth und der Proxy-Layer komplizierter würde.
- **API-Access via Personal Access Tokens:** Entwickler können in Forgejo persönliche API-Tokens erstellen, ohne die Authentik-Credentials teilen zu müssen. Mit ForwardAuth müssten API-Requests durch den Outpost, was API-Keys-im-Header-Pattern bricht.
- **OIDC ist Standard:** Sollten wir irgendwann Authentik ersetzen (z.B. durch Zitadel, Keycloak), ist der Umstieg trivial, weil OIDC interoperabel ist. Bei ForwardAuth wäre der Umstieg viel schwerer.
### Contra
- **Zweites Auth-Pattern in Authentik:** Bisher haben alle unsere Apps ForwardAuth genutzt. Jetzt kommt ein echter OIDC-Provider dazu. Mehr Komplexität in Authentik (aber: OIDC ist auch das universale Standard-Pattern, unsere ForwardAuth-Apps sind eigentlich die Ausnahme).
- **Migration bestehender Apps denkbar:** Mittelfristig könnten n8n und andere auch auf nativen OIDC migriert werden, um die Konsistenz wiederherzustellen. Das ist aber ein eigenes Projekt (siehe M7+).
## Konsequenzen
### Positiv
- Forgejo-Login-Seite zeigt einen sauberen "Sign in with authentik" Button.
- User werden automatisch beim ersten Login angelegt (via `ENABLE_AUTO_REGISTRATION=true`, `ACCOUNT_LINKING=auto`, `USERNAME=email`).
- Gruppen-Zugangskontrolle via Authentik Policy (nur Mitglieder von `forgejo-users`).
- Kein Traefik ForwardAuth Middleware nötig — Forgejo Docker-Labels sind simpel.
### Zu beachten
- **Discovery-URL muss public sein.** Der Browser und der Forgejo-Backend-Call zum Token-Endpoint müssen dieselbe Issuer-URL benutzen, sonst scheitert die JWT-Issuer-Validation. Daher `https://welcome.sdda.eu/application/o/forgejo/` überall, nicht die private `10.0.0.7`.
- **OIDC-Provider-Config liegt in Postgres** (Tabelle `login_source`), nicht in `app.ini`. Muss via `forgejo admin auth add-oauth` CLI gesetzt werden, nicht über ENV-Variablen.
- **Client Credentials in .env nur als Referenz** — die echte Speicherung ist in der DB nach dem CLI-Call.
## Follow-Up-Ideen
- Eine separate ADR für künftige Authentik-OIDC-Integrationen (OpenProject, Nextcloud, vielleicht Migration n8n/locosoft).
- `Authentik/howto-oauth2-provider.md` als Template für diese zukünftigen Apps schreiben (Teil M7.1).

View file

@ -0,0 +1,38 @@
# ADR-0004: Subdomain `code.sdda.eu`
**Status:** Accepted
**Datum:** 2026-04-11
**Entscheider:** Benjamin Weinlich
**Phase:** M7.1 — Forgejo Deployment
## Kontext
Forgejo braucht eine öffentliche Subdomain unter `sdda.eu`. Mehrere Varianten standen zur Wahl.
## Entscheidung
**`code.sdda.eu`**
## Alternativen
| Option | Pro | Contra |
|---|---|---|
| `git.sdda.eu` | Kurz, Industriestandard (GitHub, GitLab, Bitbucket) | "Git" ist technisch, Fokus auf Protokoll statt Inhalt |
| **`code.sdda.eu`** ✓ | Sprechend, User-orientiert ("hier ist unser Code"), nicht protokoll-spezifisch | Nicht ganz so "Standard" wie git.* |
| `source.sdda.eu` | Neutral, betont Source-of-Truth | Ungewöhnlich |
| `forge.sdda.eu` | Bezieht sich auf Forgejo-Namen | Tool-spezifisch; bei Migration zu anderer Software irreführend |
## Begründung
Benjamin hat `code.sdda.eu` gewählt, weil es:
- **User-zentriert** ist — "Code" ist näher am Geschäftszweck als "Git" (Protokoll)
- **Tool-agnostisch** ist — würden wir irgendwann auf GitLab / Gitea / whatever wechseln, passt der Name weiter
- **Kurz und merkbar** ist
- **Keine technischen Vorkenntnisse braucht** — ein Werkstatt-Mitarbeiter versteht "Code" schneller als "Git"
## Konsequenzen
- DNS A-Record `code.sdda.eu → 91.98.226.44` auf Webmin/BIND
- AAAA-Record `code.sdda.eu → 2a01:4f8:1c1e:5113::1` (IPv6)
- Let's Encrypt Cert automatisch über Traefik
- Authentik Redirect URI: `https://code.sdda.eu/user/oauth2/authentik/callback`
- Forgejo ROOT_URL und DOMAIN konfiguriert auf `code.sdda.eu`
## Nicht übersehen
Die Forgejo `SSH_DOMAIN` ist ebenfalls `code.sdda.eu` (Port 222) — git clone URLs sind dann `ssh://git@code.sdda.eu:222/user/repo.git`.

View file

@ -0,0 +1,59 @@
# ADR-0005: Volume-Mount auf `/data`, nicht `/var/lib/gitea`
**Status:** Accepted (Lesson Learned)
**Datum:** 2026-04-11
**Entscheider:** Benjamin Weinlich
**Phase:** M7.1 — Forgejo Deployment
## Kontext
Beim initialen Deployment wurde das Docker-Volume `forgejo-data` fälschlich auf `/var/lib/gitea` im Container gemountet. Grund: ältere Gitea/Forgejo Docs nennen teilweise `/var/lib/gitea` als APP_INI-Directory, und das wirkte als "Datenverzeichnis".
Der Bug fiel auf, als das erste Backup nur 87 Bytes hatte (leeres tar) — weil das gemountete Volume leer war, während Forgejo seine Daten tatsächlich in `/data` im Container-Layer schrieb.
## Entscheidung
Volume-Mount **`forgejo-data:/data`**, nicht `/var/lib/gitea`.
## Was Forgejo tatsächlich wo speichert
Aus Forgejo Docker Image:
- **`/data`** — Haupt-Datenverzeichnis für ALLES:
- `/data/git/` — Git-Repositories
- `/data/gitea/conf/app.ini` — Config
- `/data/gitea/avatars/` — User-Avatars
- `/data/gitea/repo-avatars/` — Repo-Avatars
- `/data/gitea/lfs/` — LFS-Objekte
- `/data/gitea/attachments/` — Issue/PR-Anhänge
- `/data/gitea/packages/` — Package Registry
- `/data/gitea/actions_log/` — CI-Logs
- `/data/gitea/actions_artifacts/` — CI-Artefakte
- `/data/gitea/ssh/` — Forgejo SSH Host Keys (gitea.rsa etc.)
- `/var/lib/gitea` — existiert nicht im Forgejo-Image
Die `FORGEJO__storage__*` ENV-Variablen erwarten relative Pfade unter `/data/gitea/`.
## Konsequenzen
### Mount korrigiert
```yaml
volumes:
- forgejo-data:/data # KORREKT
# - forgejo-data:/var/lib/gitea # FALSCH, Anfänger-Fehler
```
### Runtime-Impact
Beim Fix musste der Forgejo-Container mit `--force-recreate` neu erzeugt werden. Die initial im Container-Layer gespeicherten Daten (SSH Host Keys, leere Storage-Ordner) gingen verloren und wurden beim neuen Start frisch initialisiert. Das war akzeptabel weil:
- Postgres (eh_vehicles → forgejo-db) hatte alle User/OIDC-Daten → blieb erhalten
- SSH Host Keys werden automatisch neu generiert
- Keine Commits oder Repos existierten noch
### Backup-Implikation
Das Backup-Script `backup.sh` tart jetzt korrekt das volle `/data`, nicht ein leeres Verzeichnis. Verifiziert: 12 KB statt 87 Bytes beim Test-Run.
## Lesson Learned
**Beim Setup eines neuen Docker-Stacks:**
1. Nach `docker compose up -d` sofort checken: `docker run --rm -v <volume_name>:/data:ro alpine ls -la /data/`
2. Wenn das Volume leer aussieht, aber der Container offenbar Daten schreibt → Mount-Pfad falsch
3. Das offizielle Image-Documentation konsultieren, NICHT aus Memory copy-pasten
## Related
- `Agent.md` — "Known Issues / Gotchas" Sektion erwähnt diesen Punkt als Warnung
- `runbook-backup-restore.md` — Restore-Prozedur nutzt den korrekten Pfad

View file

@ -0,0 +1,82 @@
# ADR-0006: Silent SSO via OAuth2-Initiate-Launch-URL
**Status:** Accepted (Lesson Learned)
**Datum:** 2026-04-11
**Entscheider:** Benjamin Weinlich
**Phase:** M7.1 — Forgejo Deployment (Post-Launch Fix)
## Kontext
Nach dem Go-Live von Forgejo stellte Benjamin fest: Wenn man im Authentik-Dashboard (welcome.sdda.eu) auf die Forgejo-Kachel klickt, wird man auf `https://code.sdda.eu/` weitergeleitet — und muss dort **erneut auf "Anmelden mit authentik"** klicken. Das fühlt sich an wie "zweimal einloggen", auch wenn der zweite Klick technisch nur den OIDC-Flow triggert.
Technisch korrekt, aber UX-Fail: Das Versprechen von SSO ist "einmal einloggen, überall drin sein". Ein Extra-Klick ist kein Single-Sign-On.
## Root Cause
Die Application `forgejo` in Authentik hatte `meta_launch_url=https://code.sdda.eu/`. Das ist die Forgejo-Startseite. Wenn der User keine Forgejo-Session hat (erster Besuch), zeigt Forgejo die Login-Seite an — und der User muss manuell den OIDC-Button klicken.
Forgejo hat **keine Config-Option** "redirect to OIDC statt Login-Seite zeigen" (wie z.B. GitLab's `auto_sign_in_with_provider`). Der OIDC-Flow wird nur bei Klick auf den Button oder bei direktem Aufruf der Route `/user/oauth2/<name>` getriggert.
## Entscheidung
**Launch-URL in Authentik auf `https://code.sdda.eu/user/oauth2/authentik` ändern.**
Das ist die Forgejo-Route die den OIDC-Flow initiiert. Wenn der User schon eine Authentik-Session hat, läuft der Flow "silent" durch (ohne User-Interaktion) und landet direkt auf der Forgejo-Startseite.
## Flow-Vergleich
### Vorher (zwei Klicks)
```
[Dashboard: Klick "Forgejo"]
↓ GET https://code.sdda.eu/
[Forgejo Login-Seite erscheint]
↓ [Klick "Anmelden mit authentik"]
↓ GET https://code.sdda.eu/user/oauth2/authentik
↓ GET https://welcome.sdda.eu/application/o/authorize/ (Auth OK, silent)
↓ GET https://code.sdda.eu/user/oauth2/authentik/callback?code=...
[Forgejo Startseite]
```
### Nachher (ein Klick)
```
[Dashboard: Klick "Forgejo"]
↓ GET https://code.sdda.eu/user/oauth2/authentik
↓ GET https://welcome.sdda.eu/application/o/authorize/ (Auth OK, silent)
↓ GET https://code.sdda.eu/user/oauth2/authentik/callback?code=...
[Forgejo Startseite]
```
Die Zwischenseite mit dem Login-Form fällt komplett weg.
## Umsetzung
Via REST API (weil Authentik-MCP keine `update_application` hat):
```bash
TOKEN=<akadmin-api>
curl -X PATCH 'http://localhost:9000/api/v3/core/applications/forgejo/' \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"meta_launch_url": "https://code.sdda.eu/user/oauth2/authentik"}'
```
## Zusätzlich behoben: must_change_password
Bei der Gelegenheit fiel auf, dass Benjamins User (`bw`) den Flag `must_change_password=true` hatte. Grund: Ich hatte während des initialen Setups `forgejo admin user change-password --username bw --password <random>` aufgerufen, um den leeren Passwort-Slot zu füllen — Forgejo markiert das User-Konto dann als "muss Passwort wechseln". Für reine OIDC-User ist das unpassend, der User hat nie ein Passwort nötig.
**Fix:**
```sql
UPDATE "user"
SET must_change_password = false, passwd = '', salt = '', passwd_hash_algo = ''
WHERE lower_name = 'bw';
```
Damit ist bw ein echter password-less OIDC-User: Login geht ausschließlich via Authentik.
## Konsequenzen
- **UX:** Authentik-Dashboard → Forgejo = ein Klick, keine Zwischenseite mehr.
- **Template aktualisiert:** `../Authentik/howto-oauth2-provider.md` hat jetzt eine explizite Warnung und Launch-URL-Beispiele für Forgejo, OpenProject, Nextcloud, Grafana.
- **Gilt für alle zukünftigen OIDC-Apps:** Die Launch-URL muss immer auf die OAuth2-Initiate-Route zeigen, nicht auf die App-Startseite. Merkhilfe: Wenn der Nutzer die App-Startseite ohne Login sieht und dort klicken muss, ist es falsch konfiguriert.
## Gotcha: Direkter Zugriff via `code.sdda.eu` bleibt unverändert
Wer `https://code.sdda.eu/` direkt in den Browser eingibt (ohne den Umweg über Authentik-Dashboard), sieht weiter die Forgejo-Login-Seite mit dem manuellen OIDC-Button. Das ist OK so: Bookmark-User die direkt auf Forgejo gehen, bekommen das Login-UI — Dashboard-User kriegen silent SSO.
Man kann das noch weiter optimieren (z.B. via `[service] DEFAULT_USER_VISIBILITY=limited` + Redirect-Magic), aber dafür gibt's keinen sauberen Konfig-Pfad in Forgejo. Current state ist guter Trade-off.
## Related
- `0003-native-oidc-not-forwardauth.md` — Warum wir nativ OIDC machen
- `../Authentik/howto-oauth2-provider.md` — Template für zukünftige Apps (jetzt mit Launch-URL-Hinweis)

74
docs/adr/README.md Normal file
View file

@ -0,0 +1,74 @@
# Architecture Decision Records
Dieses Verzeichnis enthält **ADRs** — kurze Markdown-Dokumente die einzelne Architektur-Entscheidungen festhalten. Das Pattern kommt von Michael Nygard und ist gemeinsamer Standard in vielen Open-Source-Projekten.
## Warum ADRs?
- **Warum, nicht Wie:** Der Code ändert sich. Die Intention soll bleiben. Eine ADR erklärt warum etwas so gebaut wurde — in einem halben Jahr weiß niemand mehr sonst.
- **Referenz für AI-Sessions:** Wenn die AI morgen eine Änderung vorschlägt, die eine bestehende Entscheidung umkehrt, soll sie das bewusst tun und neue ADR schreiben, nicht versehentlich.
- **Audit-Trail:** Jede ADR hat Status, Datum und Entscheider. Das ist Compliance-ready.
## Format
Jede ADR folgt grob diesem Schema:
```markdown
# ADR-NNNN: <Titel>
Status: Proposed | Accepted | Superseded by ADR-NNNN | Deprecated
Datum: YYYY-MM-DD
Entscheider: <Name>
Phase: <OpenProject Phase oder Sprint>
## Kontext
Was war das Problem? Welche Optionen standen zur Wahl?
## Entscheidung
Was wurde gewählt?
## Begründung
Warum genau diese Option? Welche Prinzipien waren leitend?
## Konsequenzen
- **Positiv:** Was gewinnen wir?
- **Negativ/Trade-off:** Was geben wir auf?
## Alternativen
Was wurde verworfen und warum?
```
## Nummerierung
Fortlaufend `NNNN` beginnend bei `0001`. **Niemals** alte ADRs ändern — superseden via neue ADR mit Verweis. Die Historie ist die Wahrheit.
## Aktuelle ADRs (alle aus M7.1 — Forgejo Deployment)
| # | Titel | Status |
|---|---|---|
| 0001 | [Forgejo statt Gitea](0001-forgejo-vs-gitea.md) | Accepted |
| 0002 | [Forgejo auf ai-apps, nicht separate VM](0002-ai-apps-placement.md) | Accepted |
| 0003 | [Natives OIDC statt ForwardAuth](0003-native-oidc-not-forwardauth.md) | Accepted |
| 0004 | [Subdomain code.sdda.eu](0004-subdomain-code-sdda-eu.md) | Accepted |
| 0005 | [Volume-Mount auf /data](0005-volume-mount-data-not-var-lib.md) | Accepted (Lesson Learned) |
| 0006 | [Silent SSO via OAuth2-Initiate-Launch-URL](0006-silent-sso-launch-url.md) | Accepted (Lesson Learned) |
## Vorlage für neue ADRs
Copy-and-paste Startpunkt:
```markdown
# ADR-NNNN: <Titel>
**Status:** Proposed
**Datum:** YYYY-MM-DD
**Entscheider:** <Name>
**Phase:** <OpenProject>
## Kontext
## Entscheidung
## Begründung
## Konsequenzen
### Positiv
### Negativ / Trade-off
## Alternativen
## Referenzen
```

26
docs/guides/README.md Normal file
View file

@ -0,0 +1,26 @@
# Guides
**Guides** sind Anleitungen für Setup- und Konfigurations-Aufgaben die wiederholt vorkommen — aber nicht notfall-getrieben wie Runbooks. Sie sind das **"How do we do X here"** der Firma.
## Unterschied zu Runbooks
- **Runbook:** "Der Loco-Sync ist kaputt — was jetzt?" → reagiert auf Probleme
- **Guide:** "Wie integriere ich eine neue App mit Authentik OIDC?" → proaktive Einrichtung
## Unterschied zu ADRs
- **ADR:** "Warum haben wir diese Entscheidung getroffen?"
- **Guide:** "Wie setze ich diese Entscheidung für einen neuen Service um?"
Typisch: Eine ADR legt das Pattern fest, der Guide zeigt wie man es anwendet.
## Aktuelle Guides
### Authentik
- **[authentik-oauth2-provider.md](authentik-oauth2-provider.md)** — Wie richtet man einen nativen OIDC-Provider in Authentik für eine neue App ein? Template-based, wurde zum ersten Mal für Forgejo genutzt, ist jetzt wiederverwendbar für OpenProject, Nextcloud, Grafana, etc.
## Wann neue Guides schreiben
- Wenn eine Setup-Sequenz mehr als einmal vorkommen wird
- Wenn sie mehr als 5 Schritte hat
- Wenn die korrekte Reihenfolge nicht offensichtlich ist
- Wenn es Gotchas gibt die man schnell vergessen würde
Wenn es nur einmalig ist und nie wieder vorkommt: Kein Guide, sondern direkt als ADR dokumentieren (falls eine Entscheidung drinsteckt).

View file

@ -0,0 +1,224 @@
# How-To: OAuth2/OIDC Provider in Authentik einrichten
**Stand:** 2026-04-11
**Erstanwendung:** M7.1 — Forgejo OIDC Setup
**Zielgruppe:** AI-Agents und Benjamin — wiederverwendbares Template für künftige OIDC-Integrationen
## Wann dieses Pattern nutzen
Wenn eine Applikation OIDC (OpenID Connect) als First-Class-Client unterstützt (z.B. Forgejo, OpenProject, Nextcloud, GitLab, Grafana, Mattermost). In diesem Fall ist nativer OIDC besser als ForwardAuth weil:
- User-Identität (Name, Email, Avatar, Gruppen) kommt direkt in der App an
- Auto-Registration / Account-Linking möglich
- API-Tokens der App bleiben unabhängig
- Migration zu anderer IdP-Software trivial
Für Apps **ohne** OIDC-Support (oder zu komplex zu integrieren): ForwardAuth via Embedded Outpost — siehe das LocoSoft/n8n Pattern.
## Voraussetzungen
- Authentik läuft und ist über `welcome.sdda.eu` erreichbar
- SSH-Zugang `authentik-sso` verfügbar
- MCP Tools `authentik_*` verfügbar ODER API-Token für akadmin
- Die App (z.B. Forgejo) ist bereits live oder zumindest geplant — du brauchst die **Redirect URI** (meist `https://<app-domain>/user/oauth2/<name>/callback` oder ähnlich)
## Schritt 1: Gruppe für Zugangskontrolle (optional aber empfohlen)
Statt "jeder in Authentik" → eigene Gruppe pro App, damit User-Anlage und -Abschaltung granular funktioniert.
```python
# Via MCP Tool
mcp.authentik_create_group(name="<app-slug>-users")
```
Oder via REST API:
```bash
TOKEN=<akadmin-api-token>
curl -X POST 'http://localhost:9000/api/v3/core/groups/' \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"name": "forgejo-users", "is_superuser": false}'
```
Notiere die zurückgegebene `pk` (UUID).
## Schritt 2: OAuth2/OpenID Provider anlegen
```bash
# Vorbereitung: Flow-IDs und Scope-Mapping-IDs holen
TOKEN=<akadmin-api-token>
# Authorization Flow (Default: explicit consent)
AUTH_FLOW="9f7af462-90cf-44d8-99fa-916f8459ba13"
# Invalidation Flow (für logout)
INVAL_FLOW="697e1b4c-db65-4c21-a458-dd7df2bf51f2"
# Scope Property Mappings (diese sind bei uns fest)
SCOPE_OPENID="f878f335-46dd-4104-8dfb-fe72f21dda22"
SCOPE_PROFILE="04294271-539c-44e6-824a-5ab5c1e4613d"
SCOPE_EMAIL="602c1e0a-0c6d-48ef-bada-97d4c6e45f3c"
# Signing Certificate (RS256 statt HMAC)
CERT="663c97fa-f336-49fe-8809-118d3745547a" # authentik Self-signed
# Eigenes Client Secret generieren
CLIENT_SECRET=$(openssl rand -hex 48)
# Provider anlegen
curl -X POST 'http://localhost:9000/api/v3/providers/oauth2/' \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d "{
\"name\": \"<App-Name> OIDC\",
\"authorization_flow\": \"$AUTH_FLOW\",
\"invalidation_flow\": \"$INVAL_FLOW\",
\"client_type\": \"confidential\",
\"client_secret\": \"$CLIENT_SECRET\",
\"access_code_validity\": \"minutes=1\",
\"access_token_validity\": \"hours=24\",
\"refresh_token_validity\": \"days=30\",
\"include_claims_in_id_token\": true,
\"signing_key\": \"$CERT\",
\"sub_mode\": \"user_email\",
\"issuer_mode\": \"per_provider\",
\"redirect_uris\": [
{\"matching_mode\": \"strict\", \"url\": \"<REDIRECT_URI>\"}
],
\"property_mappings\": [
\"$SCOPE_OPENID\",
\"$SCOPE_PROFILE\",
\"$SCOPE_EMAIL\"
]
}"
```
Die Response enthält:
- `pk` — die ID des Providers (numerisch)
- `client_id` — auto-generiert, brauchst du für die App-Config
- `client_secret` — das das du mitgegeben hast (bestätigt)
**Notiere client_id und client_secret sofort.** Das Secret wird in der Web-UI später maskiert.
## Schritt 3: Application anlegen
Die Application verbindet Provider + Namen + Zugangs-Policies.
```bash
curl -X POST 'http://localhost:9000/api/v3/core/applications/' \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"name": "<App-Name>",
"slug": "<app-slug>",
"provider": <PROVIDER_PK>,
"meta_launch_url": "https://<app-domain>/user/oauth2/<auth-name>",
"meta_description": "<Beschreibung>",
"meta_publisher": "Richter & Zech",
"policy_engine_mode": "any"
}'
```
Die Response enthält die App-ID (UUID).
**Wichtig:** Der `slug` bestimmt die Discovery-URL: `https://welcome.sdda.eu/application/o/<slug>/.well-known/openid-configuration`
**KRITISCH — Silent SSO:** `meta_launch_url` muss auf die **OAuth2-Initiate-Route** der App zeigen, nicht auf die App-Startseite. Nur so wird beim Klick im Authentik-Dashboard der OIDC-Flow automatisch getriggert ohne dass der User nochmal "Sign in with..." klicken muss. Beispiele:
- **Forgejo:** `https://code.sdda.eu/user/oauth2/authentik` (der `authentik` Teil ist der Auth-Source-Name aus `forgejo admin auth add-oauth --name`)
- **OpenProject:** `https://openproject.sdda.eu/auth/oidc_authentik/callback`
- **Grafana:** `https://grafana.sdda.eu/login/generic_oauth`
- **Nextcloud:** `https://app.sdda.eu/apps/user_oidc/login/<provider_id>`
Wenn man die nackte App-URL setzt (`https://code.sdda.eu/`), zeigt die App ihre eigene Login-Seite an, und der User muss nochmal klicken. Das ist keine silent SSO — das ist eine Frustration.
## Schritt 4: Policy Binding (Gruppen-Zugangskontrolle)
Wenn du in Schritt 1 eine Gruppe angelegt hast:
```bash
APP_PK=<uuid-aus-schritt-3>
GROUP_PK=<uuid-aus-schritt-1>
curl -X POST 'http://localhost:9000/api/v3/policies/bindings/' \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d "{
\"target\": \"$APP_PK\",
\"group\": \"$GROUP_PK\",
\"enabled\": true,
\"order\": 0,
\"timeout\": 30
}"
```
Damit können nur Mitglieder der Gruppe die App nutzen. User die nicht drin sind, bekommen beim OIDC-Login "Access Denied".
## Schritt 5: User zur Gruppe hinzufügen
```python
# Via MCP
mcp.authentik_add_user_to_group(group_id=<UUID>, user_id=<INT>)
```
Oder REST:
```bash
curl -X POST "http://localhost:9000/api/v3/core/groups/$GROUP_PK/add_user/" \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"pk": <USER_ID>}'
```
## Schritt 6: Discovery-URL verifizieren
Bevor du in der App den Provider konfigurierst, check dass Authentik die Discovery-URL korrekt ausliefert:
```bash
curl -s 'https://welcome.sdda.eu/application/o/<slug>/.well-known/openid-configuration' | jq .
```
Erwartete Felder:
- `issuer` — muss mit dem Issuer matchen den die App erwartet
- `authorization_endpoint`
- `token_endpoint`
- `userinfo_endpoint`
- `jwks_uri`
## Schritt 7: Client in der App konfigurieren
Je nach App unterschiedlich. Bei **Forgejo** z.B. via CLI:
```bash
docker exec -u git forgejo forgejo admin auth add-oauth \
--name authentik \
--provider openidConnect \
--key "<CLIENT_ID>" \
--secret "<CLIENT_SECRET>" \
--auto-discover-url "https://welcome.sdda.eu/application/o/<slug>/.well-known/openid-configuration" \
--scopes "openid profile email" \
--skip-local-2fa
```
Bei **OpenProject**: `config/settings.yml` oder ENV. Bei **Nextcloud**: App "user_oidc" installieren, dann UI. Bei **Grafana**: `grafana.ini` [auth.generic_oauth]. Immer die App-Docs konsultieren.
## Schritt 8: Test-Login
1. Browser: App-URL → Login-Button "Sign in with authentik"
2. Redirect zu welcome.sdda.eu → Login
3. Evtl. Consent-Screen bestätigen
4. Redirect zurück → User ist eingeloggt
## Gotchas
- **Issuer-URL muss public sein** (nicht `http://10.0.0.7:9000`). JWT-Signatur enthält den Issuer — wenn App und Authentik unterschiedliche Issuer sehen, schlägt die Validation fehl.
- **Subject-Mode `user_email`** funktioniert gut wenn alle User einzigartige Email-Adressen haben. Bei gemeinsamen Mailboxen (info@ etc.) lieber `hashed_user_id`.
- **Include Claims in id_token = true** ist wichtig für Apps die `email`/`name` aus dem id_token lesen statt separaten userinfo-Call.
- **Strict Redirect URIs:** Immer `"matching_mode": "strict"`, niemals Regex — Security.
- **Policy Timeout = 30s** gibt Authentik Luft bei langsamen LDAP/SCIM-Backends.
## Template-Reference
Als konkretes funktionierendes Beispiel für alle oben genannten Schritte siehe:
- `../Forgejo/adr/0003-native-oidc-not-forwardauth.md`
- Die Forgejo-`.env` auf ai-apps enthält die konkreten Werte
- Die Authentik-Admin-UI unter `welcome.sdda.eu/if/admin/#/core/applications` zeigt die "Forgejo" Application als lebendes Beispiel
## Nächste Apps die dieses Template nutzen könnten
- **OpenProject** — hat nativen OIDC-Client, aktuell noch ForwardAuth nicht genutzt
- **Nextcloud** — braucht `user_oidc` App-Installation
- **Grafana** — falls wir Monitoring einführen
- **Mattermost** — falls wir Chat einführen
- Potenzielle Migration von n8n und locosoft-hilfe-system von ForwardAuth auf OIDC (großer Eingriff, nicht dringlich)

31
docs/runbooks/README.md Normal file
View file

@ -0,0 +1,31 @@
# Runbooks
**Runbooks** sind Schritt-für-Schritt-Anleitungen für wiederkehrende operative Situationen — meist Notfälle oder seltene Wartungs-Aufgaben.
## Warum Runbooks?
- **Für dich selbst in 3 Monaten:** Wenn der Loco-Sync wieder kaputt ist und du dich nicht mehr an die Debug-Schritte erinnerst.
- **Für die AI:** Eine AI-Session kann ein Runbook **lesen und ausführen**, ohne dass du jede Zeile diktieren musst.
- **Für den Vertretungsfall:** Wenn du im Urlaub bist und jemand anderes dringend muss.
## Format
Jedes Runbook beschreibt:
1. **Wann anwenden** — Symptome, Trigger
2. **Voraussetzungen** — Zugänge, Tools, Secrets
3. **Schritte** — nummeriert, kopierbar (Bash-Blöcke)
4. **Verification** — wie man erkennt dass der Fix greift
5. **Was NIE machen** — typische Fallen
## Aktuelle Runbooks
### Forgejo
- **[forgejo-admin-recovery.md](forgejo-admin-recovery.md)** — Was tun wenn Authentik down ist und niemand mehr ins Forgejo kommt. Nutzt den lokalen `admin-local` Fallback-User.
- **[forgejo-backup-restore.md](forgejo-backup-restore.md)** — Backup-Format, Restore-Szenarien (komplett / nur DB / einzelne Datei), Disaster-Recovery auf neuem Server.
## Convention
- **Dateiname:** `<stack>-<action>.md`, z.B. `forgejo-backup-restore.md`, `loco-sync-debug.md`
- **Immer Bash-Blöcke** mit echten Kommandos, nicht Pseudo-Code
- **Erwartete Outputs** hinschreiben, damit man weiß wann's geklappt hat
- **Destruktive Aktionen fett markieren** ("⚠️ löscht ALLE Daten!")
## Neue Runbooks hinzufügen
Wenn du ein Problem zum zweiten Mal hast, wird es ein Runbook. Die AI kann dir das im Moment des Fixes schreiben — du reviewst im Commit.

View file

@ -0,0 +1,96 @@
# Runbook — Admin-Recovery wenn Authentik down ist
**Kontext:** Forgejo nutzt Authentik OIDC als Primär-Auth. Wenn Authentik ausfällt, kommt niemand mehr per Login rein. Für diesen Fall gibt es einen lokalen Fallback-User `admin-local`.
## Wann dieses Runbook anwenden
- `welcome.sdda.eu` gibt 500er / nicht erreichbar
- OIDC-Callback zu Forgejo bricht ab
- Authentik-Docker-Container crasht
- authentik-sso VM ist down
## Voraussetzung
- **SSH-Zugang zu ai-apps** (`ssh ai-apps`)
- **admin-local Passwort** aus Password-Manager / Vault
## Recovery-Schritte
### 1. PROHIBIT_LOGIN lösen
Normalerweise ist `admin-local.prohibit_login = true` gesetzt, damit niemand das Passwort bruteforcen kann. Wir müssen es temporär deaktivieren.
```bash
ssh ai-apps
docker exec forgejo-db psql -U forgejo -d forgejo \
-c "UPDATE \"user\" SET prohibit_login = false WHERE lower_name = 'admin-local';"
```
Output:
```
UPDATE 1
```
### 2. Einloggen
Im Browser: https://code.sdda.eu/user/login
- Username: `admin-local`
- Passwort: aus Password-Manager
Du siehst jetzt das Forgejo Dashboard mit Admin-Rechten.
### 3. Was du während des Authentik-Ausfalls machen kannst
- Repos anlegen / ändern
- User manuell anlegen (fallback wenn jemand dringend Zugang braucht)
- System-Settings anpassen
- Backups prüfen
**Achtung:** Andere normale User können sich auch nicht einloggen während Authentik down ist. Das ist bewusst so. Wenn ein User dringend arbeiten muss, können sie:
- Von dir ein Personal Access Token bekommen (Settings → Applications)
- git clone via HTTPS mit diesem Token
### 4. Authentik wieder hochbringen
```bash
ssh authentik-sso
cd /opt/authentik
docker compose ps # Was läuft?
docker compose logs --tail 50 # Warum down?
docker compose restart # Versuch 1
# Wenn restart nicht hilft: docker compose up -d --force-recreate
```
Bei Datenbank-Problemen:
```bash
docker compose logs authentik-db # DB ok?
# Im Worst Case: Restore aus Authentik-Backup
```
### 5. Nach der Wiederherstellung: PROHIBIT_LOGIN zurücksetzen
**WICHTIG:** Sobald Authentik wieder läuft, sofort den Notfall-Fallback wieder sperren:
```bash
ssh ai-apps
docker exec forgejo-db psql -U forgejo -d forgejo \
-c "UPDATE \"user\" SET prohibit_login = true WHERE lower_name = 'admin-local';"
```
Aus dem Forgejo Admin-UI deine bw-Session ausloggen, dann via Authentik neu einloggen — sicherstellen dass OIDC wieder funktioniert.
## Warum dieses Pattern?
- **admin-local hat keinen Zugang über Authentik** (nicht in Gruppe `forgejo-users`, und nicht mal ein OIDC-linked User).
- **admin-local ist per default gesperrt** (`prohibit_login=true`).
- Zum Aktivieren braucht man DB-Zugang → bedeutet SSH-Zugang zu ai-apps → bedeutet physischen Zugang zu deinem Rechner / SSH-Key.
- Nach Recovery sofort zurück in den gesperrten Zustand — kein dauerhaftes Bypass.
## Was du NIE machen solltest
- **admin-local Passwort aus dem Password-Manager entfernen.** Das ist deine einzige Notfall-Tür.
- **admin-local Passwort in Klartext irgendwo committen.** Das ist wie GH-Secret-in-Public-Repo, nur schlimmer.
- **PROHIBIT_LOGIN dauerhaft auf false lassen nach Recovery.** Das bricht das Sicherheitsprinzip.
## Alternative für sehr lange Authentik-Ausfälle
Wenn Authentik auf Dauer nicht mehr funktioniert (z.B. Lizenz-Issue, Migration nötig):
1. admin-local temporär entsperren
2. OIDC-Auth-Source in Forgejo deaktivieren: `docker exec -u git forgejo sh -c 'cd / && forgejo admin auth update-oauth --id 1 --not-enabled'`
3. Lokale User bestehen bleibt (bw kann admin-local-Passwort bekommen oder eigenes lokales Passwort setzen)
4. Neuer Auth-Provider später einrichten
5. Doku in neue ADR schreiben
## Related
- `Agent.md` — Stack-Overview
- `../Authentik/howto-oauth2-provider.md` — Wie OIDC-Provider neu aufgesetzt wird

View file

@ -0,0 +1,138 @@
# Runbook — Forgejo Backup und Restore
## Backup-Architektur
- **Script:** `/opt/ai-apps/forgejo/backup.sh`
- **Cron:** `0 3 * * *` (täglich 03:00 UTC auf ai-apps)
- **Ziel:** `/opt/backups/forgejo/`
- **Format:**
- `forgejo-db-<YYYYMMDD-HHMMSS>.sql.gz` — pg_dump der Postgres DB
- `forgejo-data-<YYYYMMDD-HHMMSS>.tar.gz` — tar des Forgejo `/data` Volumes
- **Retention:** 14 Tage (ältere Dateien werden via `find -mtime +14 -delete` entfernt)
## Manueller Backup-Run
```bash
ssh ai-apps
bash /opt/ai-apps/forgejo/backup.sh
ls -lh /opt/backups/forgejo/
```
Erwartete Ausgabe:
```
[2026-04-11T19:39:11+00:00] backup start
db dump: 26K
data tar: 12K
[2026-04-11T19:39:12+00:00] backup complete
```
## Restore-Szenarien
### Szenario A: Komplett-Restore aus Backup (Disaster Recovery)
Voraussetzung: Backup-Files sind vorhanden (entweder in `/opt/backups/forgejo/` oder aus Offsite-Kopie).
```bash
# 1. Stack stoppen
cd /opt/ai-apps/forgejo
docker compose down
# 2. Volumes löschen (ACHTUNG: unwiderruflich!)
docker volume rm forgejo_forgejo-data forgejo_forgejo-db-data
# 3. Stack wieder starten — leere Volumes werden neu angelegt
docker compose up -d forgejo-db
# 4. Warten bis Postgres healthy
sleep 20
docker ps --filter name=forgejo-db --format '{{.Status}}' # Up (healthy)
# 5. DB restoren
gunzip -c /opt/backups/forgejo/forgejo-db-<TIMESTAMP>.sql.gz | \
docker exec -i forgejo-db psql -U forgejo -d forgejo
# 6. Data volume restoren
docker run --rm \
-v forgejo_forgejo-data:/data \
-v /opt/backups/forgejo:/backup:ro \
alpine \
sh -c "cd /data && tar -xzf /backup/forgejo-data-<TIMESTAMP>.tar.gz"
# 7. Forgejo starten
docker compose up -d forgejo
# 8. Verifikation
sleep 30
curl -s https://code.sdda.eu/api/healthz
curl -s https://code.sdda.eu/api/v1/version
docker exec -u git forgejo sh -c 'cd / && forgejo admin user list'
```
### Szenario B: Nur DB zurückspielen (z.B. nach Korruption durch Update)
```bash
cd /opt/ai-apps/forgejo
docker compose stop forgejo # nur Forgejo, nicht DB
# DB mit -c wipen und neu importieren
docker exec forgejo-db psql -U forgejo -d postgres \
-c "DROP DATABASE forgejo;"
docker exec forgejo-db psql -U forgejo -d postgres \
-c "CREATE DATABASE forgejo OWNER forgejo;"
gunzip -c /opt/backups/forgejo/forgejo-db-<TIMESTAMP>.sql.gz | \
docker exec -i forgejo-db psql -U forgejo -d forgejo
docker compose start forgejo
```
### Szenario C: Einzelne Datei / Config aus Data-Volume wiederherstellen
```bash
# Tar-Inhalt inspizieren
tar -tzf /opt/backups/forgejo/forgejo-data-<TIMESTAMP>.tar.gz | head -30
# Einzelne Datei extrahieren
mkdir -p /tmp/forgejo-restore
tar -xzf /opt/backups/forgejo/forgejo-data-<TIMESTAMP>.tar.gz \
-C /tmp/forgejo-restore gitea/conf/app.ini
# Ins laufende Volume kopieren
docker cp /tmp/forgejo-restore/gitea/conf/app.ini forgejo:/data/gitea/conf/app.ini
docker restart forgejo
```
## Disaster-Recovery: Kompletter Neuaufbau auf neuem Server
1. Neuen Host aufsetzen (Docker + Compose)
2. `/opt/ai-apps/forgejo/` Stack-Verzeichnis kopieren (inkl. `.env`, docker-compose.yml, backup.sh)
3. Backup-Files aus Offsite-Quelle holen (Nextcloud / Hetzner Snapshot)
4. Szenario A durchführen
5. DNS ggf. auf neue IP anpassen
## Verification nach Restore
```bash
# Health
curl -s https://code.sdda.eu/api/healthz
# Auth-Sources
docker exec -u git forgejo sh -c 'cd / && forgejo admin auth list'
# Erwartet: 1 authentik OAuth2 true
# User
docker exec -u git forgejo sh -c 'cd / && forgejo admin user list'
# Erwartet: admin-local + bw + alle anderen
# Repo-Zähler
docker exec forgejo-db psql -U forgejo -d forgejo \
-c "SELECT count(*) FROM repository;"
```
## Offsite-Backup-Strategie (zukünftig, M7.5)
- **Tier 2:** rclone → Nextcloud, zweiter Cron 03:30
- **Tier 3:** Hetzner Cloud Snapshot, manuell wöchentlich
Beides noch nicht eingerichtet — solange lokale 14-Tage-Retention + Hetzner-VM-Stabilität unser Sicherheitsnetz.
## Was NIE machen
- Backup-Files niemals in Git einchecken (können sensible Repo-Daten enthalten)
- Restore auf laufender Prod ohne vorheriges Backup (einfach `docker compose down` macht keine DB-Kopie!)
- `docker volume prune` wenn Forgejo noch läuft (wird Daten verlieren)

View file

@ -56,9 +56,9 @@ docker exec forgejo-db psql -U forgejo -d forgejo \
- Retention: 14 Tage - Retention: 14 Tage
- Format: `forgejo-db-<ts>.sql.gz` + `forgejo-data-<ts>.tar.gz` - Format: `forgejo-db-<ts>.sql.gz` + `forgejo-data-<ts>.tar.gz`
- Offsite: noch nicht (Tier 2 via rclone → Nextcloud geplant M7.5) - Offsite: noch nicht (Tier 2 via rclone → Nextcloud geplant M7.5)
- Restore-Prozedur: siehe `docs/runbooks/forgejo-backup-restore.md` (wird in M7.3 hinzugefügt) - Restore-Prozedur: siehe [`../../docs/runbooks/forgejo-backup-restore.md`](../../docs/runbooks/forgejo-backup-restore.md)
## Related ## Related
- `../../Agent.md` — Repo-weites Briefing - `../../Agent.md` — Repo-weites Briefing
- `../../docs/architecture/ai-apps-stacks.md` — Server-Kontext - `../../docs/architecture/ai-apps-stacks.md` — Server-Kontext
- ADRs 0001-0006 (in iCloud-Ordner, Spiegelung hier in M7.3) - ADRs 0001-0006 in [`../../docs/adr/`](../../docs/adr/) (gespiegelt in M7.3)