Files
sshkeyvault/src/worker/ablaeufe/verbindungstest.ts
T
florian.wildeandClaude Opus 5.5 61a52d4538 feat(ausrollen): Verbindungstest von Host A nach Host B (#020)
Auf A als Zielbenutzer ssh mit BatchMode und StrictHostKeyChecking:
keine Rueckfrage, keine stille Annahme eines Host-Keys. Der Test
beweist damit genau das, was ein Cronjob auf A spaeter braucht. Die
letzte Meldung von ssh landet in Vorgang und Protokoll; ein Erfolg
setzt die letzte Verwendung des Schluessels.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 10:50:43 +02:00

75 lines
3.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 <alias> 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 } };
};