Die 7-Tage-Frist wurde auf User-Entscheid aufgehoben ("es wird nichts mehr
sein"). Reihenfolge Container -> Tunnel eingehalten (umgekehrt gaebe es 530er).
Vorbedingung geprueft: alle vier Namen auf bf-public, kein Name mehr an den
Alttunneln -> Abbau gefahrlos.
7 Container entfernt, per exakter Namensliste (nicht per Filter):
LXC 400 (pve): cloudflared-bfmm, cloudflared-bfnas
macmini: cloudflared-bfpve, cloudflared-bfnas
NAS ep14: cloudflared-bfpve, cloudflared-bfmm
plus terpene-cloudflared -- auf zusaetzlichen User-Entscheid, weil
420.teilzeitladen.xyz jetzt ueber Caddy/CF laeuft. Kein Stack dahinter, also
kein Wiederauferstehen.
Beweis VOR der Tunnel-Loeschung: 420.teilzeitladen.xyz lieferte mit bereits
entferntem terpene-cloudflared echten Origin-Inhalt (assetlinks-JSON, 200) --
der Dienst haengt also nicht mehr am Tunnel.
4 Tunnel geloescht: bf-mm, bf-nas, bf-pve, terpene-db -- jede ID gegen die
Behalten-Liste (bf-public, fm) geprueft.
Schlusskontrolle: nur noch bf-public (12) + fm (4) healthy; alle vier BF-Namen
und 420 weiterhin 200.
Der terpene-db-Tunnel-Token stand kurz im docker-inspect-Output; mit der
Tunnel-Loeschung ungueltig, kein Rotationsbedarf.
TP16.3 damit vollstaendig abgeschlossen (Status ✅ 2026-09-01). Quick-Index,
Abschnittsheader, §6-Record, Ist-Stand, Runbook §6 und Handoff nachgezogen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
39 KiB
TP16.3 / TP31 Schritt 5 — Runbook: bf-public-Connector auf ap1 / ap2 / ap3, CNAME-Flip, Abbau
§1–§3 AUSGEFÜHRT 2026-08-31. Drei Connectoren laufen,
bf-publichealthy/12. §2.2, §3.4 und §3.5 sind unten korrigiert — die ursprüngliche Fassung war an drei Stellen falsch (Flash-Budget,docker save-Format,envs-Parametername). ✅ §5 (Flip) AUSGEFÜHRT 2026-09-01 — alle vier Namen aufbf-public. Offen bleibt nur §6 (Abbau). Lösttp31-schritt5-runbook.mdab (Stand 08-27). Jenes Dokument kennt ap3 nicht, unterstellt einen Registry-Pull (geht beifetch: nonicht) 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.1 | 95 MB / 128 | 712 MB | home |
❌ no |
❌ no |
| ap2 | 10.40.99.12 | hAP ax² (C52iG) |
arm64 | 7.24.1 | 95 MB / 128 | 699 MB | home |
❌ no |
❌ no |
| ap3 | 10.40.99.13 | cAP ax (cAPGi) |
arm64 | 7.24.1 | 96 MB / 128 | 689 MB | basic |
❌ no |
✅ yes |
(Versionsspalte am 2026-08-31 nachgelesen: Christoph hat alle fünf MikroTik auf 7.24.1 gehoben, RouterBOOT mitgezogen. Bis 08-30 liefen ap1/ap2 auf 7.24 — daher stand hier vorher eine Zwei-Dateien-Regel.)
✅ Seit dem Upgrade auf 7.24.1 (2026-08-31) reicht EINE .npk für alle drei APs — alle sind arm64
und alle auf derselben Version. Die frühere Zwei-Dateien-Regel war rein versionsbedingt, nicht
modellbedingt: cAP ax und hAP ax² teilen sich die arm64-Pakete.
🔴 Die Regel dahinter gilt weiter: ein container.npk muss exakt zur RouterOS-Version passen —
sonst bleibt es nach dem Reboot deaktiviert. Wer künftig einen AP einzeln hebt, hat wieder zwei Dateien.
🟡 Ein Unterschied bleibt:
- ap3 steht auf
device-mode=basic, nichthome— dort sindfetch/schedulerbereits erlaubt. Das ändert amcontainer-Flag nichts (auch dortno, 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 |
terpene-db |
b83b68f0-… |
healthy | 4 |
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 — ✅ ALLE ERLEDIGT 2026-08-31 (remote)
✅ Nichts davon ist vor Ort noch zu tun. Abschnitt bleibt als Nachweis stehen; die Fixes sind gemessen, nicht nur abgesetzt. Vor Ort startet die Arbeit bei §2/§3.
Blocker Stand Nachweis A Redirect-Schleife ( :80)✅ behoben Ingress zeigt auf https://10.40.20.40:443,originServerNameje Regel gesetztB Freigabe vlan99 → vlan20:443✅ behoben Regel via Nautobot GC deployt, Plan Completed, am Gerät verifiziert, 135/135 compliant, 0 DriftsC ROUTER_AP3_PASS✅ behoben in ~/.secrets/credentials.env(chmod 600), REST-Auth gegen ap3 = HTTP 200
1.1 ✅ Blocker A — bf-public zeigte auf :80, und Caddy antwortet dort mit 308
Erledigt 2026-08-31. Ingress zurückgelesen: alle vier Namen →
https://10.40.20.40:443,SNI= der jeweilige Hostname. DerPUTunten ist der ausgeführte Stand, nicht mehr eine Aufgabe.
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 -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 liess MGMT → Caddy nicht durch
Erledigt 2026-08-31 über Nautobot Golden Config (
gc-manual-plan.py --device crs326-core --deploy, Change-IDTP16.3-BlockerB, Plan23e5b667-…Completed), nicht per Direkt-Write —ip-firewall-filtersteht unter Drift-Kontrolle (TP26). Backup vorher:core-pre-tp16.3-blockerB-20260831.backup(TP22-Regel 3). Danachingest-core-firewall.py --apply(Re-Baseline) +--refresh-compliance⇒ 135/135, 0 Drifts. Position verifiziert: die Regel sitzt direkt vorTP5: log-onlyundTP5: deny-by-default. Funktional verifiziert:/tool fetchvon ap3 aufhttps://10.40.20.40erreicht Caddy und bekommt einen TLS-Alert von Caddy selbst — ein Alert kann nur vom Server kommen, das Paket ist also da. Der Alert ist die SNI-Abweisung (Gegenprobe: ohne SNI000, mit korrektem SNI200) und genau der Grund, warumoriginServerNamein §1.1 gesetzt ist. Kein Fehler, sondern der Beweis.
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, ohnein-interface-Einschränkung) deckt das ab. ✅ - DNS für den Container: Regeln 30–33 erlauben
LAN → 10.40.20.36/.37:53aus jedem VLAN. ✅ ⇒ im Containerdns=10.40.20.36setzen.
⚠️ 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 fehlte in ~/.secrets/credentials.env
Erledigt 2026-08-31.
ROUTER_AP3_USER/_PASSnachgetragen, Dateichmod 600, Sicherung~/.secrets/credentials.env.bak-20260831. Funktionsbeweis: REST-Auth gegen ap3 = HTTP 200.
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.
# 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 container.npk holen — eine Datei für alle drei APs
mkdir -p ~/tp16.3 && cd ~/tp16.3
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-7.24.1-arm64.npk
(URL am 2026-08-30 mit HTTP 200 verifiziert.)
⚠️ Vor dem Upload gegenprüfen, dass das Gerät wirklich auf 7.24.1 steht (/system resource print).
Stimmt die Version nicht, ist die Datei die falsche — und das merkt man erst nach dem Reboot.
2.2 cloudflared als .tar bauen — 🔴 KORRIGIERT 2026-08-31, die alte Fassung funktioniert nicht
Zwei Gründe, warum docker save des offiziellen Images hier scheitert:
- Es passt nicht ins Flash. Entpackt misst
cloudflare/cloudflared:2026.6.172.926.208 B = 69,5 MiB (docker export | wc -c), nicht die früher hier genannten 28 MB — das ist die komprimierte Archivgrösse. RouterOS braucht beim Import Tar und entpacktes Abbild gleichzeitig: 26,2 + 69,5 = 95,7 MiB gegen ~90 MiB frei. ⚠️docker image inspect .Sizelügt hier (meldet 27.463.950);docker historysummiert korrekt auf ~70 MB. - Das Format stimmt nicht. Seit Docker 25 (containerd-Image-Store) schreibt
docker saveein OCI-Layout (blobs/sha256/, gzip-Layer). RouterOS will docker-v1 und meldetno config found in manifest— klingt nach kaputtem Image, ist aber das Format.
Stattdessen: Minimal-Image aus dem Binary bauen. Das Binary im offiziellen Image ist statisch gelinkt und 36,8 MB gross; die restlichen ~33 MB sind distroless-Debian, das cloudflared nicht braucht.
# Binary + CA-Bundle aus dem gepinnten Upstream-Image ziehen
ssh macmini 'docker pull --platform linux/arm64 \
cloudflare/cloudflared@sha256:6d91c121b803126f7a5344005d17a9324788fc09d305b6e2560ec6040a7ae283 \
&& docker tag cloudflare/cloudflared@sha256:6d91c121b803126f7a5344005d17a9324788fc09d305b6e2560ec6040a7ae283 cfd:2026.6.1'
ssh macmini 'C=$(docker create cfd:2026.6.1); \
docker cp "$C":/usr/local/bin/cloudflared /tmp/cfd-bin; \
docker cp "$C":/etc/ssl/certs/ca-certificates.crt /tmp/cfd-ca.crt; docker rm -f "$C"'
scp macmini:/tmp/cfd-bin ~/tp16.3/cloudflared-linux-arm64
scp macmini:/tmp/cfd-ca.crt ~/tp16.3/ca-certificates.crt
# docker-v1-Archiv direkt bauen (kein Docker-Umweg -> Format haengt nicht am Image-Store)
python3 tools/build-cfd-image.py ~/tp16.3/cloudflared-linux-arm64 \
~/tp16.3/ca-certificates.crt ~/tp16.3/cfd-min-arm64.tar
Ergebnis: 35,3 MiB Archiv, 35,3 MiB entpackt, Spitzenbedarf 70,6 MiB — passt mit ~20 MiB Reserve, nach dem Löschen des Tars bleiben 54–55 MiB frei.
🔑 Der Pin ist die Binary-SHA256 e93824f677eb2f072b0a42b7ad8b57e1ed76c6aed51b13ed805a6064a82d424e.
cloudflared gibt sie beim Start selbst aus (Version 2026.6.1 (Checksum e93824f6…)) — damit ist im
Log nachweisbar, dass alle drei APs dasselbe Binary fahren.
⚠️ Gegenprobe vor dem Ausrollen: docker load -i + docker run --rm <tag> version auf dem macmini.
Ein Formatfehler fällt dort in Sekunden auf, auf dem AP erst nach einem 35-MiB-Upload.
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 -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
# 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 aufvlan99— das kommt von selbst zurück, braucht aber ~1–2 min. Nicht ungeduldig nachprovisionieren.
3.1 .npk hochladen
| AP | Datei |
|---|---|
ap1 (.99.11) |
container-7.24.1-arm64.npk |
ap2 (.99.12) |
container-7.24.1-arm64.npk |
ap3 (.99.13) |
container-7.24.1-arm64.npk |
✅ Seit 2026-08-31 dieselbe Datei überall (alle drei arm64 + 7.24.1).
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 -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
🔴 Reihenfolge ist Pflicht: erst der Paket-Reboot (§3.2), dann device-mode.
Der Bestätigungs-Boot installiert keine Pakete — er verbraucht eine wartende .npk, ohne sie zu
installieren. Am 2026-08-31 wurde auf ap1 versucht, beides in einem Stromzyklus zu erledigen (um einen
Gang zu sparen): danach stand container: yes, die Datei war weg und /system package print zeigte
zwei Pakete. Nachholen ging per SSH-Reboot, aber die Abkürzung spart nichts.
/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.
✅ Seit 2026-08-31 stehen diese drei Zeilen im SOLL (AP-Template Block [7], Werte aus dem
ConfigContext ap-common → ap.container). Wer sie hier direkt setzt, erzeugt daher keine
Drift mehr — aber auch keinen Bedarf: bei einem neuen AP rendert Golden Config sie mit.
3.5 Image importieren und Container bauen
# .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>"
🔑 Token-Handling. 🔴 Die Eigenschaft heisst auf ROS 7.24.1 list=, NICHT name= — mit
name= antwortet das Gerät bad parameter name bzw. die REST-API unknown parameter name.
(Das MikroMCP-Tool manage_container_env benutzt name= und ist damit hier unbrauchbar.)
Der Token darf weder in eine Kommandozeile (→ ps) noch ins Sitzungstranskript. Deshalb per Datei:
T=$(cat ~/tp16.3/bf-public.token)
TMP=$(mktemp /tmp/cfdenv.XXXXXX.rsc); chmod 600 "$TMP"
{ printf '/container/envs/add list=cfd key=TUNNEL_TOKEN value="%s"\n' "$T"
printf '/container/envs/add list=cfd key=TUNNEL_TRANSPORT_PROTOCOL value="quic"\n'; } > "$TMP"
SSHPASS="$PW" sshpass -e scp -O "$TMP" admin@<ip>:cfdenv.rsc && rm -f "$TMP"
Auf dem AP: /import file=cfdenv.rsc, danach /file/remove [find name="cfdenv.rsc"] — die Datei
enthält den Token im Klartext. Gegenprobe ohne Wert-Ausgabe:
:foreach e in=[/container/envs/find] do={:put ([/container/envs/get $e list] . " / " . \
[/container/envs/get $e key] . " / len=" . [:len [/container/envs/get $e value]])}
⚠️ Niemals /container/envs/print — das gibt den Token aus.
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 (korrigiert 2026-08-31, gemessen): ~90 MiB frei − 35,3 MiB .tar
− 35,3 MiB entpacktes Abbild ≈ 20 MiB in der Spitze, nach dem Löschen des Tars 54–55 MiB frei.
🔴 Mit dem offiziellen Image wären es 95,7 MiB Bedarf gegen 90 MiB frei — es passt schlicht nicht.
Auch mit dem Minimal-Image gilt weiter: ein Container pro AP, kein zweiter.
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 -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 — ✅ AUSGEFÜHRT 2026-09-01 (einzeln, mit Abnahme dazwischen)
Reihenfolge: n8n → gitea → hass → vw (steigende Kritikalität; vw zuletzt).
Zone-ID 1b5bc02fdf61b6b989afde543f3f4134.
5.1 Vorab: Testname statt Produktivname
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.
🔑 Das Erfolgskriterium ist praeziser, als es hier bis 2026-08-31 stand — und die frühere Fassung
war zweideutig. Gemessen am 2026-08-31 gegen 10.40.20.40:
| Antwort | Bedeutung |
|---|---|
Access-Login, danach 200 mit LEEREM Body und Header server: Caddy |
✅ Strecke bewiesen. Caddy hat geantwortet; es gibt für test-tp163 nur kein Site-Block, und genau so antwortet Caddy auf einen unbekannten Host. |
404 |
❌ Die Ingress-Regel greift nicht — das ist der Catch-all von bf-public (http_status:404). |
ERR_TOO_MANY_REDIRECTS |
❌ Blocker A ist zurück (Ingress auf :80 oder originServerName fehlt). Sofort abbrechen. |
⚠️ Ohne diese Tabelle liest man das leere 200 als „kaputt" und das 404 als „Caddy sagt nichts" — beide Male falsch herum. Der zweite, unabhängige Beweis ist immer das Caddy-Log (§5.3).
⇒ Erst wenn das steht, wird ein Produktivname angefasst. Danach Testname + Testregel wieder entfernen.
5.2 Flip je Name
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) - 15 min beobachten, kein 530/1033
🔴 KORRIGIERT 2026-09-01 — der frühere Punkt „Caddy sieht den Request:
journalctl -u caddy -n 20" funktioniert NICHT. Im Caddyfile steht keine log-Direktive; im
Journal landen nur info/warn. Ein erfolgreicher Request hinterlässt dort nichts, und die
leere Ausgabe liest sich wie „kam nie an". (Dass der öffentliche Reverse Proxy überhaupt keine
Requests protokolliert, ist eine eigene Lücke — gehört zu TP31/TP34.)
Was stattdessen funktioniert — und 2026-09-01 auch benutzt wurde:
| Werkzeug | Beweist |
|---|---|
curl --resolve <name>:443:188.114.96.10 |
Erzwingt den öffentlichen Weg. 🔴 Pflicht, denn der Haus-Resolver liefert für alle vier Namen direkt 10.40.20.40 (Split-Horizon) — ein Test von zuhause sieht Cloudflare nie. |
Verbindungstabelle am Core (/ip firewall connection) |
Zeigt 10.40.99.x → 10.40.20.40 = ein AP-Connector redet mit Caddy. Der Zähler der Blocker-B-Regel taugt dafür NICHT: cloudflared hält die Origin-Verbindung offen, es entstehen kaum neue — der Zähler steht still, obwohl alles läuft. |
| Antwort-Fingerabdruck | z.B. i_like_gitea-Cookie = echte Gitea-Antwort, nicht CF/Caddy. |
WebSocket-Handshake mit --http1.1 |
101 Switching Protocols = der Companion-App-Pfad steht. ⚠️ Über HTTP/2 gibt es keinen klassischen Upgrade — ein 400 ist dann der eigene Testfehler, nicht der Dienst. |
⚠️ Access antwortet je nach Standort verschieden, beides gesund: n8n liefert aus dem Haus
200 (die bf4-Policy trägt die WAN-IP), von außen 302 zum Login. hass/vw/gitea stehen
auf bypass (App hass, vw, gitea, alle drei in self_hosted_domains) — dort kommt nie ein
Login. Ein Service-Token für die Wildcard-App gibt es nicht (TP14.4), deshalb ist n8n von außen
nicht bis zum Origin prüfbar — das musste der User im Browser bestätigen.
🔴 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ürvw.emsenhuber.netsteht; sonst sperrt der Flip alle Bitwarden-Clients aus.
§6 Abbau — ✅ AUSGEFÜHRT 2026-09-01 (7-Tage-Frist vom User aufgehoben)
✅ Durch. 7 Container entfernt (inkl.
terpene-cloudflared, weil420.teilzeitladen.xyznun über Caddy/CF läuft — vor der Tunnel-Löschung mit echtem Origin-Inhalt belegt), 4 Tunnel gelöscht (bf-mm/bf-nas/bf-pve/terpene-db). Übrig: nurbf-public+fm. Reihenfolge Container→Tunnel eingehalten. Der Rest dieses Abschnitts bleibt als Referenz stehen.
(Original-Anleitung, Referenz)
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.
# 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 -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 -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 -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
container.npkist versionsgebunden. Seit 08-31 sind alle drei APs auf 7.24.1 ⇒ eine Datei. Weicht ein Gerät später ab, braucht es wieder seine eigene. Falsche Datei ⇒ Paket bleibt nach dem Reboot inaktiv, ohne Fehler. Nach jedem Reboot/system package printlesen, nicht annehmen.- Die Freigabe
vlan99 → vlan20:443existiert nicht — entgegen der Behauptung im Vorgänger-Runbook. Ohne §1.2 landet der Connector imTP5-WOULD-DROP-Log. Erst dort nachsehen. - Caddy antwortet auf
:80mit 308. Ein Ingress auf:80erzeugt eine Redirect-Schleife — und zwar erst beim Umstieg auf Caddy, weil die Altwege am Proxy vorbeigehen. healthy≠ „Dienst erreichbar". Der Tunnelstatus sagt nichts über das Origin; die Wildcard-Access-App*.emsenhuber.netliefert für jeden Namen ein sauberes 302. Ein toter Dienst sieht so monatelang gesund aus (dergrafana-Fall).- Kein
sshpass -p. Nur-emitSSHPASSaus der Umgebung. ~/.secrets/credentials.envin zsh sourcen schlägt still fehl (rohe WireGuard-Zeilen ab ~Z. 69) — Variablen bleiben leer, die API meldet einen „ungültigen Token". Immerbash -c.terpene-cloudflaredsteht direkt neben den zu löschenden Containern. Namen lesen.- 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).
- Reboot nie über MikroMCP (
unknown parameter delay), sondern per SSH. - WLAN-Abnahme ist eine frische DHCP-Lease je SSID, nicht die Registration-Table.
⚠️ Ergänzung 08-31:
media/untrusted/guesthaben im Normalbetrieb null Clients — dort ist gar kein Lease-Nachweis zu holen. Das ist kein Fehler, aber auch kein Beleg; nicht als „grün" buchen. - 🔴 Das offizielle
cloudflared-Image passt nicht ins Flash (69,5 MiB entpackt gegen ~90 MiB frei, Tar kommt obendrauf). Minimal-Image bauen, s. §2.2. Flash-Budgets mitdocker exportmessen, nie mitdocker image inspect .Size. - 🔴
docker saveliefert seit Docker 25 OCI statt docker-v1 →no config found in manifest. - 🔴
/container/envsnimmtlist=, nichtname=. - 🔴 Der
device-mode-Bestätigungs-Boot installiert keine Pakete — Reihenfolge s. §3.3. - ✅ ERLEDIGT 2026-08-31 — §3.4 kollidierte mit der GC-Governance:
ip addressundip firewall natstehen unter Drift-Kontrolle, direkt gesetzt ergab das zweiextra-Drifts je AP. Sie sind jetzt ins Soll gehoben (AP-Template Block[7]+ ConfigContextap.container), nicht als Transaktion geführt: ein Dauerzustand gehört ins Template. Dazu kam die 28. ComplianceRuleinterface-veth— vorher war das veth das tragende Stück ohne jede Überwachung. 🔴 Falle darin: der veth-Export trägt zwei gerätevergebene MACs; ohne Normalisierung (ConfigReplace60-/61-veth-*) erzeugt die Regel Dauer-Drift auf allen drei APs. Stand danach: 139/140, übrig nur die offene TP25-Transaktion. - 🔴 Ein Golden-Config-Job meldet
SUCCESSund kann trotzdem halb durchgelaufen sein. Am 2026-08-31 fehlte nach dem ersten Compliance-Lauf genau ein Eintrag (crs326-core/interface-veth), Job-StatusSUCCESS, im Job-Log dagegenRENDER COMPLIANCE TASK GROUP failed: Broken pipe. Abnahme ist deshalb nicht die Konformitätsquote, sondern die ANZAHL der Einträge = Regeln × Geräte. 139 statt 140 sieht aus wie „die neue Regel greift nicht" und ist ein halb gelaufener Job. Zweiter Lauf reichte. - 🟡
A - AVAILABLEin/system package printist ein Download-Katalog, kein Paket auf dem Gerät —enablewürde einen Internet-Fetch auslösen (auf ap1/ap2 gesperrt)..npkbleibt der Weg. - 🟡 MikroMCPs
export_configexportiert unvollständig (301 B statt 6,2 KiB). Für Backups/export file=…per SSH verwenden. 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 Schreibzugriffe 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.