n8n-mcp SSRF-Fix: permissive + in Portainer-Stack 174 gefaltet
- 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 <noreply@anthropic.com>
This commit is contained in:
69
docs/handoff-20260901-n8n-mcp-ssrf.md
Normal file
69
docs/handoff-20260901-n8n-mcp-ssrf.md
Normal file
@@ -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`.
|
||||
@@ -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 <wert>` **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=<wert>` 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 <PW>` 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=<redacted>/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).
|
||||
|
||||
Reference in New Issue
Block a user