feat(rotation): gefuehrte Rotation mit Test vor dem Widerruf (#023)
Einen Schlüssel zu tauschen soll keinen Ausfall riskieren: Der Nachfolger übernimmt alle aktiven Zuweisungen, wird ausgerollt und per Verbindungstest geprüft – erst wenn alle Tests seit dem letzten Ausrollen grün sind, entfernt der Widerruf aus #024 den alten Schlüssel, der dann als ROTIERT endet. Private Keys bekommen einen neuen Pfad (-rJJJJMMTT) bei gleichem Alias. Damit ssh den neuen nimmt, schreibt privat.ts je Alias nur noch einen config-Eintrag; die jüngste Zuweisung gewinnt. Der Worker setzt vorgaengerId schon beim Erzeugen, so ist die Kette auch nach einem Abbruch vollständig und die Schrittfolge wiederaufnehmbar. Personen-Keys ohne Private Key in sshkv bekommen nur einen Hinweis: dafür braucht es einen neuen Public Key von der Person. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import {
|
||||
anmelden,
|
||||
archivieren,
|
||||
aufZiel,
|
||||
hostEinrichten,
|
||||
kernAufbauen,
|
||||
PORT,
|
||||
zieleZuruecksetzen,
|
||||
} from "./hilfen";
|
||||
|
||||
/**
|
||||
* Gefuehrte Rotation im Browser (#023), gebauter Stand + Worker: Ausgangslage ist der
|
||||
* Kern-Anwendungsfall. Neuer Schluessel → Zuweisungen uebernehmen → ausrollen → Verbindung
|
||||
* testen → alten widerrufen. `ssh zu-b` von A funktioniert vorher und nachher.
|
||||
*/
|
||||
test.setTimeout(300_000);
|
||||
|
||||
test.beforeAll(() => zieleZuruecksetzen());
|
||||
|
||||
test("Rotation mit Schrittanzeige", async ({ page }) => {
|
||||
const kennung = Date.now().toString(36);
|
||||
const hostA = `zz-e2e-a-${kennung}`;
|
||||
const hostB = `zz-e2e-b-${kennung}`;
|
||||
const zweck = `zz-e2e Rotation ${kennung}`;
|
||||
|
||||
await anmelden(page);
|
||||
await hostEinrichten(page, hostA, PORT.a);
|
||||
await hostEinrichten(page, hostB, PORT.b);
|
||||
await kernAufbauen(page, zweck, hostA, hostB);
|
||||
const altDateien = aufZiel("ziel-a", "ls /root/.ssh");
|
||||
await page.reload();
|
||||
|
||||
await page.getByRole("link", { name: "Rotieren" }).click();
|
||||
await expect(page.getByRole("heading", { name: `„${zweck}“ rotieren` })).toBeVisible();
|
||||
await page.getByRole("button", { name: "Neuen Schlüssel erzeugen und Zuweisungen übernehmen" }).click();
|
||||
|
||||
// Schritt 3: neuen Schluessel ausrollen.
|
||||
await expect(page.getByRole("button", { name: "Dry-Run starten" })).toBeVisible({ timeout: 60_000 });
|
||||
await page.getByRole("button", { name: "Dry-Run starten" }).click();
|
||||
// Derselbe Alias, neue Datei: nur die IdentityFile-Zeile aendert sich.
|
||||
await expect(page.getByText(/IdentityFile ~\/\.ssh\/.*-r\d{8}$/)).toBeVisible({ timeout: 30_000 });
|
||||
await page.screenshot({ path: "docs/anleitung/bilder/rotation-desktop.png", fullPage: true });
|
||||
await page.getByRole("button", { name: /Änderung anwenden/ }).click();
|
||||
|
||||
// Schritt 4: Verbindungstest mit dem neuen Schluessel.
|
||||
await page.getByRole("button", { name: "Verbindungen testen" }).click({ timeout: 60_000 });
|
||||
|
||||
// Schritt 5: alten Schluessel widerrufen.
|
||||
await page.getByRole("button", { name: "Alten Schlüssel widerrufen" }).click({ timeout: 60_000 });
|
||||
await page.getByRole("button", { name: "Dry-Run starten" }).click({ timeout: 30_000 });
|
||||
await page.getByRole("button", { name: /Änderung anwenden/ }).click({ timeout: 30_000 });
|
||||
await expect(page.getByRole("heading", { name: "Rotation abgeschlossen" })).toBeVisible({
|
||||
timeout: 60_000,
|
||||
});
|
||||
|
||||
expect(aufZiel("ziel-a", "ssh -o BatchMode=yes zu-b id -un").trim()).toBe("root");
|
||||
const neueDateien = aufZiel("ziel-a", "ls /root/.ssh");
|
||||
const alterKey = altDateien.split("\n").find((d) => d.startsWith("id_ed25519") && !d.endsWith(".pub"))!;
|
||||
expect(neueDateien.split("\n")).not.toContain(alterKey);
|
||||
expect(neueDateien).toMatch(/-r\d{8}\n/);
|
||||
|
||||
// Kette auf der Seite des neuen Schluessels.
|
||||
await page.getByRole("link", { name: /\(rotiert / }).click();
|
||||
const kette = page.getByRole("list", { name: "Rotationskette" });
|
||||
await expect(kette.getByRole("link", { name: zweck })).toBeVisible();
|
||||
await expect(kette.getByText("rotiert", { exact: true })).toBeVisible();
|
||||
|
||||
await archivieren(page, hostA);
|
||||
await archivieren(page, hostB);
|
||||
});
|
||||
Reference in New Issue
Block a user