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>
72 lines
3.1 KiB
TypeScript
72 lines
3.1 KiB
TypeScript
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);
|
|
});
|