diff --git a/docs/cf-backup/bf-mm-ingress-20260830-vor-tp16.3-schnitt.json b/docs/cf-backup/bf-mm-ingress-20260830-vor-tp16.3-schnitt.json new file mode 100644 index 0000000..54b9b9e --- /dev/null +++ b/docs/cf-backup/bf-mm-ingress-20260830-vor-tp16.3-schnitt.json @@ -0,0 +1,20 @@ +{ + "tunnel_id": "7a322261-380a-447b-97d2-8ebce99f5ed1", + "version": 6, + "config": { + "ingress": [ + { + "service": "http://10.40.20.167:5678", + "hostname": "n8n.emsenhuber.net" + }, + { + "service": "http_status:404" + } + ], + "warp-routing": { + "enabled": false + } + }, + "source": "cloudflare", + "created_at": "2026-08-30T10:19:30.297725Z" +} diff --git a/docs/cf-backup/bf-nas-ingress-20260830-vor-tp16.3-schnitt.json b/docs/cf-backup/bf-nas-ingress-20260830-vor-tp16.3-schnitt.json new file mode 100644 index 0000000..f339d01 --- /dev/null +++ b/docs/cf-backup/bf-nas-ingress-20260830-vor-tp16.3-schnitt.json @@ -0,0 +1,24 @@ +{ + "tunnel_id": "220c9958-401b-4111-9451-f3db09bba2d5", + "version": 7, + "config": { + "ingress": [ + { + "service": "http://10.40.20.157:3000", + "hostname": "gitea.emsenhuber.net" + }, + { + "service": "http://10.40.20.127", + "hostname": "vw.emsenhuber.net" + }, + { + "service": "http_status:404" + } + ], + "warp-routing": { + "enabled": true + } + }, + "source": "cloudflare", + "created_at": "2026-08-30T08:50:21.241084Z" +} diff --git a/docs/cf-backup/bf-public-ingress-20260830-vor-tp16.3-schnitt.json b/docs/cf-backup/bf-public-ingress-20260830-vor-tp16.3-schnitt.json new file mode 100644 index 0000000..872c457 --- /dev/null +++ b/docs/cf-backup/bf-public-ingress-20260830-vor-tp16.3-schnitt.json @@ -0,0 +1,44 @@ +{ + "tunnel_id": "48d3b0b6-7233-4781-a76a-1de4a5e957ce", + "version": 2, + "config": { + "ingress": [ + { + "service": "http://10.40.20.40:80", + "hostname": "vw.emsenhuber.net", + "originRequest": { + "noTLSVerify": true + } + }, + { + "service": "http://10.40.20.40:80", + "hostname": "hass.emsenhuber.net", + "originRequest": { + "noTLSVerify": true + } + }, + { + "service": "http://10.40.20.40:80", + "hostname": "n8n.emsenhuber.net", + "originRequest": { + "noTLSVerify": true + } + }, + { + "service": "http://10.40.20.40:80", + "hostname": "gitea.emsenhuber.net", + "originRequest": { + "noTLSVerify": true + } + }, + { + "service": "http_status:404" + } + ], + "warp-routing": { + "enabled": false + } + }, + "source": "cloudflare", + "created_at": "2026-08-27T05:45:54.534390Z" +} diff --git a/docs/cf-backup/bf-pve-ingress-20260830-vor-tp16.3-schnitt.json b/docs/cf-backup/bf-pve-ingress-20260830-vor-tp16.3-schnitt.json new file mode 100644 index 0000000..f4a0232 --- /dev/null +++ b/docs/cf-backup/bf-pve-ingress-20260830-vor-tp16.3-schnitt.json @@ -0,0 +1,20 @@ +{ + "tunnel_id": "f7fdf3a6-b67d-4567-93b1-b0f273794d09", + "version": 4, + "config": { + "ingress": [ + { + "service": "http://10.40.20.105:8123", + "hostname": "hass.emsenhuber.net" + }, + { + "service": "http_status:404" + } + ], + "warp-routing": { + "enabled": true + } + }, + "source": "cloudflare", + "created_at": "2026-08-30T08:50:25.151983Z" +} diff --git a/docs/cf-backup/cnames-tunnel-20260830-vor-tp16.3-schnitt.json b/docs/cf-backup/cnames-tunnel-20260830-vor-tp16.3-schnitt.json new file mode 100644 index 0000000..d512b66 --- /dev/null +++ b/docs/cf-backup/cnames-tunnel-20260830-vor-tp16.3-schnitt.json @@ -0,0 +1,44 @@ +[ + { + "name": "ds-fm.emsenhuber.net", + "type": "CNAME", + "content": "b697d124-53bf-4a33-8fcf-384cced4db1a.cfargotunnel.com", + "proxied": true, + "id": "0da61c6143bfd0d5f9c47ca4b4147b06" + }, + { + "name": "gateway-fm.emsenhuber.net", + "type": "CNAME", + "content": "b697d124-53bf-4a33-8fcf-384cced4db1a.cfargotunnel.com", + "proxied": true, + "id": "54ac8b816765b45191469678891389bd" + }, + { + "name": "gitea.emsenhuber.net", + "type": "CNAME", + "content": "220c9958-401b-4111-9451-f3db09bba2d5.cfargotunnel.com", + "proxied": true, + "id": "3d6053529f99fc5832d9956ee6e8834c" + }, + { + "name": "hass.emsenhuber.net", + "type": "CNAME", + "content": "f7fdf3a6-b67d-4567-93b1-b0f273794d09.cfargotunnel.com", + "proxied": true, + "id": "0841148f401b5db40fa02f69d0f0e848" + }, + { + "name": "n8n.emsenhuber.net", + "type": "CNAME", + "content": "7a322261-380a-447b-97d2-8ebce99f5ed1.cfargotunnel.com", + "proxied": true, + "id": "40b311a40e41fbeae85ca097a4036e6c" + }, + { + "name": "vw.emsenhuber.net", + "type": "CNAME", + "content": "220c9958-401b-4111-9451-f3db09bba2d5.cfargotunnel.com", + "proxied": true, + "id": "2971d3ba9410e841b35ec58ffda59be4" + } +] diff --git a/docs/teilprojekte.md b/docs/teilprojekte.md index dca4975..5d650e1 100644 --- a/docs/teilprojekte.md +++ b/docs/teilprojekte.md @@ -33,10 +33,20 @@ Mgmt (VLAN99, Ist 2026-08-16): core `.99.1` · branch `.99.2` · ap1 `.99.11` · liest, liest ihn als Erstes: er wird bei jeder Änderung mitgezogen, nicht nur die TP-Abschnitte.)* - 🆕 **MCP-Bestand 2026-08-28 korrigiert:** **9 MCPs identisch** in `~/.claude.json` und `~/.config/opencode/opencode.jsonc` (vorher Asymmetrie 7 vs. 11 — `portainer-mcp`/`shelly-mcp` raus, `gitea-mcp`/`n8n-mcp` rein). **Mac mini = n8n-Host, NICHT zentraler MCP-Host** (Performance-Restriktion); zentraler MCP-Host = **DS923** (`nas-bf`, Portainer Endpoint 14). Inventur-Stand `mcp/INVENTORY.md` + Policy `docs/mcp-host-policy.md`. - 🆕 **MCP-Migration FQDN→IP (2026-08-29, TP35 Schicht 1):** `gitea-mcp` und `n8n-mcp` in `~/.config/opencode/opencode.jsonc` von CF-FQDN auf LAN-IP umgestellt — `gitea-mcp` → `http://10.40.20.167:3000/mcp` (macmini, **ohne** Bearer, intern), `n8n-mcp` → `http://10.40.20.167:3001/mcp` (mit `Bearer {env:N8N_MCP_BEARER}`). **Befund dabei:** der Handoff 1.2 nannte für gitea-mcp die falsche IP `.157:3000` (das ist die Gitea-Web-UI, `400 Invalid CSRF`); der echte gitea-mcp läuft auf `.167:3000` (`serverInfo: gitea-mcp 0.0.10`). `cloudflare`-MCP bleibt auf dem offiziellen FQDN (User-Direktive). +- 🆕 **TP16.3/TP31 Schritt 5 vorbereitet (2026-08-30):** Runbook für die Vor-Ort-Session geschrieben + ([`tp16.3-bf-public-ap-connector-runbook.md`](tp16.3-bf-public-ap-connector-runbook.md)), CF-Rollback-Snapshots + unter `docs/cf-backup/*-20260830-vor-tp16.3-schnitt.json` **neu angelegt** (es gab sie vorher nicht). + `docs/tp31-schritt5-runbook.md` ist damit **abgelöst** (read-only-Header): es kannte ap3 nicht, + unterstellte einen Registry-Pull (`fetch: no` auf ap1/ap2) und enthielt eine **falsche** + Firewall-Freigabe-Behauptung. **CF-Stand:** `bf-public` `inactive` / 0 Connectoren; die vier + öffentlichen Namen laufen noch über bf-mm/bf-nas/bf-pve **an Caddy vorbei** direkt aufs Backend. - 🆕 **Globale Verhaltensregeln konsolidiert (2026-08-29, TP35 Schicht 3):** `~/.config/opencode/AGENTS.md` ist jetzt die **SSoT** für globale Verhaltensregeln (Secrets, VLAN-Schema, 5 Lessons, SoC, Übergabe-Mechanik, Mid-Action-Checkpoint, Commit-Hinweis, Backup-Guard). `~/.claude/CLAUDE.md` hält denselben Block als Claude-Code-Spiegelung mit Verweis auf die SSoT. opencode liest AGENTS.md mit Vorrang vor `~/.claude/CLAUDE.md`. -- **Alle vier MikroTik laufen auf RouterOS 7.24** (stable, 2026-08-17 von Christoph selbst gehoben, TP27), - **RouterBOOT ebenfalls 7.24** auf allen vieren (`current-firmware` = `upgrade-firmware`, geprüft 08-18). +- **Fünf MikroTik seit 2026-08-30** — neu dazu **`ap3`** (cAP ax `cAPGi-5HaxD2HaxD`, **`10.40.99.13`**, + Uplink core `ether21`, in der MikroMCP-Registry). ⚠️ **ap3 läuft `7.24.1`, die anderen vier `7.24`** + (live gelesen 08-30) — für versionsgebundene Pakete (`container.npk`) sind das **zwei** Dateien, + nicht eine. RouterBOOT jeweils = `upgrade-firmware`. + *(Hier stand bis 08-29 „Alle vier MikroTik laufen auf RouterOS 7.24" — mit ap3 überholt.)* - **L3 + WAN-Edge = `crs326-core`** (CRS326-24G-2S+, Technik UG): Routing, DHCP (7 Server, 54 Reservierungen), DNS, NAT, **NTP-Server für den Standort** und der WG-Hub (TP10, Peers `fm`/`dw`) laufen hier. Mgmt-SVI **`10.40.99.1`** (vlan99), Gateway `.1` je VLAN. **WAN = `vlan100`** — DHCP-Client @@ -67,7 +77,13 @@ liest, liest ihn als Erstes: er wird bei jeder Änderung mitgezogen, nicht nur d **16 Sites** (4 public, 12 intern mit `.bf.`-Suffix), Zertifikate ausschliesslich per **DNS-01** mit einem eng geschnittenen `Zone:DNS:Edit`-Token (`CF_DNS_API_TOKEN`, **nicht** das account-weite). Abnahme über den Host-Header von einem zweiten Host: 11× 200, `nautobot` 302. - Offen: Schritt 5–6 (Tunnel `bf-public`, Abbau). + Offen: **Schritt 5–6** (Connector für `bf-public`, CNAME-Flip, Abbau der sechs Container) — + **Runbook steht: [`tp16.3-bf-public-ap-connector-runbook.md`](tp16.3-bf-public-ap-connector-runbook.md)**, + Vor-Ort-Session an ap1/ap2/ap3. 🔴 **Zwei Blocker davor, beide remote lösbar** (08-30 gemessen): + **(a)** der `bf-public`-Ingress zeigt auf `:80`, wo Caddy mit **308 → https** antwortet ⇒ Redirect-Schleife + über Cloudflare; Fix = `https://10.40.20.40:443` + `originServerName` je Regel (über 443 mit korrekter + SNI liefern alle vier Namen 200). **(b)** es gibt **keine** Freigabe `vlan99 → vlan20:443` — der + Connector liefe in `TP5-WOULD-DROP`. - **DNS/Resolver = AdGuard-Paar `agh-bf1`/`agh-bf2`** (LXC 110/111 auf pve, **`10.40.20.36`/`.37`**, TP23 Stufe 2 + TP31 Schritt 4 ✅ seit 2026-08-26): Upstream DoH zum CF-Gateway, conditional `[/bf.emsenhuber.net/] → 10.40.20.1`, 47 Rewrites (16 Service → Caddy `.20.40` + 10 Host-Namen @@ -101,6 +117,11 @@ liest, liest ihn als Erstes: er wird bei jeder Änderung mitgezogen, nicht nur d > - 🔜 **Session B** — shelly-mcp-Container auf DS923 (`10.40.20.10`) deployen (live, ssh). Repo-Materialien committed (`mcp/shelly/{Dockerfile,BUILD.md,requirements.txt,.gitignore}`); `.env` und Tunnel-Secret lokal — kein MCP-Client (User-Direktive). > - 🔜 **Session C** — `caddy/cf-sync.py`-Drift-Check + Backup-Guard (`~/dev/.bin/backup-guard.sh`, TP35 Schicht 5). > +> 🆕 **Dazu seit 2026-08-30: TP16.3/TP31 Schritt 5 = Vor-Ort-Session** an ap1/ap2/**ap3** +> ([Runbook](tp16.3-bf-public-ap-connector-runbook.md)). **Reihenfolge beachten:** die drei Blocker in +> §1 sind **remote** zu erledigen, *bevor* jemand in den Keller geht — sonst steht man vor einem +> Connector, der `healthy` meldet, während die Seite eine Redirect-Schleife dreht. +> > 🆕 **Strukturelle Folge = TP36:** SSoT für `teilprojekte.md`-Live-Daten via Nautobot (eigene Session, nicht heute). Befund 2026-08-28: manuell gepflegte Datei verliert gegen die Realität, sobald sie lang wird. Quickref-Index, TP-Abschnitt und Ist-Stand-Block müssen bei jedem Edit mitgezogen werden — keine schlägt die andere. **Merkregel im Lessons-Block.** > 🆕 **Neu am 2026-08-24/25:** [TP31](#tp31) löst **TP16** und **TP16.2** ab und dampft cloudflared @@ -200,7 +221,7 @@ nachmessen und dann entscheiden**. Blocker gibt es keine mehr. TP5 ist in der Na > | TP14.4 | 🔜 | CF-Access-Token | — | > | TP16 | ✅ | CF-Tunnel HA wieder voll | 2026-08-24 | > | TP16.2 | 🔜 | Härtung bypass-Dienste | — | -> | TP16.3 | ⏳ | cloudflared-Reduktion (Probe läuft) | — | +> | TP16.3 | 🔜 | `bf-public`-Connector auf ap1/ap2/**ap3** (Runbook steht) | — | > | TP17.1 | ❌ | Shelly Gen4 → Zigbee | 2026-08-23 | > | TP17.3 | ✅ | Shelly-Regression behoben | 2026-08-23 | > | TP18 | 🔜 | Garage-Objektspeicher | — | @@ -210,7 +231,7 @@ nachmessen und dann entscheiden**. Blocker gibt es keine mehr. TP5 ist in der Na > | TP25 | ✅ | WLAN-Stabilisierung (Kern erledigt) | 2026-08-24 | > | TP29 | 💡 | Anwesenheitserkennung (Idee) | — | > | TP30 | 🔜 | Credentials (Etappe 1 ✅, Rest gebündelt) | — | -> | TP31 | ✅ | Reverse Proxy Caddy (Schritt 1–4 ✅) | — | +> | TP31 | ⏳ | Reverse Proxy Caddy (1–4 ✅, 5–6 = Vor-Ort-Session) | — | > | TP32 | 🟢 | Nummern-Systematik (Konzept) | — | > | TP33 | 💡 | Sonos fern (Idee, zurückgestellt) | — | > | TP34 | 🔜 | Caddyfile via Nautobot/GC (Stub, Inventur-Handoff 2026-08-27) | — | @@ -408,7 +429,48 @@ nachmessen und dann entscheiden**. Blocker gibt es keine mehr. TP5 ist in der Na - **Reihenfolge:** (1) Client-IP-Weitergabe fixen (inkl. HA `trusted_proxies`) → (2) `/admin`-Pfade path-scoped schützen → (3) App-Limits + 2FA → (4) Edge-Rules → (5) CrowdSec/fail2ban mit CF-Bouncer. - **Abh.:** TP16 ✅ (Tunnel-HA). Verwandt: **TP20.1** (vw wandert auf Platform-Postgres — DB-HA ≠ Access, getrennt halten), **TP23** (dessen WARP-Managed-Network-Idee ist durch diesen Constraint **entlastet**: `vw` verlangt keine Device-Identity, also kollidiert „WARP zuhause aus" nicht mehr mit dem Access-Strang). -### TP16.3 — cloudflared-Reduktion: was der Mesh als Datenpfad kann (und was nicht) ▸ ⏳ **PROBE LÄUFT SEIT 2026-08-24** — `docs/tp16.3-nautobot-mesh-only-runbook.md §8` +### TP16.3 — cloudflared-Reduktion: was der Mesh als Datenpfad kann (und was nicht) ▸ 🔜 **UMSETZUNG VORBEREITET 2026-08-30** (Probe seit 2026-08-24 ✅) — `docs/tp16.3-nautobot-mesh-only-runbook.md §8` + +#### 🔜 Follow-up F2 — `bf-public`-Connector auf die APs (Runbook 2026-08-30) +**Runbook: [`tp16.3-bf-public-ap-connector-runbook.md`](tp16.3-bf-public-ap-connector-runbook.md)** +— Vor-Ort-Session an **ap1 + ap3 (Technik UG)** und **ap2 (Technik OG)**, weil der +`device-mode`-Wechsel eine **physische** Bestätigung am Gerät verlangt. + +- 🆕 **ap3 ist neu und nicht baugleich** (live 08-30): **cAP ax** (`cAPGi-5HaxD2HaxD`, `10.40.99.13`), + arm64, 96 MB Flash frei, 689 MB RAM frei ⇒ Container passt. **Aber: ROS `7.24.1` gegen `7.24`** auf + ap1/ap2 ⇒ **zwei verschiedene `container.npk`**. Und ap3 steht auf `device-mode=basic` (nicht `home`), + dort ist `fetch` bereits erlaubt — wir nutzen trotzdem auf allen dreien den `.tar`-Import, damit alle + dasselbe Image fahren (gepinnt: `cloudflared 2026.6.1`, Digest `sha256:6d91c121…`, 27,5 MB). +- 🔴 **Blocker A — Redirect-Schleife.** `bf-public` zeigt auf `http://10.40.20.40:80`; Caddy antwortet + dort mit **308 → `https:///`**, also zurück durch Cloudflare in denselben Tunnel ⇒ + `ERR_TOO_MANY_REDIRECTS` für alle vier Namen. **Fällt heute nicht auf**, weil die drei Alttunnel + **an Caddy vorbei** direkt aufs Backend gehen — die Schleife entsteht erst mit dem Umstieg. + Fix = `https://10.40.20.40:443` + `originServerName` je Regel (über 443 mit korrekter SNI: 4× 200). +- 🔴 **Blocker B — die Freigabe existiert nicht.** Die `forward`-Kette des Core wurde vollständig + gelesen (36 Regeln, Nr. 35 = `TP5: deny-by-default`): **keine** Regel `vlan99 → vlan20` für 80/443. + Die einzige MGMT→INFRA-Regel (Nr. 26) ist ein `src-port=8728`-**Rückweg** der API. + ⚠️ **Der Vorgänger-Runbook behauptet das Gegenteil** („Regel 27 deckt `vlan99→vlan20:80` ab ✅") — + Regel 27 ist `IOT->INFRA http-rueckweg`, `in-interface=vlan30`. Ohne neue Regel läuft der Connector + in `TP5-WOULD-DROP` + Drop. Egress zu CF (Regel 13 `LAN->WAN`) und DNS (Regeln 30–33) sind dagegen ✅. + ⇒ Das `masquerade` des `veth`-Netzes ist deshalb **Pflicht**, nicht Kosmetik: sonst kommen die Pakete + mit `172.17.0.2` am Core an, wo weder Route noch Regel sie kennt. +- 🟡 **Blocker C — `ROUTER_AP3_PASS` fehlt in `~/.secrets/credentials.env`** (steht nur in + `~/.claude.json`), gegen die Credential-Store-Konvention. Der `sshpass -e`-Reboot-Weg braucht es dort. + ⚠️ Nebenbefund: **`credentials.env` lässt sich in zsh nicht sourcen** — ab ~Zeile 69 stehen rohe + WireGuard-Zeilen, zsh bricht ab und lässt die Variablen **leer**; das sieht aus wie ein ungültiger + Token. Alle Snippets im Runbook laufen deshalb in `bash -c`. +- 🔩 **Kein PoE als Abkürzung:** beide CRS326 haben nachweislich gar kein PoE-Menü + (`bad command name poe`) ⇒ es gibt **keinen** Remote-Power-Cycle, die Bestätigung ist wirklich physisch. +- 🧹 **Abbau (frühestens 7 Tage nach dem Flip):** Tunnel `bf-mm`/`bf-nas`/`bf-pve` löschen und die + **sechs** Container entfernen — LXC 400 (`cloudflared-bfmm`, `-bfnas`), macmini (`-bfpve`, `-bfnas`), + NAS ep14 (`-bfpve`, `-bfmm`); alle drei Hosts 08-30 verifiziert. **Erst Container, dann Tunnel** — + umgekehrt erzeugt man 530er. 🚫 **`terpene-cloudflared` (LXC 400) und `fm.emsenhuber.net` bleiben.** +- 🔴 **Ehrliche Einschränkung (aus TP31, gilt unverändert):** solange Caddy nur auf `caddy-bf1` liegt, + ist das Origin aller drei Connectoren **derselbe Rechner** — drei Connectoren kaufen + **Hardware-Diversität am Eingang, aber keine Verfügbarkeit**. Der echte Gewinn kommt mit `caddy-bf2` + (LXC 131); für `vw` gilt dabei die TP20.1-Bremse (Schreibzugriffe auf einen read-only Standby). +- **Rollback:** `docs/cf-backup/*-20260830-vor-tp16.3-schnitt.json` (vier Ingress-Configs + CNAMEs + inkl. Record-IDs), am 2026-08-30 **neu angelegt**. - ▶️ **Schnitt ausgeführt 2026-08-24:** `nautobot.emsenhuber.net` ist aus dem Ingress von `bf-nas` entfernt (**8 → 7 Regeln**, Catch-all weiterhin letzte). Rollback-Sicherung liegt unter `docs/cf-backup/bf-nas-ingress-20260824-vor-tp16.3.json` — Rückweg ist ein `PUT`, eine Minute. diff --git a/docs/tp16.3-bf-public-ap-connector-runbook.md b/docs/tp16.3-bf-public-ap-connector-runbook.md new file mode 100644 index 0000000..6672271 --- /dev/null +++ b/docs/tp16.3-bf-public-ap-connector-runbook.md @@ -0,0 +1,574 @@ +# TP16.3 / TP31 Schritt 5 — Runbook: `bf-public`-Connector auf ap1 / ap2 / **ap3**, CNAME-Flip, Abbau + +> **Stand 2026-08-30, live gelesen. Vorbereitet, NICHT ausgeführt.** +> **Löst [`tp31-schritt5-runbook.md`](tp31-schritt5-runbook.md) ab** (Stand 08-27). Jenes Dokument +> kennt ap3 nicht, unterstellt einen Registry-Pull (geht bei `fetch: no` nicht) und enthält **eine +> nachweislich falsche Freigabe-Behauptung** (§8 Falle 2). Es bleibt als Historie liegen, ist aber +> **nicht mehr auszuführen**. + +**Charakter:** Vor-Ort-Session. Der `device-mode`-Wechsel verlangt an **jedem** der drei APs eine +**physische** Bestätigung — ap1 + ap3 in **Technik UG**, ap2 in **Technik OG**. + +--- + +## §0 Ist-Stand (live gelesen 2026-08-30, ~20:45) + +### Die drei APs — **sie sind NICHT gleich** + +| AP | IP | Modell | Arch | ROS **live** | Flash frei | RAM frei | `device-mode` | `container` | `fetch` | +|---|---|---|---|---|---|---|---|---|---| +| **ap1** | 10.40.99.11 | hAP ax² (`C52iG`) | arm64 | **7.24** | 95 MB / 128 | 686 MB | `home` | ❌ `no` | ❌ `no` | +| **ap2** | 10.40.99.12 | hAP ax² (`C52iG`) | arm64 | **7.24** | 95 MB / 128 | 694 MB | `home` | ❌ `no` | ❌ `no` | +| **ap3** | 10.40.99.13 | **cAP ax** (`cAPGi`) | arm64 | **7.24.1** | 96 MB / 128 | 689 MB | **`basic`** | ❌ `no` | ✅ `yes` | + +🔴 **Zwei Unterschiede, die je einen eigenen Arbeitsschritt kosten:** +1. **ap3 läuft 7.24.1, ap1/ap2 laufen 7.24.** Ein `container.npk` **muss exakt zur RouterOS-Version + passen** — sonst bleibt es nach dem Reboot schlicht deaktiviert. ⇒ **zwei verschiedene Downloads**, + nicht einer. Nicht aus Bequemlichkeit dieselbe Datei auf alle drei schieben. +2. **ap3 steht auf `device-mode=basic`**, nicht `home` — dort sind `fetch`/`scheduler` bereits erlaubt. + Das ändert am `container`-Flag nichts (auch dort `no`, auch dort physische Bestätigung), macht ap3 + aber zum einzigen AP, der theoretisch aus der Registry ziehen könnte. **Wir nutzen das bewusst + nicht** — gleicher `.tar`-Weg auf allen dreien, damit alle drei dasselbe Image fahren. + +*(Auf ap3 lief zum Lesezeitpunkt 4 h Uptime — frisch in Betrieb. Uplink laut Registry `core ether21`.)* + +### Cloudflare + +| Tunnel | ID | Status | Connectoren | +|---|---|---|---| +| **bf-public** | `48d3b0b6-7233-4781-a76a-1de4a5e957ce` | **inactive** | **0** ← Ziel dieser Session | +| bf-mm | `7a322261-380a-447b-97d2-8ebce99f5ed1` | healthy | 8 | +| bf-nas | `220c9958-401b-4111-9451-f3db09bba2d5` | healthy | 8 | +| bf-pve | `f7fdf3a6-b67d-4567-93b1-b0f273794d09` | healthy | 8 | +| `fm.emsenhuber.net` | `b697d124-…` | healthy | 4 | 🚫 **Standort FM — nicht anfassen** | +| `terpene-db` | `b83b68f0-…` | healthy | 4 | 🚫 **anderes Projekt — nicht anfassen** | + +**Heutige Wege der vier öffentlichen Namen** (alle noch **an Caddy vorbei**, direkt aufs Backend): + +| Name | CNAME → Tunnel | Ingress-Ziel | +|---|---|---| +| `n8n` | bf-mm | `http://10.40.20.167:5678` | +| `gitea` | bf-nas | `http://10.40.20.157:3000` | +| `vw` | bf-nas | `http://10.40.20.127` | +| `hass` | bf-pve | `http://10.40.20.105:8123` | + +**`bf-public` hat bereits alle vier Ingress-Regeln** → `http://10.40.20.40:80` (Caddy) + Catch-all 404. +⚠️ Genau dieses `:80` ist Blocker A, siehe §1.1. + +### Caddy (`caddy-bf1`, LXC 130, `10.40.20.40`) — gemessen + +| Name | `:80` | `:443` (SNI korrekt) | +|---|---|---| +| `vw` / `hass` / `n8n` / `gitea` | **308 → `https://…`** | **200** | + +### Die sechs Container, die am Ende weg müssen — alle drei Hosts verifiziert + +| Host | Zugang | Container | +|---|---|---| +| LXC 400 (pve) | `ssh root@10.40.20.100` → `pct exec 400 -- docker` | `cloudflared-bfmm`, `cloudflared-bfnas` | +| macmini | `ssh macmini` | `cloudflared-bfpve`, `cloudflared-bfnas` | +| NAS DS923 | Portainer `endpointId=14` | `cloudflared-bfpve`, `cloudflared-bfmm` | + +🚫 **`terpene-cloudflared` auf LXC 400 bleibt** (Tunnel `terpene-db`, anderes Projekt). Steht im +`docker ps` **direkt neben** den beiden zu löschenden — beim Löschen den Namen lesen, nicht die Zeile zählen. + +### Rollback-Material (2026-08-30 **neu angelegt**, es gab es vorher nicht) + +``` +docs/cf-backup/bf-public-ingress-20260830-vor-tp16.3-schnitt.json +docs/cf-backup/bf-mm-ingress-20260830-vor-tp16.3-schnitt.json +docs/cf-backup/bf-nas-ingress-20260830-vor-tp16.3-schnitt.json +docs/cf-backup/bf-pve-ingress-20260830-vor-tp16.3-schnitt.json +docs/cf-backup/cnames-tunnel-20260830-vor-tp16.3-schnitt.json # inkl. Record-IDs für den Flip-Rollback +``` + +--- + +## §1 Drei Blocker — **remote zu erledigen, BEVOR jemand in den Keller geht** + +> Alle drei sind vom Schreibtisch aus lösbar. Wer sie offen lässt, steht vor Ort vor einem +> Connector, der `healthy` meldet, während die Seite eine Redirect-Schleife dreht. + +### 1.1 🔴 Blocker A — `bf-public` zeigt auf `:80`, und Caddy antwortet dort mit 308 + +Gemessen: `curl -H "Host: vw.emsenhuber.net" http://10.40.20.40/` → **`308` → `Location: https://vw.emsenhuber.net/`**. + +Der Weg wäre also: Browser → CF → Tunnel → Caddy `:80` → **308 zurück auf denselben öffentlichen +Namen** → CF → Tunnel → Caddy `:80` → … ⇒ **`ERR_TOO_MANY_REDIRECTS`**, für alle vier Namen. + +Das fällt heute niemandem auf, weil die drei Alttunnel **an Caddy vorbei** direkt aufs Backend gehen +(§0) — die Schleife entsteht **erst** mit dem Umstieg auf Caddy als Origin. + +**Fix:** Origin auf `https://10.40.20.40:443` mit **`originServerName` je Regel** (Caddy hält für alle +vier Namen gültige LE-Zertifikate ⇒ **kein** `noTLSVerify` nötig, die Kette bleibt geprüft): + +```bash +bash -c 'set -a; . ~/.secrets/credentials.env; set +a +TID=48d3b0b6-7233-4781-a76a-1de4a5e957ce +curl -s -X PUT -H "Authorization: Bearer $CF_API_TOKEN" -H "Content-Type: application/json" \ + "https://api.cloudflare.com/client/v4/accounts/$CF_ACCOUNT_ID/cfd_tunnel/$TID/configurations" \ + -d "{\"config\":{\"ingress\":[ + {\"hostname\":\"vw.emsenhuber.net\",\"service\":\"https://10.40.20.40:443\",\"originRequest\":{\"originServerName\":\"vw.emsenhuber.net\"}}, + {\"hostname\":\"hass.emsenhuber.net\",\"service\":\"https://10.40.20.40:443\",\"originRequest\":{\"originServerName\":\"hass.emsenhuber.net\"}}, + {\"hostname\":\"n8n.emsenhuber.net\",\"service\":\"https://10.40.20.40:443\",\"originRequest\":{\"originServerName\":\"n8n.emsenhuber.net\"}}, + {\"hostname\":\"gitea.emsenhuber.net\",\"service\":\"https://10.40.20.40:443\",\"originRequest\":{\"originServerName\":\"gitea.emsenhuber.net\"}}, + {\"service\":\"http_status:404\"}]}}" | jq -r ".success"' +``` + +**Abnahme:** `.success == true`, danach Ingress zurücklesen und die vier `https://…:443` sehen. + +### 1.2 🔴 Blocker B — die Firewall lässt MGMT → Caddy **nicht** durch + +Die `forward`-Kette des Core wurde vollständig gelesen (36 Regeln, Nr. 35 = `TP5: deny-by-default`). +**Es gibt keine Regel `vlan99 → vlan20` für 80/443.** Die einzige Regel in dieser Richtung ist + +``` +26 TP5-E2: MGMT->INFRA api-rueckweg + chain=forward accept protocol=tcp in-interface=vlan99 out-interface=vlan20 src-port=8728 +``` + +— ein **Rückweg** der RouterOS-API, `src-port`, nicht `dst-port`. Sie trägt den Connector nicht. + +🔴 **Der Vorgänger-Runbook behauptet in §0 wörtlich, „Regel 27 `MGMT→INFRA TCP` deckt `vlan99→vlan20:80` +bereits ab ✅". Das ist falsch** — Regel 27 ist `IOT->INFRA http-rueckweg` (`in-interface=vlan30`). +Ohne die Regel unten läuft der Connector in Regel 34 (`TP5-WOULD-DROP`, Log) und Regel 35 (Drop). + +**Fix** (vor die Log-Regel setzen, nicht ans Ende): + +``` +/ip firewall filter add chain=forward action=accept protocol=tcp \ + in-interface=vlan99 out-interface=vlan20 dst-address=10.40.20.40 dst-port=443 \ + comment="TP31: AP-cloudflared -> caddy-bf1" \ + place-before=[find comment~"TP5: log-only"] +``` + +Über MikroMCP: `plan_changes` mit `dryRun:true`, dann `apply_plan`. + +**Was NICHT gebraucht wird:** +- **Egress zu Cloudflare** (QUIC udp/7844): Regel 13 `TP5-E2: LAN->WAN` (`out-interface-list=WAN`, + ohne `in-interface`-Einschränkung) deckt das ab. ✅ +- **DNS für den Container**: Regeln 30–33 erlauben `LAN → 10.40.20.36/.37:53` aus **jedem** VLAN. ✅ + ⇒ im Container `dns=10.40.20.36` setzen. + +⚠️ **Damit die Regel greift, muss der Container-Verkehr auch wirklich als `vlan99` ankommen** — das +`veth`-Netz `172.17.0.0/24` ist dem Core unbekannt und hätte keinen Rückweg. Deshalb ist das +`masquerade` in §3.4 **nicht optional**, sondern die Voraussetzung dieser Regel. + +### 1.3 🟡 Blocker C — `ROUTER_AP3_PASS` fehlt in `~/.secrets/credentials.env` + +Vorhanden sind `ROUTER_AP1_PASS`, `ROUTER_AP2_PASS`, `CRS326_CORE_PASS`, `CRS326_BRANCH_PASS`. +Das **ap3-Passwort steht nur in `~/.claude.json`** (MCP-Env) — gegen die globale Konvention +(`~/.secrets` ist der Credential-Store) **und** ein Problem für §3.2: der Reboot-Weg liest +`SSHPASS` aus `~/.secrets`. + +```bash +# Wert aus ~/.claude.json übernehmen und ergänzen — Wert dabei NICHT auf stdout bringen: +printf 'ROUTER_AP3_USER=admin\nROUTER_AP3_PASS=%s\n' "$(python3 -c " +import json;print(json.load(open('$HOME/.claude.json'))['mcpServers']['mikromcp']['env']['ROUTER_AP3_PASS'])")" \ + >> ~/.secrets/credentials.env +chmod 600 ~/.secrets/credentials.env +grep -c '^ROUTER_AP3_PASS=' ~/.secrets/credentials.env # muss 1 sein +``` + +⚠️ **`~/.secrets/credentials.env` ist kein sauberes `KEY=VALUE`-File** — ab Zeile ~69 stehen rohe +WireGuard-Zeilen (`Endpoint:`, `AllowedIPs:`, `PersistentKeepalive:`). In **zsh** bricht +`source` daran ab und lässt die Variablen **leer**, ohne Fehler. Alle Snippets hier laufen deshalb +in `bash -c '…'`. **Wer das übersieht, jagt einen „ungültigen Token", der in Wahrheit ein leerer ist.** + +--- + +## §2 Vorbereitung am Schreibtisch (vor der Fahrt, mit Internet) + +### 2.1 Die **zwei** `container.npk` holen — Versionen nicht mischen + +```bash +mkdir -p ~/tp16.3 && cd ~/tp16.3 +# ap1 + ap2 (7.24) +curl -fLO https://download.mikrotik.com/routeros/7.24/all_packages-arm64-7.24.zip +unzip -o -j all_packages-arm64-7.24.zip 'container-7.24-arm64.npk' +# ap3 (7.24.1) +curl -fLO https://download.mikrotik.com/routeros/7.24.1/all_packages-arm64-7.24.1.zip +unzip -o -j all_packages-arm64-7.24.1.zip 'container-7.24.1-arm64.npk' +ls -la container-*.npk +``` +*(Beide URLs am 2026-08-30 mit `HTTP 200` verifiziert.)* + +### 2.2 cloudflared als `.tar` bauen — **Registry-Pull ist auf ap1/ap2 gesperrt** (`fetch: no`) + +Auf dem macmini (hat Docker, Image liegt schon lokal): + +```bash +ssh macmini 'docker pull --platform linux/arm64 \ + cloudflare/cloudflared@sha256:6d91c121b803126f7a5344005d17a9324788fc09d305b6e2560ec6040a7ae283 \ + && docker tag cloudflare/cloudflared@sha256:6d91c121b803126f7a5344005d17a9324788fc09d305b6e2560ec6040a7ae283 cfd:2026.6.1 \ + && docker save cfd:2026.6.1 -o /tmp/cloudflared-arm64.tar && ls -la /tmp/cloudflared-arm64.tar' +scp macmini:/tmp/cloudflared-arm64.tar ~/tp16.3/ +``` + +**Gepinnt auf `cloudflared 2026.6.1`** (Digest `sha256:6d91c121…`, arm64, Image 27,5 MB) — damit alle +drei APs nachweislich dasselbe fahren und ein späterer `:latest`-Sprung nichts still verschiebt. + +### 2.3 Tunnel-Token bereitlegen + +Der Token liegt bereits als `BF_PUBLIC_TUNNEL_TOKEN` in `~/.secrets/credentials.env`. Frisch ziehen +(z. B. nach Token-Rotation) — **Ausgabe nie in eine Kommandozeile oder ins Transkript**: + +```bash +bash -c 'set -a; . ~/.secrets/credentials.env; set +a +TID=48d3b0b6-7233-4781-a76a-1de4a5e957ce +T=$(curl -s -H "Authorization: Bearer $CF_API_TOKEN" \ + "https://api.cloudflare.com/client/v4/accounts/$CF_ACCOUNT_ID/cfd_tunnel/$TID/token" | jq -r .result) +echo "Token-Länge: ${#T}" # nur die Länge, nie der Wert +printf "%s" "$T" > ~/tp16.3/bf-public.token; chmod 600 ~/tp16.3/bf-public.token' +``` + +### 2.4 Backups je AP + +```bash +# je AP, über MikroMCP oder SSH: +/system backup save name=vor-tp16.3-ap1 # bzw. -ap2 / -ap3 +/export file=vor-tp16.3-ap1 +``` + +--- + +## §3 Vor Ort — **je AP einzeln, komplett durchziehen, dann der nächste** + +> **Reihenfolge: ap3 → ap2 → ap1.** +> ap3 zuerst, weil dort am wenigsten hängt (frisch in Betrieb, 4 h Uptime) — wenn der Ablauf +> irgendwo klemmt, klemmt er am unkritischsten Gerät. **ap1 zuletzt**, weil an ihm die +> `crs326-core`-Seite des WLAN hängt. +> +> ⚠️ **Jeder Schritt hier trennt kurzzeitig das WLAN dieses APs** (zwei Reboots). Die CAPs holen +> sich ihre Config nach dem Boot vom Manager (`crs326-core`) per **L2-Discovery auf `vlan99`** — +> das kommt von selbst zurück, braucht aber ~1–2 min. **Nicht ungeduldig nachprovisionieren.** + +### 3.1 `.npk` hochladen — **die richtige Datei zum richtigen AP** + +| AP | Datei | +|---|---| +| ap1 (`.99.11`) | `container-7.24-arm64.npk` | +| ap2 (`.99.12`) | `container-7.24-arm64.npk` | +| **ap3 (`.99.13`)** | **`container-7.24.1-arm64.npk`** | + +```bash +cd ~/tp16.3 +bash -c 'set -a; . ~/.secrets/credentials.env; set +a +SSHPASS="$ROUTER_AP3_PASS" sshpass -e scp -O container-7.24.1-arm64.npk admin@10.40.99.13:' +``` + +🔴 **Niemals `sshpass -p`** — das schreibt das Passwort in die Kommandozeile und damit in `ps` und +jeden argv-Trace. **Genau so ist am 2026-08-22 das Core-Passwort in ein Transkript geraten.** +`-e` hält es in der Umgebung des Kindprozesses. + +Gegenprobe vor dem Reboot — **die Datei muss wirklich liegen**, sonst rebootet man umsonst: +``` +/file print where name~"container" +``` + +### 3.2 Reboot (installiert das Paket) + +🔴 **Der Reboot geht NICHT über MikroMCP.** Das `reboot`-Tool sendet einen `delay`-Parameter, den die +REST-API auf ROS 7.24 nicht kennt (`unknown parameter delay`); `run_command` ist für `/system reboot*` +per Deny-Policy gesperrt. Der Weg ist SSH: + +```bash +bash -c 'set -a; . ~/.secrets/credentials.env; set +a +SSHPASS="$ROUTER_AP3_PASS" sshpass -e ssh -o StrictHostKeyChecking=accept-new \ + admin@10.40.99.13 "/system reboot"' +``` + +**Abnahme nach ~90 s:** +``` +/system package print +# muss jetzt DREI Pakete zeigen: routeros, wifi-qcom, container — alle gleiche Version +``` +⚠️ **Steht `container` da mit abweichender Version oder `disabled=yes`, war es die falsche `.npk`.** +Dann: Datei löschen, richtige hochladen, erneut rebooten. Nicht weitermachen. + +### 3.3 `device-mode` — **der physische Schritt** + +``` +/system device-mode update container=yes +``` + +Die Session **bricht danach ab** und RouterOS wartet auf eine Bestätigung **am Gerät** mit einer +Frist (typisch **5 Minuten**, die Konsole nennt die genaue Deadline). Bestätigt wird durch: + +**Weg A — Kaltstart (empfohlen, eindeutig):** +> **Stecker ziehen, ~5 Sekunden warten, wieder einstecken.** + +🔴 **Wichtig: die APs hängen NICHT an PoE aus den Switches** — beide CRS326 haben nachweislich gar +kein PoE-Menü (`bad command name poe`). Es gibt also **keinen** Weg, das aus der Ferne zu tun; der +Strom kommt vom jeweiligen Netzteil bzw. Injektor am AP. + +**Weg B — Taster:** +> **Reset-Knopf am laufenden Gerät kurz drücken und loslassen.** Am hAP ax² sitzt er an der +> Unterseite, am cAP ax unter der Abdeckung an der Rückseite. + +⚠️ **Weg B nur am LAUFENDEN Gerät und nur kurz.** Den Reset-Knopf **während des Bootens** gedrückt zu +halten ist etwas völlig anderes — das setzt die Konfiguration zurück bzw. geht nach Netinstall. +Wer unsicher ist, nimmt **Weg A**; ein Kaltstart kann nicht versehentlich zum Werksreset werden. + +Verstreicht die Frist ohne Bestätigung, passiert **nichts** — die Änderung verfällt, das Gerät bleibt +wie es war. Dann einfach neu absetzen. + +**Abnahme:** +``` +/system device-mode print +# container: yes +``` + +### 3.4 Container-Netz anlegen + +``` +/interface veth add name=veth-cf address=172.17.0.2/24 gateway=172.17.0.1 +/ip address add address=172.17.0.1/24 interface=veth-cf +/ip firewall nat add chain=srcnat action=masquerade src-address=172.17.0.0/24 \ + comment="TP31: cloudflared egress" +``` + +Das `masquerade` ist **Pflicht**, nicht Kosmetik — siehe §1.2: ohne es kommen die Pakete mit +`172.17.0.2` am Core an, wo weder Route noch Firewall-Regel sie kennt. + +### 3.5 Image importieren und Container bauen + +```bash +# .tar hochladen (~28 MB) +bash -c 'set -a; . ~/.secrets/credentials.env; set +a +SSHPASS="$ROUTER_AP3_PASS" sshpass -e scp -O ~/tp16.3/cloudflared-arm64.tar admin@10.40.99.13:' +``` + +Auf dem AP: +``` +/container config set tmpdir=tmp +/container add file=cloudflared-arm64.tar interface=veth-cf root-dir=cf \ + dns=10.40.20.36 logging=yes start-on-boot=yes \ + cmd="tunnel --no-autoupdate run --token=" +``` + +🔑 **Token-Handling:** den Wert aus `~/tp16.3/bf-public.token` einsetzen. Sauberer als ihn in die +`cmd` zu schreiben ist eine Env-Variable — dann steht er nicht im `/container print`: +``` +/container envs add name=cfd key=TUNNEL_TOKEN value="" +/container envs add name=cfd key=TUNNEL_TRANSPORT_PROTOCOL value=quic +# und im add: envlist=cfd cmd="tunnel --no-autoupdate run" +``` + +Starten und aufräumen: +``` +/container start [find root-dir=cf] +/container print # status: running +/file remove [find name~"cloudflared-arm64.tar"] # ~28 MB Flash zurückholen +``` + +⚠️ **Platzrechnung:** 95 MB frei − 28 MB `.tar` − ~28 MB entpackte Layer ≈ **39 MB Rest**, und das +nur bis der `.tar` gelöscht ist. Es geht aus, aber **ohne Reserve für einen zweiten Container** — +das ist die harte Grenze aus TP31, kein Richtwert. + +### 3.6 Abnahme **auf diesem AP** + +``` +/log print where message~"cloudflared" or message~"tunnel" +# erwartet: "Registered tunnel connection" (mehrfach, je CF-Edge) +``` + +Und **die Strecke zu Caddy separat prüfen** — sie ist der Teil, den Blocker B betraf: +``` +/tool fetch url="https://10.40.20.40/" check-certificate=no output=none +``` +*(Auf ap1/ap2 ist `fetch` per `device-mode=home` gesperrt — dort stattdessen aus dem Container heraus +oder über den Drop-Log gegenprüfen, siehe §8.)* + +**Der Drop-Log ist das erste Diagnosewerkzeug, nicht das letzte:** +`10.40.20.18:514`, Dateien auf dem Docker-LXC unter `/opt/tp5-syslog/log/`, Präfix `TP5-WOULD-DROP`. +Taucht dort `10.40.99.13 → 10.40.20.40:443` auf, fehlt die Regel aus §1.2. + +### 3.7 Erst wenn dieser AP sauber ist: nächster AP + +--- + +## §4 Abnahme des Tunnels (nach allen drei APs) + +```bash +bash -c 'set -a; . ~/.secrets/credentials.env; set +a +curl -s -H "Authorization: Bearer $CF_API_TOKEN" \ + "https://api.cloudflare.com/client/v4/accounts/$CF_ACCOUNT_ID/cfd_tunnel/48d3b0b6-7233-4781-a76a-1de4a5e957ce" \ + | jq "{status:.result.status, conns:(.result.connections|length)}"' +``` + +**Kriterium: `status: "healthy"`, `conns` > 0** (erfahrungsgemäß 4 CF-Edge-Verbindungen je Connector, +bei drei APs also Richtung 12). + +🔴 **`healthy` heißt „der Connector redet mit Cloudflare" — nicht „das Origin antwortet".** Der Beweis +für die Strecke bis Caddy ist §5.1, nicht diese Zahl. Genau diese Verwechslung hat beim +`grafana`-Fall monatelang einen toten Dienst gesund aussehen lassen. + +--- + +## §5 CNAME-Flip — einzeln, mit Abnahme dazwischen + +**Reihenfolge: `n8n` → `gitea` → `hass` → `vw`** (steigende Kritikalität; `vw` zuletzt). +Zone-ID `1b5bc02fdf61b6b989afde543f3f4134`. + +### 5.1 Vorab: Testname statt Produktivname + +```bash +bash -c 'set -a; . ~/.secrets/credentials.env; set +a +Z=1b5bc02fdf61b6b989afde543f3f4134 +curl -s -X POST -H "Authorization: Bearer $CF_API_TOKEN" -H "Content-Type: application/json" \ + "https://api.cloudflare.com/client/v4/zones/$Z/dns_records" \ + -d "{\"type\":\"CNAME\",\"name\":\"test-tp163\",\"content\":\"48d3b0b6-7233-4781-a76a-1de4a5e957ce.cfargotunnel.com\",\"proxied\":true}" | jq -r .success' +``` +Dazu **eine Ingress-Regel** für `test-tp163.emsenhuber.net` → `https://10.40.20.40:443` mit +`originServerName: vw.emsenhuber.net` in `bf-public` ergänzen. Dann **von außerhalb des LAN, ohne +WARP** aufrufen. Erwartet: Access-Login, **keine Redirect-Schleife**. +⇒ Erst wenn das steht, wird ein Produktivname angefasst. Danach Testname + Testregel wieder entfernen. + +### 5.2 Flip je Name + +```bash +bash -c 'set -a; . ~/.secrets/credentials.env; set +a +Z=1b5bc02fdf61b6b989afde543f3f4134 +N=n8n # danach: gitea, hass, vw +ID=$(curl -s -H "Authorization: Bearer $CF_API_TOKEN" \ + "https://api.cloudflare.com/client/v4/zones/$Z/dns_records?name=$N.emsenhuber.net" | jq -r ".result[0].id") +curl -s -X PATCH -H "Authorization: Bearer $CF_API_TOKEN" -H "Content-Type: application/json" \ + "https://api.cloudflare.com/client/v4/zones/$Z/dns_records/$ID" \ + -d "{\"content\":\"48d3b0b6-7233-4781-a76a-1de4a5e957ce.cfargotunnel.com\",\"proxied\":true}" | jq -r .success' +``` + +### 5.3 Abnahme je Name — **von außerhalb des LAN, WARP AUS** + +- [ ] Aufruf über Mobilfunk (oder anderer Standort) → Access-Login → **200** +- [ ] **Kein** `ERR_TOO_MANY_REDIRECTS` (das wäre Blocker A) +- [ ] Caddy sieht den Request: `ssh root@10.40.20.100 'pct exec 130 -- journalctl -u caddy -n 20'` +- [ ] 15 min beobachten, kein 530/1033 + +🔴 **WARP muss aus sein.** Mit aktivem WARP läuft der Test über den Mesh und beweist den öffentlichen +Weg **nicht** — dieselbe Klasse Fehldiagnose wie beim ICMP-Fall 2026-08-13. + +**Namensspezifisch:** +- **`gitea`** — Access gilt **nur der Web-UI**, Git-/API-Pfade laufen daran vorbei. **Vor** dem Flip + einen Push von außen testen, **nach** dem Flip denselben Push wiederholen. +- **`hass`** — die Companion-App kann kein Access-Login. Nach dem Flip App auf dem Handy **über + Mobilfunk** prüfen, nicht nur den Browser. +- **`vw`** — Bitwarden läuft auf **Access-Bypass**. **Vor** dem Flip prüfen, dass die Bypass-Policy + für `vw.emsenhuber.net` steht; sonst sperrt der Flip alle Bitwarden-Clients aus. + +--- + +## §6 Abbau — **frühestens nach 7 Tagen störungsfreiem Betrieb** + +> Die drei Alttunnel sind die Rückfallebene. Solange sie stehen, ist jeder Flip eine +> Ein-Feld-Änderung zurück. **Nicht am selben Abend abbauen.** + +### 6.1 Erst die Container stoppen, dann die Tunnel löschen + +Umgekehrt genommen läuft man in 530er von Connectoren, die auf einen gelöschten Tunnel zeigen. + +```bash +# LXC 400 auf pve — terpene-cloudflared NICHT anfassen +ssh root@10.40.20.100 'pct exec 400 -- docker rm -f cloudflared-bfmm cloudflared-bfnas' +ssh root@10.40.20.100 'pct exec 400 -- docker ps --format "{{.Names}}"' # terpene-cloudflared muss bleiben + +# macmini +ssh macmini 'docker rm -f cloudflared-bfpve cloudflared-bfnas && docker ps --format "{{.Names}}"' +``` + +NAS (DS923, Portainer `endpointId=14`, Key aus `mcp/portainer/.env`): +```bash +bash -c 'set -a; . ~/dev/bf-infra/mcp/portainer/.env; set +a +K="${PORTAINER_API_KEY:-$PORTAINER_TOKEN}"; U="${PORTAINER_URL:-https://10.40.20.126:9443}" +for c in cloudflared-bfpve cloudflared-bfmm; do + ID=$(curl -sk -H "X-API-Key: $K" "$U/api/endpoints/14/docker/containers/json?all=true" \ + | jq -r ".[]|select(.Names[0]==\"/$c\")|.Id") + curl -sk -X DELETE -H "X-API-Key: $K" "$U/api/endpoints/14/docker/containers/$ID?force=true" + echo "removed $c" +done' +``` +⚠️ Sind es **Stacks** statt einzelner Container, kommen sie sonst beim nächsten Stack-Deploy zurück — +dann den Stack-Eintrag entfernen, nicht nur den Container. + +### 6.2 Dann die drei Tunnel löschen + +```bash +bash -c 'set -a; . ~/.secrets/credentials.env; set +a +for T in 7a322261-380a-447b-97d2-8ebce99f5ed1 220c9958-401b-4111-9451-f3db09bba2d5 f7fdf3a6-b67d-4567-93b1-b0f273794d09; do + curl -s -X DELETE -H "Authorization: Bearer $CF_API_TOKEN" \ + "https://api.cloudflare.com/client/v4/accounts/$CF_ACCOUNT_ID/cfd_tunnel/$T" | jq -r "{id:\"$T\",ok:.success}" +done' +``` + +🚫 **Nicht in dieser Schleife und nirgends sonst:** `b697d124-…` (`fm.emsenhuber.net`, Standort FM) +und `b83b68f0-…` (`terpene-db`). **IDs vor dem Absetzen einzeln gegen §0 prüfen.** + +### 6.3 Schlusskontrolle + +```bash +bash -c 'set -a; . ~/.secrets/credentials.env; set +a +curl -s -H "Authorization: Bearer $CF_API_TOKEN" \ + "https://api.cloudflare.com/client/v4/accounts/$CF_ACCOUNT_ID/cfd_tunnel?is_deleted=false" \ + | jq -r ".result[]|\"\(.name)\t\(.status)\tconns=\(.connections|length)\""' +``` +**Soll:** `bf-public healthy`, `fm.emsenhuber.net healthy`, `terpene-db healthy` — sonst nichts. + +--- + +## §7 Rollback + +| Lage | Rückweg | +|---|---| +| Ein Name kaputt nach Flip | CNAME zurück auf die alte Tunnel-ID — Werte in `cnames-tunnel-20260830-…json` (inkl. Record-ID) | +| `bf-public`-Ingress kaputt | `PUT …/configurations` mit `bf-public-ingress-20260830-…json` | +| Container auf einem AP stört | `/container stop` + `/container remove`, WLAN läuft davon unabhängig | +| AP-Config verkorkst | `/system backup load name=vor-tp16.3-apX` | +| Firewall-Regel unerwünscht | `/ip firewall filter remove [find comment~"TP31: AP-cloudflared"]` | + +**`device-mode=container:yes` ist nicht Teil des Rollbacks.** Zurückdrehen ginge nur mit erneutem +physischem Gang. Es schadet nicht — ohne laufenden Container ist es ein gesetztes Flag, sonst nichts. + +--- + +## §8 Fallen + +1. **Zwei ROS-Versionen, zwei `.npk`.** ap1/ap2 = 7.24, ap3 = **7.24.1**. Falsche Datei ⇒ Paket bleibt + nach dem Reboot inaktiv. **Nach jedem Reboot `/system package print` lesen**, nicht annehmen. +2. **Die Freigabe `vlan99 → vlan20:443` existiert nicht** — entgegen der Behauptung im + Vorgänger-Runbook. Ohne §1.2 landet der Connector im `TP5-WOULD-DROP`-Log. **Erst dort nachsehen.** +3. **Caddy antwortet auf `:80` mit 308.** Ein Ingress auf `:80` erzeugt eine Redirect-Schleife — + und zwar **erst** beim Umstieg auf Caddy, weil die Altwege am Proxy vorbeigehen. +4. **`healthy` ≠ „Dienst erreichbar".** Der Tunnelstatus sagt nichts über das Origin; die + Wildcard-Access-App `*.emsenhuber.net` liefert für **jeden** Namen ein sauberes 302. Ein toter + Dienst sieht so monatelang gesund aus (der `grafana`-Fall). +5. **Kein `sshpass -p`.** Nur `-e` mit `SSHPASS` aus der Umgebung. +6. **`~/.secrets/credentials.env` in zsh sourcen schlägt still fehl** (rohe WireGuard-Zeilen ab ~Z. 69) + — Variablen bleiben **leer**, die API meldet einen „ungültigen Token". Immer `bash -c`. +7. **`terpene-cloudflared` steht direkt neben den zu löschenden Containern.** Namen lesen. +8. **Ein Container pro AP, keiner mehr.** Nach dem Import bleiben ~39 MB. Caddy oder AGH gehören + nicht dazu (die eine Config ändert sich ständig, die andere schreibt dauernd auf NAND). +9. **Reboot nie über MikroMCP** (`unknown parameter delay`), sondern per SSH. +10. **WLAN-Abnahme ist eine frische DHCP-Lease je SSID, nicht die Registration-Table.** Nach zwei + Reboots je AP lohnt der Blick — die Tabelle zeigte beim CAPsMAN-Ausfall 14 h lang zufriedene + Clients ohne Adresse. + +--- + +## §9 Ehrliche Einschränkung — was diese Session **nicht** kauft + +Solange Caddy **nur** auf `caddy-bf1` (LXC 130, pve) läuft, ist das Origin aller drei Connectoren +**derselbe Rechner**. Drei Connectoren auf drei APs kaufen damit **Hardware-Diversität am Eingang, +aber keine Verfügbarkeit**: fällt pve aus, sind alle vier öffentlichen Dienste weg — mit drei +Connectoren genauso vollständig wie mit einem. + +**Der Gewinn ist real, aber begrenzt:** kein Docker-Host mehr im öffentlichen Pfad (die APs sind +eigene Maschinen ⇒ die macvlan/ipvlan-Blindheit kann dort prinzipiell nicht auftreten), sechs +Container werden drei, und die Host→Origin-Zuordnung lebt nur noch im `caddy/Caddyfile`. + +**Der echte Verfügbarkeitsgewinn kommt erst mit `caddy-bf2` (LXC 131)** und einer zweiten +Ingress-Regel. ⚠️ Für `vw` gilt dabei die TP20.1-Bremse: zwei Caddys an verschiedenen Standorten +würden die Hälfte der **Schreib**zugriffe auf einen read-only Standby schicken — das #1033-Muster, +diesmal mit Datenverlust. **Automatisches Ingress-Failover erst, wenn der DB-Teil es trägt.** + +Bis dahin ist die Reserve die manuelle: derselbe Connector kann über eine zusätzliche Ingress-Regel +**direkt** auf ein kritisches Backend zeigen, an Caddy vorbei. diff --git a/docs/tp31-schritt5-runbook.md b/docs/tp31-schritt5-runbook.md index 440830e..9f88b62 100644 --- a/docs/tp31-schritt5-runbook.md +++ b/docs/tp31-schritt5-runbook.md @@ -5,6 +5,22 @@ # Mode-Button-Druck an jedem AP, danach Reboot. Dieser Runbook-Teil muss # vor Ort ausgeführt werden. +> 🔴 **ABGELÖST 2026-08-30 — NICHT MEHR AUSFÜHREN.** +> Nachfolger: [`tp16.3-bf-public-ap-connector-runbook.md`](tp16.3-bf-public-ap-connector-runbook.md). +> Dieses Dokument bleibt als Historie stehen. Drei Gründe, warum es nicht mehr trägt: +> 1. **Es kennt `ap3` nicht** (cAP ax, `10.40.99.13`, seit 2026-08-30 in Betrieb) — und ap3 läuft +> **ROS 7.24.1** gegen 7.24 auf ap1/ap2, braucht also ein **anderes `container.npk`**. +> 2. **§2b unterstellt einen Registry-Pull.** Auf ap1/ap2 ist `fetch: no` (`device-mode=home`) — +> das Image muss als `.tar` hoch und per `/container add file=` importiert werden. +> 3. 🔴 **§0 behauptet, „Regel 27 `MGMT→INFRA TCP` deckt `vlan99→vlan20:80` bereits ab ✅".** +> **Das ist falsch.** Die `forward`-Kette des Core wurde am 2026-08-30 vollständig gelesen: +> Regel 27 ist `IOT->INFRA http-rueckweg` (`in-interface=vlan30`), und die einzige MGMT→INFRA-Regel +> (Nr. 26) ist ein `src-port=8728`-Rückweg der API. **Eine Freigabe für den Connector existiert +> nicht** — ohne sie landet er in `TP5-WOULD-DROP` + Drop. +> +> Ebenfalls im Nachfolger, hier noch nicht enthalten: der **Redirect-Loop** über `:80` +> (Caddy antwortet dort mit 308) und der Abbau der sechs Container. + ## 0. IST-STAND (live gelesen 2026-08-27) - Tunnel **bf-public** `48d3b0b6` ✅: Ingress mit 4 öffentlichen Hostnames → `10.40.20.40:80` - Token: `~/.secrets/credentials.env` → `BF_PUBLIC_TUNNEL_TOKEN`