Merge branch 'phase/032-nacharbeiten'
This commit is contained in:
@@ -2,6 +2,7 @@ import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { rotationsDatum } from "@/lib/rotation";
|
||||
import {
|
||||
rotationAbbrechen,
|
||||
rotationAltWiderrufen,
|
||||
rotationStarten,
|
||||
rotationUebernehmen,
|
||||
@@ -55,38 +56,44 @@ async function trockenlaufUndAusrollen(schluesselId: string) {
|
||||
expect(ausrollen.fehler).toBeNull();
|
||||
}
|
||||
|
||||
/** Dienst-Key wie nach dem Assistenten: Private Key auf A (Alias zu-b), Public Key auf B, ausgerollt. */
|
||||
async function alterSchluesselAusgerollt(aId: string, bId: string) {
|
||||
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: aId,
|
||||
zielbenutzer: "root",
|
||||
pfad: "~/.ssh/id_backup",
|
||||
sshConfig: { alias: "zu-b", hostName: "ziel-b", benutzer: "root", port: 22 },
|
||||
knownHostsFuer: [bId],
|
||||
},
|
||||
AKTEUR,
|
||||
);
|
||||
await publicVerwendungAnlegen({ schluesselId: alt.id, hostId: bId, zielbenutzer: "root" }, AKTEUR);
|
||||
await trockenlaufUndAusrollen(alt.id);
|
||||
return alt;
|
||||
}
|
||||
|
||||
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);
|
||||
const alt = await alterSchluesselAusgerollt(a.id, b.id);
|
||||
|
||||
// 1–2: Nachfolger erzeugen, Zuweisungen uebernehmen.
|
||||
await vorgangImWorker(await rotationStarten(alt.id, AKTEUR));
|
||||
@@ -123,4 +130,50 @@ describe("Rotation", () => {
|
||||
const ssh = await befehlAlsRoot(zielA, "ssh -o BatchMode=yes -o StrictHostKeyChecking=yes zu-b id -un");
|
||||
expect(ssh.stdout.trim()).toBe("root");
|
||||
});
|
||||
|
||||
it("bricht ab: der ausgerollte neue Schluessel verschwindet, der alte arbeitet weiter (#032)", async () => {
|
||||
const a = await eingerichteterHost(zielA, "ziel-a");
|
||||
const b = await eingerichteterHost(zielB, "ziel-b");
|
||||
const alt = await alterSchluesselAusgerollt(a.id, b.id);
|
||||
const altVorher = await prisma.schluessel.findUniqueOrThrow({ where: { id: alt.id } });
|
||||
|
||||
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 },
|
||||
});
|
||||
await trockenlaufUndAusrollen(neu.id);
|
||||
const neuerPfad = `id_backup-r${rotationsDatum().kompakt}`;
|
||||
expect((await befehlAlsRoot(zielA, "cat /root/.ssh/config")).stdout).toContain(
|
||||
`IdentityFile ~/.ssh/${neuerPfad}`,
|
||||
);
|
||||
|
||||
// Abbrechen = Widerruf des neuen Schluessels mit Dry-Run und Anwenden.
|
||||
await rotationAbbrechen(alt.id, AKTEUR, { privatErlaubt: true });
|
||||
await trockenlaufUndAusrollen(neu.id);
|
||||
expect((await widerrufAbschliessen([neu.id], AKTEUR))[0]?.status).toBe("WIDERRUFEN");
|
||||
|
||||
const authorized = (await befehlAlsRoot(zielB, "cat /root/.ssh/authorized_keys")).stdout;
|
||||
expect(authorized).not.toContain(neu.publicKey.split(" ")[1]!);
|
||||
expect(authorized).toContain(alt.publicKey.split(" ")[1]!);
|
||||
const dateien = (await befehlAlsRoot(zielA, "ls /root/.ssh")).stdout.split("\n");
|
||||
expect(dateien).toContain("id_backup");
|
||||
expect(dateien).not.toContain(neuerPfad);
|
||||
// Der Alias zeigt wieder auf die alte Datei – und funktioniert.
|
||||
const config = (await befehlAlsRoot(zielA, "cat /root/.ssh/config")).stdout;
|
||||
expect(config.match(/^Host zu-b$/gm)).toHaveLength(1);
|
||||
expect(config).toContain("IdentityFile ~/.ssh/id_backup\n");
|
||||
const ssh = await befehlAlsRoot(zielA, "ssh -o BatchMode=yes -o StrictHostKeyChecking=yes zu-b id -un");
|
||||
expect(ssh.stdout.trim()).toBe("root");
|
||||
|
||||
// Der alte Schluessel ist unveraendert, seine Zuweisungen sind ausgerollt.
|
||||
expect(await prisma.schluessel.findUniqueOrThrow({ where: { id: alt.id } })).toEqual(altVorher);
|
||||
const zustaende = [
|
||||
...(await prisma.publicVerwendung.findMany({ where: { schluesselId: alt.id } })),
|
||||
...(await prisma.privateVerwendung.findMany({ where: { schluesselId: alt.id } })),
|
||||
].map((v) => v.zustand);
|
||||
expect(zustaende).toEqual(["AUSGEROLLT", "AUSGEROLLT"]);
|
||||
expect((await rotationsStand(alt.id)).abgebrochen).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user