feat(schluessel): erzeugen, importieren, exportieren (#012)
Schluessel werden jetzt verwaltet statt nur im Prototyp gezeigt: Liste mit Filter, Anlegen (erzeugen oder importieren), Detail nach dem Prototyp aus #002, Bearbeiten der Metadaten und Download <slug>.pub. Warum so: - Erzeugen und Import eines Paars laufen als Vorgang im Worker, weil nur er den Master-Key hat. Passphrase und importierter Private Key reisen wie das Bootstrap-Passwort mit SSHKV_APP_SECRET verschluesselt im Job, nie in Vorgang.eingabe; der Worker loescht den Job danach. - Ein reiner Public Key ist kein Geheimnis und wird direkt im Web angelegt, mit verstaendlicher Meldung bei einem Duplikat. - Das Web liest nie das Chiffrat: ob ein Private Key gespeichert ist, verraet masterKeyVersion (per CHECK an das Chiffrat gekoppelt). Ein struktureller Test verlangt fuer jede zeilenliefernde Abfrage auf Schluessel ausserhalb des Workers ein festes select. - Migration: Vorgangsarten fuer Erzeugen, Import und (fuer #014) das Loeschen des Private Keys; CHECKs fuer den Slug (landet in Dateinamen) und "geloescht nur ohne Chiffrat". - Platzhalter PublicAusrollenAktion/PrivateAusrollenAktion fuer #016/#017. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
-- Schluessel erzeugen, importieren, Private Key loeschen (#012, #014).
|
||||
--
|
||||
-- Neue Vorgangsarten: Erzeugen und Import eines Paars laufen im Worker, weil nur er den
|
||||
-- Master-Key hat. Das Loeschen eines gespeicherten Private Keys laeuft ebenfalls dort, damit
|
||||
-- die Grenze „nur src/worker/geheimnisse.ts fasst das Chiffrat an“ einfach bleibt.
|
||||
ALTER TYPE "VorgangsArt" ADD VALUE 'SCHLUESSEL_ERZEUGEN';
|
||||
ALTER TYPE "VorgangsArt" ADD VALUE 'SCHLUESSEL_IMPORTIEREN';
|
||||
ALTER TYPE "VorgangsArt" ADD VALUE 'PRIVATE_KEY_LOESCHEN';
|
||||
|
||||
-- Der Slug landet in Dateinamen (<slug>.pub, ~/.ssh/id_<slug>) und Kommentaren. Was hier
|
||||
-- nicht passt, kommt nie bis in einen Pfad oder ein Shell-Skript.
|
||||
ALTER TABLE "Schluessel" ADD CONSTRAINT "schluessel_slug_gueltig" CHECK (
|
||||
"slug" ~ '^[a-z0-9-]{1,64}$'
|
||||
);
|
||||
|
||||
-- „Private Key geloescht“ und ein gespeichertes Chiffrat schliessen sich aus (#014).
|
||||
ALTER TABLE "Schluessel" ADD CONSTRAINT "schluessel_geloescht_ohne_private_key" CHECK (
|
||||
"privatGeloeschtAm" IS NULL OR "privatVerschluesselt" IS NULL
|
||||
);
|
||||
@@ -104,6 +104,12 @@ enum VorgangsArt {
|
||||
ROTATION
|
||||
WIDERRUF
|
||||
PRIVATE_KEY_ABHOLEN
|
||||
/// Schluesselpaar im Worker erzeugen und versiegeln (#012).
|
||||
SCHLUESSEL_ERZEUGEN
|
||||
/// Importiertes Paar im Worker pruefen und versiegeln (#012).
|
||||
SCHLUESSEL_IMPORTIEREN
|
||||
/// Gespeicherten Private Key endgueltig verwerfen (#014).
|
||||
PRIVATE_KEY_LOESCHEN
|
||||
}
|
||||
|
||||
enum VorgangsStatus {
|
||||
|
||||
@@ -124,10 +124,99 @@ async function demoDaten(): Promise<void> {
|
||||
console.log(`Demo-Daten geprüft: ${DEMO_HOSTS.length} Hosts, ${DEMO_GRUPPEN.length} Gruppen.`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Demo-Personen und -Schluessel (#012, #013). Nur Public Keys: einen Private Key kann nur
|
||||
* der Worker versiegeln. Die Paare werden hier frisch erzeugt und der Private Key sofort
|
||||
* verworfen – es sind Attrappen, die nirgends Zugang geben.
|
||||
*/
|
||||
const DEMO_PERSONEN = [
|
||||
{ name: "Florian Wilde", email: "florian@example.org", organisation: "IT" },
|
||||
{ name: "Anna Berger", email: "a.berger@netzwerk-gmbh.example", organisation: "Netzwerk GmbH" },
|
||||
{ name: "Mehmet Yılmaz", email: "mehmet@example.org", organisation: "IT" },
|
||||
] as const;
|
||||
|
||||
const TAG_MS = 24 * 60 * 60 * 1000;
|
||||
|
||||
const DEMO_SCHLUESSEL: {
|
||||
name: string;
|
||||
slug: string;
|
||||
kategorie: "PERSON" | "DIENST";
|
||||
person?: string;
|
||||
tags: string[];
|
||||
ablaufInTagen?: number;
|
||||
kommentar: string;
|
||||
}[] = [
|
||||
{
|
||||
name: "Laptop Florian",
|
||||
slug: "laptop-florian",
|
||||
kategorie: "PERSON",
|
||||
person: "Florian Wilde",
|
||||
tags: ["laptop"],
|
||||
kommentar: "florian@laptop",
|
||||
},
|
||||
{
|
||||
name: "Wartungszugang Netzwerk GmbH",
|
||||
slug: "wartung-netzwerk-gmbh",
|
||||
kategorie: "PERSON",
|
||||
person: "Anna Berger",
|
||||
tags: ["dienstleister"],
|
||||
ablaufInTagen: 18,
|
||||
kommentar: "anna@netzwerk-gmbh",
|
||||
},
|
||||
{
|
||||
name: "Workstation Mehmet",
|
||||
slug: "workstation-mehmet",
|
||||
kategorie: "PERSON",
|
||||
person: "Mehmet Yılmaz",
|
||||
tags: [],
|
||||
ablaufInTagen: 200,
|
||||
kommentar: "mehmet@ws-07",
|
||||
},
|
||||
{
|
||||
name: "Monitoring zabbix → alle Hosts",
|
||||
slug: "monitoring-zabbix",
|
||||
kategorie: "DIENST",
|
||||
tags: ["monitoring"],
|
||||
kommentar: "zabbix@srv-mon01",
|
||||
},
|
||||
];
|
||||
|
||||
async function demoSchluessel(): Promise<void> {
|
||||
if (process.env["SSHKV_SEED_DEMO"] !== "1") return;
|
||||
const { schluesselpaarErzeugen } = await import("../src/lib/ssh/openssh");
|
||||
const personen = new Map<string, string>();
|
||||
for (const p of DEMO_PERSONEN) {
|
||||
const vorhanden = await prisma.person.findFirst({ where: { name: p.name }, select: { id: true } });
|
||||
const person = vorhanden ?? (await prisma.person.create({ data: p, select: { id: true } }));
|
||||
personen.set(p.name, person.id);
|
||||
}
|
||||
for (const s of DEMO_SCHLUESSEL) {
|
||||
if (await prisma.schluessel.findUnique({ where: { slug: s.slug }, select: { id: true } })) continue;
|
||||
const paar = await schluesselpaarErzeugen("ED25519", { kommentar: s.kommentar });
|
||||
await prisma.schluessel.create({
|
||||
data: {
|
||||
name: s.name,
|
||||
slug: s.slug,
|
||||
typ: "ED25519",
|
||||
kategorie: s.kategorie,
|
||||
publicKey: paar.publicKeyZeile,
|
||||
fingerprint: paar.fingerprint,
|
||||
kommentar: s.kommentar,
|
||||
tags: s.tags,
|
||||
personId: s.person ? (personen.get(s.person) ?? null) : null,
|
||||
ablaufAm: s.ablaufInTagen ? new Date(Date.now() + s.ablaufInTagen * TAG_MS) : null,
|
||||
},
|
||||
select: { id: true },
|
||||
});
|
||||
}
|
||||
console.log(`Demo-Daten geprüft: ${DEMO_PERSONEN.length} Personen, ${DEMO_SCHLUESSEL.length} Schlüssel.`);
|
||||
}
|
||||
|
||||
try {
|
||||
await notfallAdmin();
|
||||
await vorlagen();
|
||||
await demoDaten();
|
||||
await demoSchluessel();
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user