From fde9f1535a8d1b9fdb351c033ad6e27c7d1d3686 Mon Sep 17 00:00:00 2001 From: Christoph Emsenhuber Date: Tue, 1 Sep 2026 21:04:11 +0200 Subject: [PATCH] n8n-mcp SSRF-Fix: permissive + in Portainer-Stack 174 gefaltet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - WEBHOOK_SECURITY_MODE=permissive → n8n-API-Anbindung (http://n8n:5678) wieder verbunden (n8n_health_check success:true); Blocker war der app-eigene SSRF-Guard, nicht Netz/Bearer. - Waisen-Container aus homelab-cline/n8n-mcp-deploy in Portainer-Stack 174 (n8n) gefaltet; n8n/redis ohne Downtime, Bearer/Port/Volume erhalten. Orphan-Ordner entfernt. - SSoT-Mirror docs/macmini-vlan20/stacks/ (gitignored) + Rollback-Backup. - TP30 Posten #8: N8N_SMTP_PASS beim Stack-Read geleakt (5. Maskierungs-Panne) → Rotation fällig. - INVENTORY + Handoff aktualisiert. Co-Authored-By: Claude Opus 4.8 --- .gitignore | 1 + docs/handoff-20260901-n8n-mcp-ssrf.md | 69 +++++++++++++++++++++++++++ docs/teilprojekte.md | 9 ++++ mcp/INVENTORY.md | 2 +- 4 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 docs/handoff-20260901-n8n-mcp-ssrf.md diff --git a/.gitignore b/.gitignore index 1b80a71..dc5862e 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,4 @@ caddy/caddy.env # Config-Backups, die Sessions vor destruktiven Edits anlegen (2026-08-30) *.bak-* +docs/macmini-vlan20/stacks diff --git a/docs/handoff-20260901-n8n-mcp-ssrf.md b/docs/handoff-20260901-n8n-mcp-ssrf.md new file mode 100644 index 0000000..c6316a3 --- /dev/null +++ b/docs/handoff-20260901-n8n-mcp-ssrf.md @@ -0,0 +1,69 @@ +# Handoff 2026-09-01 — n8n-mcp SSRF-Block ✅ GEFIXT + in Portainer-Stack 174 gefaltet + +## Kontext +Aus TP10 (Terpenfinder Auto-Refresh, Repo `blaueblueten-n8n`) ausgelagerter bf-infra-Task: +der n8n-MCP war einsatzfähig, aber seine Anbindung an die **n8n-API** war blockiert. Diese +Session hat den Blocker behoben und die Container-Definition sauber verortet. + +## Befund (Ausgangslage) +- **Netz war nie das Problem:** aus dem `n8n-mcp`-Container erreichten **beide** Wege n8n — + intern `http://n8n:5678` → HTTP 200, öffentlich `https://n8n.emsenhuber.net/healthz` → HTTP 200. +- **Einziger Blocker = App-eigener SSRF-Guard** von n8n-mcp (v2.57.1 → nach Redeploy 2.77.0): + `N8N_API_URL=http://n8n:5678` löst auf die private Docker-Bridge-IP `172.19.0.3` auf, und der + Guard lehnte private IPs im Default-Modus `strict` ab (`SSRF protection: Private IP addresses + not allowed`, `connected:false`). Der API-Client validiert `N8N_API_URL` über genau diesen + Guard (`/app/dist/services/n8n-api-client.js:104` → `SSRFProtection.validateWebhookUrl`). +- **SSoT-Drift (mit-gefixt):** der laufende Container war ein **Waise** aus + `~/dev/homelab-cline/n8n-mcp-deploy/docker-compose.yml` (kein Git-Repo), und dieses File stimmte + mit dem laufenden Container nicht überein (File: `https://n8n.emsenhuber.net` + Netz `web`; + Container: `http://n8n:5678` + Netz `n8n_default`). `n8n`+`n8n-redis` liefen dagegen im + Portainer-Stack **174** (`n8n`, Endpoint 20 = macmini). + +## Fix (durchgeführt 2026-09-01) +Entscheidung (User): **intern + permissive**, und **n8n-mcp in Portainer-Stack 174 falten**. + +1. **SSRF entschärft:** `WEBHOOK_SECURITY_MODE=permissive` gesetzt → private/LAN-IP für + `N8N_API_URL=http://n8n:5678` erlaubt. Bleibt auf der Docker-Bridge, **kein** CF/Tunnel/Caddy- + Umweg, keine 10-s-DNS-Falle, unabhängig von der Edge. Vertretbar, weil dieser Container + ausschließlich mit dem trusted LAN-n8n redet. +2. **Waise beseitigt:** orphan-Container entfernt (`docker rm -f n8n-mcp`), n8n-mcp als **dritter + Service** in Stack 174 aufgenommen (neben `n8n`+`redis`, gemeinsames Default-Netz `n8n_default`, + `depends_on: n8n`). Stack via Portainer-API redeployt (`PUT /api/stacks/174?endpointId=20`, + `prune:false`). +3. **Erhalten geblieben:** Bearer (`AUTH_TOKEN`==`MCP_AUTH_TOKEN`, len 64) **unverändert** → + bestehende Clients (opencode `http://10.40.20.167:3001/mcp`) laufen weiter; Host-Port-Bind + `10.40.20.167:3001:3000` → Caddy-Route `n8n-mcp.emsenhuber.net` (Caddyfile Z. 75) intakt; + Daten-Volume als **`external`** wiederverwendet (`n8n-mcp-deploy_n8n-mcp-data`, `nodes.db` + bleibt) — eigener `N8N_API_KEY` von n8n-mcp (≠ n8n) unverändert. + +## Verifikation (nicht behauptet — gemessen) +- `SSRFProtection.validateWebhookUrl("http://n8n:5678")` im Container → `valid:true`, + `resolved:172.19.0.3`, `mode:permissive`. +- MCP-Tool `n8n_health_check` (voller Handshake) → `{"success":true,"status":"ok", + "apiUrl":"http://n8n:5678","mcpVersion":"2.77.0","upToDate":true}`. +- `n8n`+`n8n-redis` behielten Container-ID **und** `StartedAt` (2026-07-29) → **kein n8n-Downtime** + beim Redeploy (nur der neue Service wurde erstellt). + +## SSoT / Ablage +- **Master der Stack-Definition = Portainer-Stack 174** (`n8n`, Endpoint 20, macmini). +- Gitignored Repo-Mirror + Rollback: `docs/macmini-vlan20/stacks/174-n8n.yml` (neuer Stand) und + `…/174-n8n.orig-pre-mcp-fold-20260901.yml` (Vorzustand). Neu gitignored: `docs/macmini-vlan20/stacks`. +- INVENTORY-Eintrag `mcp/INVENTORY.md` (n8n-mcp) aktualisiert. + +## Offene Punkte +- 🔴 **TP30 Posten #8:** beim Ausgeben des Stack-174-Files ist `N8N_SMTP_PASS` (Gmail-App-PW + `johnny.claw.0x00@gmail.com`) ins Transkript geraten (Redaktionsmuster hatte `PASSWORD`, nicht + bare `PASS`). **Rotation fällig** — Google App-Passwort widerrufen + Stack-174-Env nachziehen. +- ⚠️ **Toter Orphan:** `~/dev/homelab-cline/n8n-mcp-deploy/` (kein Git) kann gelöscht werden — ein + erneutes `docker compose up` dort würde sonst einen Namenskonflikt mit dem Stack-174-Container + erzeugen. Nicht kritisch (nur bei Handstart). +- ⚠️ **Client-401 in THIS Claude-Code-Session:** der n8n-mcp-Server kam in dieser Session mit + `AUTH_HEADER_REJECTED` (401) hoch — das ist ein **Client-Config-Thema** (`~/.claude.json`-Bearer + leer/falsch, vgl. globale CLAUDE.md „kein Shell → `{env:}` leer"), **nicht** der hier gefixte + Server. Der Server antwortet LAN-direkt korrekt; opencode ist unbetroffen. + +## Auswirkung auf TP10 +Keine für den Feature-Pfad: TP10 läuft über **Cloudflare Worker Cron**, nicht über n8n. Der Fix +macht n8n-mcp aber wieder als **Werkzeug** nutzbar (n8n-Workflow-Validierung, künftig optional +TP10-als-n8n-Workflow). Rückantwort ans zuweisende Projekt: +`~/dev/blaueblueten-n8n/docs/rueckantwort-n8n-mcp-fix-20260901.md`. diff --git a/docs/teilprojekte.md b/docs/teilprojekte.md index 447b88f..171243d 100644 --- a/docs/teilprojekte.md +++ b/docs/teilprojekte.md @@ -2179,6 +2179,7 @@ Umzug **nebeneinander**, ein zweiter AP am selben Punkt bringt nichts und stört | 5 | **Nautobot-Tokens** (`NAUTOBOT_SUPERUSER_API_TOKEN`, `NAUTOBOT_GITEA_TOKEN`) | Nautobot-Superuser + Gitea-Anbindung | 🔴 in der TP16.3-§2/§3-Session (2026-08-31) durch schwache `sed`-Maskierung **ins Transkript** geraten | Werte in `docs/nas-vlan20/stacks/210-nautobot.yml` (gitignored) | neu ausstellen + Stack-YML nachziehen; Superuser-Token wird von den `gc-*`-Skripten gelesen | | 6 | **MariaDB `root`** (LXC 302, `10.40.20.23`) | root-DB-Login | 🔴 am 2026-09-01 (TP39) ins Transkript geraten — die `/root/.my.cnf` trug `password ` **ohne `=`**, die Feld-Maskierung ankerte auf `=` und griff nicht | Wert in `/root/.my.cnf` (chmod 600) | `SET PASSWORD` + `.my.cnf` nachziehen; **kein** externer Konsument (Dienste nutzen eigene DB-User) ⇒ billigste Rotation der Liste | | 7 | **`TERPENE_ANDROID_KEYSTORE_PW`** | Terpene-Android-TWA-Signing (`android/android.keystore`, alias `terpene`) | 🔴 am 2026-09-01 (TP41) durch **Regex-Suffix-Loch** (`_PW` nicht abgedeckt) ins Transkript geraten | Wert in `~/.secrets/credentials.env` + Keystore-Datei | **Store-/Key-Passwort** via `keytool -storepasswd`/`-keypasswd` ändern + `credentials.env` nachziehen; Konsument = Android-Build/Play-Signing (**Cross-Projekt blaueblüten**) — den **Key selbst NICHT** neu erzeugen | +| 8 | **`N8N_SMTP_PASS`** (Gmail-App-PW für `johnny.claw.0x00@gmail.com`) | SMTP-Versand aus n8n (Portainer-Stack 174 `n8n`, macmini) | 🔴 am 2026-09-01 (n8n-mcp-SSRF-Session) beim Ausgeben des Stack-174-Files ins Transkript geraten — Redaktionsmuster hatte `PASSWORD`, aber **nicht bare `PASS`** → `N8N_SMTP_PASS=` blieb stehen | Wert in Portainer-Stack 174 (`environment`) | **Gmail-App-Passwort widerrufen** (Google-Konto → App-Passwörter) + neues erzeugen, Stack-174-Env nachziehen + redeploy; Konsument = n8n-SMTP-Node(s) | > ✅ **Der Leak-MECHANISMUS hinter #1 ist seit 2026-08-31 zu — der Wert bleibt trotzdem verbrannt.** > Alle sechs Skripte im Repo, die `sshpass -p ` benutzten (`nautobot/gc-transaktion.py` und @@ -2204,6 +2205,14 @@ Umzug **nebeneinander**, ein zweiter AP am selben Punkt bringt nichts und stört > Wert von `TERPENE_ANDROID_KEYSTORE_PW` beim maskierten Lesen stehen. **Dieselbe Merkregel, erweitert:** > die Alternation muss `PW`/`PWD` mit enthalten (`…|PW|PWD|…`), sonst greift die Feld-Ersetzung nicht. > +> 🔴 **#8 ist die fünfte Maskierungs-Panne derselben Klasse** (SHELLY 08-28, Nautobot-Tokens 08-31, +> MariaDB-root #6, Keystore-`_PW` #7): diesmal enthielt das Muster zwar `PASSWORD`, aber **nicht das +> bare `PASS`** — `N8N_SMTP_PASS` endet auf `PASS`, nicht `PASSWORD`, also blieb der Wert stehen. +> **Die Merkregel steht jetzt zum fünften Mal da und wurde zum fünften Mal gebrochen** — Konsequenz +> ist nicht noch eine Notiz, sondern ein **fixes, geteiltes Redaktions-Snippet**, das man nicht jedes +> Mal neu (und falsch) tippt: `sed -E 's/([A-Z0-9_]*(PASS|PASSWORD|PWD|PW|TOKEN|SECRET|KEY|BEARER))=[^[:space:]]*/\1=/g'` +> — ankert auf den **Schlüsselnamen inkl. Präfix** und deckt alle bisher gesehenen Suffixe ab. +> > ℹ️ **#3 war bis 2026-08-24 TP17.2** und ist jetzt vollständig hier (s. Detailabschnitt unten) — > es benutzt denselben Geräte-Durchgang wie #2, **ein Reboot statt zwei**. #4 gehört sachlich zu > **TP14.4**, das als TP bestehen bleibt (dort steckt das CF-Access-Profil, nicht nur der Token). diff --git a/mcp/INVENTORY.md b/mcp/INVENTORY.md index bc1de35..2baa5b5 100644 --- a/mcp/INVENTORY.md +++ b/mcp/INVENTORY.md @@ -41,7 +41,7 @@ |---|---|---|---|---|---|---| | **MikroMCP** (AliKarami) | MikroTik-Flotte: `crs326-core` `.99.1` · `crs326-branch` `.99.2` · ap1 `.99.11` · ap2 `.99.12` · **🆕 ap3 `.99.13` (cAP ax, 2026-08-30, Bring-up offen)** — alle RouterOS **7.24** | **jetzt: lokal (dieser Mac)** stdio / Ziel: HTTP-Container auf Mac mini `10.40.20.167` | jetzt **stdio** (`npx mikromcp serve`) / Ziel HTTP `:3000` | `ghcr.io/alikarami/mikromcp:latest` (HTTP) bzw. npm `mikromcp` (stdio) | **Registry = `bf-infra/mcp/mikromcp/config/routers.yaml`** (seit 2026-08-30 SSoT) + opencode/Claude-Code user-config | 🟢 **live (stdio), doctor 12/12, in opencode ✓Connected.** 🔴 **Zwei Drifts am 2026-08-30 gefunden und behoben:** (a) `MIKROMCP_CONFIG_PATH` in `~/.claude.json` zeigte auf **`~/dev/bf-network-r6`** (Archiv-Repo) — die identische Datei in `bf-infra` war **wirkungslos**; (b) in `opencode.jsonc` fehlte `MIKROMCP_CONFIG_PATH` **ganz** und es waren nur `ROUTER_AP1/AP2`-Creds hinterlegt → opencode kannte core/branch gar nicht. Beide Clients zeigen jetzt auf dieselbe Registry, `rosVersion` von stale 7.23/7.19.5 auf 7.24 korrigiert. ⚠️ **Neustart des MCP nötig** (Registry wird nur beim Start gelesen). ⚠️ opencode nutzt Env-Indirektion `{env:MCP_MIKROMCP_*}`, `~/.claude.json` **Klartext** — für ap3/core/branch fehlen noch `MCP_MIKROMCP_ROUTER_AP3_PASS`, `MCP_MIKROMCP_CRS326_CORE_PASS`, `MCP_MIKROMCP_CRS326_BRANCH_PASS` in `~/.secrets/exports.zsh` (TP30)** | | **gitea-mcp** (eigenbau) | Gitea REST API (Repo-Verwaltung, Issues, Wiki, …) | **Mac mini** `10.40.20.167` (Portainer-Endpoint 20) — Docker-Bridge-IP `172.17.0.3:3000` — ⚠️ **sollte auf DS923 umziehen** (Performance-Restriktion: macmini ist User-Workstation, kein zentraler MCP-Host) | **StreamableHTTP** `https://gitea-mcp.emsenhuber.net/mcp` (Caddy + CF) bzw. `http://10.40.20.167:3000/mcp` (LAN-direkt) | `docker.io/node:22-alpine` (eigener Build) | Repo `mcp/` hatte **kein** Config-Verzeichnis — Container-Definition lebt auf macmini | 🟢 **live (seit unknown, gefunden 2026-08-28), war aus dem Inventar verschwunden. serverInfo: gitea-mcp 0.0.10. Umzug auf DS923 ausstehend (Performance-Restriktion)** | -| **n8n-mcp** (czlonkowski) | n8n-public-API (Workflows, Credentials, Executions) | **Mac mini** `10.40.20.167` (Portainer-Endpoint 20, **n8n-Host** — n8n `:5678`, n8n-redis `:6379`, n8n-mcp `:3001`) — Docker-Bridge-IP `172.19.0.4` → PublicPort `3001:3000` | **StreamableHTTP** `https://n8n-mcp.emsenhuber.net/mcp` (Caddy + CF) bzw. `http://10.40.20.167:3001/mcp` (LAN-direkt) | `ghcr.io/czlonkowski/n8n-mcp:latest` | keine im Repo (`mcp.json.example` in `blaueblueten-n8n/` ist alt, stdio-Modus) | 🟢 **live (gefunden 2026-08-28, war **nicht** im Inventar). FQDN → 401 ohne Bearer (Auth-Sanierung TP16.3). LAN-direkt :3001 antwortet korrekt mit 401 = Bearer fehlt, Server lebt. Bleibt auf macmini (zusammen mit n8n — n8n-Workflows lösen Tool-Calls aus, lokale Performance-Kopplung gewollt)** | +| **n8n-mcp** (czlonkowski) | n8n-public-API (Workflows, Credentials, Executions) | **Mac mini** `10.40.20.167` (Portainer-Endpoint 20, **n8n-Host** — n8n `:5678`, n8n-redis `:6379`, n8n-mcp `:3001`) — Docker-Bridge-IP `172.19.0.4` → PublicPort `3001:3000` | **StreamableHTTP** `https://n8n-mcp.emsenhuber.net/mcp` (Caddy + CF) bzw. `http://10.40.20.167:3001/mcp` (LAN-direkt) | `ghcr.io/czlonkowski/n8n-mcp:latest` (2026-09-01 auf 2.77.0) | **Portainer-Stack 174 (`n8n`, Endpoint 20)** seit 2026-09-01 — dritter Service neben `n8n`+`redis`; gitignored Repo-Mirror `docs/macmini-vlan20/stacks/174-n8n.yml`. *(Bis 2026-09-01 Waise aus `~/dev/homelab-cline/n8n-mcp-deploy/`, jetzt beseitigt.)* | 🟢 **live + n8n-API verbunden (Fix 2026-09-01).** War bis dahin durch den **eigenen SSRF-Guard** von der n8n-API abgeschnitten (`N8N_API_URL=http://n8n:5678` → private IP `172.19.0.3` in `strict` verworfen). Fix: `WEBHOOK_SECURITY_MODE=permissive` → `n8n_health_check` = `success:true`. Bearer unverändert (Clients laufen weiter). FQDN → 401 ohne Bearer (erwartet). Bleibt auf macmini (n8n-Workflows lösen Tool-Calls aus, lokale Kopplung gewollt) | | **shelly-mcp** (eigenbau, FastMCP) | ~26 Shellys Gen3/4 — VLAN1→VLAN30-Migration (TP17) + Verwaltung | **geplant:** DS923 (`10.40.20.10`), macvlan-IP **`10.40.20.180`** | **StreamableHTTP** `http://10.40.20.180:3030/mcp` (geplant) | eigenbau `bf/shelly-mcp:0.1` (DS923-lokal gebaut aus `mcp/shelly/Dockerfile`) | `mcp/shelly/` ✓ (`Dockerfile` + `BUILD.md` + `requirements.txt`) · Stack gitignored in `docs/nas-vlan20/stacks/shelly-mcp.yml` | 🟡 **staged — Image-Build + Stack-Deploy auf DS923 ausstehend. Vorher-Stand war localhost `127.0.0.1:3030` mit `enabled=false` (Handoff 2026-08-28). Live-Verifikation 23 Shellys auf `10.40.30.180-250` (VLAN30) bestätigt 2026-08-28** | | **pve-mcp** (samik081) | Proxmox VE API (Host `.100`) | DS923 (`10.40.20.10`), macvlan-IP **`10.40.20.173`** | **StreamableHTTP** `http://10.40.20.173:3000/mcp` (+ Health `/health` → `mcp-pve 0.6.1`) | `ghcr.io/samik081/mcp-pve:latest` | — (Stack via Portainer) | 🟢 **live, in opencode + claude.json eingetragen. Drift-Korrektur 2026-08-28: war im Inventar als „SSE `/sse" gelistet, Realität ist StreamableHTTP** | | **komodo-mcp** (samik081) | Docker-Management via Komodo Core (ersetzt Portainer) | DS923 (`10.40.20.10`), macvlan-IP **`10.40.20.176`** | SSE `http://10.40.20.176:3000/sse` (geplant) | `ghcr.io/samik081/mcp-komodo:latest` | `mcp/komodo/` ✓ (`.env.example` → `.env` nach Phase 1) | 🟡 **geplant — Migration: `mcp/komodo/MIGRATION.md`** |