
Waldemar Schneider
IT-Systemintegrator, der sowohl die Produktionshalle als auch den Serverraum versteht.
© 2026 Waldemar Schneider
Portfolio

Portfolio-Website: DMZ-Architektur mit VLAN-Segmentierung
Inhalt
Projektübersicht
Die Portfolio-Website lief zunächst in einem einzelnen Container direkt im Heimnetz. In dieser Phase wurde die Infrastruktur auf eine DMZ-Architektur mit zwei isolierten VLAN-Segmenten umgestellt: ein Proxy-Container nimmt externen Traffic entgegen, ein separater App-Container verarbeitet ihn. Beide sind vom Heimnetz vollständig getrennt.
1. Situation (Ausgangslage)
Der bisherige Setup brachte externen Web-Traffic über einen Cloudflare Tunnel direkt in den Heimnetz-Bereich. Ein Container erledigte alles: Tunnel, Reverse Proxy und Flask-Applikation. Das bedeutete: wer eine Schwachstelle in Nginx oder Flask fand und in den Container einbrach, landete mitten im Heimnetz, mit direkter Sichtbarkeit auf Hypervisor, Backup-Server, NAS und andere Geräte. Es fehlte jede Netzwerkisolation zwischen öffentlich erreichbaren Diensten und der eigentlichen Heimnetz-Infrastruktur.
2. Task (Zielsetzung)
- Netzwerktrennung: Öffentlich erreichbare Dienste in ein isoliertes DMZ-Segment auslagern, das App-Backend in ein separates Backend-Segment.
- Minimale Angriffsfläche: Jeder Container bekommt nur die Netzwerkberechtigungen die er wirklich braucht, nicht mehr.
- Deployment-Workflow erhalten: Code-Deployments vom Laptop aus sollen weiterhin funktionieren, trotz Netzwerkisolation.
3. Action (Umsetzung)
A. VLAN-Segmentierung auf Proxmox und OPNsense
Die bestehende Netzwerk-Bridge auf dem Proxmox-Host wurde auf VLAN-Awareness umgestellt. Damit können VMs und Container VLAN-Tags tragen und der Switch leitet Pakete nur noch an Ports mit passendem Tag weiter.
In OPNsense wurden zwei neue VLAN-Interfaces angelegt und als eigene Netzwerksegmente konfiguriert: ein DMZ-Segment für den Proxy-Container und ein Backend-Segment für die Flask-Applikation. Beide erhalten je ein /29-Subnetz, die minimale Grösse die noch ausreichend Adressen bietet. Die Überlegung dahinter: ein /24 hätte 254 nutzbare Adressen für einen einzelnen Container. /29 bietet sechs, was Gateway, Container und Reserve abdeckt und die Angriffsfläche auch bei einem Einbruch ins Subnetz klein hält.
OPNsense fungiert als Gateway beider Segmente und kontrolliert jeden Paketfluss zwischen ihnen.
B. Firewall-Regeln: Least Privilege umsetzen
Für jedes Segment wurden Firewall-Regeln nach dem Prinzip "alles blockiert, nur Erlaubtes explizit freigeben" konfiguriert:
Der Proxy-Container darf ausgehend nur das Notwendige: DNS zum Gateway, HTTPS (TCP 443) für Cloudflare API-Aufrufe, QUIC (UDP 7844) für den eigentlichen Cloudflare-Tunnel-Datenkanal, und TCP auf Port 8000 zum App-Container. Kein Internetzugang für Updates im Normalbetrieb, keine Verbindung ins Heimnetz.
Der App-Container darf ausgehend nur DNS zum Gateway und eine einzige TCP-Verbindung: Port 3000 zum internen Git-Server (Forgejo) für Code-Deployments. Kein Internetzugang, kein Heimnetz.
Zusätzlich wurden auf dem LAN-Interface explizite Block-Regeln eingetragen, die LAN-Geräte an direktem Zugriff auf DMZ und Backend hindern. Ohne diese Regeln hätte die "Default allow LAN to any"-Regel in OPNsense die Isolation einseitig gemacht.
C. Zwei-Container-Stack einrichten und Tunnel umziehen
Auf dem Proxy-Container wurden Nginx als Reverse Proxy und der Cloudflare-Tunnel-Daemon (cloudflared) installiert. Nginx leitet alle eingehenden Anfragen an den App-Container weiter. Der Cloudflare Tunnel wurde vom alten Container auf den neuen Proxy umgezogen: cloudflared auf dem alten Container gestoppt, auf dem neuen gestartet. Der Tunnel-Connector registriert sich beim Start bei Cloudflare und der Wechsel erfolgt in Sekunden ohne DNS-Änderungen.
Auf dem App-Container (Linux, Debian 12) wurden Python-Umgebung, Flask-Applikation und Gunicorn als systemd-Dienst eingerichtet. Da Nginx jetzt in einem anderen Container sitzt, muss Gunicorn auf allen Interfaces lauschen statt nur auf localhost. Die Zugangskontrolle übernimmt die Firewall, nicht der Bind-Parameter.
D. deploy-User und SSH-Zugang
Für Deployments wurde ein dedizierter Benutzer ohne root-Rechte angelegt. Dieser darf per sudo nur zwei Befehle ausführen: Gunicorn neustarten und den Status abfragen. Die Ownership des Projekt-Verzeichnisses liegt beim deploy-User, damit git pull ohne sudo möglich ist.
Da der App-Container im Backend-VLAN nicht direkt vom Laptop erreichbar ist, wurde SSH ProxyJump konfiguriert: eine einzige Zeile in der lokalen SSH-Konfiguration macht den Proxmox-Host transparent zum Sprungbrett. Ein Befehl (ssh portfolio-app) verbindet sich automatisch über den Host zum Container. Eine Firewall-Regel auf dem LAN-Interface erlaubt genau diese Verbindung: nur vom Proxmox-Host, nur auf Port 22, nur zum App-Container.
4. Result (Ergebnis & Verifikation)
Die Website ist weiterhin öffentlich erreichbar unter https://ws-informatik.ch. Der gesamte Stack wurde nach der Migration mit 12 systematischen Tests verifiziert:
- Proxy kann Gateway erreichen, DNS auflösen und HTTPS-Verbindungen aufbauen. Verbindungen ins Heimnetz schlagen fehl.
- App-Container kann DNS auflösen und den internen Git-Server erreichen. Verbindungen ins Internet und ins Heimnetz schlagen fehl.
- Laptop-Pings auf DMZ und Backend-Adressen schlagen fehl (bidirektionale Isolation).
- Deployment-Workflow funktioniert: git push vom Laptop, git pull auf dem App-Container, Gunicorn-Neustart. Änderung sofort live.
Jedes "blockiert" in den Tests ist das gewünschte Ergebnis. Die Isolation ist vollständig in beide Richtungen.
Lessons Learned
- VLAN-Awareness ist kein Nice-to-have: Subnetze allein trennen auf Layer 3. Ohne VLAN-Tagging hängt jeder Container an derselben Bridge und kann seine IP beliebig ändern. VLANs setzen die Trennung auf Layer 2, wo ein Container-Prozess nichts dagegen tun kann.
- LAN-Block-Regeln vergessen sich leicht: Die "Default allow LAN to any"-Regel in OPNsense ist historisch gewachsen und erlaubt sehr viel. Neue Segmente brauchen immer explizite Block-Regeln auf dem LAN-Interface, sonst ist die Isolation einseitig.
- cloudflared nutzt QUIC auf UDP 7844: Wer nur TCP 443 in der Firewall freigibt, sieht den Tunnel scheitern mit "timeout: no recent network activity". Der eigentliche Datenkanal läuft über QUIC, nicht HTTPS. TCP 443 wird nur für Registrierungs-API-Aufrufe genutzt.
- Deaktivierte Firewall-Regeln als Update-Mechanismus: Statt dauerhaftem Internetzugang für apt-Updates funktioniert ein sauberer Toggle: Update-Regel aktivieren, Updates durchführen, Regel deaktivieren. Im Normalbetrieb kein Internetzugang, keine dauerhafte Angriffsfläche.
- 0.0.0.0 vs. 127.0.0.1 bei Gunicorn: Laufen Nginx und Gunicorn im selben Container, genügt localhost. Laufen sie in verschiedenen Containern/VLANs, muss Gunicorn auf allen Interfaces lauschen. Die Zugangskontrolle gehört dann in die Firewall.