SSH-Deployment zu Windows Server mit Linux VM: Der sichere Weg

Einleitung

Ich habe einen Windows Server 2025, auf dem eine Linux VM mit Docker-Containern läuft. Für das Deployment meiner statischen Photo-Gallery wollte ich eine sichere Lösung ohne viele offene Ports. Die Antwort: SSH-Tunneling - ein einzelner SSH-Port für alles.

Das Setup

  • Windows Server 2025 mit öffentlicher IP
  • Linux VM auf dem Windows Server (intern, keine öffentliche IP)
  • Docker-Container mit nginx auf der VM
  • Lokaler Rechner (Windows/Linux) für Deployment

Ziel

  • Deployment via rsync zur Linux VM
  • RDP-Zugriff zum Windows Server (ohne offenen RDP-Port)
  • Zugriff auf Docker-Services (Portainer etc.)
  • Alles über einen einzigen SSH-Port (22222)

Warum SSH-Tunneling?

Vorteile

Ein Port für alles - Nur SSH Port 22222 nach außen offen
Sicherer - Weniger Angriffsfläche als 5+ offene Ports
Weniger Bot-Traffic - Non-Standard-Port drastisch weniger gescannt
Flexibel - RDP, SSH, Portainer, MySQL etc. über Tunnel erreichbar
Einfach - SSH-Key-Auth, keine VPN-Komplexität

Alternative: VPN

WireGuard wäre sicherer und professioneller, aber:

  • ~30 Minuten Setup vs. 5 Minuten SSH
  • Für ein privates Deployment-Setup ist SSH völlig ausreichend

Warum Port 22222 statt 22?

Nachteile eines öffentlichen SSH-Ports auf Port 22

  • Tausende Bot-Angriffe pro Tag - Automatisierte Scanner suchen Port 22
  • Log-Spam - Logs voll mit fehlgeschlagenen Login-Versuchen
  • Server-Last - SSH muss ständig Anfragen abweisen

Port 22222 reduziert das drastisch

  • 99% weniger Bot-Traffic - Scanner suchen nur Standard-Ports
  • Saubere Logs - Echte Probleme fallen sofort auf
  • Leicht zu merken - 5x die 2

Hinweis: Das ist "Security by Obscurity" - nicht technisch sicherer, aber praktisch viel angenehmer im Alltag.

Schritt 1: OpenSSH Server auf Windows Server 2025

Installation

# Als Administrator:

# Prüfen ob verfügbar:
Get-WindowsCapability -Online | Where-Object Name -like 'OpenSSH.Server*'

# Installieren:
Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0

# Dienst aktivieren:
Set-Service -Name sshd -StartupType 'Automatic'
Start-Service sshd

# Prüfen:
Get-Service sshd
# Sollte: Status = Running

Konfiguration auf Port 22222

# Config bearbeiten:
notepad C:\ProgramData\ssh\sshd_config

# Zeile ändern (# entfernen!):
Port 22222

# Speichern und Dienst neu starten:
Restart-Service sshd

# Prüfen ob Port offen:
netstat -an | findstr "22222"
# Sollte zeigen: TCP 0.0.0.0:22222 ... LISTENING

Windows Firewall

# Firewall-Regel erstellen:
New-NetFirewallRule -Name "OpenSSH-Server-In-TCP-22222" `
    -DisplayName "OpenSSH Server (sshd) Port 22222" `
    -Enabled True -Direction Inbound -Protocol TCP `
    -Action Allow -LocalPort 22222

# Alte Port-22-Regel deaktivieren (optional):
Disable-NetFirewallRule -Name "OpenSSH-Server-In-TCP"

# Prüfen:
Get-NetFirewallRule -DisplayName "*22222*" | Format-List

Wichtig: Die Firewall-Regel ist sofort aktiv, kein Neustart nötig!

Cloud-Provider Firewall

In der Cloud-Console deines Providers:

  • Server auswählen → Firewall/Security Groups
  • Neue Regel: TCP Port 22222, Quelle: 0.0.0.0/0 (oder nur deine IP)
  • Port 22 kann geschlossen bleiben

Ersten Test durchführen

# Von deinem lokalen Rechner:
Test-NetConnection -ComputerName SERVER-IP -Port 22222

# Sollte zeigen: TcpTestSucceeded : True

# Oder mit telnet/nc:
telnet SERVER-IP 22222
# oder
nc -zv SERVER-IP 22222

Schritt 2: SSH-Key-Authentifizierung

Auf dem lokalen Rechner

# Vorhandenen Key nutzen oder neuen erstellen:
ssh-keygen -t ed25519 -f C:\Users\USERNAME\.ssh\id_ed25519

# Bei "passphrase" ENTER drücken (kein Passwort für automatisches Deployment)
# Oder Passwort setzen und später mit ssh-agent verwalten

# Public Key anzeigen:
type C:\Users\USERNAME\.ssh\id_ed25519.pub
# Ausgabe kopieren (komplett!)

Linux/Mac:

# Key erstellen:
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519

# Public Key anzeigen:
cat ~/.ssh/id_ed25519.pub

Auf dem Windows Server

Per RDP verbinden und:

# Ordner erstellen (falls nicht vorhanden):
mkdir C:\ProgramData\ssh -ErrorAction SilentlyContinue

# Authorized Keys Datei bearbeiten:
notepad C:\ProgramData\ssh\administrators_authorized_keys

# Public Key einfügen (komplette Zeile)
# Speichern

# WICHTIG: Berechtigungen setzen
icacls C:\ProgramData\ssh\administrators_authorized_keys /inheritance:r
icacls C:\ProgramData\ssh\administrators_authorized_keys /grant "SYSTEM:(F)"
icacls C:\ProgramData\ssh\administrators_authorized_keys /grant "Administrators:(F)"

Warum diese Berechtigungen? OpenSSH auf Windows verweigert die Key-Auth, wenn andere Benutzer Zugriff auf die Datei haben. Die icacls-Befehle stellen sicher, dass nur SYSTEM und Administrators lesen können.

Testen

# Windows:
ssh -p 22222 Administrator@SERVER-IP

# Linux/Mac:
ssh -p 22222 Administrator@SERVER-IP

# Sollte OHNE Passwort einloggen! 🎉

Passwort-Authentifizierung deaktivieren (optional, aber empfohlen)

Auf dem Windows Server:

# sshd_config bearbeiten:
notepad C:\ProgramData\ssh\sshd_config

# Folgende Zeilen ändern/hinzufügen:
PasswordAuthentication no
PermitRootLogin no
MaxAuthTries 3

# Speichern und Dienst neu starten:
Restart-Service sshd

Schritt 3: SSH-Config für einfachen Zugriff

Windows

# Erstellen/Bearbeiten:
notepad C:\Users\USERNAME\.ssh\config

Linux/Mac

# Erstellen/Bearbeiten:
nano ~/.ssh/config

Config-Inhalt

# Windows Server mit Tunneln
Host myserver
    HostName SERVER-IP-ADDRESS
    Port 22222
    User Administrator
    IdentityFile ~/.ssh/id_ed25519
    
    # RDP-Tunnel (Windows Server)
    LocalForward 3389 localhost:3389
    
    # SSH-Tunnel zur VM
    LocalForward 2223 VM-INTERNAL-IP:22
    
    # Portainer-Tunnel
    LocalForward 9000 VM-INTERNAL-IP:9000
    
    # Optional: MySQL
    LocalForward 3306 VM-INTERNAL-IP:3306

# Linux VM über Tunnel
Host vm-docker
    HostName localhost
    Port 2223
    User vm-username
    # Erfordert aktiven Tunnel zu 'myserver'

VM-IP herausfinden:

Auf dem Windows Server:

# Hyper-V:
Get-VMNetworkAdapter -VMName <vm-name> | Select IPAddresses

# Docker:
docker inspect <container-name> | findstr IPAddress

# WSL2:
wsl hostname -I

Was ist LocalForward?

LocalForward erstellt einen Port auf deinem lokalen Rechner, der den Traffic durch den SSH-Tunnel zum Ziel leitet:

LocalForward 3389 localhost:3389
             │    │        └─ Port auf Server
             │    └─ Host vom Server aus gesehen
             └─ Port auf lokalem Rechner

Bedeutet: localhost:3389 auf deinem Rechner → SSH-Tunnel → localhost:3389 auf dem Server (RDP)

Schritt 4: Tunnel nutzen

SSH zum Windows Server

# Tunnel starten (Terminal bleibt offen):
ssh myserver

# Jetzt laufen im Hintergrund:
# - RDP auf localhost:3389
# - VM-SSH auf localhost:2223
# - Portainer auf localhost:9000

Wichtig: Das Terminal muss offen bleiben, solange du die Tunnel nutzen willst!

RDP über Tunnel

# In neuem Terminal/Tab:

# Windows:
mstsc /v:localhost:3389

# Linux:
remmina -c rdp://localhost:3389

# Mac:
open rdp://localhost:3389

Verbindet zu Windows Server, OHNE offenen RDP-Port! 🔒

SSH zur VM

# In neuem Terminal/Tab:
ssh vm-docker

# Verbindet zur Linux VM über den Tunnel

Portainer/Services im Browser

http://localhost:9000

Tunnel prüfen

# Welche Ports sind lokal offen?

# Windows:
netstat -an | findstr "LISTENING"

# Linux/Mac:
netstat -tuln | grep LISTEN

# Sollte zeigen:
# 127.0.0.1:3389  (RDP)
# 127.0.0.1:2223  (VM-SSH)
# 127.0.0.1:9000  (Portainer)

Schritt 5: Deployment einrichten

VM vorbereiten

Auf der Linux VM:

# SSH-Key hinzufügen:
mkdir -p ~/.ssh
nano ~/.ssh/authorized_keys
# Public Key einfügen

# Berechtigungen:
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys

# nginx Volume-Mount-Pfad prüfen:
docker inspect nginx-container | grep Mounts -A 10
# z.B.: /var/www/html → /usr/share/nginx/html

Projekt-Config

In deinem Deployment-Projekt:

# projects/my-website/project.config
nano projects/my-website/project.config
#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# Deployment Configuration
#━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

# Remote server hostname or SSH config alias
REMOTE_HOST="vm-docker"

# Remote username
REMOTE_USER="vm-username"

# Remote path where the site will be deployed
REMOTE_PATH="/var/www/html/my-website"

Deployment testen

# Terminal 1: Tunnel starten
ssh myserver

# Terminal 2: Deployment
cd ~/code/MyProject
./deploy.sh -p my-website

# 🎉 Deployment läuft über den Tunnel!

Troubleshooting: rsync nicht gefunden

Wenn rsync auf Windows fehlt:

# Git for Windows installieren (bringt rsync mit)
# Oder via WSL2:
wsl rsync -avz ...

# Oder Cygwin installieren

Workflow im Alltag

# 1. Tunnel starten (einmal am Tag):
ssh myserver

# 2. Arbeiten:
# - RDP: mstsc /v:localhost:3389
# - Portainer: http://localhost:9000
# - Deploy: ./deploy.sh -p my-website
# - VM-SSH: ssh vm-docker

# 3. Tunnel läuft solange Terminal offen bleibt

Optional: Persistent Tunnel

Für dauerhaften Tunnel ohne offenes Terminal:

Linux/Mac: autossh

# autossh installieren:
sudo apt install autossh  # Debian/Ubuntu
brew install autossh      # Mac

# Tunnel starten:
autossh -M 0 -f -N myserver

# -M 0: Kein Monitoring-Port
# -f: Im Hintergrund
# -N: Keine Shell

# Beenden:
pkill autossh

Windows: Task Scheduler

# tunnel-start.ps1 erstellen:
Start-Process ssh -ArgumentList "-N myserver" -WindowStyle Hidden

# Als geplante Aufgabe:
# Task Scheduler → Create Task
# Trigger: At logon
# Action: PowerShell -File C:\Path\tunnel-start.ps1

Windows: NSSM (Non-Sucking Service Manager)

# NSSM installieren (via Chocolatey):
choco install nssm

# SSH als Windows-Dienst:
nssm install SSHTunnel "C:\Windows\System32\OpenSSH\ssh.exe" "-N myserver"
nssm start SSHTunnel

# Dienst verwalten:
nssm stop SSHTunnel
nssm remove SSHTunnel

Systemd Service (Linux)

# ~/.config/systemd/user/ssh-tunnel.service
[Unit]
Description=SSH Tunnel to Server
After=network.target

[Service]
Type=simple
ExecStart=/usr/bin/ssh -N myserver
Restart=always
RestartSec=10

[Install]
WantedBy=default.target

# Aktivieren:
systemctl --user enable ssh-tunnel
systemctl --user start ssh-tunnel

# Status prüfen:
systemctl --user status ssh-tunnel

Sicherheit

Was haben wir erreicht?

Nur ein Port offen: SSH Port 22222
Key-only Auth: Keine Passwörter
Non-Standard-Port: Drastisch weniger Bot-Scans
Verschlüsselt: Alle Verbindungen über SSH-Tunnel
Keine direkten Ports: RDP, Portainer etc. nur intern erreichbar

Weitere Härtung (optional)

# sshd_config auf dem Server:
PasswordAuthentication no
PermitRootLogin no
MaxAuthTries 3
ClientAliveInterval 300
ClientAliveCountMax 2

fail2ban für zusätzlichen Schutz

Windows: IPBan (Open Source Alternative zu fail2ban)

# IPBan installieren:
# https://github.com/DigitalRuby/IPBan

# Blockiert automatisch IPs nach mehreren fehlgeschlagenen Login-Versuchen

Linux (auf der VM):

sudo apt install fail2ban

# Config für SSH:
sudo nano /etc/fail2ban/jail.local

[sshd]
enabled = true
port = 22
maxretry = 3
bantime = 3600

SSH-Key mit Passwort schützen

Wenn du deinen SSH-Key mit Passwort schützen willst (empfohlen für zusätzliche Sicherheit):

# Key mit Passwort erstellen:
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519
# Passphrase eingeben

# SSH-Agent nutzen (speichert Passwort für Session):

# Windows:
Start-Service ssh-agent
Set-Service -Name ssh-agent -StartupType Automatic
ssh-add ~/.ssh/id_ed25519
# Passwort einmal eingeben

# Linux/Mac:
eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_ed25519
# Passwort einmal eingeben

# Danach funktioniert SSH ohne Passwort-Abfrage!

Troubleshooting

Timeout bei SSH-Verbindung

# 1. SSH-Dienst läuft?
Get-Service sshd
# Sollte: Status = Running

# 2. Port prüfen:
netstat -an | findstr "22222"
# Sollte: TCP 0.0.0.0:22222 ... LISTENING

# 3. Windows Firewall prüfen:
Get-NetFirewallRule -DisplayName "*22222*"
# Sollte: Enabled = True

# 4. Cloud Firewall prüfen in Provider-Console

# 5. Von außen testen:
Test-NetConnection -ComputerName SERVER-IP -Port 22222
# Sollte: TcpTestSucceeded = True

Permission Denied (publickey)

# Public Key korrekt auf Server?
# Auf Server:
type C:\ProgramData\ssh\administrators_authorized_keys

# Berechtigungen korrekt?
icacls C:\ProgramData\ssh\administrators_authorized_keys
# Sollte nur SYSTEM und Administrators haben

# Key auf lokalem Rechner vorhanden?
# Lokal:
dir C:\Users\USERNAME\.ssh\
ssh-add -l  # Zeigt geladene Keys

# SSH mit Debug-Output:
ssh -vvv -p 22222 Administrator@SERVER-IP
# Zeigt genau wo es scheitert

Tunnel funktioniert nicht

# SSH mit Debug-Output:
ssh -vvv myserver

# Port-Forwarding aktiv?
# Lokal:
netstat -an | findstr "3389"  # RDP
netstat -an | findstr "2223"  # VM-SSH

# VM-IP korrekt in SSH-Config?
# Auf Server testen:
ping VM-INTERNAL-IP

rsync: command not found

# Windows:
# Git for Windows installieren (bringt rsync mit)
# Oder WSL2 nutzen:
wsl rsync -avz ...

# Linux/Mac:
sudo apt install rsync  # Debian/Ubuntu
brew install rsync      # Mac

Connection timed out nach einiger Zeit

# SSH-Config erweitern:
notepad ~/.ssh/config

# Hinzufügen:
Host myserver
    # ... bestehende Einstellungen ...
    ServerAliveInterval 60
    ServerAliveCountMax 3

# Hält Verbindung mit regelmäßigen "Heartbeats" aktiv

Monitoring und Logging

SSH-Logs auf Windows Server

# Event Viewer:
Get-EventLog -LogName Application -Source sshd -Newest 20

# Oder:
Get-WinEvent -LogName "OpenSSH/Operational" -MaxEvents 20

# Fehlgeschlagene Logins:
Get-WinEvent -LogName "OpenSSH/Operational" | 
    Where-Object {$_.Message -like "*Failed*"}

SSH-Logs auf Linux VM

# Live-Logs:
sudo tail -f /var/log/auth.log

# Fehlgeschlagene Logins:
sudo grep "Failed password" /var/log/auth.log

# Erfolgreiche Logins:
sudo grep "Accepted publickey" /var/log/auth.log

Tunnel-Status überwachen

# Skript: check-tunnel.sh
#!/bin/bash

if netstat -tuln | grep -q ":3389"; then
    echo "✅ RDP-Tunnel läuft"
else
    echo "❌ RDP-Tunnel nicht aktiv"
fi

if netstat -tuln | grep -q ":2223"; then
    echo "✅ VM-SSH-Tunnel läuft"
else
    echo "❌ VM-SSH-Tunnel nicht aktiv"
fi

Best Practices

1. Separate Keys für verschiedene Zwecke

# Persönlicher Key (mit Passwort):
~/.ssh/id_ed25519_personal

# Deployment Key (ohne Passwort):
~/.ssh/id_ed25519_deploy

# Server-Management Key:
~/.ssh/id_ed25519_server

2. SSH-Config strukturieren

# ~/.ssh/config

# === Production Servers ===
Host prod-*
    User admin
    IdentityFile ~/.ssh/id_ed25519_server
    ServerAliveInterval 60

Host prod-web
    HostName web.example.com
    Port 22222

Host prod-db
    HostName db.example.com
    Port 22222

# === Development ===
Host dev-*
    User developer
    IdentityFile ~/.ssh/id_ed25519_dev

# === Deployment ===
Host deploy-*
    User deployer
    IdentityFile ~/.ssh/id_ed25519_deploy

3. Regelmäßige Security-Audits

# Checklist:
# ✅ Nur Key-Auth aktiviert?
# ✅ Root-Login deaktiviert?
# ✅ fail2ban/IPBan läuft?
# ✅ Logs regelmäßig prüfen?
# ✅ Unbenutzte Keys entfernen?
# ✅ Windows Updates installiert?
# ✅ SSH-Version aktuell?

4. Backup der SSH-Keys

# Keys verschlüsselt sichern:
tar czf ssh-keys-backup.tar.gz ~/.ssh/
gpg -c ssh-keys-backup.tar.gz
# Passwort eingeben

# In Cloud/USB speichern:
# ssh-keys-backup.tar.gz.gpg

Zusammenfassung

SSH-Tunneling ist eine elegante, sichere Lösung für Server-Management und Deployment:

Vorteile

  • Einfach: Keine VPN-Komplexität
  • Sicher: Minimale Angriffsfläche (nur ein Port)
  • Flexibel: Beliebige Services tunneln
  • Produktionsreif: Bewährt in vielen Projekten
  • Kostenlos: Keine zusätzliche Software nötig
  • Plattformübergreifend: Windows, Linux, Mac

Nachteile

  • ⚠️ Terminal muss offen bleiben (außer bei Background-Lösungen)
  • ⚠️ Bei Verbindungsabbruch muss neu verbunden werden
  • ⚠️ Nicht ideal für viele gleichzeitige Nutzer

Wann VPN stattdessen?

  • 🔐 Hochsensible Daten (Finanzen, Gesundheit)
  • 👥 Mehrere Team-Mitglieder brauchen Zugriff
  • 🏢 Firmen-Compliance verlangt VPN
  • 🌐 Gesamtes Netzwerk soll erreichbar sein
  • 🔄 24/7 Verbindung ohne manuelle Tunnel

Für Hobby-/Semi-Professional-Projekte ist SSH-Tunneling perfekt!

Ressourcen

Dokumentation

Tools

  • PuTTY - SSH-Client für Windows (GUI)
  • WinSCP - SCP/SFTP-Client für Windows
  • Remmina - RDP-Client für Linux
  • autossh - Persistent SSH-Tunnel

Sicherheit


Tags: #SSH #Windows-Server #Deployment #DevOps #Security #Tunneling #Docker #RDP

Hinweis: Diese Anleitung basiert auf echten Produktions-Erfahrungen mit einem Windows Server 2025 und Linux VM Setup. Alle Befehle wurden getestet und funktionieren.