import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest"; import { prisma } from "@/lib/db"; import { rotationsDatum } from "@/lib/rotation"; import { rotationAltWiderrufen, rotationStarten, rotationUebernehmen, rotationsStand, } from "@/server/services/rotation"; import { zieleVonPrivatVerwendungen, zieleVonVerwendungen } from "@/server/services/soll"; import { privateVerwendungAnlegen, publicVerwendungAnlegen } from "@/server/services/verwendungen"; import { widerrufAbschliessen } from "@/server/services/widerruf"; import { deploymentKeySicherstellen, schluesselpaarVersiegeltErzeugen } from "@/worker/geheimnisse"; import { datenbankLeeren } from "../hilfen/db"; import { vorgangImWorker, warteschlangeStoppen } from "../hilfen/vorgang"; import { eingerichteterHost, masterKeyAnlegen, vorgangLaufen } from "./hilfen/host"; import { befehlAlsRoot, zielA, zielB, zielBereit } from "./hilfen/ziele"; /** * Rotation gegen echte Hosts (#023): der neue Schluessel funktioniert (Verbindungstest gruen), * der alte ist danach von beiden Hosts verschwunden, und `ssh ` von A geht weiter – * derselbe Alias zeigt jetzt auf die neue Datei. */ const AKTEUR = { id: "", name: "Test-Admin" }; let aufraeumen: () => void; beforeAll(async () => { await Promise.all([zielBereit(zielA), zielBereit(zielB)]); aufraeumen = masterKeyAnlegen(); }); afterAll(async () => { aufraeumen(); await warteschlangeStoppen(); }); beforeEach(async () => { await datenbankLeeren(); await deploymentKeySicherstellen(); const reset = "userdel -r sshkv 2>/dev/null; rm -f /etc/sudoers.d/sshkv*; rm -rf /root/.ssh; true"; await Promise.all([befehlAlsRoot(zielA, reset), befehlAlsRoot(zielB, reset)]); AKTEUR.id = (await prisma.benutzer.create({ data: { name: "Test-Admin", rolle: "ADMIN" } })).id; }); async function trockenlaufUndAusrollen(schluesselId: string) { const ziele = [ ...(await zieleVonVerwendungen({ schluesselId })), ...(await zieleVonPrivatVerwendungen({ schluesselId })), ]; const trockenlauf = await vorgangLaufen("TROCKENLAUF", { eingabe: { ziele }, angefordertVonId: AKTEUR.id }); expect(trockenlauf.status).toBe("ERLEDIGT"); const ausrollen = await vorgangLaufen("AUSROLLEN", { eingabe: { trockenlaufId: trockenlauf.id }, angefordertVonId: AKTEUR.id, }); expect(ausrollen.fehler).toBeNull(); } describe("Rotation", () => { it("tauscht den Schluessel aus, ohne dass der Alias aufhoert zu funktionieren", async () => { const a = await eingerichteterHost(zielA, "ziel-a"); const b = await eingerichteterHost(zielB, "ziel-b"); const paar = await schluesselpaarVersiegeltErzeugen("ED25519", "sshkv:backup"); const alt = await prisma.schluessel.create({ data: { id: paar.id, name: "Backup", slug: "backup", typ: "ED25519", kategorie: "DIENST", publicKey: paar.publicKeyZeile, fingerprint: paar.fingerprint, kommentar: "sshkv:backup", ...paar.versiegelt, }, select: { id: true, publicKey: true }, }); await privateVerwendungAnlegen( { schluesselId: alt.id, hostId: a.id, zielbenutzer: "root", pfad: "~/.ssh/id_backup", sshConfig: { alias: "zu-b", hostName: "ziel-b", benutzer: "root", port: 22 }, knownHostsFuer: [b.id], }, AKTEUR, ); await publicVerwendungAnlegen({ schluesselId: alt.id, hostId: b.id, zielbenutzer: "root" }, AKTEUR); await trockenlaufUndAusrollen(alt.id); // 1–2: Nachfolger erzeugen, Zuweisungen uebernehmen. await vorgangImWorker(await rotationStarten(alt.id, AKTEUR)); await rotationUebernehmen(alt.id, AKTEUR, { privatErlaubt: true }); const neu = await prisma.schluessel.findFirstOrThrow({ where: { vorgaengerId: alt.id }, select: { id: true, publicKey: true }, }); // 3: ausrollen – beide Schluessel liegen jetzt nebeneinander, der Alias zeigt auf den neuen. await trockenlaufUndAusrollen(neu.id); const neuerPfad = `id_backup-r${rotationsDatum().kompakt}`; const config = (await befehlAlsRoot(zielA, "cat /root/.ssh/config")).stdout; expect(config.match(/^Host zu-b$/gm)).toHaveLength(1); expect(config).toContain(`IdentityFile ~/.ssh/${neuerPfad}`); // 4: Verbindungstest mit dem neuen Schluessel. const verwendung = await prisma.privateVerwendung.findFirstOrThrow({ where: { schluesselId: neu.id } }); const test = await vorgangLaufen("VERBINDUNGSTEST", { eingabe: { privatVerwendungId: verwendung.id } }); expect(test.fehler).toBeNull(); expect((await rotationsStand(alt.id)).schritt).toBe(4); // 5: alten widerrufen. await rotationAltWiderrufen(alt.id, AKTEUR, { privatErlaubt: true }); await trockenlaufUndAusrollen(alt.id); expect((await widerrufAbschliessen([alt.id], AKTEUR))[0]?.status).toBe("ROTIERT"); const authorized = (await befehlAlsRoot(zielB, "cat /root/.ssh/authorized_keys")).stdout; expect(authorized).not.toContain(alt.publicKey.split(" ")[1]!); expect(authorized).toContain(neu.publicKey.split(" ")[1]!); const dateien = (await befehlAlsRoot(zielA, "ls /root/.ssh")).stdout.split("\n"); expect(dateien).not.toContain("id_backup"); expect(dateien).toContain(neuerPfad); const ssh = await befehlAlsRoot(zielA, "ssh -o BatchMode=yes -o StrictHostKeyChecking=yes zu-b id -un"); expect(ssh.stdout.trim()).toBe("root"); }); });