Deployment-User in GitLab-Prozessen
Wer Produktionsserver direkt aus GitLab-Runnern erreichbar macht, baut Angriffsfläche auf. SSH-Bastion-Server, Jump Hosts und dedizierte Deployment-User trennen Zugriffspfade sauber und machen Deployment-Verbindungen auditierbar, widerrufbar und ohne Personal-SSH-Keys umsetzbar.
Inhaltsverzeichnis
- 1. Warum ein Bastion-Host statt direktem SSH
- 2. Architektur: Bastion, Jump Host und Zielsystem
- 3. Getrennte Deployment-User anlegen und absichern
- 4. ProxyJump in GitLab CI/CD konfigurieren
- 5. Known Hosts für Bastion und Zielserver verwalten
- 6. GitLab-Variablen und SSH-Agent im Runner
- 7. Direkter SSH vs. Bastion-Routing im Vergleich
- 8. Typische Fehlerbilder und Diagnose
- 9. Rollback über Bastion-Verbindung
- 10. Zusammenfassung
- 11. FAQ
1. Warum ein Bastion-Host statt direktem SSH
In vielen Magento-Projekten ist der Produktionsserver direkt über eine öffentliche IP per SSH erreichbar. Der GitLab-Runner verbindet sich mit einem hinterlegten SSH-Key direkt auf Port 22 und führt Deploy-Skripte aus. Das funktioniert – bis ein Runner kompromittiert wird, ein Schlüssel verloren geht oder ein Team-Mitglied das Projekt verlässt. Ohne Zwischenschicht gibt es keine Möglichkeit, den Zugriff gezielt zu unterbrechen, ohne den gesamten SSH-Key-Satz des Servers zu rotieren.
Ein Bastion-Host ist ein dedizierter Einstiegspunkt ins interne Netz. Der Produktionsserver ist nicht mehr direkt aus dem Internet erreichbar – nur der Bastion-Host ist exponiert, und ausschließlich über ihn führt der Weg weiter. GitLab-Runner verbinden sich zuerst mit dem Bastion und springen von dort per ProxyJump auf den Zielserver. Das reduziert die Angriffsfläche, ermöglicht zentrales Logging aller SSH-Verbindungen und macht Zugriffspfade auditierbar.
Getrennte Deployment-User – also eigene Linux-Accounts ohne sudo-Rechte und mit eng definierten authorized_keys-Einschränkungen – sorgen dafür, dass ein kompromittierter Deployment-Key keinen Root-Zugriff erlaubt. Diese drei Komponenten zusammen bilden eine vertretbare Sicherheitsarchitektur für automatisierte Magento-Deployments über GitLab.
2. Architektur: Bastion, Jump Host und Zielsystem
Die typische Netzwerktopologie für gesicherte GitLab-Deployments besteht aus drei Schichten. Der GitLab-Runner läuft entweder als Shared Runner auf gitlab.com oder als self-hosted Runner in einem separaten Netz. Er hat ausschließlich Zugriff auf den Bastion-Host, der in einer DMZ sitzt und nur Port 22 nach außen öffnet. Der eigentliche Zielserver – der Webserver mit dem Magento-Deployment-Pfad – ist nur aus dem internen Netz erreichbar und hat keine direkte Verbindung ins Internet.
Der Jump Host ist in vielen Setups identisch mit dem Bastion-Host. Der Unterschied ist terminologisch: Ein Bastion ist der erste exponierte Punkt, ein Jump Host ist die Weiterleitungsinstanz. In der Praxis übernimmt ein Server beide Rollen. Der GitLab-Runner verwendet SSH ProxyJump, um in einem einzigen SSH-Kommando durch den Bastion auf den Zielserver zu verbinden – ohne dass der Bastion ein separates Login erfordert. Das reduziert die Komplexität bei gleichbleibender Sicherheit.
3. Getrennte Deployment-User anlegen und absichern
Ein Deployment-User ist ein dedizierter Linux-Account, der ausschließlich für automatisierte Deploy-Prozesse existiert. Er hat kein Passwort, keine sudo-Rechte und keinen interaktiven Shell-Zugang, der über das Notwendige hinausgeht. Auf dem Zielserver legt man ihn mit adduser --disabled-password deploy an. In der ~/.ssh/authorized_keys dieses Users werden ausschließlich GitLab-spezifische Deployment-Keys hinterlegt – keine persönlichen Entwickler-Keys.
Für zusätzliche Sicherheit kann jeder authorized_keys-Eintrag mit einer command=-Einschränkung versehen werden: Der SSH-Key darf dann nur ein bestimmtes Skript ausführen, nicht beliebige Befehle. Das ist für einfache Deployments manchmal zu restriktiv, für hochsichere Umgebungen aber der richtige Ansatz. Auf dem Bastion-Host legt man ebenfalls einen eigenen User an, durch den der Runner springt – der Bastion-User selbst braucht keine Schreibrechte auf dem Zielserver.
stages:
- build
- deploy
- verify
variables:
GIT_STRATEGY: fetch
# SSH agent is initialized in before_script
SSH_OPTS: "-o StrictHostKeyChecking=yes -o BatchMode=yes"
.ssh_setup: &ssh_setup
before_script:
# Load private key from GitLab CI variable (file type)
- eval $(ssh-agent -s)
- chmod 600 "$SSH_PRIVATE_KEY"
- ssh-add "$SSH_PRIVATE_KEY"
# Write known_hosts for bastion and target
- mkdir -p ~/.ssh && chmod 700 ~/.ssh
- echo "$SSH_KNOWN_HOSTS_BASTION" >> ~/.ssh/known_hosts
- echo "$SSH_KNOWN_HOSTS_TARGET" >> ~/.ssh/known_hosts
- chmod 644 ~/.ssh/known_hosts
# Configure ProxyJump transparently for all SSH calls
- |
cat >> ~/.ssh/config <<EOF
Host $DEPLOY_HOST
ProxyJump $BASTION_USER@$BASTION_HOST
User $DEPLOY_USER
IdentityFile $SSH_PRIVATE_KEY
StrictHostKeyChecking yes
EOF
- chmod 600 ~/.ssh/config
deploy:production:
stage: deploy
<<: *ssh_setup
script:
- ssh $SSH_OPTS $DEPLOY_USER@$DEPLOY_HOST "bash -s" < scripts/deploy.sh
environment:
name: production
only:
- tags
4. ProxyJump in GitLab CI/CD konfigurieren
Die ProxyJump-Direktive in ~/.ssh/config ersetzt den älteren ProxyCommand-Ansatz mit ssh -W. Beide lösen dasselbe Problem – der Unterschied liegt in Lesbarkeit und Fehlertoleranz. ProxyJump ist seit OpenSSH 7.3 verfügbar und auf allen modernen Linux-Systemen vorhanden. Die Direktive teilt dem SSH-Client mit: Verbinde zuerst mit dem Bastion-Host, und nutze diese Verbindung als Tunnel für den eigentlichen Zielserver. Aus Sicht der Anwendungsschicht sieht es aus wie ein direktes SSH-Kommando.
In GitLab-Pipelines schreibt man die ~/.ssh/config im before_script dynamisch, weil Runner-Umgebungen nach jedem Job zurückgesetzt werden. Die relevanten Werte – Bastion-Hostname, Bastion-User, Deploy-Hostname und Deploy-User – kommen aus GitLab CI/CD-Variablen. So enthält die .gitlab-ci.yml keine hardkodierten Hostnamen und kann für Staging und Production dieselbe Konfiguration verwenden, getrennt durch Environment-Scopes.
5. Known Hosts für Bastion und Zielserver verwalten
Das StrictHostKeyChecking-Flag auf yes zu setzen ist Pflicht in automatisierten Deployments. Wer es auf no setzt, um Verbindungsprobleme zu umgehen, öffnet die Tür für Man-in-the-Middle-Angriffe. Stattdessen hinterlegt man die SSH-Fingerprints beider Server – Bastion und Zielserver – als GitLab CI/CD-Variablen und schreibt sie im before_script in die ~/.ssh/known_hosts des Runners.
Die Fingerprints erhält man auf dem Server mit ssh-keyscan -H hostname. Der Output enthält eine oder mehrere Zeilen im known_hosts-Format und wird direkt als Variable SSH_KNOWN_HOSTS_BASTION bzw. SSH_KNOWN_HOSTS_TARGET in GitLab hinterlegt. Bei Server-Migrationen oder Key-Rotationen müssen diese Variablen aktiv aktualisiert werden – sonst schlägt die Pipeline mit einem Host-Verification-Fehler fehl, was das gewünschte Verhalten ist.
6. GitLab-Variablen und SSH-Agent im Runner
Für SSH-Bastion-Setups braucht die Pipeline folgende Variablen: SSH_PRIVATE_KEY als File-Variable (nicht masked string), SSH_KNOWN_HOSTS_BASTION, SSH_KNOWN_HOSTS_TARGET, BASTION_HOST, BASTION_USER, DEPLOY_HOST, DEPLOY_USER und DEPLOY_PATH. Der SSH-Private-Key muss als File-Variable hinterlegt werden, weil der ssh-add-Befehl einen Dateipfad erwartet, keine Stringvariable. GitLab schreibt File-Variablen in eine temporäre Datei und übergibt den Pfad als Umgebungsvariable.
Der SSH-Agent im Runner muss für jeden Job neu gestartet werden – er überlebt den Job-Wechsel nicht. Das eval $(ssh-agent -s) im before_script startet einen neuen Agent, und ssh-add lädt den Key. Wichtig: Der Agent-Prozess stirbt mit dem Job-Container, sodass der Key nicht in andere Jobs oder Runner überläuft. In Docker-Executors ist der Aufräumprozess automatisch; bei Shell-Executors empfiehlt sich trap "ssh-agent -k" EXIT als Sicherheitsnetz.
# Required CI/CD variables (set in GitLab project settings):
# SSH_PRIVATE_KEY — Type: File, deploy key without passphrase
# SSH_KNOWN_HOSTS_BASTION — bastion host fingerprint (ssh-keyscan output)
# SSH_KNOWN_HOSTS_TARGET — target server fingerprint
# BASTION_HOST — hostname or IP of the bastion server
# BASTION_USER — SSH user on the bastion (e.g. jump)
# DEPLOY_HOST — internal hostname of the target server
# DEPLOY_USER — deployment-only user on the target server
# DEPLOY_PATH — absolute base path, e.g. /var/www/magento
deploy:staging:
stage: deploy
before_script:
- eval $(ssh-agent -s)
- ssh-add "$SSH_PRIVATE_KEY"
- mkdir -p ~/.ssh && chmod 700 ~/.ssh
- printf '%s\n' "$SSH_KNOWN_HOSTS_BASTION" "$SSH_KNOWN_HOSTS_TARGET" \
>> ~/.ssh/known_hosts
- |
cat > ~/.ssh/config <<EOF
Host $DEPLOY_HOST
ProxyJump ${BASTION_USER}@${BASTION_HOST}
User $DEPLOY_USER
StrictHostKeyChecking yes
BatchMode yes
EOF
- chmod 600 ~/.ssh/config ~/.ssh/known_hosts
script:
- ssh "$DEPLOY_USER@$DEPLOY_HOST" "echo 'Connection via bastion: OK'"
- scp scripts/deploy.sh "$DEPLOY_USER@$DEPLOY_HOST:/tmp/deploy_$CI_JOB_ID.sh"
- ssh "$DEPLOY_USER@$DEPLOY_HOST" "bash /tmp/deploy_$CI_JOB_ID.sh && rm -f /tmp/deploy_$CI_JOB_ID.sh"
environment:
name: staging
url: https://staging.mironsoft.de
only:
- main
7. Direkter SSH vs. Bastion-Routing im Vergleich
Der Vergleich zwischen direktem SSH-Zugriff und Bastion-Routing zeigt, wo die wesentlichen Unterschiede in Sicherheit und Wartbarkeit liegen. Beide Ansätze ermöglichen automatisierte Deployments, aber mit grundlegend unterschiedlichen Risikoprofilen.
| Kriterium | Direkter SSH | Bastion + Jump Host | Bewertung |
|---|---|---|---|
| Angriffsfläche | Produktionsserver direkt exponiert | Nur Bastion exponiert | Bastion klar besser |
| Key-Widerruf | Alle Server einzeln aktualisieren | Bastion-Eintrag löschen genügt | Bastion effizienter |
| Audit-Logging | Nur auf Zielserver | Zentrales Log auf Bastion | Bastion auditierbar |
| Komplexität | Einfaches Setup | Höherer initialer Aufwand | Direkter SSH einfacher initial |
| User-Trennung | Gleicher User für alle Zwecke | Dedizierter Deploy-User pro Schicht | Bastion sauberer |
Der initiale Mehraufwand für ein Bastion-Setup amortisiert sich schnell: Wenn ein Runner-Key rotiert werden muss, genügt eine einzige Änderung auf dem Bastion statt auf jedem Zielserver. Bei wachsenden Infrastrukturen mit mehreren Staging- und Produktionsservern ist das ein erheblicher operativer Vorteil.
8. Typische Fehlerbilder und Diagnose
Das häufigste Problem bei Bastion-Setups ist die known_hosts-Verwaltung. Wenn der Bastion-Host oder der Zielserver eine neue Host-Key-Version hat – nach einer Migration, einem OS-Upgrade oder einem absichtlichen Key-Wechsel – schlägt StrictHostKeyChecking=yes fehl. Die Fehlermeldung ist eindeutig: Host key verification failed. Die Lösung ist das Aktualisieren der GitLab-Variablen SSH_KNOWN_HOSTS_BASTION oder SSH_KNOWN_HOSTS_TARGET. Wer die Variable nicht findet, hat meist vergessen, sie mit Environment-Scope anzulegen.
Ein zweites häufiges Problem: Der SSH-Agent ist im Runner nicht aktiv, weil eval $(ssh-agent -s) fehlt oder der Key mit ssh-add nicht geladen wurde. Das äußert sich als Permission denied (publickey), obwohl die Variable korrekt gesetzt ist. Die Diagnose: Im Pipeline-Log prüfen, ob ssh-add -l vor dem Deploy-Schritt einen Key listet. Ein dritter Fehlerfall betrifft ProxyJump-Konfigurationsfehler in der ~/.ssh/config: Tippfehler beim Hostnamen oder fehlende Leerzeichen nach den Direktiven führen zu kryptischen Verbindungsfehlern.
9. Rollback über Bastion-Verbindung
Rollback-Jobs in einer GitLab-Pipeline nutzen dieselbe SSH-Konfiguration wie Deploy-Jobs. Der einzige Unterschied: statt des Deploy-Skripts wird ein Rollback-Skript aufgerufen, das den current-Symlink auf das vorherige Release-Verzeichnis setzt. Da die Verbindungskette über den Bastion führt, muss der Rollback-Job denselben before_script-Block mit SSH-Agent und ProxyJump-Konfiguration enthalten.
Ein bewährtes Muster ist ein separater rollback-Stage mit when: manual in GitLab. Der Job erscheint in der Pipeline-Ansicht und kann mit einem Klick ausgelöst werden. Als Parameter übergibt man das Rollback-Ziel-Release entweder als hartkodierten Wert oder als Variable. Wer Rollback regelmäßig in der Staging-Umgebung testet, weiß im Produktionsfall, dass der Weg funktioniert – das ist die wichtigste Voraussetzung für echtes Zero-Downtime-Deployment.
rollback:production:
stage: rollback
when: manual
allow_failure: false
before_script:
# Same SSH setup as deploy job — bastion jump must work for rollback too
- eval $(ssh-agent -s)
- ssh-add "$SSH_PRIVATE_KEY"
- mkdir -p ~/.ssh && chmod 700 ~/.ssh
- printf '%s\n' "$SSH_KNOWN_HOSTS_BASTION" "$SSH_KNOWN_HOSTS_TARGET" \
>> ~/.ssh/known_hosts
- |
cat > ~/.ssh/config <<EOF
Host $DEPLOY_HOST
ProxyJump ${BASTION_USER}@${BASTION_HOST}
User $DEPLOY_USER
StrictHostKeyChecking yes
BatchMode yes
EOF
- chmod 600 ~/.ssh/config ~/.ssh/known_hosts
script:
# Determine previous release and switch symlink atomically
- |
ssh "$DEPLOY_USER@$DEPLOY_HOST" bash -s <<'REMOTE'
set -euo pipefail
cd "$DEPLOY_PATH"
# List releases sorted newest-first, pick second (previous) entry
PREV=$(ls -1dt releases/*/ | sed -n '2p' | tr -d '/')
if [[ -z "$PREV" ]]; then
echo "[ERROR] No previous release found for rollback" >&2
exit 1
fi
ln -sfn "$DEPLOY_PATH/$PREV" "$DEPLOY_PATH/current"
cd "$DEPLOY_PATH/current"
bin/magento cache:flush
echo "[OK] Rolled back to $PREV"
REMOTE
environment:
name: production
only:
- tags
10. Zusammenfassung
SSH-Bastion-Server und Jump Hosts sind kein Luxus für große Infrastrukturen, sondern eine vertretbare Sicherheitsmaßnahme ab dem Moment, in dem automatisierte Deployments auf Produktionssysteme zugreifen. Die Kombination aus Bastion als einzigem exponierten Einstiegspunkt, ProxyJump für transparentes SSH-Routing und dedizierten Deployment-Usern ohne sudo-Rechte schafft eine klare, auditierbare Verbindungsarchitektur. GitLab CI/CD lässt sich mit wenigen before_script-Zeilen für dieses Modell konfigurieren, ohne die Pipeline-Struktur zu verkomplizieren.
Der operativ wichtigste Schritt ist das regelmäßige Testen des gesamten Pfades – nicht nur von Staging nach Staging, sondern den kompletten Bastion-Jump-Deploy-Verify-Rollback-Zyklus auf einer produktionsähnlichen Umgebung. Wer das tut, kennt die Verbindungskette und kann im Störfall sicher handeln, statt unter Druck improvisionieren zu müssen.
SSH-Bastion und Jump Hosts in GitLab — Das Wichtigste auf einen Blick
Architektur
Bastion als einziger exponierter Punkt, ProxyJump für transparentes Routing, Produktionsserver nicht direkt erreichbar.
Deployment-User
Dedizierter Linux-Account ohne sudo, authorized_keys nur mit Deploy-Keys, keine persönlichen Entwickler-Schlüssel.
GitLab-Konfiguration
SSH_PRIVATE_KEY als File-Variable, Known Hosts beider Server als Variable, ~/.ssh/config dynamisch im before_script schreiben.
Rollback-Fähigkeit
Rollback-Job nutzt denselben SSH-Setup-Block – über denselben Bastion-Pfad, mit manuellem Trigger in GitLab-UI.