Zielbenutzer wie deploy legt sshkv bei Bedarf an, mit ~/.ssh und optional einer sudoers-Datei: keine Rechte, bestimmte Befehle oder voll. Die Datei entsteht unter einem Namen, den sudo ignoriert, und wird erst nach visudo -c aktiv. Eine fremde Datei gleichen Namens ueberschreibt sshkv nie, Systemkonten wie root oder sshkv fasst es nicht an. Befehle muessen absolute Pfade sein; Zeichen mit Sonderbedeutung in sudoers sind ausgeschlossen, damit die Liste so wirkt, wie sie aussieht. Vorgang und Oberflaeche folgen mit #019/#020. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
121 lines
4.1 KiB
TypeScript
121 lines
4.1 KiB
TypeScript
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
|
import { deploymentKeySicherstellen } from "@/worker/geheimnisse";
|
|
import { hostVerbinden } from "@/worker/ssh/host";
|
|
import { befehlAusfuehren } from "@/worker/ssh/verbindung";
|
|
import { zielkontoAnwenden, zielkontoEntfernen } from "@/worker/ssh/zielkonto";
|
|
import { datenbankLeeren } from "../hilfen/db";
|
|
import { eingerichteterHost, masterKeyAnlegen } from "./hilfen/host";
|
|
import { befehlAlsRoot, zielB, zielBereit } from "./hilfen/ziele";
|
|
|
|
/** Zielkonten und sudoers gegen ein echtes sshd (#018). */
|
|
|
|
let aufraeumen: () => void;
|
|
beforeAll(async () => {
|
|
await zielBereit(zielB);
|
|
aufraeumen = masterKeyAnlegen();
|
|
});
|
|
afterAll(() => aufraeumen());
|
|
|
|
beforeEach(async () => {
|
|
await datenbankLeeren();
|
|
await deploymentKeySicherstellen();
|
|
await befehlAlsRoot(
|
|
zielB,
|
|
"userdel -r sshkv 2>/dev/null; userdel -r deploy 2>/dev/null; rm -f /etc/sudoers.d/sshkv /etc/sudoers.d/sshkv-deploy; true",
|
|
);
|
|
});
|
|
|
|
async function verbunden() {
|
|
const host = await eingerichteterHost(zielB, "ziel-b");
|
|
return { host, verbindung: await hostVerbinden(host) };
|
|
}
|
|
|
|
describe("Zielkonto", () => {
|
|
it("legt den Benutzer an und erlaubt genau die freigegebenen Befehle", async () => {
|
|
const { host, verbindung } = await verbunden();
|
|
try {
|
|
const erstes = await zielkontoAnwenden(verbindung, host, {
|
|
benutzer: "deploy",
|
|
modus: "BEFEHLE",
|
|
befehle: ["/usr/bin/id -un"],
|
|
});
|
|
expect(erstes.angelegt).toBe(true);
|
|
expect(
|
|
(
|
|
await zielkontoAnwenden(verbindung, host, {
|
|
benutzer: "deploy",
|
|
modus: "BEFEHLE",
|
|
befehle: ["/usr/bin/id -un"],
|
|
})
|
|
).angelegt,
|
|
).toBe(false);
|
|
const erlaubt = await befehlAusfuehren(
|
|
verbindung,
|
|
"sudo -n runuser -u deploy -- sudo -n /usr/bin/id -un",
|
|
);
|
|
expect(erlaubt.stdout.trim()).toBe("root");
|
|
const verboten = await befehlAusfuehren(
|
|
verbindung,
|
|
"sudo -n runuser -u deploy -- sudo -n /usr/bin/whoami",
|
|
);
|
|
expect(verboten.code).not.toBe(0);
|
|
const rechte = await befehlAlsRoot(
|
|
zielB,
|
|
"stat -c '%a %U' /etc/sudoers.d/sshkv-deploy /home/deploy/.ssh",
|
|
);
|
|
expect(rechte.stdout).toBe("440 root\n700 deploy\n");
|
|
} finally {
|
|
verbindung.end();
|
|
}
|
|
});
|
|
|
|
it("wechselt auf vollen Zugriff und wieder auf keinen", async () => {
|
|
const { host, verbindung } = await verbunden();
|
|
try {
|
|
await zielkontoAnwenden(verbindung, host, { benutzer: "deploy", modus: "VOLL" });
|
|
expect(
|
|
(await befehlAusfuehren(verbindung, "sudo -n runuser -u deploy -- sudo -n whoami")).stdout.trim(),
|
|
).toBe("root");
|
|
await zielkontoAnwenden(verbindung, host, { benutzer: "deploy", modus: "KEINER" });
|
|
expect(
|
|
(
|
|
await befehlAlsRoot(zielB, "test -e /etc/sudoers.d/sshkv-deploy && echo da || echo weg")
|
|
).stdout.trim(),
|
|
).toBe("weg");
|
|
} finally {
|
|
verbindung.end();
|
|
}
|
|
});
|
|
|
|
it("ueberschreibt keine fremde sudoers-Datei", async () => {
|
|
const { host, verbindung } = await verbunden();
|
|
await befehlAlsRoot(
|
|
zielB,
|
|
"echo 'deploy ALL=(ALL) ALL' > /etc/sudoers.d/sshkv-deploy && chmod 440 /etc/sudoers.d/sshkv-deploy",
|
|
);
|
|
try {
|
|
await expect(
|
|
zielkontoAnwenden(verbindung, host, { benutzer: "deploy", modus: "VOLL" }),
|
|
).rejects.toThrow(/nicht von sshkv/);
|
|
expect((await befehlAlsRoot(zielB, "cat /etc/sudoers.d/sshkv-deploy")).stdout).toBe(
|
|
"deploy ALL=(ALL) ALL\n",
|
|
);
|
|
} finally {
|
|
verbindung.end();
|
|
}
|
|
});
|
|
|
|
it("entfernt sudoers und Benutzer beim Widerruf", async () => {
|
|
const { host, verbindung } = await verbunden();
|
|
try {
|
|
await zielkontoAnwenden(verbindung, host, { benutzer: "deploy", modus: "VOLL" });
|
|
await zielkontoEntfernen(verbindung, host, "deploy", true);
|
|
const rest = await befehlAlsRoot(zielB, "id deploy 2>&1; ls /etc/sudoers.d/");
|
|
expect(rest.stdout).toMatch(/no such user/);
|
|
expect(rest.stdout).not.toContain("sshkv-deploy");
|
|
} finally {
|
|
verbindung.end();
|
|
}
|
|
});
|
|
});
|