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>
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
import { CheckCheck, KeyRound, ShieldCheck } from "lucide-react";
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import { AusrollenPanel } from "@/components/ausrollen/AusrollenPanel";
|
||||
import { Umstellen } from "@/components/deploymentkey/Umstellen";
|
||||
import { Badge } from "@/components/ui/Badge";
|
||||
import { Breadcrumb } from "@/components/ui/Breadcrumb";
|
||||
import { Karte } from "@/components/ui/Karte";
|
||||
import { MonoKopieren } from "@/components/ui/MonoKopieren";
|
||||
import { Schrittleiste } from "@/components/ui/Schrittleiste";
|
||||
import { Seitenkopf } from "@/components/ui/Seitenkopf";
|
||||
import { VorgangsKnopf } from "@/components/vorgang/VorgangsKnopf";
|
||||
import { rotationsSchrittAktion } from "@/server/actions/deploymentkey";
|
||||
import { bestaetigungFrisch, sitzungErforderlich } from "@/server/auth/sitzungspflicht";
|
||||
import { rotationsStand } from "@/server/services/deploymentkey";
|
||||
|
||||
export const metadata: Metadata = { title: "Deployment-Key rotieren" };
|
||||
|
||||
const SCHRITTE = ["Erzeugen", "Verteilen", "Prüfen", "Umstellen", "Aufräumen"] as const;
|
||||
|
||||
/**
|
||||
* Rotation des Instanz-Deployment-Keys (#031). Der neue Key kommt erst neben den alten,
|
||||
* wird geprueft und erst dann zum Standard; der alte verschwindet zuletzt.
|
||||
*/
|
||||
export default async function DeploymentKeySeite() {
|
||||
const { benutzer } = await sitzungErforderlich("deploymentKeyVerwalten", "/deployment-key");
|
||||
const stand = await rotationsStand();
|
||||
const fehlgeschlagen = (stand.pruefung ?? []).filter((p) => !p.ok);
|
||||
const aktuell =
|
||||
stand.phase === "bereit"
|
||||
? 0
|
||||
: stand.phase === "aufraeumen"
|
||||
? 4
|
||||
: !stand.pruefung
|
||||
? 1
|
||||
: fehlgeschlagen.length
|
||||
? 2
|
||||
: 3;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6 sm:gap-7">
|
||||
<Seitenkopf
|
||||
oben={
|
||||
<Breadcrumb
|
||||
pfad={[{ titel: "Einstellungen", href: "/einstellungen" }, { titel: "Deployment-Key" }]}
|
||||
/>
|
||||
}
|
||||
titel="Deployment-Key rotieren"
|
||||
untertitel="Ohne Unterbrechung: der neue Key steht erst neben dem alten, wird geprüft und löst ihn dann ab."
|
||||
/>
|
||||
<Schrittleiste schritte={SCHRITTE} aktuell={aktuell} beschriftung="Schritte der Rotation" />
|
||||
|
||||
{!bestaetigungFrisch(benutzer.bestaetigtAm) && (
|
||||
<p className="text-accent text-[13.5px]">
|
||||
Jeder Schritt verlangt eine frische Bestätigung Ihrer Anmeldung.{" "}
|
||||
<Link href="/bestaetigen?ziel=%2Fdeployment-key" className="underline">
|
||||
Jetzt bestätigen
|
||||
</Link>
|
||||
</p>
|
||||
)}
|
||||
|
||||
<Karte titel="Aktueller Deployment-Key" untertitel={`Benutzt auf ${stand.hosts.length} Host(s)`}>
|
||||
{stand.aktuell ? (
|
||||
<MonoKopieren
|
||||
wert={stand.aktuell.fingerprint}
|
||||
beschriftung="Fingerprint des aktuellen Keys"
|
||||
umbrechen
|
||||
/>
|
||||
) : (
|
||||
<p className="text-accent text-[14px]">Noch kein Deployment-Key – läuft der Worker?</p>
|
||||
)}
|
||||
{stand.nachfolger && (
|
||||
<div className="mt-4">
|
||||
<p className="text-muted mb-1 text-[13px]">Neuer Key (noch nicht Standard)</p>
|
||||
<MonoKopieren
|
||||
wert={stand.nachfolger.fingerprint}
|
||||
beschriftung="Fingerprint des neuen Keys"
|
||||
umbrechen
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Karte>
|
||||
|
||||
{stand.phase === "bereit" && stand.aktuell && (
|
||||
<Karte
|
||||
titel="1 · Neuen Key erzeugen"
|
||||
untertitel="Der Worker erzeugt ein neues ed25519-Paar. Noch ändert sich auf keinem Host etwas."
|
||||
>
|
||||
<VorgangsKnopf
|
||||
aktion={rotationsSchrittAktion.bind(null, "erzeugen")}
|
||||
beschriftung="Neuen Deployment-Key erzeugen"
|
||||
variante="primaer"
|
||||
icon={<KeyRound className="size-4" />}
|
||||
/>
|
||||
</Karte>
|
||||
)}
|
||||
|
||||
{stand.phase === "verteilen" && (
|
||||
<>
|
||||
<Karte
|
||||
titel="2 · Neuen Key verteilen"
|
||||
untertitel="Der neue Key kommt zusätzlich in den verwalteten Block – der alte bleibt vorerst."
|
||||
>
|
||||
<AusrollenPanel auswahl={{ deploymentKey: true }} offen={stand.hosts.length} />
|
||||
</Karte>
|
||||
<Karte
|
||||
titel="3 · Anmeldung mit dem neuen Key prüfen"
|
||||
untertitel="sshkv meldet sich auf jedem Host einmal mit dem neuen Key an."
|
||||
>
|
||||
<VorgangsKnopf
|
||||
aktion={rotationsSchrittAktion.bind(null, "pruefen")}
|
||||
beschriftung="Neuen Key prüfen"
|
||||
icon={<ShieldCheck className="size-4" />}
|
||||
/>
|
||||
{stand.pruefung && (
|
||||
<ul className="mt-4 flex flex-col gap-1.5" aria-label="Ergebnis der Prüfung">
|
||||
{stand.pruefung.map((p) => (
|
||||
<li key={p.hostId} className="flex flex-wrap items-center gap-2 text-[14px]">
|
||||
<span className="font-mono">{p.hostName}</span>
|
||||
{p.ok ? (
|
||||
<Badge variante="ok">angemeldet</Badge>
|
||||
) : (
|
||||
<Badge variante="gefahr">fehlgeschlagen</Badge>
|
||||
)}
|
||||
{p.fehler && <span className="text-muted text-[13px]">{p.fehler}</span>}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</Karte>
|
||||
{stand.pruefung && (
|
||||
<Karte
|
||||
titel="4 · Umstellen"
|
||||
untertitel="Ab jetzt benutzt sshkv den neuen Key; der alte gilt als rotiert."
|
||||
>
|
||||
<Umstellen fehlgeschlagen={fehlgeschlagen} />
|
||||
</Karte>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{stand.phase === "aufraeumen" && stand.vorgaenger && (
|
||||
<Karte
|
||||
titel="5 · Alten Key entfernen"
|
||||
untertitel="Dry-Run und Ausrollen entfernen den alten Key aus allen Blöcken; danach wird sein Private Key verworfen."
|
||||
>
|
||||
<div className="flex flex-col gap-5">
|
||||
<p className="text-muted text-[13px]">
|
||||
Alter Key: <span className="font-mono">{stand.vorgaenger.fingerprint}</span>
|
||||
</p>
|
||||
<AusrollenPanel auswahl={{ deploymentKey: true }} offen={stand.hosts.length} />
|
||||
<div>
|
||||
<VorgangsKnopf
|
||||
aktion={rotationsSchrittAktion.bind(null, "abschliessen")}
|
||||
beschriftung="Rotation abschließen"
|
||||
variante="primaer"
|
||||
icon={<CheckCheck className="size-4" />}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Karte>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user