Files
htl-reverse-proxy-tls-lab/challenges
hkoeck e639aa4f88 Entferne Port-Env-Indirektion und dokumentiere alle lab.sh-Verben
Schein-Konfigurierbarkeit beseitigt: HTTP_PORT/HTTPS_PORT liessen sich
zwar im Compose-Mapping setzen, aber nginx (statische Config) und alle
Test-Kommandos in MD/HTML waren auf 8080/8443 hartkodiert. Der einzige
reale Grund die Vars zu aendern (Port-Konflikt) brach also still die
gesamte Doku. Ports sind im Lab feste Konstanten -> hartkodieren.

- docker-compose.yml + Snippets (easyrsa-hints.md, solutions.html):
  ${HTTP_PORT:-8080}/${HTTPS_PORT:-8443} -> feste 8080/8443
- .env.example entfernt (enthielt nur diese zwei Vars), .env-Erzeugung
  aus bootstrap.sh entfernt; .gitignore behaelt .env als Vorsorge
- README: .env-Bullet raus

Doku-Luecke geschlossen:
- README: up/down/logs ergaenzt (vorher nur in lab.sh usage()),
  jeweils mit Kurzbeschreibung

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 22:43:05 +02:00
..

Workshop Challenges (Reverse Proxy + TLS Focus)

Diese Aufgaben sind bewusst auf manuelle Proxy-Konfiguration ausgelegt.

Abgabe-Format pro Challenge

  • 1-3 Minuten Demo
  • Done-Check Command live ausfuehren
  • 2-3 Saetze erklaeren: was geaendert, warum, welche Wirkung

Wo laufen die Kommandos? (besonders Windows)

Die meisten Teilnehmer arbeiten unter Windows mit Docker Desktop + WSL. Wichtig:

  • Stack steuern (./scripts/lab.sh ... in WSL oder der PowerShell-Wrapper scripts/workshop.ps1): geht aus PowerShell oder aus dem WSL-Terminal.
  • Alle Challenge-Kommandos (curl, openssl, grep, wget, for-Schleifen, ./scripts/compose.sh ...): laufen im WSL-Terminal (bash), nicht in PowerShell.
  • Warnung: In PowerShell ist curl ein Alias fuer Invoke-WebRequest und versteht die hier genutzten Flags (-I, -k, --cacert) nicht. Immer im WSL-Terminal testen.
  • Das Repo liegt im WSL-Dateisystem oder unter /mnt/c/...; arbeite am besten aus dem WSL-Terminal im Projektordner.

Arbeitsmodus

  1. Dateien anpassen (proxy/nginx.conf, docker-compose.yml).
  2. Deployen: nach reinen nginx.conf-Aenderungen reicht ./scripts/lab.sh proxy-reload; nach Aenderungen an docker-compose.yml (z. B. neue Ports oder Services) immer ./scripts/lab.sh redeploy, da der Container neu erstellt werden muss.
  3. Testen mit curl / openssl / Wireshark.
  4. Bei Problemen: ./scripts/compose.sh logs reverse-proxy.

Arbeitsweise (wichtig)

  • Es wird additiv in einer einzigen proxy/nginx.conf gearbeitet: jede Challenge erweitert den Stand der vorherigen. Loesungen nicht wieder loeschen - die TLS-Challenges (10-12) bauen direkt aufeinander auf.
  • Ausnahme Challenge 9 (Debugging): Diese ueberschreibt die nginx.conf bewusst. Vorher sichern (cp proxy/nginx.conf proxy/nginx.conf.bak), danach wiederherstellen.
  • Jede Angabe nennt einen Ausgangszustand (worauf baue ich auf) und einen Zielzustand / Akzeptanz (woran erkenne ich "fertig"). Der Done-Check ist die pruefbare Form des Zielzustands.
  • Komplett zuruecksetzen geht jederzeit mit ./scripts/lab.sh reset (Container/Volumes) bzw. ./scripts/lab.sh reset-hard (zusaetzlich lokale Datei-Aenderungen verwerfen).

Easy

1) Routing verstehen (aktiv)

Ziel

  • Verstehen, wie Nginx Requests per Pfad an unterschiedliche Upstreams schickt.

Datei

  • proxy/nginx.conf (nur lesen)

Schritte (Muss)

  • Vor dem Test in proxy/nginx.conf nachsehen, welche location auf welchen upstream zeigt.
  • Dann erst Requests ausfuehren.
  • In eigenen Worten erklaeren, warum die Antworten unterschiedlich sind.

Zielzustand / Akzeptanz

  • Du kannst fuer /service/a und /service/b jeweils Location -> Upstream -> Backend benennen und begruenden, warum die Antworten unterschiedlich sind.

Done-Check

curl http://localhost:8080/service/a   # -> "Reverse Proxy Target A"
curl http://localhost:8080/service/b   # -> "Reverse Proxy Target B"

Warum wichtig

  • Das ist die Kernkompetenz bei Reverse Proxys: Request-Fluss lesen und korrekt begruenden.

2) Drittes Backend manuell hinzufuegen

Ziel

  • Setup sicher erweitern, ohne bestehende Routen kaputt zu machen.

Ausgangszustand

  • Stack laeuft, /service/a und /service/b antworten.
  • backends/c/index.html liegt bereits im Repo (Starterseite).

Dateien

  • docker-compose.yml
  • proxy/nginx.conf
  • backends/c/index.html (vorhanden, darf angepasst werden)

Schritte (Muss)

  • Service backend-c in Compose einbauen.
  • Upstream + Route /service/c in Nginx anlegen.
  • Deployen mit ./scripts/lab.sh redeploy (Compose-Aenderung -> Container-Neustart noetig).

Zielzustand / Akzeptanz

  • /service/c liefert Backend C.
  • /service/a und /service/b funktionieren unveraendert weiter (keine Regression).

Done-Check

curl http://localhost:8080/service/c   # -> enthaelt "Reverse Proxy Target C"
curl http://localhost:8080/service/a   # -> weiterhin "Reverse Proxy Target A"
curl http://localhost:8080/service/b   # -> weiterhin "Reverse Proxy Target B"

Warum wichtig

  • In realen Umgebungen kommen neue Services laufend dazu. Saubere Erweiterung ohne Regression ist entscheidend.

3) Eigene Route mit Rewrite oder Alias-Route

Ziel

  • URL-Design vom Backend-Pfad entkoppeln.

Datei

  • proxy/nginx.conf

Schritte (Muss)

  • Route /demo/a anbieten, die auf Backend A fuehrt.
  • Entweder per rewrite oder per eigener proxy-Route loesen.
  • Deployen mit ./scripts/lab.sh proxy-reload.

Zielzustand / Akzeptanz

  • /demo/a liefert dieselbe Antwort wie /service/a (Backend A), obwohl der externe Pfad ein anderer ist.

Done-Check

curl http://localhost:8080/demo/a   # -> "Reverse Proxy Target A"

Warum wichtig

  • Der Proxy entkoppelt externe API-Pfade von internen Service-Pfaden und ermoeglicht saubere Migrationen.

Medium

4) Security Headers setzen

Ziel

  • Browserseitige Basishaertung aktivieren.

Datei

  • proxy/nginx.conf

Schritte (Muss)

  • Mindestens setzen:
    • X-Content-Type-Options: nosniff
    • X-Frame-Options: DENY
    • Referrer-Policy: strict-origin-when-cross-origin
    • Permissions-Policy: camera=(), microphone=(), geolocation=()
    • Cross-Origin-Opener-Policy: same-origin
    • Cross-Origin-Resource-Policy: same-origin
  • Optional (Bonus): Content-Security-Policy fuer statische Seiten (mit Inline-Styles bewusst beruecksichtigen)
  • Deployen mit ./scripts/lab.sh proxy-reload.

Fallstrick (add_header-Vererbung)

  • Header am besten einmal im server {}-Block setzen, dann gelten sie fuer alle Locations.
  • Achtung: Sobald in einem location {}-Block ein add_header steht, verwirft Nginx in dieser Location alle add_header aus dem server-Block. Dann muessen sie dort wiederholt werden. Pruefen mit curl -I direkt auf der jeweiligen Route.

Zielzustand / Akzeptanz

  • curl -I http://localhost:8080/ zeigt alle geforderten Security-Header in der Antwort.

Done-Check

curl -I http://localhost:8080/
# -> zeigt X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Permissions-Policy, COOP, CORP

Warum wichtig

  • Diese Header reduzieren typische Browser-Angriffsvektoren und gehoeren zu Security-Baselines.

5) Interne Route absichern

Ziel

  • Zugriff auf eine interne Route gezielt begrenzen.

Datei

  • proxy/nginx.conf

Schritte (Muss)

  • Route /internal/status bauen.
  • Nur 127.0.0.1 erlauben, alle anderen verbieten.
  • Deployen mit ./scripts/lab.sh proxy-reload.

Wichtiger Hinweis

  • Host-Request ueber localhost:8080 kommt aus Docker-Sicht oft nicht von 127.0.0.1.

Zielzustand / Akzeptanz

  • Aufruf vom Host liefert 403; Aufruf von innerhalb des Proxy-Containers (127.0.0.1) liefert die Antwort.

Done-Check

curl -i http://localhost:8080/internal/status
# -> Erwartet typischerweise 403 vom Host (by design)

./scripts/compose.sh exec -T reverse-proxy sh -lc "wget -qO- http://127.0.0.1/internal/status"
# -> "internal ok" (funktioniert nur container-intern)

Warum wichtig

  • Nicht jeder Endpoint soll oeffentlich sein; Zugriffskontrolle direkt am Proxy ist oft die erste Schutzschicht.

6) Logging verbessern

Ziel

  • Besser debuggen koennen.

Datei

  • proxy/nginx.conf

Schritte (Muss)

  • Eigenes log_format mit Upstream-Infos anlegen.
  • Access-Log auf das neue Format umstellen.
  • Deployen mit ./scripts/lab.sh proxy-reload.

Hinweis

  • log_format gehoert in den http {}-Block (nicht in server {} oder location {}), sonst startet Nginx mit "log_format" directive is not allowed here nicht. access_log darf in http, server oder location stehen.

Zielzustand / Akzeptanz

  • Nach einem Request erscheint im Log eine Zeile im neuen Format inklusive Upstream-Infos (upstream=..., urt=...).

Done-Check

curl http://localhost:8080/service/a
./scripts/compose.sh logs reverse-proxy
# -> neue Log-Zeile mit upstream=... und urt=...

Warum wichtig

  • Gute Logs verkuerzen Incident- und Debug-Zeit drastisch und sind zentral fuer Betrieb/Security.

7) Load Balancing konfigurieren

Ziel

  • Kernfunktion eines Reverse Proxys praktisch zeigen.

Dateien

  • docker-compose.yml
  • proxy/nginx.conf

Ausgangszustand

  • /service/a zeigt aktuell nur eine Instanz (Backend A). backends/a2/index.html liegt bereits im Repo.

Schritte (Muss)

  • Zweite Instanz von Backend A (backend-a2) anlegen.
  • upstream backend_a auf beide Instanzen erweitern.
  • Mehrfach-Requests zeigen, dass beide Instanzen antworten.
  • Deployen mit ./scripts/lab.sh redeploy (Compose-Aenderung).

Zielzustand / Akzeptanz

  • Bei mehreren Aufrufen von /service/a antworten beide Instanzen (Round-Robin), also sowohl Target A als auch Target A2.

Done-Check (Beispiel)

for i in $(seq 1 8); do
  curl -s http://localhost:8080/service/a | grep -o "INSTANCE=[A-Za-z0-9]*"
done
# -> Mischung aus "INSTANCE=A" und "INSTANCE=A2" ueber die 8 Requests
  • Der unsichtbare Marker <!-- INSTANCE=A --> bzw. INSTANCE=A2 steckt im HTML der Backends. [A-Za-z0-9]* matched das ganze Token eindeutig (kein Prefix-Problem wie bei A vs. A2).

Warum wichtig

  • Lastverteilung ist eine der wichtigsten Funktionen eines Reverse Proxys fuer Skalierung und Verfuegbarkeit.

8) Response Header Minimization

Ziel

  • Unnoetige Header aus Upstream-Responses entfernen.

Datei

  • proxy/nginx.conf

Schritte (Muss)

  • Mit proxy_hide_header mindestens einen durchgereichten Backend-Header ausblenden (z. B. ETag, Last-Modified).
  • Kurz erklaeren, warum weniger Fingerprinting-Infos hilfreich sind.
  • Deployen mit ./scripts/lab.sh proxy-reload.

Abgrenzung zu Challenge 4

  • Challenge 4 setzt aktive Schutz-Header.
  • Challenge 8 entfernt unnoetige Header aus Upstream-Responses.

Zielzustand / Akzeptanz

  • Der gewaehlte Header (z. B. ETag) taucht in curl -I http://localhost:8080/service/a nicht mehr auf, vorher war er sichtbar.

Done-Check

curl -I http://localhost:8080/service/a
# -> der ausgeblendete Header (z. B. ETag) fehlt jetzt in der Antwort

Warum wichtig

  • Weniger Response-Metadaten bedeuten weniger Angriffsoberflaeche fuer Fingerprinting und Reconnaissance.

9) Debugging Challenge (kaputte Config reparieren)

Ziel

  • Fehlerdiagnose in Nginx ueben.

Ausgangszustand

  • Eigene proxy/nginx.conf ist in Betrieb. proxy/nginx.broken.conf enthaelt absichtlich mehrere Fehler.

Datei

  • proxy/nginx.broken.conf

Schritte (Muss)

  • Zuerst die eigene proxy/nginx.conf sichern (z. B. cp proxy/nginx.conf proxy/nginx.conf.bak) - diese Challenge ueberschreibt sie.
  • Defekte Config testweise als aktive Config verwenden.
  • Mindestens 2-3 Fehler finden und fixen.
  • Symptome und Diagnoseweg erklaeren.
  • Am Ende die eigene Config wiederherstellen (cp proxy/nginx.conf.bak proxy/nginx.conf).

Erwartete Fehlerarten (Beispiel aus nginx.broken.conf)

  • Upstream-Name passt nicht zum referenzierten Namen in proxy_pass.
  • Falscher Upstream-Port (8080 statt 80).
  • Fehlender Trailing Slash in proxy_pass bei Prefix-Location.

Zielzustand / Akzeptanz

  • Nach den Fixes liefern /service/a und /service/b wieder ihre Backends; Nginx startet ohne Config-Fehler.

Done-Check

curl http://localhost:8080/service/a   # -> "Reverse Proxy Target A"
curl http://localhost:8080/service/b   # -> "Reverse Proxy Target B"
./scripts/compose.sh logs reverse-proxy

Warum wichtig

  • Debugging unter Druck ist Praxisalltag; diese Aufgabe trainiert systematisches Vorgehen mit Logs und Config-Tests.

Hard (TLS)

10) HTTPS von 0 mit Easy-RSA

Ziel

  • Eigene CA + Server-Zertifikat fuer localhost erstellen.

Ausgangszustand

  • Stack laeuft auf HTTP (8080). Noch kein TLS, kein Port 8443.
  • easy-rsa und openssl sind installiert (siehe challenges/easyrsa-hints.md).

Dateien

  • docker-compose.yml (Port 8443:443 + Cert-Volume)
  • proxy/nginx.conf (TLS-Serverblock)
  • certs/easyrsa/* (PKI), certs/live/* (Runtime-Cert + Key)

Schritte (Muss)

  • CA + Server-Zertifikat fuer localhost mit SAN erstellen (--subject-alt-name="DNS:localhost,IP:127.0.0.1").
  • Nur Runtime-Cert + Key bereitstellen (nicht die ganze PKI mounten).
  • Proxy auf 443 erweitern (Mapping 8443:443) und TLS in Nginx aktivieren.
  • Mit ./scripts/lab.sh redeploy deployen (Compose-Aenderung -> Container-Neustart noetig).
  • Root-CA in den System-Trust-Store importieren.

Zielzustand / Akzeptanz

  • curl https://localhost:8443/service/a liefert Backend A ohne -k (CA wird vertraut).
  • Kein SAN-/Hostname-Fehler.

Done-Check

curl https://localhost:8443/service/a
# Erwartet: HTML von Backend A, KEIN "SSL certificate problem"
# Falls CA noch nicht global importiert:
curl --cacert certs/easyrsa/pki/ca.crt https://localhost:8443/service/a

Warum wichtig

  • TLS korrekt einzurichten ist Basis fuer Vertraulichkeit, Integritaet und Vertrauensaufbau im Netzwerk.

Haeufigster Blocker

  • Ohne SAN scheitert curl mit no alternative certificate subject name matches target host name -> beim Signieren --subject-alt-name setzen (Details in challenges/easyrsa-hints.md).

11) HTTP -> HTTPS Redirect

Voraussetzung

  • Challenge 10 muss abgeschlossen sein.
  • Nutze deine bestehende nginx.conf aus Challenge 10 als Basis.

Ziel

  • HTTP sauber auf HTTPS umlenken.

Ausgangszustand

  • Aus Challenge 10: HTTPS laeuft auf 8443, HTTP auf 8080 liefert noch direkt Inhalte.

Datei

  • proxy/nginx.conf

Schritte (Muss)

  • HTTP Requests auf HTTPS redirecten (return 301 https://$host:8443$request_uri;).
  • /healthz darf optional auf HTTP bleiben.
  • Deployen mit ./scripts/lab.sh proxy-reload (reine nginx.conf-Aenderung).

Zielzustand / Akzeptanz

  • HTTP-Aufrufe auf 8080 antworten mit 301 und Location: https://localhost:8443/....

Done-Check

curl -I http://localhost:8080/service/a
# Erwartet: HTTP/1.1 301 ... und Location: https://localhost:8443/service/a

Warum wichtig

  • Redirect erzwingt verschluesselten Zugriff und verhindert versehentliche Nutzung unsicherer HTTP-Endpunkte.

12) TLS Haertung + Chain Check + HSTS

Voraussetzung

  • Challenge 10 und 11 muessen abgeschlossen sein.
  • Erweitere dieselbe nginx.conf weiter.

Ziel

  • TLS nicht nur aktivieren, sondern sauber haerten.

Datei

  • proxy/nginx.conf

Ausgangszustand

  • Aus Challenge 10+11: HTTPS auf 8443 laeuft, HTTP wird umgeleitet. TLS ist aber noch ungehaertet (keine Protokoll-Beschraenkung, kein HSTS).

Schritte (Muss)

  • TLS auf 1.2/1.3 beschraenken (ssl_protocols TLSv1.2 TLSv1.3;).
  • HSTS setzen (Strict-Transport-Security).
  • Zertifikatskette pruefen und kurz erklaeren.
  • Deployen mit ./scripts/lab.sh proxy-reload.

Hinweis

  • Im HTTP-Basissetup ist HSTS absichtlich noch nicht aktiv. Das ist Teil der Aufgabe.

Warnung (HSTS-Falle im Browser)

  • HSTS merkt sich der Browser host-weit (localhost), nicht pro Port. Nach einem Besuch von https://localhost:8443 erzwingt der Browser https auch fuer http://localhost:8080 -> das HTTP-Lab scheint dann "kaputt".
  • Zum Pruefen daher curl nutzen (curl speichert HSTS nicht).
  • Falls der Browser haengt: HSTS fuer localhost zuruecksetzen (Chrome: chrome://net-internals/#hsts -> "Delete domain security policies" -> localhost).
  • max-age ist im Lab bewusst kurz (1h), damit der Effekt von selbst verfaellt.

Zielzustand / Akzeptanz

  • curl -I zeigt den Strict-Transport-Security-Header.
  • openssl s_client verhandelt TLSv1.2 oder TLSv1.3 und endet mit Verify return code: 0 (ok).

Done-Check

curl -I https://localhost:8443/service/a
# -> enthaelt: Strict-Transport-Security: max-age=3600; includeSubDomains
openssl s_client -connect localhost:8443 -servername localhost
# -> Protocol: TLSv1.3 (oder 1.2), Verify return code: 0 (ok)

Warum wichtig

  • Reines "HTTPS an" reicht nicht: erst Haertung + HSTS reduzieren Downgrade- und Fehlkonfigurationsrisiken.

Bonus Expert

13) Wireshark: HTTP vs HTTPS sauber analysieren

Ziel

  • Nachweisbar zeigen, was im Klartext sichtbar ist und was durch TLS geschuetzt wird.

Ausgangszustand

  • HTTPS laeuft (Challenge 10), 8080 und 8443 sind erreichbar. Wireshark/tshark ist installiert (siehe challenges/wireshark-hints.md).

Schritte (Muss)

  • HTTP auf 8080 mitschneiden.
  • HTTPS auf 8443 mitschneiden.
  • ClientHello, ServerHello, Certificate markieren.
  • Technisch erklaeren, warum HTTP lesbar ist und HTTPS ohne Keys nicht.

Zielzustand / Akzeptanz

  • HTTP-Mitschnitt: Pfad und Header sind im Klartext lesbar (Follow Stream).
  • HTTPS-Mitschnitt: nur der TLS-Handshake ist sichtbar, die Nutzdaten sind ohne Key nicht lesbar.

Vorgehen (empfohlen)

  1. Capture auf passendem Interface starten (lokal meist lo).
  2. HTTP Request senden und lesbaren Stream zeigen.
  3. HTTPS Request senden und TLS Handshake analysieren.
  4. Ergebnisse in 3-5 Bulletpoints zusammenfassen.

Optional

  • TLS Decrypt mit SSLKEYLOGFILE.

Done-Check

  • 3-4 Screenshots + 3-5 Bulletpoints Auswertung.

Warum wichtig

  • Wer den Unterschied auf Paketebene gesehen hat, versteht TLS nicht nur theoretisch, sondern praktisch.

Typische Fehler

  • falsches Interface gewaehlt
  • HTTPS noch nicht korrekt aktiv
  • Keylog gesetzt, aber Browser nicht aus derselben Shell gestartet

Zusatz-Hints

  • challenges/easyrsa-hints.md
  • challenges/wireshark-hints.md