feat(ausrollen): Private Key mit ssh-config und known_hosts (#017)

Der Kern-Anwendungsfall im Worker: Private Key auf Host A, Public Key
auf Host B, dazu ein Block in ~/.ssh/config von A und der vertraute
Host-Key von B in known_hosts - danach meldet sich A per Alias bei B
an, ohne Rueckfrage zum Host-Key (Integrationstest gegen zwei echte
sshd).

Gleiches Muster wie bei Public Keys: Plan je Datei mit Hashes, dann
genau dieser Plan. Der Inhalt eines Private Keys steht nie im Plan
oder Protokoll, eine fremde Datei am Zielpfad wird nie ueberschrieben,
und von einem alten Private Key bleibt keine Sicherungskopie liegen.
Der config-Block steht am Anfang, weil ssh den ersten Treffer nimmt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-12 10:49:28 +02:00
co-authored by Claude Opus 5.5
parent 7b6323a6a4
commit edc38d0c27
10 changed files with 1008 additions and 53 deletions
+31 -1
View File
@@ -94,7 +94,12 @@ export async function sollFuer(hostId: string, zielbenutzer: string): Promise<So
return { eintraege, pflicht, verwendungIds: { drin, raus } };
}
export type Ziel = { readonly hostId: string; readonly zielbenutzer: string };
/** Ein Ziel des Ausrollens: authorized_keys (public) oder ~/.ssh-Dateien eines Quell-Hosts (privat). */
export type Ziel = {
readonly hostId: string;
readonly zielbenutzer: string;
readonly art?: "public" | "privat";
};
/** Alle (Host, Benutzer)-Paare, die eine Public-Key-Verwendung betrifft – Gruppen aufgeloest. */
export async function zieleVonVerwendungen(where: {
@@ -140,3 +145,28 @@ export async function zieleVonVerwendungen(where: {
}
return [...ziele.values()];
}
/** Ziele der Private-Key-Verwendungen (#017): je Quell-Host und Benutzer ein Ziel. */
export async function zieleVonPrivatVerwendungen(where: {
schluesselId?: string;
hostId?: string;
}): Promise<Ziel[]> {
const verwendungen = await prisma.privateVerwendung.findMany({
where: {
...(where.schluesselId ? { schluesselId: where.schluesselId } : {}),
...(where.hostId ? { hostId: where.hostId } : {}),
zustand: { not: "ENTFERNT" },
host: { archiviertAm: null },
},
select: { hostId: true, zielbenutzer: true },
});
const ziele = new Map<string, Ziel>();
for (const v of verwendungen) {
ziele.set(`${v.hostId}\u0000${v.zielbenutzer}`, {
hostId: v.hostId,
zielbenutzer: v.zielbenutzer,
art: "privat",
});
}
return [...ziele.values()];
}