Eine begonnene Rotation ließ sich nicht zurücknehmen; nahm ein Host den neuen Key dauerhaft nicht an, blieb nur das Übergehen. Abbrechen verwirft jetzt den neuen Key samt Private Key, ein Dry-Run nimmt ihn wieder von den Hosts, und eine neue Rotation kann sofort starten. Nebenbei: Aktionen der Benutzerverwaltung (#035) bekommen im Audit-Log eine eigene Gruppe, statt den Filter „Anmeldung“ zu verwässern; der E2E-Test für lokale Konten hängt nicht mehr von der Reihenfolge ab. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
188 lines
7.4 KiB
TypeScript
188 lines
7.4 KiB
TypeScript
import { CheckCheck, KeyRound, ShieldCheck, Undo2 } 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.abgebrochen && (
|
||
<Karte
|
||
titel="Abgebrochene Rotation aufräumen"
|
||
untertitel="Der verworfene Key kann noch in den Blöcken stehen. Ein Dry-Run zeigt es; steht alles auf Soll, ist nichts mehr zu tun."
|
||
>
|
||
<p className="text-muted mb-4 font-mono text-[13px] break-all">{stand.abgebrochen.fingerprint}</p>
|
||
<AusrollenPanel auswahl={{ deploymentKey: true }} offen={0} />
|
||
</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>
|
||
)}
|
||
<Karte
|
||
titel="Rotation abbrechen"
|
||
untertitel="Solange nicht umgestellt ist: der neue Key wird verworfen, der alte bleibt unverändert. Danach per Dry-Run den neuen wieder von den Hosts entfernen."
|
||
className="border-danger-border"
|
||
>
|
||
<VorgangsKnopf
|
||
aktion={rotationsSchrittAktion.bind(null, "abbrechen")}
|
||
beschriftung="Rotation abbrechen"
|
||
variante="gefahr"
|
||
icon={<Undo2 className="size-4" />}
|
||
/>
|
||
</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>
|
||
);
|
||
}
|