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>
75 lines
3.2 KiB
TypeScript
75 lines
3.2 KiB
TypeScript
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 } };
|
||
};
|