kohlmajer.de



Ein VPS als Internet-Außenposten

— zuletzt geändert am
941 Wörter, Lesezeit 6 min

Vor etwa neun Monaten habe ich einen ARM-Server bei netcup gemietet und angefangen, Dienste darauf zu bündeln, die vorher auf mehreren Maschinen lagen. Das Ziel war nicht, alles selbst zu hosten, weil man es kann, sondern weil ich wissen wollte, wo meine Daten liegen und um Infrastruktur und Kosten zu sparen. Inzwischen läuft der Server stabil genug, dass ich aufschreiben kann, was funktioniert hat und was nicht.

Alles liegt verschlüsselt

Die Festplatte ist vollverschlüsselt. Nach jedem Neustart muss ich die Partition von Hand entsperren, bevor irgendetwas läuft — LUKS mit Argon2id als Schlüsselableitungsfunktion; der Server sendet mir eine Info, falls der LUKS-Container (noch) gesperrt ist.

cryptsetup luksOpen /dev/vda4 vda4_crypt
mount /dev/mapper/vda4_crypt /mnt/vda4_crypt
cd /mnt/vda4_crypt/docker-neu && docker compose up -d

Drei Befehle, dann läuft der Stack. Dafür hat der Hoster keinen Zugriff auf die Nutzdaten, solange die Partition gesperrt ist. Das war mir wichtig genug, um den Komfortverlust in Kauf zu nehmen.

Zwölf Container, eine Compose-Datei

Alle Dienste laufen als Docker-Container, gesteuert über eine einzige Compose-Datei. Caddy als Reverse-Proxy terminiert TLS für alle Domains und holt die Zertifikate automatisch über Let’s Encrypt. Dahinter stehen unter anderem:

  • Stalwart als Mailserver, der den alten Postfix/Dovecot-Verbund ersetzt. Zwei Domains, fünf Postfächer, automatische DKIM-Rotation, CalDAV und CardDAV inklusive.
  • Vaultwarden als Passwortmanager, nur über WireGuard-VPN erreichbar, mit Hardware-Schlüssel als zweitem Faktor.
  • PostgreSQL mit PostGIS und TimescaleDB für Zeitreihendaten — Telegraf sammelt Metriken von verschiedenen Servern, die nach sieben Tagen automatisch komprimiert werden.
  • CrowdSec als Intrusion-Detection-System, das auffällige Zugriffe erkennt und per nftables-Firewall aussperrt — mit eigenen Erkennungsregeln für Stalwart und für unerwünschte KI-Crawler.
  • Ein Notaus-System, das per Kommandozeile den gesamten Stack herunterfährt und die verschlüsselte Partition sperrt.

Dazu kommen Unbound als DNS-Resolver für das VPN, RustDesk für Fernwartung, ein Minecraft-Server und diese Webseite.

Architekturdiagramm des VPS: Caddy als Reverse-Proxy vor Stalwart, Hugo und weiteren Diensten, PostgreSQL als zentrale Datenbank, CrowdSec an der Firewall, alles auf einer LUKS-verschlüsselten Partition.

Die Netzwerke sind absichtlich getrennt — nicht jeder Container darf mit jedem sprechen. Caddy sieht die HTTP-Dienste, Stalwart sieht die Datenbank, und CrowdSec sieht die Logs:

networks:
  proxy:     # Caddy ↔ HTTP-Dienste
  mail:      # Stalwart intern
  database:  # PostgreSQL ↔ Stalwart, Telegraf
  crowdsec:  # CrowdSec ↔ Caddy

VPN-geschützte Dienste wie Vaultwarden blockiert Caddy per abort für alles außerhalb des WireGuard-Subnetzes:

pwd.example.de {
    @blocked not remote_ip 10.0.0.0/24
    abort @blocked
    reverse_proxy vaultwarden:80
}

Die Mail-Migration

Der aufwendigste Teil war der Umzug der Familienpost. Der alte Server lief seit 2021 mit Postfix und Dovecot auf einer separaten Ubuntu-Maschine ebenfalls bei netcup. Stalwart speichert alles in PostgreSQL statt im Dateisystem, was den Umzug per Verzeichniskopie ausschloss.

Am Ende habe ich imapsync benutzt: Postfach für Postfach vom alten Server zum neuen kopiert, dann die DNS-Einträge umgestellt, einen letzten Delta-Sync gefahren und den alten Server abgeschaltet.

imapsync \
  --host1 alt.example.de --user1 andreas --passfile1 pass1.txt \
  --host1 neu.example.de --user2 andreas --passfile2 pass2.txt \
  --ssl1 --ssl2 --no-modulesversion

Das Werkzeug ist idempotent — man kann es beliebig oft laufen lassen, ohne Duplikate zu erzeugen. 926 MiB für das größte Postfach, fünf Postfächer insgesamt, und am Ende 10 von 10 Punkten bei mail-tester.com.

DANE und die wandernden Zwischenzertifikate

Für die Maildomains sind DANE-Records im DNS hinterlegt — kryptographische Fingerabdrücke, die dem sendenden Server bestätigen, dass er mit dem richtigen Zertifikat spricht. Das funktionierte gut, bis Let’s Encrypt im Sommer 2026 neue Zwischenzertifikate ausrollte.

Mein Automatisierungsskript berechnete den Fingerabdruck des Zwischenzertifikats dynamisch aus der aktuellen Zertifikatskette — ein TLSA »2 1 1«-Record, also der SHA-256-Hash des Public Keys der Intermediate CA:

openssl x509 -in chain.pem -noout -pubkey \
  | openssl pkey -pubin -outform DER \
  | sha256sum

Das ist elegant, aber fragil: sobald Let’s Encrypt ein neues Intermediate ausliefert, stimmt der Hash im DNS nicht mehr — und fremde Mailserver verweigern die Zustellung.

Die Lösung war, alle sechs aktuellen und zukünftigen Intermediate-Hashes als statische Liste ins Skript zu übernehmen. Pro Port entstehen jetzt sieben TLSA-Records — ein dynamischer »3 1 1« für das eigene Zertifikat und sechs statische »2 1 1« für die LE-Intermediates:

_25._tcp.mail  TLSA  3 1 1 <hash-aus-dem-eigenen-cert>
_25._tcp.mail  TLSA  2 1 1 cbbc559b44d524d6a132bdac67...
_25._tcp.mail  TLSA  2 1 1 885bf0572252c6741dc9a52f50...
               ...   (4 weitere LE-Intermediate-Hashes)

So hält der eine Wert mit jeder Zertifikatserneuerung Schritt, während die anderen sechs für Jahre gültig bleiben. Die Hashes stammen aus Viktor Dukhovnis DANE-Survey — einem automatisierten Prüfer, der auch das Problem überhaupt erst gemeldet hat.

IPv6 war kaputt, ohne dass es auffiel

Beim Testen der DANE-Records stellte sich heraus, dass Stalwart über IPv6 zwar TCP-Verbindungen annahm, aber kein SMTP-Banner lieferte. Die Ursache war banal: Docker erstellt für benutzerdefinierte Netzwerke keine IPv6-NAT-Regeln, solange man das nicht ausdrücklich konfiguriert. Für IPv4 passiert das automatisch. Für IPv6 muss man dem Docker-Daemon eine Zeile mitgeben:

{
  "ipv6": true,
  "fixed-cidr-v6": "2001:db8:1::/64",
  "ip6tables": true
}

Die entscheidende Zeile ist ip6tables — ohne sie existiert die IPv6-NAT-Chain DOCKER zwar, bleibt aber leer, und kein eingehender IPv6-Verkehr erreicht die Container.

Das hatte ich monatelang nicht bemerkt, weil die meisten Mailserver zuerst IPv4 versuchen und dort alles funktionierte. Erst der DANE-Prüfer meldete »1 of 2 peers« — und meinte damit die IPv6-Adresse.

Lehre: Ein Dienst, der auf zwei Protokollen lauscht, muss auf beiden getestet werden. »Es geht ja« reicht nicht, wenn man nicht weiß, über welchen Weg es gerade geht.

Was ich daraus gelernt habe

  • Verschlüsselung im Ruhezustand kostet (nur) Komfort. Jeder Neustart braucht Handarbeit. Das ist der Preis dafür, dass die Daten bei Verlust oder einer Beschlagnahmung geschützt sind (solange der LUKS-Container nicht entsperrt ist).
  • Eine Compose-Datei ist Dokumentation. Wer den Stack verstehen will, liest sie. Wer sie nicht versteht, sollte nichts ändern.
  • Automatisierung muss die Ausnahme kennen. Mein DKIM-Rotationsskript lief ein halbes Jahr fehlerfrei, bis die Zwischenzertifikate wechselten. Ein Monitoring, das nur den Normalfall prüft, ist keines.
  • IPv6 ist kein optionales Extra. Mailserver, die nur über IPv4 erreichbar sind, landen bei manchen Anbietern in der Warteschlange oder im Spam. Und DANE-Prüfer schauen auf beide Adressen.