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>
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
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 } };
|
||||
};
|
||||
@@ -2,6 +2,7 @@ import type { HandlerTabelle } from "@/worker/ausfuehren";
|
||||
import { ausrollenHandler, trockenlaufHandler } from "@/worker/ablaeufe/ausrollen";
|
||||
import { bootstrapHandler } from "@/worker/ablaeufe/bootstrap";
|
||||
import { erreichbarkeitHandler, hostKeyAbrufenHandler } from "@/worker/ablaeufe/hostkey";
|
||||
import { verbindungstestHandler } from "@/worker/ablaeufe/verbindungstest";
|
||||
|
||||
/**
|
||||
* Welche Vorgangsart wer ausfuehrt. Waechst mit den Issues: Abgleich (#021) …
|
||||
@@ -12,4 +13,5 @@ export const HANDLER: HandlerTabelle = {
|
||||
BOOTSTRAP: bootstrapHandler,
|
||||
TROCKENLAUF: trockenlaufHandler,
|
||||
AUSROLLEN: ausrollenHandler,
|
||||
VERBINDUNGSTEST: verbindungstestHandler,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user