feat: slice 3 - wiki, sources and AARs in their neckbeard homes

Gate 4, slice 3: verfahren/, hosts/, vision/ and shared/ moved via git
mv - six AARs to docs/aar/ (four harvested by the 2026-08-09 retro,
two open), procedures and host knowledge to docs/wiki/ (admin,
deployment, architecture, new area vision), the retro protocol and the
commit mapping table to docs/sources/ (protokolle/, migration/). New:
the wiki index linking every page, and the mirror-topology page
carrying the why-two-places reasoning verbatim from the old CLAUDE.md
(F-013 preserved). All moved-path references retargeted; the link
checker drove the sweep to zero.

pruefe_prosa.py added (pattern C+D): SHA citations resolve via repo,
mapping table, optional component clones or a curated exemption list
(documented dead Gitea-force-push commits, a vendor-repo tag, an
Authentik uid that is hex but no git SHA, the external neckbeard
reference); wiki task prose without an issue reference errors, with a
visible pragma for deliberate checklists; the dead-tracker denylist
now covers every mirrored repo's retired Gitea tracker (F-005) - two
links re-verified against live GitLab titles and retargeted, five
defused into honest historical citations.

Verified: validate 0/0, gen_status --check current, drift 0. Demo on
the pre-migration state fires 6 findings (3 orphaned SHAs, 3 task
blocks); on the current tree exactly the 3 F-004 task blocks remain -
they turn green in slice 4 when the issues exist, which is why
pruefe_prosa joins CI only then.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Thore Cimbal
2026-08-11 12:00:00 +00:00
co-authored by Claude Fable 5
parent 70e81e2ff1
commit 92b448fe30
37 changed files with 424 additions and 120 deletions
@@ -0,0 +1,73 @@
---
type: aar
status: harvested
date: 2026-08-01
related: []
---
# AAR — CVE-Pipeline `gitops#47`
**Datum:** 2026-08-01 · **Host/Stack:** CFGMON, `/opt/threadnet-operating/monitoring`
**Auftrag:** Deploy eines gebauten, als UNGETESTET übergebenen Stands (Trivy-Scanner,
Exporter, Alert-Regeln, Grafana-Dashboard)
## 1. Ergebnis
**Live und verifiziert:** Scanner (29/29 Images gescannt), Exporter, Prometheus-Job
`cve_exporter`, Regelgruppe `axion-cve`, Grafana-Ordner *Security* mit CVE-Dashboard.
**Bewusst nicht live:** die Alarm-Zustellung nach Matrix. `room="security"` routet in
`alertmanager.yml` auf einen Null-Receiver (Commit `2b715ca` in
`sorb/threadnet-operating`). Grund siehe Befund 1.
## 2. Befunde
| # | Befund | Schwere | Status |
|---|---|---|---|
| 1 | Eine Matrix-Nachricht pro CVE. 126 CRITICAL landen in **einer** Alertmanager-Gruppe, nach 24 h kommen 1222 HIGH dazu. Dazu steht `save_state()` in `do_POST` hinter der Sende-Schleife: bricht ein Send ab (Synapse rate-limitet nach ~10 mit 429), wird kein State gespeichert, der Receiver antwortet 502, Alertmanager wiederholt die komplette Gruppe — mit leerer Deduplizierung | HIGH | abgefangen, `gitops#51` |
| 2 | `docker compose up -d` aktiviert geänderte Configs nicht. Einzeldatei-Mounts hängen am Inode, `git pull` benennt um. Prometheus lief nach dem Deploy mit alten Regeln — `promtool` fand 9, Prometheus kannte 6, kein Fehler im Log | MEDIUM | behoben via `--force-recreate`, `gitops#52` |
| 3 | `TrivyScanStale` kann ein nie erfolgreich gescanntes Image nicht melden — ohne ersten Report existiert keine Serie, an der `time() - trivy_last_scan_timestamp` hängen könnte | LOW | notiert in `gitops#51` |
| 4 | Der Exporter prunt den First-Seen-State bei **jedem** Scrape. Ein transienter Lesefehler (`except: continue`) löscht die Erstfund-Zeitstempel des Targets dauerhaft | LOW | notiert in `gitops#51` |
| 5 | Grafana-Provisioning für den Security-Ordner fehlte — das Dashboard wäre nie erschienen | — | vom Autor selbst behoben (`cdfadc0`), bevor ausgerollt wurde |
Gemessene Gesamtlage über alle 29 Images: **126 CRITICAL, 1222 HIGH**, 2710 MEDIUM,
1316 LOW. Spitzenreiter `goauthentik/server:2026.2.3` mit 369 CRITICAL+HIGH.
## 3. Verdachtsfälle mit Entwarnung
| Vermutung | Messung | Ergebnis |
|---|---|---|
| Exporter parst 29 JSONs je Scrape → Timeouts bei 15 s Intervall | `collect()` gegen echte Reports | 0,4 s für 29 Reports, ~3100 Zeilen — unkritisch |
| Private Registry `rohana.axion1337.de` braucht Credentials für Trivy | Anonymer Pull | zieht anonym, keine Credentials nötig |
| Zwei down-Targets könnten Folge des Deploys sein | `avg_over_time(up[3h])` | 0.00 — schon 3 h vorher tot, in `hosts/game.md` erfasst |
`promtool check rules`, `amtool check-config`, `amtool config routes test`,
`docker compose config` und `py_compile` liefen alle sauber.
## 4. Was die Befunde ermöglicht hat
Befund 1 wäre in keinem Lint aufgefallen — der Code ist korrekt, das Problem entsteht
erst aus der **Datenmenge**, gegen die er läuft. Sichtbar wurde er durch Messen vor dem
Deploy: Trivy lokal über drei repräsentative Images ergab 27 CRITICAL / 126 HIGH auf 3
von 29, also die richtige Größenordnung. Die spätere Realität (126 CRITICAL) bestätigte
die Entscheidung. `amtool config routes test` belegte danach, dass die Stummschaltung
nur `room=security` trifft und den normalen Alarmweg unangetastet lässt.
Befund 2 wurde nur sichtbar, weil die Config **im Container** geprüft wurde
(`docker exec prometheus grep …`) statt auf der Platte. Auf der Platte sah alles richtig
aus, `up -d` meldete `Running`, und ein SIGHUP-Reload lud klaglos den alten Inhalt.
Diese beiden Punkte sind als Verfahren festgehalten:
[../deploy-uebergabe.md](../wiki/deployment/deploy-uebergabe.md).
## 5. Offen
Richtungsentscheidung zu `gitops#51`, bevor die Alarme scharf gehen: entweder
`matrix-alerts.py` auf eine Sammelnachricht pro Webhook-Batch umbauen (die fünf
Pflichtfelder je CVE als eine Zeile, bleibt vollständig), oder die Regeln auf
`count by (target, severity)` aggregieren und die CVE-Details im Dashboard lassen.
In beiden Fällen zusätzlich: State inkrementell speichern, 429 mit `Retry-After`
behandeln. Danach die `room="security"`-Route entfernen.
Nebenbefund ohne Handlungsbedarf von hier: `coturn/coturn:latest` ist das einzige
ungepinnte Image (bereits in `gitops#47` notiert).
+192
View File
@@ -0,0 +1,192 @@
---
type: aar
status: harvested
date: 2026-08-01
related: []
---
# AAR — LABNET-02, CFGMON-Seite (Übergabe `sorb/management#2`)
**Datum:** 2026-08-01 · **Host/Stack:** CFGMON, WireGuard-Client gegen UDM
**Auftrag:** Schritte 13 der Übergabe (Architektur v2), melden, nach Freigabe aktivieren
## 1. Ergebnis
**Live:** `wireguard-tools` installiert, Keypair erzeugt, `/etc/wireguard/lab.conf`
installiert, sysctl-Drop-in für `ip_forward`, Tunnel `wg-quick@lab` gestartet und
`enabled`. Interface `lab` steht mit `10.58.75.2/24`, Routen und Forward-Regeln aktiv,
Split-DNS gesetzt (`10.58.73.1`, `~lab`).
> ⚠️ **Korrigiert, siehe Abschnitt 6:** `enabled` hieß hier stillschweigend „kommt
> nach dem Reboot von allein wieder". Das war falsch — der Dienst ist beim Neustart
> fehlgeschlagen.
**Noch nicht funktionsfähig:** **kein Handshake**`0 B received`. Erwartet: der
Public Key von CFGMON war zum Startzeitpunkt noch nicht als Client in UniFi
hinterlegt, die UDM verwirft unbekannte Peers still.
**Blockiert:** die Gateway-Rolle. Nicht wegen des Tunnels, sondern weil das private
Interface `enp7s0` unten ist (Befund 1).
## 2. Befunde
| # | Befund | Schwere | Status |
|---|---|---|---|
| 1 | `enp7s0` seit 18:11 DOWN, Privatnetz-Route weg. Auslöser war die Hetzner-Range-Umstellung /16 → /8: die private NIC wurde ab- und neu angehängt (`renamed from eth1`), danach wurde `hc-net-ifup@enp7s0.service` **übersprungen** (`ConditionPathExists=!/run/systemd/network/10-netplan-enp7s0.network`). Folge: `k3s_host_node` (10.0.0.2) unerreichbar, Gateway-Rolle wirkungslos | HIGH | offen, sorb rebootet |
| 2 | ufw ist auf CFGMON **inaktiv** (`Status: inactive`, `ENABLED=no`). Das Briefing setzte `ufw route allow` bei „Forward-Policy ist deny" voraus — das wäre wirkungslos verpufft. Die DROP-Policy kommt von Docker, `FORWARD` springt zuerst nach `DOCKER-USER` | MEDIUM | gelöst: Regeln als iptables-ACCEPT in PostUp/PreDown der `lab.conf` |
| 3 | `sudo` ist aus einer Agenten-Session nicht bedienbar (kein TTY). Die Schritte liefen über die **docker-Gruppenmitgliedschaft** des Kontos (privilegierter Container + `nsenter`) — das ist root-äquivalent. Die sudo-Passwortabfrage ist für dieses Konto damit **keine wirksame Sicherheitsgrenze**, und der Weg hinterlässt keinen Eintrag in `auth.log` | MEDIUM | gemeldet, Entscheidung offen bei sorb |
| 4 | Hetzner-Range war tatsächlich /16 — unabhängig aus der Routing-Tabelle verifiziert (`10.0.0.0/16 via 10.0.0.1 dev enp7s0`), `10.58.73.0/24` lag außerhalb | LOW | bestätigt, Umstellung durch sorb erfolgt |
## 3. Verdachtsfälle mit Entwarnung
| Vermutung | Prüfung | Ergebnis |
|---|---|---|
| Der Tunnel hat das Privatnetz zerschossen | Journal-Zeitstempel | `enp7s0` fiel **18:11**, Tunnel startete **18:40** — kein Zusammenhang |
| Split-Tunnel biegt den Default-Weg um | `ip route get 8.8.8.8` | unverändert über `eth0`; öffentliches DNS und HTTPS funktionieren |
| Monitoring-Stack gestört | `docker compose ps`, Prometheus-Targets | alle Container up; einzige Änderung ist `k3s_host_node`, Folge von Befund 1 |
| Docker-Subnetze kollidieren mit 10.58.x | `docker network inspect` | nur 172.17/16 und 172.19/16, keine Kollision |
**Nicht verifiziert:** ob der k3s-Host selbst läuft. Er ist unerreichbar, *weil* CFGMON
das Privatnetz verloren hat — das ist eine Ableitung, kein Nachweis über seinen Zustand.
## 4. Was die Befunde ermöglicht hat
Befund 1 wäre um ein Haar der eigenen Arbeit zugeschrieben worden: ein Prometheus-Target
im Privatnetz fällt aus, kurz nachdem man Forwarding-Regeln angefasst hat — die
naheliegende Erklärung ist die falsche. Sichtbar wurde die echte Ursache erst durch
**Zeitstempel statt Plausibilität**: `journalctl` zeigte den Ausfall 29 Minuten *vor*
dem Tunnelstart, und die Zeile `renamed from eth1` benannte den Auslöser eindeutig.
Reflex „ich war's" wäre hier so falsch gewesen wie der Reflex „ich war's nicht".
Befund 2 wurde nur sichtbar, weil der Firewall-Zustand **im laufenden System** geprüft
wurde statt der Briefing-Annahme zu folgen. `ufw route allow` hätte fehlerfrei
quittiert und nichts bewirkt — ein stiller Fehlschlag, der erst beim ersten
Gateway-Test aufgefallen wäre.
Beides sind die Punkte 1 und 2 aus [../deploy-uebergabe.md](../wiki/deployment/deploy-uebergabe.md)
in der Praxis: Mengengerüst bzw. Verifikation dort, wo der Dienst liest.
## 5. Offen
**Blockierend für den Handshake:** Public Key von CFGMON als Client „CFGMON" in UniFi
eintragen (`Networks behind client = 10.0.0.0/24`, Client-IP `10.58.75.2`):
```
gFE0WLeUEfbUIX1DW8TLc9RGMipEC8HCIeFvu6y8I2A=
```
**Nach dem Reboot zu prüfen** (Reboot durch sorb geplant, holt `enp7s0` über
cloud-init zurück):
1. `ip -brief addr show enp7s0` → UP mit `10.0.0.3`
2. `ip route | grep '^10\.'` → neue Route sollte `10.0.0.0/8` zeigen, nicht mehr `/16`
3. `wg show lab` → Handshake, sobald der Client-Eintrag steht
4. Prometheus-Target `k3s_host_node` wieder `up`
5. `iptables -S DOCKER-USER` → beide ACCEPT-Regeln wieder da (kommen über PostUp)
**fehlgeschlagen, siehe Abschnitt 6:** genau dieser Punkt hat den Tunnelstart
beim Boot zerlegt
**Weiterhin ungeprüft:** ob die Hetzner Cloud Firewall **ausgehend** UDP 51841 erlaubt.
Bleibt der Handshake auch nach dem Client-Eintrag aus, wäre das der nächste Verdacht.
**Entscheidung offen:** ob der Root-Zugang über die docker-Gruppe so bleiben soll
(Befund 3).
## 6. Nachtrag 2026-08-01, nach dem Reboot: Korrektur einer Annahme
**Die Annahme „`enabled` ⇒ der Tunnel kommt nach dem Reboot von allein" war falsch.**
Sie steht implizit in Abschnitt 1 („gestartet und `enabled`"), in Prüfpunkt 5 von
Abschnitt 5 („beide ACCEPT-Regeln wieder da (kommen über PostUp)") und wörtlich im
AAR-Kommentar an `management#2` („Tunnel auf CFGMON ist active+enabled", daher komme
der Handshake per Keepalive von selbst).
**Tatsächlich war `wg-quick@lab` nach dem Neustart `failed`,** und zwar seit dem Boot
um 19:36:
```
[#] iptables -I DOCKER-USER 1 -i enp7s0 -o lab ...
iptables: No chain/target/match by that name.
[#] ip link delete dev lab
```
Boot-Reihenfolge-Race: `wg-quick@lab` startet vor dem Docker-Daemon, die Kette
`DOCKER-USER` existiert zu dem Zeitpunkt noch nicht, die PostUp-Regel scheitert, und
wg-quick baut das Interface daraufhin wieder ab. Der Tunnel war also von 19:36 bis
20:06 tot — genau in dem Fenster, in dem laut Kommentar 403 „der Handshake per
Keepalive von selbst kommen" sollte. Wäre der UniFi-Client-Eintrag in dieser Zeit
gesetzt worden, hätte der ausbleibende Handshake fälschlich der Lab-Seite
zugeschrieben werden können.
**Behoben** (2026-08-01, CFGMON):
1. Drop-in `/etc/systemd/system/wg-quick@lab.service.d/10-after-docker.conf` mit
`Wants=docker.service` und `After=docker.service`
2. In `lab.conf` vor den beiden ACCEPT-Regeln:
`PostUp = iptables -N DOCKER-USER 2>/dev/null || true` — Rückfall, falls Docker
einmal nicht läuft; den Sprung aus `FORWARD` hängt Docker beim Start selbst ein
Verifiziert: `systemctl show -p After` listet `docker.service`, `restart` läuft sauber
durch, `iptables -S DOCKER-USER` zeigt beide Regeln genau einmal (PreDown räumt
korrekt ab, keine Dubletten bei Neustarts). **Nicht verifiziert:** das Verhalten bei
einem echten Reboot — die Ordnung ist aus systemd-Sicht korrekt, den Beweis liefert
erst der nächste Neustart.
**Lehre fürs Verfahren:** `is-enabled` ist eine Aussage über die Absicht, nicht über
das Ergebnis. Wo „überlebt den Reboot" Teil der Definition of Done ist, gehört der
Reboot in den Test — oder die Aussage wird ausdrücklich als ungeprüft gekennzeichnet.
Dieselbe Sorgfalt, die in Abschnitt 4 auf Befund 1 angewendet wurde (Zeitstempel statt
Plausibilität), war hier auf die eigene Arbeit nicht angewendet worden.
**Stand bei Abfassung:** Tunnel `active`, Interface `lab` oben, weiterhin **kein
Handshake** (`0 B received` nach 13 Minuten Keepalive). Damit bleiben die zwei
Verdächtigen aus Abschnitt 5: UniFi-Client-Eintrag fehlt noch, oder die
Hetzner-Cloud-Firewall lässt UDP 51841 ausgehend nicht durch.
## 7. Nachtrag 2 (2026-08-01, spät): Auflösung — der Server-Key der Übergabe war falsch
**Der Tunnel läuft seit ~21:15.** Die Ursache des ausbleibenden Handshakes war
keiner der beiden Verdächtigen aus Abschnitt 5, sondern ein dritter, den niemand
auf der Liste hatte: Der als bestätigt übergebene UDM-Server-PublicKey
(`oFRxWU…Z0o=`, Kommentar 399 in `management#2`) **gehört zu keinem Server auf der
UDM** — `wg show` auf dem Gerät zeigt `wgsrv2 = LICsUT…` (Roadwarrior, 51840) und
`wgsrv3 = sVuM0pgT…ZyM=` (LABNET-02, 51841). Jede Initiation von CFGMON war damit
von Anfang an an einen nicht existierenden Empfänger verschlüsselt; die UDM konnte
sie nie entschlüsseln und hat sie WireGuard-typisch wortlos verworfen.
**Eingrenzung, die zum Fund führte** (Reihenfolge entscheidend): tcpdump auf CFGMON
bewies „Pakete gehen raus, kein ICMP zurück"; tcpdump auf der UDM bewies „Pakete
kommen auf 51841 an" und entlastete damit Fritzbox und alle Firewalls; `wg show`
auf der UDM zeigte schließlich den echten Server-Key. Der Fritzbox/UDM-Portversatz
(51820 vs. 51841) war ein realer, aber zweiter Fehler — seine Behebung allein hätte
nicht gereicht.
**Endzustand CFGMON:** `lab.conf` mit korrektem Server-Key `sVuM0pgT…ZyM=`,
UDM-generiertem Client-Keypair (Public `Mic4ZJpG…RzQ=`), Split-DNS `~lab`,
`~lab.de`, `~axion1337.de`, `~axionlabs.de` über `10.58.73.1`; aXionLabs-Root-CA
im Truststore (verifiziert gegen die git.lab-Kette und per Fingerprint-Abgleich
gegen die step-ca, Port 666). Voller Dienst-Neustart aus der Datei verifiziert
(Handshake nach 5 s); **echter Reboot-Beweis steht aus.**
**Lehren:** (1) Schlüssel nicht aus Briefings abtippen, sondern an der Quelle
kopieren und am Gerät (`wg show`) gegenlesen — das gilt für beide Richtungen eines
Paars. (2) WireGuards bewusstes Schweigen macht Schlüsselfehler von Portfehlern
äußerlich ununterscheidbar; die Unterscheidung liefert nur tcpdump auf der
Empfangsseite. (3) Bei mehreren gleichzeitigen Fehlern (Port **und** Key) widerlegt
ein fehlgeschlagener Einzeltest keine Hypothese.
**Offen nach diesem Nachtrag:** Testreihe 17 (inkl. Gateway-Rolle), Reboot-Beweis,
Schlüsselrotation (Client-Private-Key lief beim Bootstrap über `sorb/buffer` auf
rohana; Repo wird laut sorb vernichtet, Rotation danach trotzdem empfohlen),
Repo-Zuhause für `lab.conf` + systemd-Drop-in (zurückgestellt bis nach der
Testreihe).
## 8. Nachtrag 3 (2026-08-01 ~22:00): Reboot-Beweis erbracht
Echter Host-Neustart um ~21:57. Ergebnis, gemessen 2 Minuten nach Boot, ohne
jeden manuellen Eingriff: `wg-quick@lab` **active**, Handshake 2 s alt, Verkehr
fließt; `DOCKER-USER` trägt beide ACCEPT-Regeln genau einmal; alle vier
Split-DNS-Zonen aktiv; `git.lab` auflösbar und pingbar. Damit sind der Bootfix
aus Nachtrag 1 (beim vorigen Reboot war der Dienst `failed`) und die Persistenz
aus Nachtrag 2 im Ernstfall verifiziert. Aus der Offen-Liste von Nachtrag 2
gestrichen: der Reboot-Beweis. Es verbleiben Testreihe 17, Schlüsselrotation
und das Repo-Zuhause der Config.
+87
View File
@@ -0,0 +1,87 @@
---
type: aar
status: harvested
date: 2026-08-01
related: []
---
# AAR — LABNET-02, Lab-Seite (UDM/UniFi, Einzäunung und Abnahme)
**Datum:** 2026-08-01 · **Host/Stack:** MorninglightMountain (UDM Pro), UniFi Policy Engine
**Auftrag:** Architektur v2 planen, Lab-Seite koordinieren, Testreihe 17 abnehmen
**Gegenstück:** [CFGMON-Seite](2026-08-01-labnet02-cfgmon.md) · Issue: `management#12`
## 1. Ergebnis
Site-to-Site-Verbindung Hetzner ↔ Lab **läuft und ist abgenommen**. Ein Hetzner-Server
erreicht `git.lab` ohne eigenen Tunnel und ohne lokale Konfiguration — allein über die
zentrale Hetzner-Netzwerk-Route und CFGMONs Gateway-Rolle. Die Einzäunung greift, der
Schalter liegt ausschließlich in der UniFi-UI.
Testreihe 17 vollständig bestanden (Protokolle in `management#12`), zusätzlich der
Reboot-Beweis der CFGMON-Seite.
## 2. Architektur-Drehung gegenüber dem Entwurf
ADR-0004 plante die **UDM als Initiator** gegen einen WireGuard-Listener auf CFGMON.
Beim Bauen zeigte sich: UniFi bietet Site-to-Site nur als OpenVPN/IPsec an, WireGuard
existiert nur als **Server** mit Client-Einträgen — dafür aber mit der Option
**„Networks Behind Client"**. Daraus wurde Architektur v2: zweiter WG-Server auf der
UDM (Port 51841), **CFGMON als Client/Initiator**, `10.0.0.0/24` als Netz hinter dem
Client. Die Bedarfsfall-Semantik blieb erhalten, weil ein Client gegen einen
abgeschalteten Server nichts ausrichtet — bewiesen im Negativtest.
Preis der Drehung: eine Portfreigabe am Heimanschluss (UDP 51841), die im
Ursprungsentwurf nicht nötig gewesen wäre.
## 3. Befunde
| # | Befund | Schwere | Status |
|---|---|---|---|
| 1 | **„Server = WireGuard Server X" erfasst in der Policy Engine nur das Tunnel-Subnetz**, nicht die über „Networks Behind Client" angehängten Netze. Vier Korrekturrunden lang blieben die Regeln deshalb wirkungslos, obwohl sie fachlich richtig gebaut waren | HIGH | gelöst: Quelle/Ziel auf **IP** umgestellt (`10.58.75.0/24` + `10.0.0.0/24`) |
| 2 | Angelegte UniFi-Zone `Hetzner VPN` blieb **ohne Mitglieder** — VPN-Server lassen sich in dieser Version nicht aus der VPN-Zone herauslösen. Ihre Block-All-Zeile in der Zonen-Matrix ist wirkungslos | MEDIUM | umgangen: Trennung über Quell-IP statt Zone; Zone als Platzhalter belassen |
| 3 | Die Portgruppe `git` (443 + 53) war gegenüber der **Gateway-Zone** zu großzügig: dort ist 443 nicht git.lab, sondern die **Router-Admin-Oberfläche**. Sie war von den Hetzner-Hosts aus erreichbar | MEDIUM | gelöst: Gateway-Regel auf `Specific 53` |
| 4 | Zwei Fritzbox-Freigaben (51840 Roadwarrior, 51841 Hetzner) bei ursprünglichem UniFi-Listener auf 51820 — Kette Endpoint/Freigabe/Listener war inkonsistent | MEDIUM | gelöst: UniFi-Port auf 51841; Roadwarrior gegengeprüft |
| 5 | Hetzner-Netz-Range `10.0.0.0/16` deckte das Routen-Ziel `10.58.73.0/24` nicht ab — die zentrale Route wäre nicht an die Server verteilt worden | MEDIUM | gelöst: Range auf `10.0.0.0/8` erweitert (nachträglich möglich, nur Erweitern) |
## 4. Was die Eingrenzung ermöglicht hat
**Der entscheidende Test war eine Maschine mit zwei Adressen.** CFGMON ist über
`10.58.75.2` (Tunnel) *und* `10.0.0.3` (Hetzner-Netz) erreichbar. Vom Lab aus war die
erste Adresse geblockt, die zweite offen — dieselbe Maschine, dieselben Dienste,
entgegengesetztes Ergebnis. Damit war Befund 1 nicht mehr Vermutung, sondern Messung.
**Timeout vs. „Connection refused" trennt Firewall von fehlendem Dienst.** Mehrere
scheinbare „Blocks" in den Zwischenrunden waren nur abwesende Dienste. Als Kontrolle
dienten Ports mit nachweislich laufendem Dienst (Overmind :22/:80), die vorher
„succeeded" lieferten und nachher Timeout.
**Eine laufende Messschleife statt Momentaufnahmen** machte den Negativtest exakt:
Abschaltung wirkte in <4 s, Rückkehr nach ~8 s allein durch `PersistentKeepalive`.
## 5. Lehren
1. **Zonen-Zuordnung ist interface-basiert, Feinauswahl ist es nicht.** Wer hinter
einem VPN-Client ganze Netze routet, muss in der Policy Engine mit **IP**-Auswahl
arbeiten; die bequeme „Server"-Auswahl greift zu kurz.
2. **Bei „Block alles außer …" gehört die Portbedingung in BEIDE Portfelder.** Der
Rückverkehr wird an seinem *Quell*-Port erkannt. Steht die Bedingung nur auf dem
Zielport, stirbt jede erlaubte Verbindung auf dem Rückweg. (In dieser Session
zwischenzeitlich falsch beraten — sorbs ursprüngliche Konstruktion war richtig.)
3. **Ausnahmelisten sind zielabhängig.** Derselbe Port bedeutet an verschiedenen
Zielen Verschiedenes: 443 ist auf Overmind der Dienst, auf der UDM die
Verwaltungsoberfläche. Eine Portgruppe pro Ziel, nicht eine für alles.
4. **Syslog Logging an der Regel hätte Runden gespart.** Es beantwortet die Frage
„greift die Regel überhaupt?" sofort, statt sie aus Wirkungen zu erschließen.
5. **Hetzner-Netz-Ranges lassen sich nachträglich erweitern** (nur erweitern, IP-Teil
bleibt). Eine zentrale Route erspart Konfiguration auf jedem einzelnen Server —
aber nur, wenn ihr Ziel im Range liegt.
## 6. Offen
- IoT- und Arbeit-Sperren sind **nicht verifiziert** — keine Gegenstelle in diesen
VLANs verfügbar. Bei nächster Gelegenheit von einem Gerät dort gegentesten.
- Regel-Beschreibungsfelder in UniFi sind leer; Verweis auf LABNET-02/ADR-0004 fehlt.
- Portgruppe heißt `git`, enthält aber 443 + 53 — sprechender Name wäre besser.
- Folgearbeit (eigenes Issue): Deploy-Übergabe-Issues nach git.lab holen,
Gitea-Ausnahme in ADR-0002/README/CLAUDE.md zurückbauen.
@@ -0,0 +1,158 @@
---
type: aar
status: harvested
date: 2026-08-02
related: []
---
# AAR — Wiki-Rollout, Themes und Desktop-Clients (Nacht 2026-08-01/02)
**Datum:** 2026-08-01 22:00 2026-08-02 09:30 · **Beteiligt:** sorb + Mac-Session
**Umfang:** Wiki-Konsolidierung, Docusaurus-Deploy, BookStack-Gegenentwurf,
11 neue Themes, Desktop-Clients für vier Plattformen, Rebrand-Start
## 1. Ergebnis
| Was | Stand |
|---|---|
| gitops-Wiki (15 Seiten) auf git.lab, inhaltlich korrigiert | ✅ |
| Docusaurus-Wiki unter `axionwiki.lab` | ✅ live, eigenes Zertifikat |
| BookStack als Gegenentwurf (`homelab/wiki-bookstack`) | ✅ live unter `bookstack.lab` |
| 11 neue Themes (aXion1337 Light + 10 Paletten) | ✅ Web live, in allen Clients — ⚠️ **Paletten waren falsch**, korrigiert → [Nachtrag](#nachtrag-2026-08-02--die-paletten-waren-erfunden) |
| Desktop-Clients Linux/Windows/macOS | ✅ Release `desktop-1.12.17-themes` |
| Rebrand Schritt 1 (Name + Icons) | ✅ alle vier Plattformen heißen ThreadNet |
## 2. Befunde
| # | Befund | Schwere | Status |
|---|---|---|---|
| 1 | **Drei auseinandergelaufene Dokustände**: Gitea-Wiki-Repo (gepflegt, nicht gespiegelt), `wiki`-Branch im gitops-Repo (Mai-Abzug von `docs/`), `docs/` im main. Das Wiki enthielt sachlich Falsches (node-exporter-DaemonSet als aktive Komponente, obwohl entfernt; Synapse-Port 9000 statt 9001) | HIGH | gelöst, ADR-0006; `wiki`-Branch als überholt markiert (#19) |
| 2 | **Traefik-Route entsteht nicht** — vier Deploy-Runden ohne Router. Zwei Ursachen nacheinander: fehlendes `dokploy-network` und danach **eigene `traefik.*`-Labels neben denen von Dokploy** | HIGH | gelöst; Merksatz unten |
| 3 | **`/favicon.ico` lieferte HTTP 200 mit `text/html`** — die nginx-`try_files`-Kette gab die 404-Seite mit Erfolgsstatus aus. Safari hielt das Icon für vorhanden und zeigte den Buchstaben-Fallback | MEDIUM | gelöst: Datei im Wurzelverzeichnis + `try_files $uri =404` für Assets |
| 4 | **CI-Job-Container kennt die Lab-CA nicht** (`unable to get local issuer certificate`); der erste Fix als globale CI-Variable brach den Checkout des eigenen Repos | MEDIUM | gelöst: CA im Repo, `GIT_SSL_CAINFO` **im Sync-Skript** |
| 5 | **Icons wurden nie vergrößert**: `PIL.thumbnail()` skaliert ausschließlich nach unten, das 277-px-Motiv blieb in 1024er-Icons eine Briefmarke (54 % × 38 % Füllung) | MEDIUM | gelöst mit `resize()` aus dem Original: 81 % |
| 6 | **Nur macOS bekam neue Icons** — Windows (`.ico`) und Web (`res/vector-icons/`, `manifest.json`) blieben auf Element | MEDIUM | gelöst, `c51b681` |
| 7 | **BookStack-Stack hatte fünf Fehler**: nicht existierende Image-Tags, `healthcheck.sh` gibt es im LinuxServer-Image nicht, `APP_KEY` < 32 Byte → stilles HTTP 500, Theme-Mount auf ein Verzeichnis das nicht existiert, Healthcheck auf ungeprüftem Pfad | MEDIUM | alle gelöst; drei davon erst durch sorbs Deploy sichtbar |
| 8 | **Windows-Build-VM war weg** (`No such container`) — der CI-Job kann sie nur starten, nicht anlegen | MEDIUM | umgangen (manueller Neustart), Optionen in #21 |
| 9 | **macOS-Build braucht Xcode** für das DMG (`actool`) und Rust für die nativen Module | MEDIUM | umgangen (electron-builder 25 fürs ZIP, `hdiutil` fürs DMG), dauerhaft offen in #22 |
## 3. Was die Eingrenzung ermöglicht hat
**Die Traefik-Logs.** Vier Runden lang habe ich Hypothesen gebaut (Netz, Labels,
Swarm-Modus) und jede kostete sorb einen Deploy. Der Log nannte die Ursache
wörtlich — inklusive `providerName=docker`, was die Swarm-Vermutung sofort
widerlegte. **Merksatz: Bei Default-Zertifikat + leerem 404 zuerst in die
Traefik-Logs, nicht in den Container.**
**Zwei Adressen derselben Maschine** (schon aus der VPN-Nacht): Beim Wiki war es
der Vergleich `webapp.asar` vs. `app.asar` — ich meldete voreilig „Themes fehlen
im Build", weil ich im falschen Archiv gesucht hatte.
**Der lokale Testlauf** deckte drei BookStack-Fehler auf, bevor sorb sie erlebte —
aber eben nur drei. Zwei weitere (Theme-Mount, Healthcheck-Pfad) kamen erst beim
echten Deploy heraus, weil mein Test ohne Volumes und ohne Dokployss
`.env`-Behandlung lief. **Ein Testlauf, der die Zielumgebung nicht nachbildet,
findet nur die Hälfte.**
## 4. Lehren
1. **Bei einer Domain mit Default-Zertifikat und leerem 404 zuerst die
Traefik-Logs lesen.** Ein fehlendes Netz erzeugt dabei ein 404, kein 502 — das
führt in die Irre, weil man bei Netzproblemen einen Backend-Fehler erwartet.
2. **Keine eigenen `traefik.*`-Labels neben denen von Dokploy.** Ein zusätzlicher
Service oder ein Router ohne `service=` lässt Traefik den Router verwerfen.
3. **Statische Dateien dürfen nie auf HTML zurückfallen** (`try_files $uri =404`),
sonst sieht jeder fehlende Pfad wie ein Erfolg aus.
4. **`thumbnail()` vergrößert nicht.** Wer Icons erzeugt, braucht `resize()` — und
die Quelle in voller Auflösung.
5. **Ein Rebrand ist mehr als eine Datei.** Icons leben pro Plattform an eigenen
Orten; wer nur eine ersetzt, merkt es erst, wenn der Nutzer fragt.
6. **Testumgebung ≠ Zielumgebung.** Der lokale Docker-Lauf fand die Fehler, die
das Image betreffen — nicht die, die aus Dokployss `.env`-Handling und den
Volumes entstehen.
7. **Verweise auf Issues prüfen, bevor sie in ein Release wandern.** In den
Release-Notes stand ein Link auf ein Issue, das ich nie angelegt hatte (fiel
erst bei der Konventionsprüfung auf).
## 4a. Nachtrag (2026-08-02 vormittags): drei weitere Runden
**BookStack lief erst nach fünf Anläufen.** Die Ursachen kamen nacheinander und
maskierten einander:
| # | Ursache | Wie sie sich zeigte |
|---|---|---|
| 1 | Anführungszeichen im `APP_KEY` (Dokploy schreibt Werte 1:1 in eine `.env`) | Deploy bricht ab: `unterminated quoted value` |
| 2 | `BOOKSTACK_TAG=25.07` aus meiner ersten `.env.example` — den Tag gibt es nicht | `manifest unknown` |
| 3 | **Healthcheck auf `/login` schlug fehl → Container `unhealthy` → Traefik überspringt ihn komplett** | Default-Zertifikat + leeres 404, **identisch zum Bild eines fehlenden Netzes** |
| 4 | `DB_PASSWORD` nachträglich geändert; MariaDB legt Zugangsdaten nur beim ersten Start an | `Access denied for user 'bookstack'` |
| 5 | `APP_KEY` weder 32 Byte noch mit `base64:`-Präfix | `Unsupported cipher or incorrect key length` |
**Die wichtigste neue Lehre:** Ein **`unhealthy` Container ist für Traefik
unsichtbar** — kein Router, kein Service, egal wie korrekt die Labels sind. Das
Symptom ist ununterscheidbar von einem fehlenden Netz. Ein Healthcheck, der nicht
im laufenden Container verifiziert wurde, ist damit kein Sicherheitsnetz, sondern
ein Risiko. Ich hatte ihn zweimal ungeprüft geändert (`/status``/login`).
**Rebrand:** Der Name saß erst nach drei Anläufen überall. `productName` regelt
den App-Namen (macOS/Windows), **`name`** den Linux-Paketnamen, das Binary und den
`/opt`-Pfad — und die CI braucht `VARIANT_PATH`, sonst greift die Variante gar
nicht. Icons: `PIL.thumbnail()` skaliert **nur nach unten**, weshalb das Motiv in
1024er-Icons nie vergrößert wurde; und ein Rebrand betrifft `.png`, `.ico`,
`.icns`, sieben Web-Icons und das Manifest — nicht eine Datei.
**Muster über beide Nächte:** Meine teuersten Fehler entstanden nicht durch
falsche Analysen, sondern durch **ungeprüfte Änderungen** — ein Healthcheck ohne
Test, ein Issue-Verweis ohne Existenzprüfung, ein Icon-Skript ohne Blick aufs
Ergebnis. Die Diagnose war jedes Mal gut, sobald echte Daten vorlagen
(Traefik-Logs, Laravel-Log, `docker inspect`).
## 5. Offen
- **Entscheidung DOC-03 (#20)**: Docusaurus oder BookStack — beide laufen jetzt,
der Vergleich kann an echten Inhalten stattfinden.
- **Navbar-Logo im Wiki**: HTML, CSS und Bild werden nachweislich korrekt
ausgeliefert, im Browser aber nicht sichtbar. Braucht einen Blick in die
Entwicklerkonsole.
- **macOS reproduzierbar bauen** (#22), **Windows-VM-Robustheit** (#21).
- **Rebrand-Rest**: About-Attribution im Client, `brand` in der Prod-Config,
Signing (ThreadNet-Web#6) — ohne Signatur bleibt für Nutzer auf macOS der
`xattr`-Schritt und auf Windows die SmartScreen-Warnung.
## Nachtrag 2026-08-02 — die Paletten waren erfunden
Nachmittags nachgetragen, weil der Befund das Ergebnis oben relativiert.
**Was war.** Die zehn Themes aus dem Rollout trugen nicht die Farben aus Anthropics
[theme-factory-Skill](https://github.com/anthropics/skills/tree/main/skills/theme-factory),
sondern **meine Auslegung ihrer Namen**. Ich hatte den Skill benannt, aber nie
seine Farbwerte gelesen. Aufgefallen ist es sorb an „Sunset Boulevard": Er hatte
gedämpftes Terrakotta erwartet, bekam Koralle und Pink. Die Prüfung an der Quelle
zeigte, dass fast alle zehn danebenlagen — am gröbsten beim Grundcharakter:
**sieben der zehn sind hell gemeint, ich hatte sechs dunkel angelegt.**
**Warum es nicht auffiel.** Erfundene Farben sehen nicht falsch aus. Ein Theme
namens „Ocean Depths" in dunklem Türkis wirkt stimmig — es fällt erst auf, wenn
jemand die Vorlage kennt. Anders als ein kaputter Healthcheck erzeugt eine
erfundene Palette kein Symptom, auf das man stoßen könnte.
**Falle für die nächste Runde.** Ob ein Theme hell oder dunkel gemeint ist, steht
in den Skill-Beschreibungen **nicht verlässlich** — „Warm Sand · backgrounds"
findet sich bei einem Theme, dessen Showcase-Seite dunkel ist. Belastbar ist nur
`theme-showcase.pdf`: Seiten rendern, Hintergrundfarbe messen. Werte und Fallen
stehen in [`shared/branding.md`](../wiki/architecture/branding.md).
**Bestätigung des Musters aus Abschnitt 4.** Auch das war kein Analysefehler,
sondern eine **ungeprüfte Änderung** — dieselbe Wurzel wie Healthcheck, toter
Issue-Verweis und Icon-Skript. Nur diesmal ohne Fehlermeldung, die es aufdeckt.
Die Lehre schärft sich damit: Es genügt nicht, Ergebnisse zu prüfen — bei
Vorlagen ist die **Quelle** zu prüfen, bevor etwas daraus abgeleitet wird.
**Nebenbefund.** sorbs von Hand eingestelltes BookStack-Schema und die offizielle
Sunset-Boulevard-Palette sind bis auf zwei Ziffern identisch (`#e76e51`/`#e76f51`,
`#f3a261`/`#f4a261`) — unabhängig voneinander auf demselben Coolors-Satz gelandet.
**Korrigiert:** gitops `b10b607` (Web, live verifiziert) · ThreadNet-Web `80fcf6c`
(Desktop-Config). Das BookStack-CSS lag bereits richtig.
⚠️ **Die released Desktop-Binaries tragen weiter die alten Farben** — die Themes
stecken in `resources/webapp.asar`. Abgestimmt so belassen; der nächste reguläre
Build zieht die Korrektur mit (nachgehalten in ThreadNet-Web#11, `status:wartet`).
@@ -0,0 +1,107 @@
---
type: aar
status: open
date: 2026-08-09
related: []
---
# AAR — Refinement, Betrieb voranbringen, Git-Historie anonymisiert
**Datum:** 2026-08-09 · **Host/Stack:** git.lab, Gitea, K3s-Cluster (Authentik,
Synapse, Element Web) · **Auftrag:** Backlog-Refinement, danach gezielt den
Betrieb von ThreadNet voranbringen statt weiter Befunde anzuhäufen
## 1. Ergebnis
**Live und verifiziert:**
- MFA-Pflicht für die Gruppe `authentik Admins` — Stage, Bindung und
Gruppenzuordnung in der Datenbank geprüft, Standard-Login für alle anderen
unverändert
- `matrix-recovery-flow`-Blueprint läuft erfolgreich — `SELECT … WHERE status
<> 'successful'` liefert 0 Zeilen
- Web-Client zeigt einen Fehlerbericht-Weg, der **lokal** bleibt
(`bug_report_endpoint_url: "local"`) — vorher unbemerkt an element.io
- 251 Commits über vier Repos auf 12:00-UTC-Zeitstempel umgeschrieben, Force-
gepusht, Mirrors und Flux verifiziert synchron
- Stillstandsprüfung läuft täglich per Zeitplan, fand beim ersten Lauf zwei
vorher unbekannte Repos ohne Push-Mirror
- `game-operating` gespiegelt und secret-frei verifiziert (Coolify-
Magievariablen, keine echten Werte)
- Call-Widget stempelt sich mit Paketversion + Commit statt „dev"
- `@concierge`-Bot für Gäste-Einladungen gebaut und deployt
**Bewusst nicht live:**
- Desktop-Client hat den lokalen Fehlerbericht-Weg erst mit dem nächsten Build
(Config geändert, kein Rebuild ausgelöst)
- `@concierge` läuft nicht — wartet auf Matrix-Konto, Authentik-Token,
Einladungsraum, zwei Gruppen, Secret (alles Zugangsdaten, sorbs Seite)
- Authentik-Teil der Stillstandsprüfung übersprungen ohne Token (Fehlen wird
ausgewiesen, nicht verschwiegen)
- `gameserver` weiterhin ohne Mirror — zwei Repos gleichen Namens mit
unterschiedlichem Stand, Klärung vor jedem Eingriff nötig
## 2. Befunde
| # | Befund | Schwere | Status |
|---|---|---|---|
| 1 | `matrix-recovery-flow`-Blueprint scheiterte seit Tagen bei jedem Lauf, während Flux grün meldete | HIGH | behoben |
| 2 | Ursache des Blueprint-Fehlers war zweifach verdeckt: `!KeyOf` löst beim Fehlschlag gegen ein leeres Blueprint auf und wirft dieselbe Ausnahme erneut — die echte Meldung ging im eigenen Logging unter | HIGH | behoben, `!Find` statt `!KeyOf` |
| 3 | Web-Client sendete Fehlerberichte an `rageshakes.element.io` — die Desktop-Bereinigung vom 2026-08-01 hatte den Web-Build nie erreicht, weil der beim Bauen Elements eigene `develop/config.json` kopiert | HIGH | behoben, `"local"` gesetzt |
| 4 | `game-operating` und `gameserver` ohne Push-Mirror; bei `gameserver` liegt auf Gitea ein anderer Stand als auf git.lab | MEDIUM | `game-operating` behoben, `gameserver` offen (management#32) |
| 5 | Nach dem Privat-Stellen von `game-operating` auf Gitea übersprang die Stillstandsprüfung den Mirror-Abgleich klaglos, statt es als Befund zu werten | MEDIUM | behoben |
| 6 | Header-Hilfsfunktion baute `Authorization: token: <wert>` (doppelter Doppelpunkt) — still ungültig, hätte beim Eintragen des Authentik-Tokens wie ein falscher Token ausgesehen | MEDIUM | behoben, vor dem ersten echten Einsatz gefunden |
| 7 | Gitops-Leitfaden 04 nannte 7 Themes mit teils erfundenen Namen (`Gruvbox Dark`, `Wal`); tatsächlich 17 | LOW | behoben |
| 8 | threadnet-call-Doku beschrieb einen manuellen npm-Publish, der seit 2026-08-06 automatisiert läuft | LOW | behoben |
| 9 | `overmind.md` nannte „sechs gespiegelte Repos" — nach dem Mirror für `game-operating` sind es sieben | LOW | behoben |
| 10 | Neu angelegter Deployment-Guide (`@concierge`) fehlte im eigenen Index | LOW | behoben |
| 11 | Tag-Push (Force, für die Historien-Anonymisierung) löste in ThreadNet-Web drei Release-Pipelines neu aus; nur weil die geschützten Registry-Variablen im Zeitfenster fehlten, wurde `v0.4.0` nicht mit heutigem Code überschrieben | HIGH | Sperre nachgezogen (ThreadNet-Web#14), Ursache war Zufall, nicht Schutz |
## 3. Verdachtsfälle mit Entwarnung
- **`game-operating` öffentlich auf Gitea** — Secret-Scan über alle fünf
öffentlich gewordenen Dateien: kein echter Credential-Wert, ausschließlich
Coolify-Magievariablen und ein leeres `api_key`-Feld. Erste Prüfung lieferte
fälschlich „sauber", weil der Rohpfad falsch war (5×11-Byte-„Not found"-
Antworten) — erst nach Gegenprobe der Dateigrößen als Fehlmessung erkannt und
mit korrektem Pfad wiederholt.
- **Meine erste Diagnose zu #60** („Passwort-Wiederherstellung vermutlich tot")
— falsch. Der Flow hatte durchgehend alle sechs Bindungen, der Blueprint-
Fehler betraf nur künftige Blueprint-Läufe, nicht die längst angelegten
Objekte.
## 4. Was die Befunde ermöglicht hat
- **Direktes Auslesen der Authentik-Datenbank statt Vertrauen auf den
Flux-Status.** Blueprint-Fehler #1/#2 waren nur so sichtbar — Flux, die
ConfigMap und der Cluster-Zustand insgesamt meldeten durchgehend grün.
- **`ak apply_blueprint` von Hand** hat den durch das eigene Logging
verdeckten Fehler #2 erst zugänglich gemacht — der reguläre Weg (Worker-Log)
zeigte nur die Folgeausnahme.
- **Den Ist-Zustand vor einer Änderung auslesen statt der Issue-Beschreibung
zu glauben** hat Befund #3 aufgedeckt — die Annahme im Issue betraf nur den
Desktop-Client, `config.json` auf dem Web-Server sagte etwas anderes.
- **Die Stillstandsprüfung selbst** (aus der gestrigen Retro gebaut) hat
Befund #4 im ersten Lauf gefunden — eine dynamische Projektliste statt einer
im Code gepflegten hat zwei Repos zutage gebracht, die niemand auf dem
Schirm hatte.
- **Content-Length-Gegenprobe nach dem Secret-Scan** hat die eigene
Fehlmessung beim `game-operating`-Check aufgedeckt, bevor sie als „sauber"
ins Protokoll ging.
- **Baumvergleich (Tree-Hash) vor und nach jedem Rewrite-Schritt** — 251 von
251 Paaren über Tree *und* Commit-Nachricht verifiziert, keine Annahme.
## 5. Offen
- **`@concierge` aktivieren** — fünf Zugangsdaten-Schritte, Checkliste in
gitops#48
- **`gameserver`-Mirror** — Standklärung nötig, management#32
- **Stillstandsprüfung Authentik-Teil** — `AUTHENTIK_URL`/`AUTHENTIK_TOKEN`,
management#31, bewusst aufgeschoben (sorb, 2026-08-09)
- **Rageshake vs. Zammad** — durch den lokalen Fix entschärft, aber nicht
entschieden, ThreadNet-Web#9
- **`report_event.admin_message_md`** nicht gesetzt — wer Inhalte meldet,
sieht keinen Kontaktweg; braucht nur eine Angabe (welcher Raum/Kontakt) von
sorb, dann eine Zeile Config
- **Desktop-Build** für den lokalen Fehlerbericht-Weg noch ausständig
- **ADR-0009** (Commit-Konventionen/Anonymisierung) nachträglich verfasst —
Lehre aus der Retro, in `decisions/` dokumentiert
@@ -0,0 +1,112 @@
---
type: aar
status: open
date: 2026-08-11
related: []
---
# AAR — `@apo` konnte nicht telefonieren: fehlende Synapse-`profiles`-Zeile
**Datum:** 2026-08-11 · **Beteiligt:** sorb + Mac-Session · **Stack:** Synapse,
MAS, Authentik, Element Web / Element Call, MatrixRTC (K3s-Cluster)
**Auftrag:** `@apo` kann sich anmelden und schreiben, aber **kein Call kommt
zustande** — Grundursache finden und beheben, ohne weiter zu raten.
## 1. Ergebnis
**Behoben und verifiziert:**
- `@apo` telefoniert wieder. Grundursache belegt: dem Konto fehlte die Zeile in
Synapses `profiles`-Tabelle. Fix war ein einzelnes `INSERT` der
Registrierungs-Default-Zeile, an zwei gesunden Konten (`clark`,
`calltest01`) gegengeprüft.
- Gegenprobe nach dem Fix: `displayname` gesetzt (vorher keine Zeile),
`open_id_tokens` **0 → 6**, aktives `org.matrix.msc3401.call.member` im Raum.
- Dokumentiert: Runbook `docs/troubleshooting/CALLS-FEHLEN-PROFILE-ZEILE.md` im
gitops-Repo (inkl. Index-Eintrag), Merksatz im Session-Gedächtnis.
**Nebenbefund, separat behoben:**
- **Kontoübernahme-Lücke:** Der MAS-Upstream-Provider stand auf
`claims_imports.localpart.on_conflict: add` — bei Localpart-Kollision verknüpfte
MAS die neue Upstream-Identität mit einem **bestehenden** Konto (inkl.
Dienstkonten ohne Upstream-Link). Auf `on_conflict: fail` umgestellt
(gitops `ef04d86`, nach git.lab gepusht), dokumentiert als
[gitops#61](https://git.lab/axion1337.chat/axion1337.chat-gitops/-/issues/61),
`priority:high`. Ausgelöst durch die live reproduzierte case-sensitive Dublette
`boje`/`Boje`; das Zweitkonto `boje` (Authentik-ID 11) wurde gelöscht.
**Deployment verifiziert:** Das SOPS-Values-Secret aktualisierte Flux, aber MAS
lief noch mit der alten Config im Speicher (Pod älter als die Änderung) — erst
ein `rollout restart` machte `fail` aktiv. „Committet" ≠ „deployed" ≠ „aktiv".
## 2. Die Kausalkette (belegt, nicht vermutet)
| Glied | Beleg |
|---|---|
| `@apo` hat **keine `profiles`-Zeile** | `SELECT count(*) … = 0`, während `clark`/`sorb`/`calltest01` je eine haben |
| Displayname-Setzen crasht | `PUT …/displayname → 500`, `TypeError: 'NoneType' object is not subscriptable` in `_check_profile_size` (`storage/databases/main/profile.py:354`) — `txn.fetchone()` liefert `None`, `row[0]` fliegt |
| kein Displayname → Widget-Init bricht ab | Call-Klick erzeugte **null** Server-Aktivität: kein `openid/request_token`, kein `call.member`; Browser-Log damals „Messaging present but not yet started" (iframe meldet nie `ContentLoaded`) |
| kein Widget → kein Token → keine SFU | `@apo` als einziger aktiver Nutzer mit **0** Einträgen in `open_id_tokens` (die nicht geprunt werden) |
Herkunft der fehlenden Zeile: `@apo` ist ein **Vor-Authentik-Konto**, das durch
sechs Identitäts-Resets ging. Deaktivieren löscht in Synapse das Profil,
Reaktivieren legt es nicht neu an. `frank` (noch älter, nie zurückgesetzt) behielt
seine Zeile. Ob einer der früheren manuellen Eingriffe der auslösende Reset war,
ist nicht mehr zweifelsfrei zu klären — die Zeile ist jetzt wieder da.
## 3. Was ausgeschlossen wurde (gemessen)
| Verdacht | Warum entkräftet |
|---|---|
| Krypto / Cross-Signing (18 Pseudo-Geräte aus 6 Resets) | Testraum ist **unverschlüsselt** → Call braucht keine Krypto; `clark` telefoniert mit ebenfalls zurückgesetzten Schlüsseln |
| Server-Call-Pfad (SFU, RTC-Auth, OpenID-Endpoint) | `calltest01`/`sorb` bekommen sauber 200 auf `openid/request_token` und die Federation-Auflösung |
| `@apo`s Token / Session | `/sync` läuft durchgehend mit 200, Messaging intakt |
| Login-Verknüpfung MAS↔Authentik | `subject` = Authentik-`uid` `2fafe38b…`, korrekt |
## 4. Was zur Lösung geführt hat
- **Der Sprung von „welcher Nutzer telefoniert nicht" zu „welche *Tabelle* ist
anders".** Der Durchbruch war die `open_id_tokens`-Abfrage über *alle* aktiven
Nutzer: `@apo` = 0, alle anderen zweistellig+. Ein Vergleich statt einer
Einzelbetrachtung.
- **Ein unverschlüsselter Testraum** hat das größte Ablenkungsfeld
(Cross-Signing) in einem Schritt geschlossen.
- **Der Live-Mitschnitt beim echten Call-Klick** zeigte die Abwesenheit jeder
Aktivität — nicht ein Fehler, sondern *nichts* war der Befund.
- **Der Nutzer-Hinweis „Anzeigename konnte nicht gesetzt werden"** lieferte den
500er mit vollständigem Stacktrace — die letzte Meile von Korrelation zu
Ursache.
- **Der entscheidende Kontext kam von sorb:** „`apo` ist ein Alt-Konto von vor
der Authentik-Integration." Das lenkte die Suche von „angesammelter Müll" auf
„Migrations-/Provisionierungs-Lücke".
## 5. Lehren für die Zukunft
1. **Bei Call-Problemen zuerst `open_id_tokens` je Nutzer vergleichen.** 0 bei
einem sonst aktiven Konto ist das schnellste, eindeutigste Alarmsignal und
trennt Client- von Server-Ursache in einer Abfrage.
2. **Immer im unverschlüsselten Raum reproduzieren, bevor man Krypto verdächtigt.**
Das schließt einen ganzen Ursachenblock kostenlos aus.
3. **„Nichts passiert" ist ein Messergebnis, kein Sackgassen-Signal.** Die
Abwesenheit eines `openid`-Aufrufs hat den Fehler lokalisiert, nicht ein
Fehlercode.
4. **Alt-/mehrfach-zurückgesetzte Konten gegen frisch provisionierte diffen,
nicht nur gegen die Erwartung.** Der Unterschied war eine *fehlende* Zeile —
sichtbar nur im direkten Vergleich mit `clark`/`calltest01`.
5. **Jeder DB-Schreib strukturiert: betroffene Zeile vorher anzeigen, an einem
gesunden Konto gegenprüfen, per `INSERT … ON CONFLICT DO NOTHING` statt
Überschreiben.** Das ist die direkte Konsequenz aus den früheren
unstrukturierten MAS-Eingriffen dieses Vorgangs — und diesmal eingehalten.
6. **Beiläufige Symptome ernst nehmen:** die Dublette `boje`/`Boje` beim
Testkonto-Anlegen war der Faden, der die Kontoübernahme-Lücke (gitops#61)
aufdeckte — ein Sicherheitsfund, der ohne den `@apo`-Vorgang unentdeckt
geblieben wäre.
## 6. Offen / Folgetodos
- **gitops#61** (`on_conflict`-Härtung) ist gepusht und rollt über Flux; der
case-insensitive Eindeutigkeits-Check im `matrix-invitation`-Prompt-Stage
(damit der Nutzer schon bei der Registrierung statt erst beim Login scheitert)
ist dort als bewusst offener Rest vermerkt.
- Verwaiste Altlasten bei `@apo` (10 `local_notification_settings` für längst
gelöschte Geräte, 18 Cross-Signing-Pseudoeinträge) sind **kosmetisch** und
wurden bewusst **nicht** angefasst — sie haben mit dem Call-Problem nichts zu
tun, und ein weiterer Eingriff widerspräche der Lehre oben.