import { prisma } from "@/lib/db"; import { shellQuote } from "@/lib/shell"; import { privatPfadPruefen } from "@/lib/ssh/ssh-config"; import { protokollieren } from "@/server/services/protokoll"; import { VorgangsFehler, type Handler } from "@/worker/ausfuehren"; import { hostVerbinden } from "@/worker/ssh/host"; import { befehlAusfuehren } from "@/worker/ssh/verbindung"; /** * Verbindungstest Host A → Host B (#020): auf A als Zielbenutzer * `ssh -o BatchMode=yes -o StrictHostKeyChecking=yes true`. * * BatchMode verhindert jede Rueckfrage (Passwort, Passphrase), StrictHostKeyChecking jede * stille Annahme eines Host-Keys – der Test beweist also genau das, was ein Cronjob auf A * spaeter auch braucht. */ export const verbindungstestHandler: Handler = async ({ vorgang }) => { const verwendungId = (vorgang.eingabe as { privatVerwendungId?: unknown } | null)?.privatVerwendungId; if (typeof verwendungId !== "string") throw new VorgangsFehler("Kein Private-Key-Ziel angegeben."); const verwendung = await prisma.privateVerwendung.findUnique({ where: { id: verwendungId }, include: { host: true, schluessel: { select: { id: true, fingerprint: true } } }, }); if (!verwendung || verwendung.zustand !== "AUSGEROLLT") { throw new VorgangsFehler("Der Private Key ist auf diesem Host (noch) nicht ausgerollt."); } const konfiguration = verwendung.sshConfig as { alias?: string; hostName?: string; benutzer?: string; port?: number; } | null; const ziel = konfiguration?.alias ? shellQuote(konfiguration.alias) : (() => { throw new VorgangsFehler("Für den Test braucht die Zuweisung einen ~/.ssh/config-Eintrag (Alias)."); })(); privatPfadPruefen(verwendung.pfad); const ssh = `ssh -o BatchMode=yes -o StrictHostKeyChecking=yes -o ConnectTimeout=10 ${ziel} true`; const praefix = verwendung.host.anmeldeart === "ROOT" ? "" : "sudo -n "; const befehl = `${praefix}runuser -u ${shellQuote(verwendung.zielbenutzer)} -- sh -c ${shellQuote(`cd ~ && ${ssh}`)}`; const verbindung = await hostVerbinden(verwendung.host); let ergebnis; try { ergebnis = await befehlAusfuehren(verbindung, befehl); } finally { verbindung.end(); } const erfolg = ergebnis.code === 0; // Die letzte Zeile von ssh auf stderr ist die aussagekraeftige (Permission denied, Host key …). const meldung = ergebnis.stderr.trim().split("\n").filter(Boolean).pop()?.slice(0, 300) ?? ""; await protokollieren({ akteur: "System", aktion: "verbindungstest", ergebnis: erfolg ? "ERFOLG" : "FEHLER", hostName: verwendung.host.name, zielbenutzer: verwendung.zielbenutzer, fingerprint: verwendung.schluessel.fingerprint, details: { alias: konfiguration?.alias ?? null, meldung }, }); if (!erfolg) { throw new VorgangsFehler( `Verbindung von ${verwendung.zielbenutzer}@${verwendung.host.name} nach ${konfiguration?.alias} fehlgeschlagen: ${meldung || `Exit-Code ${ergebnis.code}`}`, ); } await prisma.schluessel.update({ where: { id: verwendung.schluessel.id }, data: { zuletztVerwendetAm: new Date() }, }); return { ergebnis: { erfolg: true, alias: konfiguration?.alias ?? null } }; };