# 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.