diff --git a/CLAUDE.md b/CLAUDE.md index e07ad79..3ccd870 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -87,7 +87,12 @@ GW je VLAN = `.1` (crs326-core L3) · Mgmt VLAN99: core `.99.1`, branch `.99.2`, Reads immer safe; Writes mit `dryRun:true` previewen, dann `apply_plan`. **Passwort setzen via `manage_user set-password`** (`/user set*` ist per Deny-Policy blockiert). ⚠️ **Reboot geht auf ROS 7.24 gar nicht ueber MikroMCP:** das `reboot`-Tool sendet `delay`, die REST-API kennt den Parameter nicht (`unknown parameter delay`), und `run_command` ist fuer `/system reboot*` gesperrt — Weg ist `SSHPASS="$PW" sshpass -e ssh admin@ "/system reboot"` (PW aus `~/.secrets`, im Output maskieren). 🔴 **Niemals `sshpass -p`** — das schreibt das Passwort in die Kommandozeile, wo es in `ps` und jedem argv-Trace steht; **genau so ist am 2026-08-22 das Core-Passwort in ein - Sitzungstranskript geraten** und musste rotiert werden. `-e` hält es in der Umgebung des Kindprozesses. `bridge/vlan/set` nur auf `[find … and !dynamic]`. + Sitzungstranskript geraten** und musste rotiert werden. `-e` hält es in der Umgebung des Kindprozesses. + ✅ **Alle sechs Skripte im Repo sind am 2026-08-31 umgestellt** (`nautobot/gc-transaktion.py`, + `routeros/import-core-rsc.py` und die vier inzwischen nach + `routeros/archiv-tp15-cutover/` archivierten Cutover-Skripte) — gemessen + mit `ps`-Stichproben plus Negativkontrolle, nicht nur gelesen. **Neue Skripte erben die Regel nicht + von selbst**: wer einen sshpass-Aufruf schreibt, baut `env=dict(os.environ, SSHPASS=pw)` mit ein. `bridge/vlan/set` nur auf `[find … and !dynamic]`. 🔴 **Zweiter Leak-Weg, gleiche Klasse: `list_wifi_interfaces` gibt `security.passphrase` ALLER Profile im Klartext zurück** — am 2026-08-24 hat ein Read, der nur die `cap-wifiN`-Zuordnung finden sollte, damit **fünf WLAN-PSKs** (`main`/`iot`/`media`/`untrusted`/`guest`) in ein Transkript diff --git a/docs/handoff-20260831-fund-e-hebel1-gezogen.md b/docs/handoff-20260831-fund-e-hebel1-gezogen.md index 6da54ed..cfe16ca 100644 --- a/docs/handoff-20260831-fund-e-hebel1-gezogen.md +++ b/docs/handoff-20260831-fund-e-hebel1-gezogen.md @@ -102,24 +102,72 @@ Rollback-Weg (der Snapshot liegt bereit): `python3 nautobot/gc-transaktion.py rollback --change-id TP25-fund-e-ft-over-ds` — erzeugt einen **Vorschlag**, der gelesen werden will, und pusht bewusst nicht selbst. -## 🔧 Nebenbefund: `sshpass -p` in sechs Skripten +## ✅ Nebenbefund erledigt: `sshpass -p` ist repo-weit weg (2026-08-31, zweiter Durchgang) -`nautobot/gc-transaktion.py` rief `sshpass -p ` auf — das schreibt das Passwort in `argv`, wo es -in `ps` und jedem argv-Trace steht. Das ist die Klasse, an der am 2026-08-22 das Core-Passwort -verbrannt ist, und CLAUDE.md verbietet sie ausdrücklich. **In `gc-transaktion.py` behoben** -(`SSHPASS`-Env + `-e`), weil das Skript für diese Aufgabe laufen musste. - -🔴 **Fünf weitere Dateien haben dasselbe Muster und sind NICHT angefasst** (Stop-Bedingung — -gehört in eine eigene Runde, sinnvollerweise mit TP30): +`sshpass -p ` schreibt das Passwort in `argv`, wo es in `ps` und jedem argv-Trace steht — die +Klasse, an der am 2026-08-22 das Core-Passwort verbrannt ist. **Alle sechs betroffenen Skripte laufen +jetzt über `SSHPASS`-Env + `sshpass -e`:** ``` -routeros/import-core-rsc.py Z. 50, 74 -routeros/apply-core-cutover-l3.py Z. 29, 55 -routeros/backup-core.py Z. 29, 41 -routeros/restore-core.py Z. 30, 42 -routeros/backup-ap1.py Z. 29, 41 +nautobot/gc-transaktion.py (im ersten Durchgang, weil es laufen musste) +routeros/import-core-rsc.py +routeros/archiv-tp15-cutover/apply-core-cutover-l3.py ┐ +routeros/archiv-tp15-cutover/backup-core.py │ am selben Tag archiviert, +routeros/archiv-tp15-cutover/restore-core.py │ s. unten -- aber VOR dem +routeros/archiv-tp15-cutover/backup-ap1.py ┘ Verschieben noch gefixt ``` +**Warum Archiv-Code trotzdem gefixt wurde:** Archiv wird abgeschrieben. Wer sich dort ein +sshpass-Muster holt, soll das richtige finden. + +**Nachweis — gemessen, nicht gelesen:** + +| Prüfung | Ergebnis | +|---|---| +| Alle 13 `subprocess.run`, die sshpass starten, tragen `env=` | ✓ per AST geprüft (ohne `env` wäre `SSHPASS` leer und alles kaputt) | +| Signaturen passen zu den Aufrufen | ✓ per AST, Arity-Abgleich — `py_compile` findet so etwas **nicht** | +| Echter Lauf des gepatchten Codes gegen den lebenden Core | ✓ `backup-core.py` importiert, IP zur Laufzeit auf `10.40.99.1`, Antwort `name: crs326-core`, rc=0 | +| Passwort in `argv`? | **nein** | +| Passwort in `ps` während der Verbindung? | **nein**, 40 Stichproben | +| **Negativkontrolle** — Dummy-Wert über die alte `-p`-Form | **36 Treffer** ⇒ die Probe ist nicht blind, das „nein" oben hat Aussagekraft | + +🔴 **Für TP30 ändert das nichts.** Der Weg ist zu, der Wert bleibt verbrannt — ein Passwort, das +einmal in einem Transkript stand, wird nicht dadurch sauber, dass die Leitung dicht ist. +Rotation von Posten #1 bleibt fällig. + +⚠️ **Mitgefixt, weil es in umgebautem Code lag:** `apply-core-cutover-l3.py` griff in `run_silent` +auf ein `pw` zu, das dort gar nicht existierte — **jeder Fehlerfall endete in einem `NameError` +statt in der gedachten Meldung.** + +### 🔴 Und dabei aufgefallen: die IPs in diesen fünf Skripten sind Schrott + +Live geprüft am 2026-08-31, **bewusst NICHT stillschweigend geändert** — nur je ein lauter Header +in die Datei: + +| Skript | Ziel-IP | Realität | +|---|---|---| +| `import-core-rsc.py`, `apply-core-cutover-l3.py`, `backup-core.py` | `10.40.41.211` | **tot** — VLAN41 gibt es seit 2026-08-27 nicht mehr | +| `restore-core.py` | `192.168.88.1` | **tot** — Werks-Temp-Pfad; dazu ein hartkodierter Backup-Dateiname vom 2026-08-06 | +| `backup-ap1.py` | `10.40.10.1` | 🔴 **antwortet — ist aber der CORE** (VLAN10-SVI, tcp/22 offen). ap1 ist `10.40.99.11`. | + +**`backup-ap1.py` ist der gefährliche:** er scheitert nicht, er sichert das **falsche Gerät** und legt +die Datei als `ap1-pre-core-cutover-*.backup` ab. Wer dieses Backup später zurückspielt, spielt eine +Core-Config auf einen AP. + +✅ **Entschieden (Christoph, 2026-08-31): archiviert, nicht repariert.** Die vier liegen jetzt unter +[`routeros/archiv-tp15-cutover/`](../routeros/archiv-tp15-cutover/README.md) mit README nach dem +Muster von [`switch/`](../switch/README.md) — der Cutover ist am 2026-08-08 durchgezogen, die Skripte +haben ihre Aufgabe erfüllt. + +⚠️ **Die Pfad-Nebenwirkungen des Verschiebens sind absichtlich NICHT repariert** (`backup-core.py` +schriebe jetzt ins Archivverzeichnis, `apply-core-cutover-l3.py` fände seine `.rsc` nicht mehr). +Sie zu reparieren hieße, die Skripte wieder lauffähig zu machen — das ist gerade nicht die Absicht. + +**`import-core-rsc.py` bleibt**, wo es ist: als einziges generisch nützlich. Sein Default-Host ist +aber ebenfalls tot ⇒ **`--host` mitgeben**. (Und in `--host` steckt ein zweiter latenter Bug: +`sys.argv[1]` wird als RSC-Pfad gelesen, bevor auf `--host` geprüft wird — die Option kann so, wie +sie dokumentiert ist, nie greifen. **Nicht gefixt**, gehört zu einer eigenen Runde.) + **Entwarnung zum Snapshot:** `/export` auf ROS 7.24 gibt WLAN-Passphrasen **gar nicht** aus (0 Vorkommen im Vollexport, live geprüft). Die Transaktionsakte im Git ist sauber. diff --git a/docs/teilprojekte-archiv.md b/docs/teilprojekte-archiv.md index e71e48c..683c15b 100644 --- a/docs/teilprojekte-archiv.md +++ b/docs/teilprojekte-archiv.md @@ -758,6 +758,12 @@ FT kaschierte das bisher als „Roaming" statt „schwache Abdeckung". +> 📦 **Nachtrag 2026-08-31:** die vier Skripte, die den Cutover begleitet haben +> (`apply-core-cutover-l3`, `backup-core`, `restore-core`, `backup-ap1`), liegen jetzt unter +> [`routeros/archiv-tp15-cutover/`](../routeros/archiv-tp15-cutover/README.md). Sie zeigten alle +> auf tote Adressen — `backup-ap1.py` allerdings auf eine **lebende falsche** (`10.40.10.1` ist die +> VLAN10-SVI des Core, nicht ap1) und hätte still das falsche Gerät gesichert. +> ### TP15 — HW-Refresh: Switching + Router ▸ ✅ **ABGESCHLOSSEN (2026-08-08)** — Bring-up beider CRS326 + Branch-Tausch (2026-07-23) **und** Core-Tausch/L3-Umzug (2026-08-08, Durchführung s. TP21.1) sind durch — `docs/hw-refresh-requirements.md`, Memory `project_mikrotik_commit_nautobot_gc` > 🔌 **Scope-Änderung 2026-08-16 (User-Entscheid): die DC-USV ist aus diesem TP herausgelöst und heißt jetzt **TP24**.** Sie war der letzte offene Rest hier, hat mit Switching/Routing sachlich nichts zu tun und trägt eigene Entscheidungen (Elektrik, Batterie, Standort). TP15 ist damit inhaltlich zu. > *Vorherige Header-Fassung (Stand 2026-08-05, überholt): „🟢 CRS326-Bring-up + Branch-Tausch DURCH (2026-07-23); Core-Tausch/L3-Umzug **vorbereitet, Cutover-Config materialisiert (2026-08-05)**, Cutover = nächstes Wartungsfenster."* diff --git a/docs/teilprojekte.md b/docs/teilprojekte.md index 700713a..86ea66d 100644 --- a/docs/teilprojekte.md +++ b/docs/teilprojekte.md @@ -1773,6 +1773,16 @@ Umzug **nebeneinander**, ein zweiter AP am selben Punkt bringt nichts und stört | 3 | **Shelly-Gerätepasswort** *(vormals TP17.2)* | 23 Shellys | Härtung (bisher **keine** Auth auf den Geräten) | ✅ Werkzeug fertig (`migrate.py cmd_auth` + Digest-Fallback) | **je Gerät ein HA-Re-Auth-Flow** — das ist der Aufwand, nicht `SetAuth` | | 4 | **CF-Access-Service-Token** (`CF_API_TOKEN`) | Cloudflare-Zugang, s. TP14.4 | ⏳ **läuft am 2027-01-09 ohnehin ab** — harter Termin, unabhängig vom Projektende | Runbook liegt (`tp14.4-cf-access-token-runbook.md`) | Token neu ausstellen + `~/.secrets` nachziehen; **account-owned**, anderer verify-Endpoint | +> ✅ **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 +> `routeros/import-core-rsc.py` sowie die vier TP15-Cutover-Skripte, die am selben Tag nach +> `routeros/archiv-tp15-cutover/` gewandert sind), laufen jetzt über `SSHPASS`-Env + `sshpass -e` +> — **auch die archivierten**, denn Archiv-Code wird abgeschrieben. **Kontrolliert gemessen**, nicht behauptet: mit `-e` taucht +> das Passwort in 40 `ps`-Stichproben während einer laufenden Verbindung **nicht** auf, und die +> Negativkontrolle (ein Dummy-Wert über die alte `-p`-Form) wurde **36-mal** gesehen — die Probe ist +> also nicht blind. 🔴 **Das ändert an #1 nichts:** ein Wert, der einmal in einem Transkript stand, +> wird nicht dadurch sauber, dass der Weg dorthin geschlossen ist. Rotation bleibt fällig. +> > ℹ️ **#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/routeros/archiv-tp15-cutover/README.md b/routeros/archiv-tp15-cutover/README.md new file mode 100644 index 0000000..f054244 --- /dev/null +++ b/routeros/archiv-tp15-cutover/README.md @@ -0,0 +1,62 @@ +# 📦 ARCHIV — TP15/TP21.1 Core-Cutover (2026-08-06 … 2026-08-08) + +> **Dieses Verzeichnis ist Historie. Die Skripte sind ERLEDIGT und werden nicht mehr ausgeführt.** +> Sie haben den Umzug des L3-Routers von **ap1** auf **`crs326-core`** begleitet — Backup davor, +> Staged-Config einspielen, Restore als Rückweg. Der Cutover ist am **2026-08-08** durchgezogen; +> seither ist `crs326-core` der produktive L3-Router und ap1 ein reiner CAP. +> Archiviert am **2026-08-31** auf Christophs Entscheid. +> Projektkonvention ist *fortschreiben statt löschen* — dieselbe wie bei [`switch/`](../../switch/README.md). + +## 🔴 Warum sie nicht liegen bleiben durften, wo sie lagen + +Alle vier zeigten auf **Adressen, die es nicht mehr gibt** — und einer davon ist nicht harmlos +kaputt, sondern **falsch**. Live geprüft am 2026-08-31: + +| Datei | Ziel-IP im Code | Realität | +|---|---|---| +| `apply-core-cutover-l3.py` | `10.40.41.211` | ❌ tot — das VLAN-Schema `10.40.41.x` ist seit **2026-08-27** abgeschafft | +| `backup-core.py` | `10.40.41.211` | ❌ tot, dito | +| `restore-core.py` | `192.168.88.1` | ❌ tot — Werks-Temp-Pfad; dazu ein hartkodierter Backup-Dateiname vom 2026-08-06 | +| `backup-ap1.py` | `10.40.10.1` | 🔴 **antwortet — ist aber der CORE** (VLAN10-SVI, tcp/22 offen). ap1 ist seit dem Cutover `10.40.99.11`. | + +🔑 **Die Merkregel aus `backup-ap1.py` ist die wichtigste Zeile in dieser Datei:** +das Skript wäre **nicht** gescheitert. Es hätte den **Core** gesichert und die Datei +`ap1-pre-core-cutover-*.backup` genannt. Wer so ein Backup Monate später zurückspielt, spielt eine +**Core-Config auf einen AP**. Ein Skript, das still das falsche Gerät trifft, ist gefährlicher als +eines, das gar nicht läuft — deshalb liegt es hier und nicht mehr neben den benutzbaren Werkzeugen. + +## Was hier **nicht** mehr gilt + +| Datei | Status | +|---|---| +| `apply-core-cutover-l3.py` | ❌ nicht ausführen — tote IP; liest zudem `../tp15-core-cutover-intended.rsc`, das **gitignored** ist und durch den Verzeichniswechsel ohnehin nicht mehr gefunden würde | +| `backup-core.py` | ❌ nicht ausführen — tote IP; schriebe seine Ausgabe jetzt in **dieses** Archivverzeichnis (`OUT_DIR = Path(__file__).parent`) | +| `restore-core.py` | ❌ nicht ausführen — tote IP + Dateiname von 2026-08-06 | +| `backup-ap1.py` | 🔴 **auf keinen Fall ausführen** — s. oben, es trifft das falsche Gerät | + +⚠️ **Die Pfad-Nebenwirkungen des Verschiebens sind absichtlich nicht repariert.** Sie zu reparieren +hieße, die Skripte wieder lauffähig zu machen — das ist genau *nicht* die Absicht. + +## ✅ Was an ihnen trotzdem noch gepflegt wurde + +Am **2026-08-31** sind sie — vor dem Archivieren — von `sshpass -p ` auf **`SSHPASS`-Env + +`sshpass -e`** umgestellt worden, zusammen mit `nautobot/gc-transaktion.py` und +`../import-core-rsc.py`. Grund: `-p` schreibt das Passwort in `argv`, wo es in `ps` und jedem +argv-Trace steht — so ist am **2026-08-22** das Core-Passwort verbrannt. +**Auch Archiv-Code wird kopiert.** Wer hier ein Muster abschreibt, soll das richtige abschreiben. + +In `apply-core-cutover-l3.py` wurde dabei ein latenter Bug mitgefixt: `run_silent` griff auf ein +`pw` zu, das in der Funktion gar nicht existierte — **jeder Fehlerfall endete in einem `NameError` +statt in der gedachten Meldung.** + +## Wo die Wahrheit heute steht + +- **Ist-Stand + TP-Steuerung:** [`docs/teilprojekte.md`](../../docs/teilprojekte.md) +- **Config-Weg für Netzgeräte:** ausschließlich **Nautobot Golden Config** (TP21/TP22) — + `nautobot/gc-configplan.py`, Teständerungen als Transaktion über `nautobot/gc-transaktion.py` +- **Backup eines Geräts heute:** `/system backup save` direkt am Gerät (so ist am 2026-08-31 + `pre-tp25-funde-ftoverds.backup` auf dem Core entstanden) — **kein** Skript nötig +- **Aktuelle Adressen:** `crs326-core` `10.40.99.1` · `crs326-branch` `10.40.99.2` · + ap1 `10.40.99.11` · ap2 `10.40.99.12` · ap3 `10.40.99.13` +- **Interaktiv/Debug:** MikroMCP · **noch benutzbar geblieben:** [`../import-core-rsc.py`](../import-core-rsc.py) + (generisch, aber ebenfalls mit totem Default-Host — `--host` mitgeben) diff --git a/routeros/apply-core-cutover-l3.py b/routeros/archiv-tp15-cutover/apply-core-cutover-l3.py similarity index 62% rename from routeros/apply-core-cutover-l3.py rename to routeros/archiv-tp15-cutover/apply-core-cutover-l3.py index 323e1f2..3b75a50 100644 --- a/routeros/apply-core-cutover-l3.py +++ b/routeros/archiv-tp15-cutover/apply-core-cutover-l3.py @@ -3,8 +3,18 @@ Creds aus ~/.claude.json. Alle operativen Objekte in der rsc sind disabled=yes, also bleibt der aktive Datenpfad unberührt. + +🔴 PASSWORT NIE UEBER argv. `sshpass -p ` schreibt es in die Kommandozeile, wo es +in `ps` und jedem argv-Trace steht -- genau so ist am 2026-08-22 das Core-Passwort in +ein Sitzungstranskript geraten und musste rotiert werden. `sshpass -e` liest es aus der +Umgebung des Kindprozesses. Umgestellt 2026-08-31. + +⚠️ CORE_IP 10.40.41.211 ist TOT (VLAN41 abgeschafft 2026-08-27); der Core ist heute +10.40.99.1. Und die gelesene RSC ist gitignored -- dieses Skript ist ein TP15-Artefakt +vom Cutover 2026-08-08. Bewusst nicht stillschweigend geaendert, s. Handoff 2026-08-31. """ import json +import os import pathlib import subprocess import sys @@ -24,9 +34,14 @@ def load_core_creds(): return user, pw -def ssh_base(user, pw): +# Das Passwort geht ausschliesslich ueber die Umgebung an sshpass -- nie als Argument. +def ssh_env(pw): + return dict(os.environ, SSHPASS=pw) + + +def ssh_base(user): return [ - "sshpass", "-p", pw, + "sshpass", "-e", "ssh", "-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null", @@ -35,8 +50,11 @@ def ssh_base(user, pw): ] -def run_silent(base, cmd): - r = subprocess.run(base + [cmd], capture_output=True, text=True) +# 🔴 pw ist Parameter, nicht Global: die alte Fassung griff hier auf ein `pw` zu, das +# in dieser Funktion gar nicht existierte -- jeder Fehlerfall endete in einem +# NameError statt in der gedachten Meldung. Mitgefixt 2026-08-31. +def run_silent(base, cmd, pw): + r = subprocess.run(base + [cmd], capture_output=True, text=True, env=ssh_env(pw)) if r.returncode != 0: out = (r.stdout + r.stderr).replace(pw, "***") raise SystemExit(f"Befehl fehlgeschlagen (rc={r.returncode}):\n{out}") @@ -47,25 +65,25 @@ def main(): if not RSC.exists(): raise SystemExit(f"{RSC} fehlt") user, pw = load_core_creds() - base = ssh_base(user, pw) + base = ssh_base(user) print(f"crs326-core L3-Cutover-Config laden ({RSC.name}) ...") # 1) Datei auf Router kopieren r = subprocess.run( - ["sshpass", "-p", pw, + ["sshpass", "-e", "scp", "-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null", "-o", "PubkeyAuthentication=no", str(RSC), f"{user}@{CORE_IP}:{RSC.name}"], - capture_output=True, text=True, + capture_output=True, text=True, env=ssh_env(pw), ) if r.returncode != 0: raise SystemExit(f"SCP fehlgeschlagen: {r.stderr.replace(pw, '***')}") # 2) Import auslösen - run_silent(base, f'/import file-name="{RSC.name}"') + run_silent(base, f'/import file-name="{RSC.name}"', pw) print(" Import abgeschlossen (staged / disabled).") print(" Nächster Schritt: L2-Portplan + Cutover-Enable.") diff --git a/routeros/backup-ap1.py b/routeros/archiv-tp15-cutover/backup-ap1.py similarity index 65% rename from routeros/backup-ap1.py rename to routeros/archiv-tp15-cutover/backup-ap1.py index 5f64831..98b4d52 100644 --- a/routeros/backup-ap1.py +++ b/routeros/archiv-tp15-cutover/backup-ap1.py @@ -2,8 +2,20 @@ """Erstelle ein Binary-Backup + einen Text-Export von ap1 vor dem TP15-Cutover. Creds aus ~/.claude.json (mikromcp-env). Backup-Dateien landen unter routeros/. + +🔴 PASSWORT NIE UEBER argv. `sshpass -p ` schreibt es in die Kommandozeile, wo es +in `ps` und jedem argv-Trace steht -- genau so ist am 2026-08-22 das Core-Passwort in +ein Sitzungstranskript geraten und musste rotiert werden. `sshpass -e` liest es aus der +Umgebung des Kindprozesses. Umgestellt 2026-08-31. + +🔴 AP1_IP 10.40.10.1 ist NICHT ap1 -- das ist die VLAN10-SVI des CORE (live geprueft +2026-08-31: tcp/22 offen). ap1 ist seit dem Core-Cutover 10.40.99.11. Dieses Skript +wuerde also den CORE sichern und die Datei "ap1-pre-core-cutover-*.backup" nennen -- +es scheitert nicht, es benennt falsch. VOR DEM NAECHSTEN LAUF die IP korrigieren. +Bewusst nicht stillschweigend geaendert, s. Handoff 2026-08-31. """ import json +import os import pathlib import subprocess import sys @@ -24,9 +36,14 @@ def load_ap1_creds(): return user, pw -def ssh_cmd(user, pw, remote_command): +# Das Passwort geht ausschliesslich ueber die Umgebung an sshpass -- nie als Argument. +def ssh_env(pw): + return dict(os.environ, SSHPASS=pw) + + +def ssh_cmd(user, remote_command): return [ - "sshpass", "-p", pw, + "sshpass", "-e", "ssh", "-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null", @@ -36,9 +53,9 @@ def ssh_cmd(user, pw, remote_command): ] -def scp_cmd(user, pw, remote_file, local_file): +def scp_cmd(user, remote_file, local_file): return [ - "sshpass", "-p", pw, + "sshpass", "-e", "scp", "-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null", @@ -57,22 +74,22 @@ def main(): # 1) Binary backup + Text-Export auf ap1 erzeugen create_cmd = ssh_cmd( - user, pw, + user, f'/system backup save name="{backup_name}" dont-encrypt=yes; ' f'/export file="{backup_name}"' ) print(f" Erzeuge Backup/Export auf ap1 ...") - subprocess.run(create_cmd, check=True) + subprocess.run(create_cmd, check=True, env=ssh_env(pw)) # 2) Herunterladen local_backup = OUT_DIR / f"{backup_name}.backup" local_export = OUT_DIR / f"{backup_name}.rsc" print(f" Lade {backup_name}.backup herunter ...") - subprocess.run(scp_cmd(user, pw, f"{backup_name}.backup", local_backup), check=True) + subprocess.run(scp_cmd(user, f"{backup_name}.backup", local_backup), check=True, env=ssh_env(pw)) print(f" Lade {backup_name}.rsc herunter ...") - subprocess.run(scp_cmd(user, pw, f"{backup_name}.rsc", local_export), check=True) + subprocess.run(scp_cmd(user, f"{backup_name}.rsc", local_export), check=True, env=ssh_env(pw)) # 3) Verifizieren backup_size = local_backup.stat().st_size diff --git a/routeros/backup-core.py b/routeros/archiv-tp15-cutover/backup-core.py similarity index 69% rename from routeros/backup-core.py rename to routeros/archiv-tp15-cutover/backup-core.py index c3396c0..0d01b11 100644 --- a/routeros/backup-core.py +++ b/routeros/archiv-tp15-cutover/backup-core.py @@ -2,8 +2,17 @@ """Erstelle ein Binary-Backup + einen Text-Export von crs326-core vor dem TP15-Cutover. Creds aus ~/.claude.json (mikromcp-env, CRS326_CORE_*). Backup-Dateien landen unter routeros/. + +🔴 PASSWORT NIE UEBER argv. `sshpass -p ` schreibt es in die Kommandozeile, wo es +in `ps` und jedem argv-Trace steht -- genau so ist am 2026-08-22 das Core-Passwort in +ein Sitzungstranskript geraten und musste rotiert werden. `sshpass -e` liest es aus der +Umgebung des Kindprozesses. Umgestellt 2026-08-31. + +⚠️ CORE_IP 10.40.41.211 ist TOT (VLAN41 abgeschafft 2026-08-27); der Core ist heute +10.40.99.1. Bewusst nicht stillschweigend geaendert, s. Handoff 2026-08-31. """ import json +import os import pathlib import subprocess import sys @@ -24,9 +33,14 @@ def load_core_creds(): return user, pw -def ssh_cmd(user, pw, remote_command): +# Das Passwort geht ausschliesslich ueber die Umgebung an sshpass -- nie als Argument. +def ssh_env(pw): + return dict(os.environ, SSHPASS=pw) + + +def ssh_cmd(user, remote_command): return [ - "sshpass", "-p", pw, + "sshpass", "-e", "ssh", "-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null", @@ -36,9 +50,9 @@ def ssh_cmd(user, pw, remote_command): ] -def scp_cmd(user, pw, remote_file, local_file): +def scp_cmd(user, remote_file, local_file): return [ - "sshpass", "-p", pw, + "sshpass", "-e", "scp", "-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null", @@ -56,21 +70,21 @@ def main(): print(f"crs326-core Backup via SSH {user}@{CORE_IP}") create_cmd = ssh_cmd( - user, pw, + user, f'/system backup save name="{backup_name}" dont-encrypt=yes; ' f'/export file="{backup_name}"' ) print(f" Erzeuge Backup/Export auf crs326-core ...") - subprocess.run(create_cmd, check=True) + subprocess.run(create_cmd, check=True, env=ssh_env(pw)) local_backup = OUT_DIR / f"{backup_name}.backup" local_export = OUT_DIR / f"{backup_name}.rsc" print(f" Lade {backup_name}.backup herunter ...") - subprocess.run(scp_cmd(user, pw, f"{backup_name}.backup", local_backup), check=True) + subprocess.run(scp_cmd(user, f"{backup_name}.backup", local_backup), check=True, env=ssh_env(pw)) print(f" Lade {backup_name}.rsc herunter ...") - subprocess.run(scp_cmd(user, pw, f"{backup_name}.rsc", local_export), check=True) + subprocess.run(scp_cmd(user, f"{backup_name}.rsc", local_export), check=True, env=ssh_env(pw)) backup_size = local_backup.stat().st_size export_size = local_export.stat().st_size diff --git a/routeros/restore-core.py b/routeros/archiv-tp15-cutover/restore-core.py similarity index 65% rename from routeros/restore-core.py rename to routeros/archiv-tp15-cutover/restore-core.py index fe9ae91..c2ee743 100644 --- a/routeros/restore-core.py +++ b/routeros/archiv-tp15-cutover/restore-core.py @@ -3,8 +3,18 @@ Creds aus ~/.claude.json (mikromcp-env, CRS326_CORE_*). Backup-Datei wird per SCP hochgeladen und /system backup load ausgefuehrt. + +🔴 PASSWORT NIE UEBER argv. `sshpass -p ` schreibt es in die Kommandozeile, wo es +in `ps` und jedem argv-Trace steht -- genau so ist am 2026-08-22 das Core-Passwort in +ein Sitzungstranskript geraten und musste rotiert werden. `sshpass -e` liest es aus der +Umgebung des Kindprozesses. Umgestellt 2026-08-31. + +⚠️ CORE_IP 192.168.88.1 ist der Werks-Temp-Pfad und antwortet nicht; der Core ist heute +10.40.99.1. Der hartkodierte BACKUP-Dateiname stammt vom 2026-08-06. +Bewusst nicht stillschweigend geaendert, s. Handoff 2026-08-31. """ import json +import os import pathlib import subprocess import sys @@ -25,9 +35,14 @@ def load_core_creds(): return user, pw -def ssh_cmd(user, pw, remote_command): +# Das Passwort geht ausschliesslich ueber die Umgebung an sshpass -- nie als Argument. +def ssh_env(pw): + return dict(os.environ, SSHPASS=pw) + + +def ssh_cmd(user, remote_command): return [ - "sshpass", "-p", pw, + "sshpass", "-e", "ssh", "-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null", @@ -37,9 +52,9 @@ def ssh_cmd(user, pw, remote_command): ] -def scp_cmd(user, pw, local_file, remote_file): +def scp_cmd(user, local_file, remote_file): return [ - "sshpass", "-p", pw, + "sshpass", "-e", "scp", "-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null", @@ -50,7 +65,7 @@ def scp_cmd(user, pw, local_file, remote_file): def run_silent(cmd, pw): - r = subprocess.run(cmd, capture_output=True, text=True) + r = subprocess.run(cmd, capture_output=True, text=True, env=ssh_env(pw)) if r.returncode != 0: out = (r.stdout + r.stderr).replace(pw, "***") raise SystemExit(f"Befehl fehlgeschlagen (rc={r.returncode}):\n{out}") @@ -66,10 +81,10 @@ def main(): print(f"crs326-core Restore via SSH {user}@{CORE_IP}") print(f" Lade {BACKUP} hoch ...") - run_silent(scp_cmd(user, pw, local_backup, BACKUP), pw) + run_silent(scp_cmd(user, local_backup, BACKUP), pw) print(f" Spiele Backup zurueck ...") - run_silent(ssh_cmd(user, pw, f'/system backup load name="{BACKUP}" password=""'), pw) + run_silent(ssh_cmd(user, f'/system backup load name="{BACKUP}" password=""'), pw) print(" Backup geladen, Router rebootet.") print(" Warte auf Wiederverbindung an 10.40.41.211 ...") diff --git a/routeros/import-core-rsc.py b/routeros/import-core-rsc.py index 7f757a8..2fa39d9 100644 --- a/routeros/import-core-rsc.py +++ b/routeros/import-core-rsc.py @@ -4,6 +4,14 @@ Usage: python3 import-core-rsc.py [--host 10.40.41.211] Creds kommen aus ~/.claude.json (mcpServers.mikromcp.env) envPrefix CRS326_CORE_. + +🔴 PASSWORT NIE UEBER argv. `sshpass -p ` schreibt es in die Kommandozeile, wo es +in `ps` und jedem argv-Trace steht -- genau so ist am 2026-08-22 das Core-Passwort in +ein Sitzungstranskript geraten und musste rotiert werden. `sshpass -e` liest es aus der +Umgebung des Kindprozesses. Umgestellt 2026-08-31. + +⚠️ Der Default-HOST 10.40.41.211 ist TOT (VLAN41 abgeschafft 2026-08-27). +Der Core ist heute 10.40.99.1. Bewusst nicht stillschweigend geaendert -- s. Handoff 2026-08-31. """ import json @@ -41,13 +49,16 @@ pw = get("CRS326_CORE_PASS") if not pw: sys.exit("CRS326_CORE_PASS fehlt in ~/.claude.json") +# Das Passwort geht ausschliesslich hierueber an sshpass -- nie als Argument. +SSH_ENV = dict(os.environ, SSHPASS=pw) + # Remote-Dateiname: alphanumerisch + Unterstrich/Punkt/Minus remote_name = re.sub(r"[^A-Za-z0-9_.-]", "_", RSC_PATH.name) def ssh_cmd(remote_command: str) -> list: return [ - "sshpass", "-p", pw, + "sshpass", "-e", "ssh", "-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null", @@ -59,7 +70,7 @@ def ssh_cmd(remote_command: str) -> list: def run_silent(cmd: list): - r = subprocess.run(cmd, capture_output=True, text=True) + r = subprocess.run(cmd, capture_output=True, text=True, env=SSH_ENV) if r.returncode != 0: out = (r.stdout + r.stderr).replace(pw, "***") raise SystemExit(f"SSH-Befehl fehlgeschlagen (rc={r.returncode}):\n{out}") @@ -71,7 +82,7 @@ run_silent(ssh_cmd(f"/file/remove [find name=\"{remote_name}\"]; :delay 500ms")) # 2) SCP scp = [ - "sshpass", "-p", pw, + "sshpass", "-e", "scp", "-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null", @@ -80,14 +91,14 @@ scp = [ str(RSC_PATH), f"{user}@{HOST}:/{remote_name}", ] -r = subprocess.run(scp, capture_output=True, text=True) +r = subprocess.run(scp, capture_output=True, text=True, env=SSH_ENV) if r.returncode != 0: out = (r.stdout + r.stderr).replace(pw, "***") raise SystemExit(f"SCP fehlgeschlagen (rc={r.returncode}):\n{out}") # 3) Import — mit verbose, damit wir die Zeile sehen, wenn ein Fehler auftritt import_cmd = f"/import file-name={remote_name} verbose=yes" -res = subprocess.run(ssh_cmd(import_cmd), capture_output=True, text=True) +res = subprocess.run(ssh_cmd(import_cmd), capture_output=True, text=True, env=SSH_ENV) out = (res.stdout + res.stderr).replace(pw, "***") if res.returncode != 0: raise SystemExit(f"Import fehlgeschlagen (rc={res.returncode}):\n{out}")