Files
florian.wildeandClaude Opus 5.5 cab6f45510 feat(deploymentkey): geführte Rotation des Deployment-Keys (#031)
Bisher ließ sich der Instanz-Key nur von Hand auf jedem Host tauschen.
Die Rotation läuft jetzt in fünf Schritten: Nachfolger erzeugen, per
Dry-Run neben den alten verteilen, Anmeldung je Host prüfen, umstellen,
alten Key aus allen Blöcken entfernen und seinen Private Key verwerfen.

Der Nachfolger steht während der Rotation in Block und Pflichtliste,
damit kein Host ohne funktionierenden Key dasteht. Umgestellt wird nur
nach erfolgreicher Prüfung oder bewusstem Übergehen eines Hosts.

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

70 lines
2.4 KiB
TypeScript

import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { randomBytes } from "node:crypto";
import { tmpdir } from "node:os";
import path from "node:path";
import { prisma } from "@/lib/db";
import { verschluesseln } from "@/lib/geheimnisse";
import { vorgangAusfuehren } from "@/worker/ausfuehren";
import { masterKeyVergessen } from "@/worker/geheimnisse";
import { HANDLER } from "@/worker/handler";
import { rootPasswort, type Ziel } from "./ziele";
/** Gemeinsame Vorbereitung fuer Integrationstests: Master-Key, Host mit Bootstrap (#016). */
export function masterKeyAnlegen(): () => void {
const verzeichnis = mkdtempSync(path.join(tmpdir(), "sshkv-master-"));
const datei = path.join(verzeichnis, "master.key");
writeFileSync(datei, randomBytes(32), { mode: 0o600 });
process.env["SSHKV_MASTER_KEY_FILE"] = datei;
masterKeyVergessen();
return () => rmSync(verzeichnis, { recursive: true, force: true });
}
export async function vorgangLaufen(
art:
| "HOSTKEY_ABRUFEN"
| "BOOTSTRAP"
| "TROCKENLAUF"
| "AUSROLLEN"
| "VERBINDUNGSTEST"
| "ZIELKONTO_ANLEGEN"
| "ZIELKONTO_ENTFERNEN"
| "ABGLEICH"
| "ROTATION",
daten: {
hostId?: string;
eingabe?: object;
geheim?: string;
angefordertVonId?: string;
},
) {
const vorgang = await prisma.vorgang.create({
data: {
art,
hostId: daten.hostId ?? null,
eingabe: daten.eingabe ?? {},
angefordertVonId: daten.angefordertVonId ?? null,
},
});
await vorgangAusfuehren(vorgang.id, HANDLER, daten.geheim);
return prisma.vorgang.findUniqueOrThrow({ where: { id: vorgang.id } });
}
/** Host anlegen, Host-Key vertrauen, Bootstrap per root-Passwort. */
export async function eingerichteterHost(
ziel: Ziel,
name: string,
anmeldeart: "DIENSTBENUTZER" | "ROOT" = "DIENSTBENUTZER",
) {
const host = await prisma.host.create({ data: { name, adresse: ziel.host, port: ziel.port, anmeldeart } });
await vorgangLaufen("HOSTKEY_ABRUFEN", { hostId: host.id });
await prisma.hostSchluessel.updateMany({ where: { hostId: host.id }, data: { status: "VERTRAUT" } });
const bootstrap = await vorgangLaufen("BOOTSTRAP", {
hostId: host.id,
eingabe: { modus: "passwort", benutzer: "root" },
geheim: verschluesseln(rootPasswort()),
});
if (bootstrap.status !== "ERLEDIGT") throw new Error(`Bootstrap fehlgeschlagen: ${bootstrap.fehler}`);
return prisma.host.findUniqueOrThrow({ where: { id: host.id } });
}