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>
728 lines
39 KiB
Markdown
728 lines
39 KiB
Markdown
# 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-public` healthy/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 auf `bf-public`.
|
||
> Offen bleibt nur **§6 (Abbau)**.
|
||
> **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.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`**, 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 — ✅ **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`, `originServerName` je Regel gesetzt |
|
||
> | **B** Freigabe `vlan99 → vlan20:443` | ✅ behoben | Regel via Nautobot GC deployt, Plan `Completed`, am Gerät verifiziert, **135/135 compliant, 0 Drifts** |
|
||
> | **C** `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. Der `PUT` unten 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
|
||
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-ID `TP16.3-BlockerB`, Plan `23e5b667-…` **Completed**), **nicht** per Direkt-Write — `ip-firewall-filter`
|
||
> steht unter Drift-Kontrolle (TP26). Backup vorher: `core-pre-tp16.3-blockerB-20260831.backup` (TP22-Regel 3).
|
||
> Danach `ingest-core-firewall.py --apply` (Re-Baseline) + `--refresh-compliance` ⇒ **135/135, 0 Drifts.**
|
||
> **Position verifiziert:** die Regel sitzt direkt **vor** `TP5: log-only` und `TP5: deny-by-default`.
|
||
> **Funktional verifiziert:** `/tool fetch` von ap3 auf `https://10.40.20.40` erreicht 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 SNI `000`, mit korrektem SNI `200`) und genau
|
||
> der Grund, warum `originServerName` in §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`,
|
||
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` fehlte in `~/.secrets/credentials.env`
|
||
|
||
> **Erledigt 2026-08-31.** `ROUTER_AP3_USER`/`_PASS` nachgetragen, Datei `chmod 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`.
|
||
|
||
```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 `container.npk` holen — **eine Datei für alle drei APs**
|
||
|
||
```bash
|
||
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:
|
||
|
||
1. **Es passt nicht ins Flash.** Entpackt misst `cloudflare/cloudflared:2026.6.1`
|
||
**72.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 .Size`
|
||
lügt hier** (meldet 27.463.950); `docker history` summiert korrekt auf ~70 MB.
|
||
2. **Das Format stimmt nicht.** Seit Docker 25 (containerd-Image-Store) schreibt `docker save` ein
|
||
**OCI-Layout** (`blobs/sha256/`, gzip-Layer). RouterOS will docker-v1 und meldet
|
||
**`no 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.
|
||
|
||
```bash
|
||
# 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
|
||
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
|
||
|
||
| 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).
|
||
|
||
```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**
|
||
|
||
🔴 **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
|
||
|
||
```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>"
|
||
```
|
||
|
||
🔑 **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:
|
||
|
||
```bash
|
||
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
|
||
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
|
||
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
|
||
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ür `vw.emsenhuber.net` steht; 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`, weil `420.teilzeitladen.xyz` nun
|
||
> ü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: nur `bf-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.
|
||
|
||
```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. **`container.npk` ist 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 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.**
|
||
⚠️ Ergänzung 08-31: `media`/`untrusted`/`guest` haben 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.
|
||
11. 🔴 **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 mit `docker export`
|
||
messen, nie mit `docker image inspect .Size`.**
|
||
12. 🔴 **`docker save` liefert seit Docker 25 OCI statt docker-v1** → `no config found in manifest`.
|
||
13. 🔴 **`/container/envs` nimmt `list=`, nicht `name=`.**
|
||
14. 🔴 **Der `device-mode`-Bestätigungs-Boot installiert keine Pakete** — Reihenfolge s. §3.3.
|
||
15. ✅ **ERLEDIGT 2026-08-31 — §3.4 kollidierte mit der GC-Governance:** `ip address` und
|
||
`ip firewall nat` stehen unter Drift-Kontrolle, direkt gesetzt ergab das **zwei `extra`-Drifts
|
||
je AP**. Sie sind jetzt **ins Soll gehoben** (AP-Template Block `[7]` + ConfigContext
|
||
`ap.container`), **nicht** als Transaktion geführt: ein Dauerzustand gehört ins Template.
|
||
Dazu kam die **28. ComplianceRule `interface-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 (`ConfigReplace` `60-`/`61-veth-*`) erzeugt die Regel Dauer-Drift auf allen
|
||
drei APs. Stand danach: **139/140**, übrig nur die offene TP25-Transaktion.
|
||
16. 🔴 **Ein Golden-Config-Job meldet `SUCCESS` und kann trotzdem halb durchgelaufen sein.**
|
||
Am 2026-08-31 fehlte nach dem ersten Compliance-Lauf genau **ein** Eintrag
|
||
(`crs326-core`/`interface-veth`), Job-Status `SUCCESS`, im Job-Log dagegen
|
||
`RENDER 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.
|
||
17. 🟡 **`A - AVAILABLE` in `/system package print` ist ein Download-Katalog**, kein Paket auf dem
|
||
Gerät — `enable` würde einen Internet-Fetch auslösen (auf ap1/ap2 gesperrt). `.npk` bleibt der Weg.
|
||
18. 🟡 **MikroMCPs `export_config` exportiert 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 **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.
|