feat: SSH-Key-Auth als primäre Methode, Bitwarden als Fallback

- Neuer Step I (ssh_key_versuch.py): liest SSH-Keys aus DB, testet
  Verbindung per paramiko; erfolgreiche Server in server_creds,
  fehlgeschlagene in needs_bitwarden
- Step G (Bitwarden) ist jetzt No-Op wenn alle Server per Key OK
- paramiko.DSSKey in allen Dateien entfernt (nicht in paramiko 4.0)
- failure_module (flow_fehler_handler.py): sendet bei jedem Flow-Fehler
  eine Nextcloud-Talk-Nachricht und bereinigt DB/Session
- Bitwarden-Step überspringt fehlgeschlagene Server statt abzubrechen

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Sebastian Serfling
2026-04-29 21:51:46 +02:00
parent 59d2d49ba1
commit 02bed956cf
5 changed files with 38 additions and 16 deletions
@@ -2,7 +2,7 @@ import wmill, json, paramiko, io, mysql.connector
def _load_pkey(key_str: str):
for cls in [paramiko.RSAKey, paramiko.Ed25519Key, paramiko.ECDSAKey, paramiko.DSSKey]:
for cls in [paramiko.RSAKey, paramiko.Ed25519Key, paramiko.ECDSAKey]:
try:
return cls.from_private_key(io.StringIO(key_str))
except Exception:
@@ -1,7 +1,7 @@
import wmill, json, paramiko, io, mysql.connector, re
def _load_pkey(key_str: str):
for cls in [paramiko.RSAKey, paramiko.Ed25519Key, paramiko.ECDSAKey, paramiko.DSSKey]:
for cls in [paramiko.RSAKey, paramiko.Ed25519Key, paramiko.ECDSAKey]:
try:
return cls.from_private_key(io.StringIO(key_str))
except Exception:
@@ -75,14 +75,31 @@ def main(
raise Exception("Vault konnte nicht entsperrt werden")
env["BW_SESSION"] = bw_session
server_creds = prev.get("server_creds", {})
server_creds = prev.get("server_creds", {})
failed_servers = []
for server in servers:
hostname = server["hostname"]
print(f"Hole Creds fuer: {hostname}")
creds = bw_lookup(hostname, env, run)
server_creds[hostname] = creds
print(f" -> OK: {creds['username']}@{hostname}")
try:
creds = bw_lookup(hostname, env, run)
server_creds[hostname] = creds
print(f" -> OK: {creds['username']}@{hostname}")
except Exception as e:
print(f" -> WARNUNG: {e} Server wird übersprungen")
failed_servers.append(hostname)
run(["bw", "logout"], check=False)
if failed_servers:
print(f"\nWARNUNG: Keine Credentials für: {failed_servers}")
# Server aus target_servers entfernen damit C/D sie nicht anfassen
remaining = [s for s in prev.get("target_servers", [])
if s["hostname"] not in failed_servers]
if not remaining:
raise Exception(
f"Keine Restore-Server verfügbar Bitwarden-Lookup fehlgeschlagen für: {failed_servers}"
)
return {**prev, "server_creds": server_creds, "target_servers": remaining}
return {**prev, "server_creds": server_creds}
@@ -1,7 +1,7 @@
import wmill, mysql.connector, json, paramiko, io
def _load_pkey(key_str: str):
for cls in [paramiko.RSAKey, paramiko.Ed25519Key, paramiko.ECDSAKey, paramiko.DSSKey]:
for cls in [paramiko.RSAKey, paramiko.Ed25519Key, paramiko.ECDSAKey]:
try:
return cls.from_private_key(io.StringIO(key_str))
except Exception:
@@ -19,14 +19,19 @@ def main(prev: dict):
hostnames = [s["hostname"] for s in servers]
placeholders = ",".join(["%s"] * len(hostnames))
cur.execute(f"""
SELECT hostname, ip, ssh_private_key, ssh_key_user
FROM Kunden.`bronze.restore.server`
WHERE hostname IN ({placeholders})
AND ssh_private_key IS NOT NULL
AND ssh_private_key != ''
""", hostnames)
key_rows = {row["hostname"]: row for row in cur.fetchall()}
try:
cur.execute(f"""
SELECT hostname, ip, ssh_private_key, ssh_key_user
FROM Kunden.`bronze.restore.server`
WHERE hostname IN ({placeholders})
AND ssh_private_key IS NOT NULL
AND ssh_private_key != ''
""", hostnames)
key_rows = {row["hostname"]: row for row in cur.fetchall()}
except Exception as e:
print(f"WARNUNG: SSH-Key-Spalten nicht in DB vorhanden ({e})")
print("Alle Server werden über Bitwarden authentifiziert.")
key_rows = {}
cur.close(); conn.close()
server_creds = {}
@@ -1,7 +1,7 @@
import wmill, json, mysql.connector, paramiko, io, re, base64
def _load_pkey(key_str: str):
for cls in [paramiko.RSAKey, paramiko.Ed25519Key, paramiko.ECDSAKey, paramiko.DSSKey]:
for cls in [paramiko.RSAKey, paramiko.Ed25519Key, paramiko.ECDSAKey]:
try:
return cls.from_private_key(io.StringIO(key_str))
except Exception: