Ein Abgleich liest je Host alle authorized_keys in einem SSH-Aufruf, als jeweiliger Benutzer, und vergleicht sie mit dem Soll: fehlende, von Hand geaenderte und unbekannte Keys, falsche Rechte, beschaedigte Bloecke und veraenderte Private Keys. Befunde werden geoeffnet und wieder geschlossen, Zustaende von Verwendungen und Host nachgezogen. Auf dem Host aendert der Abgleich nichts. Der Zeitplan liegt in pg-boss und feuert auch bei mehreren Workern nur einmal. Auf der Hostseite stehen Abweichungen und der Ist-Zustand je Benutzer; unbekannte Keys lassen sich uebernehmen - nur bekannt machen oder mit Dry-Run in den verwalteten Block holen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
import { generateKeyPairSync } from "node:crypto";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { authorizedKeysAbgleichen } from "@/lib/ssh/abgleich";
|
||||
import { blockSetzen, type SollEintrag } from "@/lib/ssh/authorized-keys";
|
||||
import { fingerprintSha256 } from "@/lib/ssh/openssh";
|
||||
|
||||
/** Abgleich einer authorized_keys mit dem Soll (#021). */
|
||||
|
||||
function key(kommentar: string) {
|
||||
const { publicKey } = generateKeyPairSync("ed25519");
|
||||
const roh = publicKey.export({ format: "der", type: "spki" }).subarray(-32);
|
||||
const typ = Buffer.from("ssh-ed25519");
|
||||
const blob = Buffer.concat([Buffer.from([0, 0, 0, typ.length]), typ, Buffer.from([0, 0, 0, 32]), roh]);
|
||||
return {
|
||||
publicKey: `ssh-ed25519 ${blob.toString("base64")} ${kommentar}`,
|
||||
fingerprint: fingerprintSha256(blob),
|
||||
};
|
||||
}
|
||||
|
||||
const DEPLOY = key("deploy");
|
||||
const BACKUP = key("backup");
|
||||
const FREMD = key("hacker@irgendwo");
|
||||
const BEKANNT = key("bekannt");
|
||||
|
||||
const soll: SollEintrag[] = [DEPLOY, { ...BACKUP, optionen: 'from="10.0.0.1"' }];
|
||||
const bekannt = new Set([DEPLOY.fingerprint, BACKUP.fingerprint, BEKANNT.fingerprint]);
|
||||
|
||||
describe("authorized_keys abgleichen", () => {
|
||||
it("ist ohne Befund, wenn der Block dem Soll entspricht", () => {
|
||||
const inhalt = blockSetzen(`${BEKANNT.publicKey}\n`, soll).inhalt;
|
||||
const ergebnis = authorizedKeysAbgleichen(inhalt, soll, bekannt, {
|
||||
sshVerzeichnis: "700",
|
||||
datei: "600",
|
||||
eigentuemerPasst: true,
|
||||
});
|
||||
expect(ergebnis.befunde).toEqual([]);
|
||||
expect(ergebnis.vorhanden).toEqual([DEPLOY.fingerprint, BACKUP.fingerprint]);
|
||||
expect(ergebnis.eintraege.map((e) => e.herkunft)).toEqual(["bekannt", "verwaltet", "verwaltet"]);
|
||||
});
|
||||
|
||||
it("findet fehlende, geaenderte und zusaetzliche Keys im Block", () => {
|
||||
const inhalt = blockSetzen("", [DEPLOY, BACKUP, FREMD]).inhalt; // BACKUP ohne Optionen, FREMD zu viel
|
||||
const arten = authorizedKeysAbgleichen(inhalt, soll, bekannt).befunde.map((b) => [b.art, b.fingerprint]);
|
||||
expect(arten).toEqual([
|
||||
["ZUSAETZLICH_IM_BLOCK", BACKUP.fingerprint],
|
||||
["ZUSAETZLICH_IM_BLOCK", FREMD.fingerprint],
|
||||
]);
|
||||
const leer = authorizedKeysAbgleichen(null, soll, bekannt).befunde.map((b) => b.art);
|
||||
expect(leer).toEqual(["FEHLENDER_SCHLUESSEL", "FEHLENDER_SCHLUESSEL"]);
|
||||
});
|
||||
|
||||
it("meldet nur unbekannte Keys ausserhalb des Blocks", () => {
|
||||
const inhalt = blockSetzen(`${FREMD.publicKey}\n${FREMD.publicKey}\n${BEKANNT.publicKey}\n`, soll).inhalt;
|
||||
const befunde = authorizedKeysAbgleichen(inhalt, soll, bekannt).befunde;
|
||||
expect(befunde).toHaveLength(1);
|
||||
expect(befunde[0]).toMatchObject({ art: "UNBEKANNTER_SCHLUESSEL", fingerprint: FREMD.fingerprint });
|
||||
expect(befunde[0]?.text).toContain("hacker@irgendwo");
|
||||
});
|
||||
|
||||
it("prueft Rechte und Eigentuemer", () => {
|
||||
const befunde = authorizedKeysAbgleichen("", [], bekannt, {
|
||||
sshVerzeichnis: "777",
|
||||
datei: "664",
|
||||
eigentuemerPasst: false,
|
||||
}).befunde;
|
||||
expect(befunde.map((b) => b.art)).toEqual(["RECHTE", "RECHTE", "RECHTE"]);
|
||||
expect(
|
||||
authorizedKeysAbgleichen("", [], bekannt, {
|
||||
sshVerzeichnis: "755",
|
||||
datei: "644",
|
||||
eigentuemerPasst: true,
|
||||
}).befunde,
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it("meldet einen beschaedigten Block", () => {
|
||||
expect(authorizedKeysAbgleichen("# BEGIN SSHKV MANAGED\n", [], bekannt).befunde[0]?.art).toBe(
|
||||
"DATEI_GEAENDERT",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -209,3 +209,14 @@ describe("Diff", () => {
|
||||
expect(kurz[0]?.text).toMatch(/8 unveränderte Zeile/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("uebernommene Keys (#022)", () => {
|
||||
it("entfernt nur ausdruecklich genannte Zeilen ausserhalb des Blocks", () => {
|
||||
const vorher = `# Kommentar\n${FREMD.zeile}\n${BACKUP.zeile}\n`;
|
||||
const ergebnis = blockSetzen(vorher, soll(FREMD), [], [FREMD.fingerprint]);
|
||||
expect(ergebnis.inhalt).toBe(
|
||||
`# Kommentar\n${BACKUP.zeile}\n# BEGIN SSHKV MANAGED – nicht manuell bearbeiten\n${FREMD.zeile}\n# END SSHKV MANAGED\n`,
|
||||
);
|
||||
expect(ergebnis.hinweise).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user