Files
sshkeyvault/tests/geheimnisse-worker.test.ts
T
florian.wildeandClaude Opus 5.5 3b28f73c95 docs(betrieb): Handbuch, Bedrohungsanalyse, geprobte Sicherung (#029)
Handbuch mit Bildern fuer alle Ablaeufe, darunter der Kern-
Anwendungsfall und der Deployment-Zugang fuer Claude Code.
Betriebsdoku zu Installation, authentik und Reverse Proxy sowie eine
Bedrohungsanalyse mit Notfallplan fuer den Fall, dass sshkv selbst
kompromittiert wird.

Sicherung und Wiederherstellung als Skripte, auf einem Wegwerf-Stapel
geprobt. Dabei fielen zwei Fehler auf: das Schema der Warteschlange
liess sich nicht zurueckspielen, und ein Abbruch ohne Transaktion
loeschte es halb. Jetzt nur Schema public, in einer Transaktion, mit
Pruefwert des Master-Keys. Der Worker startet nicht, wenn der
Master-Key nicht zur Datenbank passt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 16:46:38 +02:00

95 lines
3.6 KiB
TypeScript

import { execFileSync } from "node:child_process";
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { randomBytes } from "node:crypto";
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
import { prisma } from "@/lib/db";
import { privateKeyLesen } from "@/lib/ssh/openssh";
import {
deploymentKeySicherstellen,
masterKeyVergessen,
privateKeyLaden,
privateKeyOeffnen,
} from "@/worker/geheimnisse";
import { datenbankLeeren } from "./hilfen/db";
/** Verschluesselte Ablage im Worker (#006). */
let verzeichnis: string;
beforeAll(() => {
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();
});
afterAll(() => {
rmSync(verzeichnis, { recursive: true, force: true });
});
beforeEach(async () => {
await datenbankLeeren();
});
describe("Deployment-Key", () => {
it("wird einmal erzeugt und danach wiederverwendet", async () => {
const [a, b] = await Promise.all([deploymentKeySicherstellen(), deploymentKeySicherstellen()]);
expect(a).toBe(b);
expect(await prisma.schluessel.count({ where: { kategorie: "DEPLOYMENT" } })).toBe(1);
expect(await deploymentKeySicherstellen()).toBe(a);
});
it("laesst sich oeffnen und passt zum Public Key", async () => {
const id = await deploymentKeySicherstellen();
const schluessel = await prisma.schluessel.findUniqueOrThrow({ where: { id } });
const klartext = await privateKeyLaden(id);
const info = privateKeyLesen(klartext.toString("utf8"));
klartext.fill(0);
expect(info.fingerprint).toBe(schluessel.fingerprint);
});
it("laesst sich nicht unter fremder ID oeffnen (Zusatzdaten)", async () => {
const id = await deploymentKeySicherstellen();
const schluessel = await prisma.schluessel.findUniqueOrThrow({ where: { id } });
await expect(privateKeyOeffnen({ ...schluessel, id: "andere-id" })).rejects.toThrow();
});
it("steht in keinem Datenbank-Dump im Klartext", async () => {
const id = await deploymentKeySicherstellen();
const klartext = (await privateKeyLaden(id)).toString("utf8");
// Der Teil zwischen Kopf und Fuss ist das eigentliche Geheimnis.
const kern = klartext.split("\n").slice(1, -2).join("");
const url = new URL(process.env["DATABASE_URL"] ?? "");
const dump = execFileSync(
"docker",
["exec", "sshkv-dev-db", "pg_dump", "-U", url.username, "--data-only", url.pathname.slice(1)],
{ encoding: "utf8", maxBuffer: 64 * 1024 * 1024 },
);
expect(dump).toContain("Deployment-Key der Instanz");
expect(dump).not.toContain("PRIVATE KEY");
expect(dump).not.toContain(kern.slice(0, 40));
});
});
describe("Master-Key-Pruefung beim Start (#029)", () => {
it("meldet einen falschen Master-Key verstaendlich", async () => {
const { masterKeyPruefen } = await import("@/worker/geheimnisse");
const id = await deploymentKeySicherstellen();
await expect(masterKeyPruefen(id)).resolves.toBeUndefined();
const falsch = path.join(verzeichnis, "falsch.key");
writeFileSync(falsch, randomBytes(32), { mode: 0o600 });
const richtig = process.env["SSHKV_MASTER_KEY_FILE"];
process.env["SSHKV_MASTER_KEY_FILE"] = falsch;
masterKeyVergessen();
try {
await expect(masterKeyPruefen(id)).rejects.toThrow(/passt nicht zur Datenbank/);
} finally {
process.env["SSHKV_MASTER_KEY_FILE"] = richtig;
masterKeyVergessen();
}
});
});