Files
bf-infra/routeros/archiv-tp15-cutover/restore-core.py
Christoph Emsenhuber 6f8ba8c8b5 sshpass -p repo-weit weg + vier TP15-Cutover-Skripte archiviert
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.
Nach gc-transaktion.py (voriger Commit) jetzt auch die fuenf unter routeros/:
SSHPASS-Env + sshpass -e.

Nachweis, gemessen statt gelesen:
- alle 13 subprocess.run mit sshpass tragen env= (per AST geprueft; ohne env
  waere SSHPASS leer und alles still kaputt)
- Signaturen passen zu den Aufrufen (AST-Arity; py_compile findet das NICHT)
- echter Lauf des gepatchten Codes gegen den lebenden Core: backup-core.py
  importiert, IP zur Laufzeit auf 10.40.99.1 -> "name: crs326-core", rc=0
- Passwort in argv? nein. In ps waehrend der Verbindung? nein, 40 Stichproben
- Negativkontrolle: derselbe Test mit einem Dummy-Wert ueber die alte -p-Form
  -> 36 Treffer. Die Probe ist also nicht blind, das "nein" hat Aussagekraft

Mitgefixt, weil es im umgebauten 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.

Fuer TP30 aendert das NICHTS: der Weg ist zu, der Wert bleibt verbrannt.
Posten #1 (MikroTik-admin) bleibt faellig. Im TP so hingeschrieben, damit es
niemand als erledigt liest.

--- Archivierung (Christoph-Entscheid) ---

Beim Fixen fiel auf, dass alle vier Cutover-Skripte auf tote Adressen zeigten:
  apply-core-cutover-l3.py  10.40.41.211    tot (VLAN41 seit 2026-08-27 weg)
  backup-core.py            10.40.41.211    tot
  restore-core.py           192.168.88.1    tot (Werks-Temp-Pfad)
  backup-ap1.py             10.40.10.1      ANTWORTET -- ist aber der CORE

backup-ap1.py war der gefaehrliche: er waere nicht gescheitert, er haette den
Core gesichert und die Datei ap1-pre-core-cutover-*.backup genannt. Wer das
spaeter zurueckspielt, spielt Core-Config auf einen AP.

Entscheid war archivieren, nicht reparieren -- der Cutover ist am 2026-08-08
durch, die Skripte haben ihre Aufgabe erfuellt. Sie liegen jetzt unter
routeros/archiv-tp15-cutover/ mit README nach dem Muster von switch/.
Die Pfad-Nebenwirkungen des Verschiebens sind ABSICHTLICH nicht repariert
(backup-core.py schriebe jetzt ins Archivverzeichnis, apply-core-cutover-l3.py
faende seine .rsc nicht mehr) -- sie zu reparieren hiesse, die Skripte wieder
lauffaehig zu machen, und das ist gerade nicht die Absicht.

Der sshpass-Fix wurde trotzdem VOR dem Verschieben gemacht: Archiv-Code wird
abgeschrieben, also soll dort das richtige Muster stehen.

import-core-rsc.py bleibt, wo es ist -- als einziges generisch nuetzlich. Sein
Default-Host ist ebenfalls tot (--host mitgeben), und in --host steckt ein
zweiter latenter Bug: sys.argv[1] wird als RSC-Pfad gelesen, bevor auf --host
geprueft wird. NICHT gefixt, eigene Runde.

Zeiger gesetzt in: CLAUDE.md (sshpass-Gotcha), teilprojekte.md (TP30),
teilprojekte-archiv.md (TP15), Handoff 2026-08-31.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 08:34:08 +02:00

95 lines
3.0 KiB
Python

#!/usr/bin/env python3
"""Stelle das crs326-core Backup vom Cutover-Stand wieder her.
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 <PW>` 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
CORE_IP = "192.168.88.1"
BACKUP = "crs326-core-pre-cutover-20260806-073712.backup"
CLAUDE_JSON = pathlib.Path.home() / ".claude.json"
OUT_DIR = pathlib.Path(__file__).resolve().parent
def load_core_creds():
cfg = json.loads(CLAUDE_JSON.read_text())
env = cfg.get("mcpServers", {}).get("mikromcp", {}).get("env", {})
user = env.get("CRS326_CORE_USER") or env.get("ROUTER_USER")
pw = env.get("CRS326_CORE_PASS") or env.get("ROUTER_PASS")
if not user or not pw:
raise SystemExit("CRS326_CORE_USER/PASS nicht in ~/.claude.json mikromcp.env gefunden")
return 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_cmd(user, remote_command):
return [
"sshpass", "-e",
"ssh",
"-o", "StrictHostKeyChecking=no",
"-o", "UserKnownHostsFile=/dev/null",
"-o", "PubkeyAuthentication=no",
f"{user}@{CORE_IP}",
remote_command,
]
def scp_cmd(user, local_file, remote_file):
return [
"sshpass", "-e",
"scp",
"-o", "StrictHostKeyChecking=no",
"-o", "UserKnownHostsFile=/dev/null",
"-o", "PubkeyAuthentication=no",
str(local_file),
f"{user}@{CORE_IP}:{remote_file}",
]
def run_silent(cmd, pw):
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}")
return r
def main():
local_backup = OUT_DIR / BACKUP
if not local_backup.exists():
raise SystemExit(f"{local_backup} fehlt")
user, pw = load_core_creds()
print(f"crs326-core Restore via SSH {user}@{CORE_IP}")
print(f" Lade {BACKUP} hoch ...")
run_silent(scp_cmd(user, local_backup, BACKUP), pw)
print(f" Spiele Backup zurueck ...")
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 ...")
if __name__ == "__main__":
main()