known_hosts, Agent, Keys und Sicherheit
SSH in Automatisierungs-Skripten richtig zu verwenden ist anspruchsvoller als es aussieht: StrictHostKeyChecking zu deaktivieren löst das Problem des ersten Verbindungsaufbaus — aber öffnet gleichzeitig Man-in-the-Middle-Angriffe. Dieser Artikel zeigt, wie SSH-Automatisierung ohne Sicherheitskompromisse funktioniert.
Inhaltsverzeichnis
- 1. Warum SSH-Automatisierung Sorgfalt erfordert
- 2. known_hosts: Fingerprints vorprovisionieren
- 3. StrictHostKeyChecking richtig konfigurieren
- 4. ssh-agent in Shell-Skripten und CI
- 5. BatchMode: non-interaktive SSH-Verbindungen
- 6. SSH-Key-Verwaltung für Automatisierung
- 7. Jump-Hosts und Bastion-Proxies
- 8. SSH in CI/CD-Pipelines sicher einrichten
- 9. SSH-Optionen im Sicherheitsvergleich
- 10. Zusammenfassung
- 11. FAQ
1. Warum SSH-Automatisierung Sorgfalt erfordert
Die einfachste Lösung für das Problem "SSH fragt interaktiv nach Host-Bestätigung" ist -o StrictHostKeyChecking=no. Diese Option findet sich in unzähligen CI-Skripten, Tutorials und Deployment-Konfigurationen — und ist in den meisten Kontexten ein Sicherheitsproblem. Sie deaktiviert den Schutzmechanismus, der Man-in-the-Middle-Angriffe verhindert. In einer produktiven SSH-Automatisierung bedeutet das: Jeder, der den DNS-Namen oder die IP-Adresse des Zielservers kontrolliert oder umleiten kann, kann eine Verbindung abfangen, ohne dass das Skript es merkt. Credentials, Deployment-Artefakte und sensitive Konfigurationen werden an den Angreifer übertragen.
Das eigentliche Problem, das StrictHostKeyChecking=no lösen soll, ist ein Workflow-Problem: Die known_hosts-Datei des Automatisierungs-Accounts enthält den Fingerprint des Zielservers nicht. Die korrekte Lösung ist nicht, die Prüfung zu deaktivieren, sondern den Fingerprint vorab bereitzustellen. In der SSH-Automatisierung bedeutet das: ssh-keyscan beim Server-Provisioning aufrufen, den Fingerprint in die known_hosts des Automatisierungs-Accounts oder in ein projektspezifisches Known-Hosts-File schreiben und dieses in der CI-Konfiguration als Secret hinterlegen. Das löst das Workflow-Problem ohne Sicherheitskompromiss.
Ein weiterer häufiger Fehler in der SSH-Automatisierung: Private Keys werden als Klartextdateien in Repositories, Container-Images oder Environment-Variablen gespeichert, die in Logs erscheinen. Die korrekte Handhabung: Keys nur in geschützten Secret-Stores (GitHub Secrets, Vault, AWS Secrets Manager) hinterlegen, beim Build in temporäre Dateien mit eingeschränkten Berechtigungen (chmod 600) schreiben und nach Verwendung sicher löschen. Diese drei Grundsätze — kein StrictHostKeyChecking=no, keine Keys in Repositories, keine Keys in Logs — bilden das Fundament sicherer SSH-Automatisierung.
2. known_hosts: Fingerprints vorprovisionieren
Die known_hosts-Datei speichert den öffentlichen Host-Key jedes SSH-Servers, mit dem sich der Client verbunden hat. Bei der ersten Verbindung fragt SSH interaktiv, ob der Fingerprint akzeptiert werden soll — ein Mechanismus, der in der SSH-Automatisierung nicht funktioniert. Die Lösung ist ssh-keyscan: Dieses Tool fragt den öffentlichen Key eines Servers ab, ohne eine authentifizierte Verbindung aufzubauen, und gibt ihn im known_hosts-Format aus. Der Output kann direkt in die known_hosts-Datei geschrieben werden: ssh-keyscan -H hostname >> ~/.ssh/known_hosts. Das -H-Flag hashes den Hostnamen, was verhindert, dass die known_hosts-Datei als Netzwerk-Topologie-Leak verwendet werden kann.
Für die SSH-Automatisierung in CI-Pipelines empfiehlt sich ein projektspezifisches known_hosts-File anstelle der globalen ~/.ssh/known_hosts. Das File wird beim Server-Provisioning erstellt, als CI-Secret gespeichert und vor jeder SSH-Verbindung temporär in ~/.ssh/ oder in einen temporären Pfad geschrieben. In den SSH-Optionen wird es mit -o UserKnownHostsFile=/pfad/zu/known_hosts explizit referenziert. So bleibt die known_hosts-Verwaltung unabhängig vom CI-Runner-Environment und kann versioniert und auditiert werden. Das Server-seitige Rotieren von Host-Keys (nach einem Incident oder regulär) erfordert dann ein Update des CI-Secrets — das ist der erwünschte Prozess, der eine bewusste Entscheidung erfordert.
#!/usr/bin/env bash
# ssh-automation-setup.sh — provision known_hosts and agent for automation
set -euo pipefail
IFS=$'\n\t'
KNOWN_HOSTS_FILE="${1:?Usage: $0 <known_hosts_file> <host1> [host2...]}"
shift
# Collect host fingerprints securely (with hashed hostnames)
provision_known_hosts() {
local hosts=("$@")
local tmpfile
tmpfile="$(mktemp)"
trap 'rm -f -- "$tmpfile"' EXIT
local host
for host in "${hosts[@]}"; do
echo "Scanning: $host" >&2
# -T timeout, -H hash hostname, query ed25519 and rsa keys
ssh-keyscan -T 10 -H -t ed25519,rsa "$host" >> "$tmpfile" 2>/dev/null || {
echo "[WARN] Could not scan $host" >&2
}
done
# Deduplicate and sort for stable output
sort -u "$tmpfile" > "$KNOWN_HOSTS_FILE"
chmod 600 "$KNOWN_HOSTS_FILE"
echo "[OK] Written ${#hosts[@]} host(s) to $KNOWN_HOSTS_FILE" >&2
}
provision_known_hosts "$@"
# Verify: connect with strict checking enabled
verify_connection() {
local host="$1"
ssh -o StrictHostKeyChecking=yes \
-o UserKnownHostsFile="$KNOWN_HOSTS_FILE" \
-o BatchMode=yes \
-o ConnectTimeout=10 \
"$host" 'echo OK' 2>&1 | head -1
}
3. StrictHostKeyChecking richtig konfigurieren
Das SSH-Optionspaar StrictHostKeyChecking hat drei relevante Werte für die SSH-Automatisierung: yes (Standard in neueren OpenSSH-Versionen) verbietet die Verbindung, wenn der Host nicht in known_hosts ist oder der Key geändert hat. no akzeptiert jeden Host und jeden Key ohne Prüfung — nie in Produktion verwenden. accept-new (OpenSSH 7.6+) ist der sinnvolle Mittelweg für Szenarien, in denen neue Hosts im laufenden Betrieb hinzukommen: Neue Hosts werden akzeptiert und zur known_hosts-Datei hinzugefügt, aber geänderte Keys von bekannten Hosts werden weiterhin als Fehler behandelt. Das ist der empfohlene Wert für Dynamic Infrastructure, wo neue Server-Instanzen automatisch verbunden werden müssen.
Ein häufig übersehener Aspekt: Wenn ein Server seinen Host-Key ändert (nach Neuinstallation, Key-Rotation oder Migration), schlägt die SSH-Automatisierung mit einer Warnung fehl. Das ist beabsichtigt — aber in automatisierten Deployment-Pipelines ohne Monitoring erscheint die Warnung in Logs, die niemand liest, und das Skript schlägt mit einem kryptischen Fehler fehl. Die Lösung: Den bekannten alten Key explizit aus known_hosts entfernen (ssh-keygen -R hostname) und den neuen Key per ssh-keyscan hinzufügen, als Teil des Server-Reprovisioning-Prozesses. Das macht den Prozess explizit und nachvollziehbar statt still fehlschlagend.
4. ssh-agent in Shell-Skripten und CI
Der ssh-agent hält entschlüsselte Private Keys im Speicher und stellt sie über ein Unix-Socket dem SSH-Client zur Verfügung. In der SSH-Automatisierung eliminiert er die Notwendigkeit, den Key-Passphrase bei jeder Verbindung einzugeben. In interaktiven Shells ist der Agent oft vom Desktop-Environment oder von der Login-Session gestartet und über die Umgebungsvariable SSH_AUTH_SOCK zugänglich. In nicht-interaktiven Kontexten (Cron-Jobs, CI-Runners) muss der Agent explizit gestartet werden: eval "$(ssh-agent -s)" startet einen neuen Agent-Prozess und setzt die Umgebungsvariablen in der aktuellen Shell. Der Key wird dann mit ssh-add /pfad/zum/key geladen.
In CI-Pipelines für die SSH-Automatisierung gibt es eine wichtige Sicherheitsentscheidung: Soll der Private Key direkt als CI-Secret gespeichert werden, oder soll ein Key ohne Passphrase für CI verwendet werden? Für hohe Sicherheit: Keys mit Passphrase, die Passphrase separat als CI-Secret, Agent mit automatischem ssh-add. Für Praktikabilität: Dedizierte CI-Keys ohne Passphrase, mit eingeschränkten Berechtigungen auf dem Zielserver (command=-Einschränkung in authorized_keys). In beiden Fällen: Der Agent-Prozess muss am Ende des Skripts gestoppt werden (ssh-agent -k), und der Key-Inhalt darf nie in Logs erscheinen. Daher immer ssh-add - (aus stdin) statt echo "$key" | ssh-add — Letzteres könnte in Process-Listen sichtbar werden.
#!/usr/bin/env bash
# ssh-agent-ci.sh — start ssh-agent, load key, cleanup after use
set -euo pipefail
IFS=$'\n\t'
# Private key from CI secret (never echo to stdout/stderr)
SSH_PRIVATE_KEY="${SSH_PRIVATE_KEY:?SSH_PRIVATE_KEY must be set}"
# Start agent and ensure cleanup
start_agent() {
# eval sets SSH_AGENT_PID and SSH_AUTH_SOCK in current shell
eval "$(ssh-agent -s)" > /dev/null
# Register agent kill in cleanup
trap 'ssh-agent -k > /dev/null 2>&1 || true' EXIT
}
load_key() {
local key_content="$1"
# ssh-add from stdin: key never appears in ps output or shell history
printf '%s\n' "$key_content" | ssh-add - 2>/dev/null
echo "[OK] SSH key loaded ($(ssh-add -l | wc -l) key(s) in agent)" >&2
}
start_agent
load_key "$SSH_PRIVATE_KEY"
# Now SSH connections use the agent — no password prompts
ssh -o StrictHostKeyChecking=yes \
-o BatchMode=yes \
-o ConnectTimeout=15 \
deploy@production.example.com \
'bash -s' < deploy-commands.sh
# Agent is killed automatically via trap EXIT
5. BatchMode: non-interaktive SSH-Verbindungen
Die SSH-Option BatchMode=yes ist in der SSH-Automatisierung unverzichtbar. Sie deaktiviert alle interaktiven Prompts: keine Passwort-Eingabe, keine Fingerprint-Bestätigung, keine Passphrases. Wenn SSH in BatchMode fehlschlägt, gibt es einen klaren Nicht-Null-Exit-Code statt einer blockierenden Eingabeaufforderung. Ohne BatchMode kann ein nicht-interaktives Skript an einem SSH-Prompt hängen — potenziell für immer, bis ein externes Timeout eingreift. In CI-Pipelines führt das zu Jobs, die nie enden und Slots blockieren.
Ergänzend zu BatchMode empfiehlt sich ConnectTimeout für die SSH-Automatisierung: Ein expliziter Timeout verhindert, dass ein nicht erreichbarer Host das Skript für die TCP-Verbindungs-Timeout-Zeit blockiert (die auf manchen Systemen mehrere Minuten beträgt). ServerAliveInterval und ServerAliveCountMax erkennen verlorene Verbindungen zu laufenden Remote-Prozessen und ermöglichen sauberes Failover statt endlosem Hängen. Die Kombination BatchMode=yes ConnectTimeout=30 ServerAliveInterval=60 ServerAliveCountMax=3 ist ein guter Standard für robuste SSH-Automatisierung in Deployment-Skripten.
6. SSH-Key-Verwaltung für Automatisierung
Die Key-Verwaltung ist der kritischste Sicherheitsaspekt der SSH-Automatisierung. Die wichtigste Grundregel: Für Automatisierung dedizierte Keys erstellen, die keine weiteren Zugriffsrechte haben. Kein Teilen von privaten Schlüsseln zwischen Personen und Automatisierungs-Accounts, kein Teilen eines Automatisierungs-Keys zwischen verschiedenen Umgebungen (Staging und Production müssen verschiedene Keys haben). Der authorized_keys-Eintrag auf dem Zielserver kann mit Optionen eingeschränkt werden: command="nur-dieser-befehl" erlaubt nur einen einzigen spezifischen Befehl, no-pty verhindert Pseudo-Terminal-Zuweisung, no-agent-forwarding und no-port-forwarding beschränken weitere SSH-Features.
In modernen Setups für die SSH-Automatisierung werden Private Keys nicht statisch gespeichert, sondern von einem Secret-Manager dynamisch generiert und zeitlich befristet ausgestellt. Vault von HashiCorp hat eine SSH-Secrets-Engine, die für jede Verbindung ein kurzlebiges Zertifikat ausstellt, das nach wenigen Minuten oder Stunden abläuft. Das eliminiert das Problem der Key-Rotation vollständig: Es gibt keine langlebigen Private Keys mehr, die gestohlen werden könnten. Für Umgebungen ohne Vault ist das Minimum: Keys mit ssh-keygen -t ed25519 (moderne, sichere Algorithmuswahl), regelmäßige Rotation (mindestens jährlich) und sofortige Revokation bei Verdacht auf Kompromittierung.
7. Jump-Hosts und Bastion-Proxies
In vielen Produktionsumgebungen sind Zielserver nicht direkt aus dem Internet oder dem CI-Runner erreichbar, sondern nur über einen Bastion-Host (Jump-Host). Die klassische, unsichere Lösung: Den Private Key auf den Bastion-Host kopieren und von dort aus auf Zielserver verbinden (-A Agent-Forwarding). Das ist problematisch: Ein Angreifer, der den Bastion-Host kompromittiert, kann den weitergeleiteten Agent-Socket nutzen, um sich mit beliebigen Zielservern zu verbinden, solange die Agent-Session aktiv ist. Für die SSH-Automatisierung ist die sichere Alternative ProxyJump (oder die ältere ProxyCommand-Variante): Alle Verbindungen werden durch den Bastion-Host getunnelt, aber der Agent-Socket bleibt auf dem ursprünglichen Client.
Die OpenSSH-Option -J user@bastion oder in der ~/.ssh/config als ProxyJump konfiguriert, erstellt einen direkten TCP-Tunnel über den Bastion-Host. Das Kryptographie-Material (Private Keys) verlässt nie den CI-Runner — der Bastion-Host sieht nur den verschlüsselten Datenstream. Für komplexe Topologien können mehrere Jump-Hosts in einer Kette angegeben werden: -J bastion1,bastion2. In der ssh_config kann die Jump-Host-Konfiguration für verschiedene Hostnamen-Pattern unterschiedlich konfiguriert werden, was verschiedene Umgebungen (Staging über Bastion A, Production über Bastion B) sauber trennt.
#!/usr/bin/env bash
# ssh-jump-deployment.sh — deploy through bastion with ProxyJump
set -euo pipefail
IFS=$'\n\t'
readonly BASTION="${BASTION_HOST:?BASTION_HOST must be set}"
readonly TARGET="${TARGET_HOST:?TARGET_HOST must be set}"
readonly DEPLOY_USER="${DEPLOY_USER:-deploy}"
readonly KNOWN_HOSTS_FILE="${KNOWN_HOSTS_FILE:?KNOWN_HOSTS_FILE must be set}"
# Build common SSH options (no agent forwarding, strict host checking)
SSH_OPTS=(
-o "StrictHostKeyChecking=yes"
-o "UserKnownHostsFile=${KNOWN_HOSTS_FILE}"
-o "BatchMode=yes"
-o "ConnectTimeout=30"
-o "ServerAliveInterval=60"
-o "ServerAliveCountMax=3"
-o "ForwardAgent=no" # Never forward agent — security risk
-o "LogLevel=ERROR" # Suppress banner noise in CI output
)
# Connect to target through bastion via ProxyJump
# Private key never touches the bastion host
run_remote() {
local cmd="$1"
ssh "${SSH_OPTS[@]}" \
-J "${DEPLOY_USER}@${BASTION}" \
"${DEPLOY_USER}@${TARGET}" \
"$cmd"
}
# Copy deployment artifact through bastion
deploy_artifact() {
local local_file="$1"
local remote_path="$2"
scp -o "StrictHostKeyChecking=yes" \
-o "UserKnownHostsFile=${KNOWN_HOSTS_FILE}" \
-o "BatchMode=yes" \
-o "ForwardAgent=no" \
-J "${DEPLOY_USER}@${BASTION}" \
"$local_file" \
"${DEPLOY_USER}@${TARGET}:${remote_path}"
}
echo "Deploying to $TARGET via $BASTION..."
deploy_artifact "app-release.tar.gz" "/tmp/app-release.tar.gz"
run_remote "bash /opt/deploy/install.sh /tmp/app-release.tar.gz"
echo "[OK] Deployment complete"
8. SSH in CI/CD-Pipelines sicher einrichten
Die SSH-Automatisierung in CI/CD-Pipelines hat spezifische Sicherheitsanforderungen, die über normale Shell-Skripte hinausgehen. Der Private Key wird als CI-Secret gespeichert (GitHub Secrets, GitLab CI Variables mit Masked und Protected), in einem Before-Step aus dem Secret in eine temporäre Datei mit chmod 600 geschrieben und nach dem Deployment-Step gelöscht. Das Known-Hosts-File wird ebenfalls als CI-Secret gespeichert — es enthält keine Geheimnisse, aber seine Integrität ist für die Sicherheit der Verbindung entscheidend. Wenn das Known-Hosts-File modifiziert werden kann, kann ein Angreifer die Host-Key-Prüfung umgehen.
Ein wichtiger Sicherheitsaspekt für die SSH-Automatisierung in Multi-Stage-Pipelines: Deploy-Keys sollten nur für den Deploy-Step zugänglich sein, nicht für alle Pipeline-Jobs. In GitHub Actions bedeutet das: Den Key als Environment-Secret für den spezifischen Job setzen, nicht als Repository-Secret, das für alle Workflows gilt. In GitLab CI: Environment-spezifische Variables mit Scope auf Production-Environment beschränken, sodass Feature-Branch-Pipelines keinen Zugriff auf Production-Deployment-Keys haben. Das Principle of Least Privilege — jeder Job bekommt nur die Credentials, die er tatsächlich braucht — ist die wichtigste Maßnahme gegen Credential-Leakage bei kompromittierten Supply-Chain-Komponenten.
9. SSH-Optionen im Sicherheitsvergleich
Für die SSH-Automatisierung gibt es für jede unsichere Abkürzung eine sichere Alternative, die denselben Workflow-Bedarf erfüllt ohne Sicherheitskompromisse.
| Option / Praxis | Unsicher | Sicher | Risiko |
|---|---|---|---|
| Host-Überprüfung | StrictHostKeyChecking=no |
yes + Known-Hosts-Secret |
Man-in-the-Middle-Angriff |
| Key-Zugriff | Key in Repository / Image | CI-Secret + chmod 600 tmpfile | Credential-Leakage |
| Jump-Host-Zugang | Agent-Forwarding (-A) | ProxyJump (-J) | Agent-Hijacking auf Bastion |
| Interaktivität | Kein BatchMode — hängt | BatchMode=yes + ConnectTimeout | Pipeline hängt endlos |
| Key-Berechtigung | Voller Shell-Zugriff | command= in authorized_keys | Voller Server-Zugriff bei Key-Kompromittierung |
Die Tabelle zeigt: Jede unsichere Praxis in der SSH-Automatisierung hat eine direkte, sichere Alternative. Der häufigste Einwand — "aber das ist komplizierter einzurichten" — stimmt für den ersten Setup. Danach ist der Betrieb einer sicheren SSH-Automatisierung nicht aufwändiger als eine unsichere, und das Risikoprofil ist fundamental besser. Besonders command=-Einschränkungen in authorized_keys sind hochwertig: Selbst wenn ein Deployment-Key kompromittiert wird, kann der Angreifer damit nur den einen konfigurierten Befehl ausführen — keinen freien Shell-Zugriff.
Mironsoft
Shell-Automatisierung, DevOps-Tooling und Deployment-Infrastruktur
SSH-Automatisierung ohne Sicherheitsabkürzungen?
Wir überprüfen bestehende SSH-Automatisierungen auf Sicherheitslücken, ersetzen unsichere StrictHostKeyChecking=no-Konfigurationen durch korrekte Known-Hosts-Provisioning und richten sichere CI/CD-SSH-Pipelines mit ProxyJump und beschränkten Keys ein.
Security-Audit
Analyse bestehender SSH-Konfigurationen auf StrictHostKeyChecking=no und Key-Leakage
CI-Setup
Known-Hosts-Provisioning, ssh-agent und BatchMode für CI/CD-Pipelines
Jump-Host-Setup
ProxyJump-Konfiguration für Bastion-Hosts ohne Agent-Forwarding-Risiko
10. Zusammenfassung
Sichere SSH-Automatisierung in Shell-Skripten und CI/CD-Pipelines beruht auf wenigen, konsequent angewandten Prinzipien: Niemals StrictHostKeyChecking=no — stattdessen Known-Hosts-Files beim Server-Provisioning befüllen und als CI-Secret verwalten. Immer BatchMode=yes und ConnectTimeout für non-interaktive Verbindungen. Agent-Forwarding durch ProxyJump ersetzen, das Cryptographie-Material nie den Bastion-Host berühren lässt. Private Keys nur als CI-Secrets, nie in Repositories oder Container-Images. command=-Einschränkungen in authorized_keys begrenzen den Schaden bei Key-Kompromittierung.
Diese Maßnahmen erfordern beim ersten Einrichten etwas mehr Aufwand als die einfachen, unsicheren Alternativen. Aber der Betrieb ist danach nicht komplizierter — und das Risikoprofil ist fundamental besser. Gerade in Deployment-Pipelines, die mit Root-äquivalenten Berechtigungen auf Produktionsservern operieren, ist SSH-Automatisierung-Sicherheit kein optionales Feature, sondern eine Grundvoraussetzung für verantwortungsvolle Operations-Praxis.
SSH in der Automatisierung — Das Wichtigste auf einen Blick
Known-Hosts statt StrictHostKeyChecking=no
ssh-keyscan -H beim Server-Provisioning, Output als CI-Secret, -o UserKnownHostsFile beim SSH-Aufruf — kein Sicherheitskompromiss.
BatchMode + ConnectTimeout
BatchMode=yes verhindert blockierende Prompts. ConnectTimeout=30 verhindert endloses Hängen bei nicht erreichbaren Hosts.
ProxyJump statt Agent-Forwarding
-J bastion statt -A. Private Keys verlassen nie den CI-Runner, kein Agent-Hijacking-Risiko auf dem Bastion-Host.
command= in authorized_keys
Automation-Keys auf einen Befehl beschränken. Bei Key-Kompromittierung: nur dieser eine Befehl ausführbar, kein freier Shell-Zugriff.