all systems self-hosted

~/selfhostjournal $

← Alle Artikel

,

Traefik absichern: Geo-Blocking, CrowdSec und Single Sign-on

10–16 Minuten

Kurz gesagt

Problem: Sobald Port 80 und 443 offen sind, scannen Bots aus aller Welt deine Dienste, probieren Passwörter aus und suchen nach bekannten Sicherheitslücken. Traefik allein leitet all das weiter.

Lösung: Mehrere Schichten vor jedem Dienst: Geo-Blocking für Länder, aus denen du nie zugreifst, CrowdSec gegen bekannte Angreifer und Angriffsmuster, Single Sign-on vor Apps ohne eigenen guten Login, dazu ein paar Grundregeln für die Container.

Getestet mit: Traefik 3.7, GeoBlock-Plugin 0.3.8, CrowdSec 1.8 mit dem Traefik-Bouncer-Plugin 1.7 und AppSec, Authentik 2026.8, Docker Compose auf einem kleinen Heimserver

Das ist der zweite Teil meines Traefik-Setups. Der erste Teil erklärt die Grundlagen: Entrypoints, Router, Labels und Zertifikate. Hier geht es um die Frage, die Traefik selbst nicht beantwortet: Soll diese Anfrage überhaupt herein? Ein Reverse Proxy entscheidet, wohin eine Anfrage geht. Die Schichten in diesem Artikel entscheiden, ob.

Die Schichten im Überblick

Jede Anfrage an Port 443 durchläuft diese Prüfungen der Reihe nach. Jede ist billig, und jede hält eine andere Art von Besucher auf:

#SchichtHält aufGilt für
1GeoBlockAlles aus Ländern, die ich nicht erlaubealle Dienste
2CrowdSec (IP-Reputation)IP-Adressen, die für Angriffe bekannt sind, und IPs, die sich auf meinem Server danebenbenehmenalle Dienste
3CrowdSec AppSec (WAF)Anfragen, die wie Angriffe aussehen: SQL-Injection, Path Traversal, bekannte Exploitsalle Dienste
4Single Sign-on (Authentik)Alle, die nicht angemeldet sindApps ohne eigenen guten Login
5Der Login der App selbstAlle ohne Kontojede App

Keine Schicht ist perfekt. GeoBlock lässt sich mit einem VPN austricksen, CrowdSec kennt nicht jeden Angreifer, und jeder Login kann einen Fehler haben. Zusammen machen sie aus „jeder im Internet“ ein „wenige Leute, in wenigen Ländern, mit gültigem Login“.

1. Eine Zeile für alle Dienste

Man könnte Middlewares an jeden Router einzeln hängen. Ich hänge die zwei globalen stattdessen an den Entrypoint websecure, in der statischen Konfiguration von Traefik. So kann kein Dienst vergessen werden, auch keiner, den ich nächstes Jahr dazunehme:

    command:
      # ... die Konfiguration aus Teil 1 ...

      # Schutz für JEDE Seite auf 443: zuerst GeoBlock, dann CrowdSec, dann Security-Header (Schritt 5).
      # Beides schnell abschalten: Zeile auskommentieren und docker compose up -d
      - "--entrypoints.websecure.http.middlewares=geoblock@docker,crowdsec@docker,secheaders@docker"

      # Die zwei Plugins, beim Start von plugins.traefik.io geladen
      - "--experimental.plugins.geoblock.modulename=github.com/PascalMinder/geoblock"
      - "--experimental.plugins.geoblock.version=v0.3.8"
      - "--experimental.plugins.crowdsec.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
      - "--experimental.plugins.crowdsec.version=v1.7.1"
  • Die Reihenfolge zählt: GeoBlock läuft zuerst, weil es die billigste Prüfung ist. Anfragen aus gesperrten Ländern erreichen CrowdSec gar nicht erst, das hält dessen Logs und Arbeit klein.
  • @docker heißt, die Middleware ist mit Docker-Labels definiert, hier am Traefik-Container selbst (nächste Abschnitte).
  • Die Plugin-Versionen sind fest, aus demselben Grund wie das Traefik-Image in Teil 1: eine neue Plugin-Version kann Optionen ändern.
  • Port 80 ist nicht abgedeckt, und das ist in Ordnung: Er antwortet nur mit einer Umleitung auf HTTPS, und dort greifen dann alle Prüfungen.

Tipp: Der Kommentar über der Zeile ist Absicht. Wenn du dich je aussperrst (zum Beispiel auf Reisen), schalten eine auskommentierte Zeile und ein Neustart alle Filter ab. Schreib dir das irgendwo auf, wo du es auch unter Stress findest.

2. GeoBlock: nur die Länder, die du brauchst

Meine Dienste nutzt meine Familie, und wir alle sind in zwei europäischen Ländern. Es gibt keinen Grund, warum jemand aus anderen Ländern meine Login-Seiten erreichen sollte. GeoBlock schlägt das Land jeder Besucher-IP nach und lässt nur die erlaubten durch. Diese Labels kommen an den Traefik-Container:

    labels:
      # --- GeoBlock: nur die aufgeführten Länder erlauben (+ private/Heim-IPs) ---
      - "traefik.http.middlewares.geoblock.plugin.geoblock.countries[0]=DE"
      - "traefik.http.middlewares.geoblock.plugin.geoblock.countries[1]=AT"
      - "traefik.http.middlewares.geoblock.plugin.geoblock.allowLocalRequests=true"
      - "traefik.http.middlewares.geoblock.plugin.geoblock.allowUnknownCountries=false"
      - "traefik.http.middlewares.geoblock.plugin.geoblock.unknownCountryApiResponse=nil"
      - "traefik.http.middlewares.geoblock.plugin.geoblock.blackListMode=false"

      # Ausnahmen: ein Dienst, der dich aus dem Ausland erreichen muss
      - "traefik.http.middlewares.geoblock.plugin.geoblock.allowedIPAddresses[0]=203.0.113.0/24"

      # Länderabfrage
      - "traefik.http.middlewares.geoblock.plugin.geoblock.api=https://get.geojs.io/v1/ip/country/{ip}"
      - "traefik.http.middlewares.geoblock.plugin.geoblock.apiTimeoutMs=750"
      - "traefik.http.middlewares.geoblock.plugin.geoblock.cacheSize=100"
      - "traefik.http.middlewares.geoblock.plugin.geoblock.forceMonthlyUpdate=true"

      # Logging: nur abgelehnte Anfragen, in eine eigene Datei
      - "traefik.http.middlewares.geoblock.plugin.geoblock.silentStartUp=false"
      - "traefik.http.middlewares.geoblock.plugin.geoblock.logLocalRequests=false"
      - "traefik.http.middlewares.geoblock.plugin.geoblock.logAllowedRequests=false"
      - "traefik.http.middlewares.geoblock.plugin.geoblock.logApiRequests=false"
      - "traefik.http.middlewares.geoblock.plugin.geoblock.logFilePath=/var/log/traefik/geoblock.log"
  • countries: die erlaubten Länder als Zwei-Buchstaben-Codes. Meine sind andere; wähl deine eigenen. Mit blackListMode=false ist das eine Erlaubnisliste: alles, was nicht drinsteht, wird gesperrt. Andersherum (wenige Länder sperren, den Rest erlauben) schützt viel weniger.
  • allowLocalRequests=true: private Adressen (dein Heimnetz) sind immer erlaubt. Ohne diese Zeile sperrst du dich zu Hause aus, sobald eine Anfrage mit lokaler Adresse ankommt.
  • allowUnknownCountries=false: findet die Abfrage kein Land, wird die Anfrage gesperrt. Sicherer, um den Preis einer seltenen Fehlsperre.
  • allowedIPAddresses: Ausnahmen für Dienste, die dich aus einem anderen Land aufrufen. Bei mir ist das der Connector eines KI-Assistenten zu Home Assistant, der aus dem veröffentlichten IP-Bereich des Anbieters in den USA kommt. Erlaube nur Bereiche, die der Anbieter offiziell veröffentlicht, nie einen geratenen.
  • Abfrage und Cache: unbekannte IP-Adressen werden bei einem Online-Dienst nachgeschlagen (hier geojs.io), höchstens 750 ms lang. Die letzten 100 Ergebnisse bleiben im Cache, damit wiederkehrende Besucher keine neuen Abfragen auslösen.
  • Log-Datei: jede gesperrte Anfrage landet in geoblock.log. Es ist erstaunlich lehrreich, sie nach einem Tag zu lesen: das meiste ist automatisiertes Scannen.

Bevor du es einschaltest: Auf Reisen in ein anderes Land sperrst du dich selbst aus, außer du verbindest dich vorher per VPN mit deinem Heimnetz. Und die Länderabfrage schickt die IP-Adressen der Besucher an einen externen Dienst. Für ein privates Homelab, das nur die Familie nutzt, ist das für mich in Ordnung; bei einer öffentlichen Website mit Besuchern gehört es in die Datenschutzerklärung, oder du nimmst stattdessen eine Offline-Länderdatenbank.

Let’s Encrypt funktioniert auch mit GeoBlock. Die TLS-Challenge aus Teil 1 beantwortet Traefik, bevor irgendeine Middleware läuft, deshalb werden die Prüfserver im Ausland nicht gesperrt.

3. CrowdSec: bekannte Angreifer und Angriffsmuster

CrowdSec macht zwei Dinge. Es liest das Access-Log von Traefik und merkt, wenn sich eine IP-Adresse wie ein Angreifer verhält (viele fehlgeschlagene Logins, Suche nach /wp-login.php auf einem Server ohne WordPress und so weiter). Und es teilt das mit einer Community-Liste, sodass du auch Adressen sperrst, die gestern jemand anderen angegriffen haben. Dazu arbeitet seine Komponente AppSec als Web Application Firewall (WAF): Sie prüft jede Anfrage gegen Regeln für typische Angriffe, bevor Traefik sie weiterleitet.

Es gibt zwei Teile: die CrowdSec-Engine (ein eigener Container) und einen Bouncer in Traefik, der die Engine zu jeder Anfrage befragt.

Die Engine

  crowdsec:
    image: crowdsecurity/crowdsec:v1.8.1
    container_name: crowdsec
    restart: unless-stopped
    environment:
      # Erkennungsregeln: Traefik-Log-Parser, typische Web-Angriffe, bekannte CVEs, AppSec (WAF)
      COLLECTIONS: "crowdsecurity/traefik crowdsecurity/base-http-scenarios crowdsecurity/http-cve crowdsecurity/appsec-virtual-patching crowdsecurity/appsec-generic-rules"
      # Registriert den Traefik-Bouncer mit dem Schlüssel aus .env
      BOUNCER_KEY_traefik: ${CROWDSEC_BOUNCER_KEY}
      TZ: Europe/Berlin
    volumes:
      - ./crowdsec/config:/etc/crowdsec
      - ./crowdsec/data:/var/lib/crowdsec/data
      - ./crowdsec/acquis.d:/etc/crowdsec/acquis.d:ro
      - ./traefik-logs:/var/log/traefik:ro
    networks:
      - proxy
    # Keine veröffentlichten Ports: Traefik erreicht es als crowdsec:8080 und crowdsec:7422
  • Collections sind Regelpakete aus dem CrowdSec-Hub: ein Parser für das Log-Format von Traefik, Szenarien für typische Web-Angriffe und bekannte Sicherheitslücken (CVEs) sowie die AppSec-Regelsätze.
  • BOUNCER_KEY_traefik registriert Traefik als Bouncer mit einem Schlüssel, den du selbst wählst. Erzeug einen und schreib ihn in die .env-Datei neben der Compose-Datei, nie in die Compose-Datei selbst: echo "CROWDSEC_BOUNCER_KEY=$(openssl rand -hex 32)" >> .env.
  • Der Log-Ordner von Traefik ist nur lesend eingebunden, damit CrowdSec das Access-Log aus Teil 1 lesen kann.
  • Keine veröffentlichten Ports: nur Traefik spricht mit CrowdSec, über das gemeinsame Netzwerk proxy.

Was CrowdSec liest, steht in crowdsec/acquis.d/traefik.yaml:

# 1. Traefik-Access-Log: Brute Force, Scans, bekannte Exploits
filenames:
  - /var/log/traefik/access.log
labels:
  type: traefik
---
# 2. AppSec (WAF): Traefik schickt jede Anfrage hierher, bevor es sie weitergibt
source: appsec
name: appsec
listen_addr: 0.0.0.0:7422
appsec_config: crowdsecurity/appsec-default
labels:
  type: appsec

Der Bouncer in Traefik

    labels:
      # --- CrowdSec-Bouncer ---
      - "traefik.http.middlewares.crowdsec.plugin.crowdsec.enabled=true"
      - "traefik.http.middlewares.crowdsec.plugin.crowdsec.crowdsecMode=stream"
      - "traefik.http.middlewares.crowdsec.plugin.crowdsec.crowdsecLapiScheme=http"
      - "traefik.http.middlewares.crowdsec.plugin.crowdsec.crowdsecLapiHost=crowdsec:8080"
      - "traefik.http.middlewares.crowdsec.plugin.crowdsec.crowdsecLapiKey=${CROWDSEC_BOUNCER_KEY}"
      - "traefik.http.middlewares.crowdsec.plugin.crowdsec.updateIntervalSeconds=60"
      # AppSec (WAF)
      - "traefik.http.middlewares.crowdsec.plugin.crowdsec.crowdsecAppsecEnabled=true"
      - "traefik.http.middlewares.crowdsec.plugin.crowdsec.crowdsecAppsecHost=crowdsec:7422"
      - "traefik.http.middlewares.crowdsec.plugin.crowdsec.crowdsecAppsecFailureBlock=true"
      - "traefik.http.middlewares.crowdsec.plugin.crowdsec.crowdsecAppsecUnreachableBlock=false"
      # das Heimnetz nie sperren
      - "traefik.http.middlewares.crowdsec.plugin.crowdsec.clientTrustedIPs[0]=192.168.10.0/24"
  • crowdsecMode=stream: der Bouncer lädt alle 60 Sekunden die komplette Liste der gesperrten Adressen und prüft Anfragen lokal. Das ist schnell, und Traefik muss CrowdSec nicht bei jeder Anfrage fragen.
  • crowdsecAppsecFailureBlock=true: kann AppSec über eine Anfrage nicht entscheiden (ein Fehler bei der Prüfung), wird sie gesperrt.
  • crowdsecAppsecUnreachableBlock=false: ist der CrowdSec-Container nicht erreichbar, werden Anfragen durchgelassen. Das ist eine bewusste Abwägung: Während eines CrowdSec-Updates bleiben meine Dienste online, und GeoBlock filtert weiter. Wenn du lieber offline als ungeschützt bist, setz es auf true.
  • clientTrustedIPs: dein Heimnetz wird nie gesperrt. Ohne diese Zeile kann ein Familienmitglied, das fünfmal das falsche Passwort eintippt, das ganze Haus aussperren.

Tipp: Docker Compose füllt ${CROWDSEC_BOUNCER_KEY} auch in Labels aus der .env-Datei, deshalb steht der Schlüssel in keiner Datei, die du teilen oder öffentlich sichern würdest. Halte .env aus jedem Git-Repository heraus.

Befehle für den Alltag

# welche Adressen sind gerade gesperrt, und warum?
docker exec crowdsec cscli decisions list

# was wurde zuletzt erkannt?
docker exec crowdsec cscli alerts list

# wird das Log gelesen, sprechen Bouncer und AppSec mit CrowdSec?
docker exec crowdsec cscli metrics

# eine Adresse entsperren (z. B. ein Freund, der gesperrt wurde)
docker exec crowdsec cscli decisions delete --ip 198.51.100.7

# eine Adresse von Hand für 24 Stunden sperren
docker exec crowdsec cscli decisions add --ip 198.51.100.7 --duration 24h

Das Access-Log und das GeoBlock-Log wachsen jeden Tag, deshalb rotiere ich sie mit logrotate auf dem Host (7 Tage, komprimiert). copytruncate lässt die Datei an ihrem Platz, sodass weder Traefik noch CrowdSec neu starten müssen:

# /etc/logrotate.d/traefik
/pfad/zu/traefik/traefik-logs/*.log {
    daily
    rotate 7
    compress
    missingok
    notifempty
    copytruncate
}

4. Single Sign-on vor Apps

Manche Apps haben keinen eigenen Login, oder nur einen schwachen: das Dashboard von Traefik, das Zigbee2MQTT-Frontend, kleine Admin-Werkzeuge. Für sie fragt Traefik vor jeder Anfrage meinen Identity Provider Authentik: Ist diese Person angemeldet und darf sie diese App nutzen? Wenn nicht, sieht sie die Login-Seite von Authentik, mit Zwei-Faktor-Anmeldung. Das nennt sich Forward Auth.

Die Middleware wird einmal definiert, mit Labels am Authentik-Server-Container:

    labels:
      # Pfad des eingebauten Outposts (nötig für Forward Auth auf anderen Domains)
      - traefik.http.routers.authentik-outpost.rule=HostRegexp(`.+`) && PathPrefix(`/outpost.goauthentik.io/`)
      - traefik.http.routers.authentik-outpost.priority=1000
      - traefik.http.routers.authentik-outpost.entrypoints=websecure
      - traefik.http.routers.authentik-outpost.tls=true
      - traefik.http.routers.authentik-outpost.service=authentik

      # Forward-Auth-Middleware für Apps OHNE eigenen Login
      - traefik.http.middlewares.authentik.forwardauth.address=http://authentik-server:9000/outpost.goauthentik.io/auth/traefik
      - traefik.http.middlewares.authentik.forwardauth.trustForwardHeader=true
      - traefik.http.middlewares.authentik.forwardauth.authResponseHeaders=X-authentik-username,X-authentik-groups,X-authentik-email,X-authentik-name,X-authentik-uid
  • Der Outpost-Router fängt den Pfad /outpost.goauthentik.io/ auf jedem Hostnamen ab und schickt ihn an Authentik. Dorthin kommt die Login-Umleitung zurück. priority=1000 sorgt dafür, dass er gegen den Router der App gewinnt.
  • forwardauth.address: diese Adresse fragt Traefik zu jeder Anfrage. Authentik antwortet mit „ja“ (die Anfrage geht weiter) oder mit einer Umleitung auf die Login-Seite.
  • authResponseHeaders: nach erfolgreichem Login gibt Authentik Benutzername, Gruppen und E-Mail als Header an die App weiter. Apps, die sie verstehen, können den Benutzer automatisch anmelden.

Eine App zu schützen ist dann ein zusätzliches Label an ihrem Router, hier für das Traefik-Dashboard aus Teil 1:

      - "traefik.http.routers.traefik.middlewares=authentik@docker"

In Authentik braucht jede geschützte App einen Proxy Provider im Forward-Auth-Modus (einzelne Anwendung) und eine Anwendung, die an die erlaubten Benutzer oder Gruppen gebunden ist. Ohne das kennt Authentik den Hostnamen nicht und lehnt alle ab.

Nicht für jede App: Forward Auth funktioniert nur im Browser. Mobile Apps und andere Clients, die mit einer API sprechen (die Home-Assistant-App, Passwortmanager-Apps, Sync-Clients), können die Login-Seite von Authentik nicht anzeigen und funktionieren einfach nicht mehr. Für diese Apps nutze ich ihr eigenes Single Sign-on über OpenID Connect, wo sie es unterstützen (für WordPress habe ich das in diesem Artikel erklärt), und sonst ihren eigenen Login plus die Schichten oben.

5. Security-Header für jeden Dienst

Security-Header sind kurze Anweisungen, die der Server mit jeder Seite an den Browser schickt. Viele Apps setzen einige davon, aber selten alle, und jede App macht es anders. Eine dritte Middleware am Entrypoint ergänzt die wichtigsten bei jedem Dienst und entfernt die Header, die Angreifern nur verraten, welche Software und Version du einsetzt. Diese Labels kommen an den Traefik-Container, und die Entrypoint-Zeile aus Schritt 1 bekommt einen dritten Eintrag: geoblock@docker,crowdsec@docker,secheaders@docker.

    labels:
      # --- Security-Header für jede Seite auf 443 ---
      # HSTS: Browser nutzen nur noch https, für 1 Jahr
      - "traefik.http.middlewares.secheaders.headers.stsSeconds=31536000"
      - "traefik.http.middlewares.secheaders.headers.contentTypeNosniff=true"
      # Softwarenamen und Versionen verbergen (ein leerer Wert entfernt den Header)
      - "traefik.http.middlewares.secheaders.headers.customResponseHeaders.Server="
      - "traefik.http.middlewares.secheaders.headers.customResponseHeaders.X-Powered-By="
  • HSTS (stsSeconds): nach dem ersten Besuch weigert sich der Browser ein Jahr lang, die Seite über reines HTTP zu öffnen, auch wenn ihn jemand in einem öffentlichen WLAN dorthin umleiten will. includeSubdomains und preload habe ich bewusst weggelassen: Sie binden auch jede andere Subdomain deiner Domain an HTTPS, und preload lässt sich nur sehr schwer rückgängig machen.
  • contentTypeNosniff: der Browser vertraut dem angegebenen Dateityp und rät nicht. Das verhindert, dass ein hochgeladenes „Bild“ als Skript ausgeführt wird.
  • Server und X-Powered-By entfernen: vorher haben meine Dienste jedem Dinge wie Server: nginx oder X-Powered-By: PHP/8.5 erzählt. Das macht nichts angreifbar, erspart einem Angreifer aber den ersten Schritt, Exploits für genau diese Version zu suchen.

Nicht jeder Header gehört hierher: X-Frame-Options und eine Content Security Policy hängen von der App ab. Meine Office-Suite wird in Nextcloud in einem Frame angezeigt; ein zentraler „nie in einem Frame anzeigen“-Header würde sie kaputt machen. Diese überlasse ich den Apps, die sie meist sowieso richtig setzen. Öffne nach dem Hinzufügen der Middleware jeden Dienst einmal und prüf, ob er noch funktioniert.

Prüfen geht mit einem Befehl pro Dienst. Du solltest strict-transport-security und x-content-type-options sehen und keine server-Zeile:

curl -s -o /dev/null -D - https://app.example.com/ | grep -i -E "strict-transport|x-content-type|^server|x-powered"

6. Grundregeln für die Container

Ein paar kleine Einstellungen an den Containern selbst, die nichts kosten:

  traefik:
    image: traefik:v3.7
    security_opt:
      - no-new-privileges:true
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      # ...
  • no-new-privileges: Prozesse im Container können sich später keine zusätzlichen Rechte verschaffen (zum Beispiel über ein setuid-Programm). Bricht je jemand in Traefik ein, macht das den nächsten Schritt schwerer.
  • Docker-Socket nur lesend: Traefik muss Container-Informationen nur lesen. Aber Achtung: :ro schützt nur die Socket-Datei; die Docker-API dahinter nimmt trotzdem Befehle an. Strenger ist ein Socket-Proxy, ein kleiner Container, der nur die Lese-Anfragen durchlässt, die Traefik braucht. Für mein Monitoring-Portal nutze ich so einen schon, und Traefik soll auch noch umziehen.
  • Keine veröffentlichten Ports bei keinem Dienst außer Traefik (80 und 443). Alles andere ist nur über Traefik erreichbar und damit über alle Schichten oben. Eine vergessene ports:-Zeile umgeht sie alle.
  • exposedbydefault=false aus Teil 1 gehört auch hierher: nichts wird aus Versehen öffentlich.

7. Testen, ob es funktioniert

Trau keinem Sicherheits-Setup, das du nicht getestet hast. Diese Prüfungen dauern fünf Minuten. Mach die Angriffstests von außerhalb deines Heimnetzes (mobile Daten, nicht WLAN), weil dein Heimnetz auf jeder Erlaubnisliste steht:

# AppSec sollte typische Angriffs-Anfragen mit 403 sperren
curl -s -o /dev/null -w "%{http_code}n" "https://app.example.com/?id=1%27%20OR%20%271%27=%271"
curl -s -o /dev/null -w "%{http_code}n" "https://app.example.com/../../etc/passwd"
curl -s -o /dev/null -w "%{http_code}n" "https://app.example.com/.env"

# hat CrowdSec es gesehen?
docker exec crowdsec cscli alerts list | head

# GeoBlock: Anfragen aus anderen Ländern stehen hier
tail -n 5 traefik-logs/geoblock.log

# Forward Auth: ohne Login solltest du zu Authentik umgeleitet werden (302)
curl -s -o /dev/null -w "%{http_code} %{redirect_url}n" https://traefik.example.com/

Um GeoBlock aus einem anderen Land zu testen, nimm ein VPN mit Ausgang in einem gesperrten Land oder einen der vielen Online-Dienste, die eine Website „von einem anderen Standort“ prüfen. Du solltest ein 403 bekommen.

Was ich (noch) nicht mache

  • Zentrale Frame- und Content-Richtlinien (X-Frame-Options, Content Security Policy): sie hängen zu sehr von der jeweiligen App ab, deshalb setzen die Apps sie selbst (siehe Schritt 5).
  • Eigene TLS-Optionen: brauche ich nicht. Ich habe mit openssl s_client geprüft: Die Standardwerte von Traefik lehnen TLS 1.0 und 1.1 schon ab und akzeptieren nur 1.2 und 1.3.
  • Rate Limiting: die ratelimit-Middleware von Traefik auf Login-Seiten. CrowdSec deckt das meiste ab, weil es Brute-Force-Versuche sperrt.
  • Der Socket-Proxy für Traefik, wie oben erwähnt.

Was ich gelernt habe

  • Middlewares am Entrypoint schlagen Middlewares pro Router: neue Dienste sind ab der ersten Sekunde geschützt, ohne dass ich an etwas denken muss.
  • Erlaubnislisten schlagen Sperrlisten. Die zwei Länder aufzuzählen, die ich brauche, ist sicherer und einfacher als die hundert, die ich nicht brauche.
  • Das Heimnetz immer ausnehmen, in GeoBlock und in CrowdSec. Sonst bist du selbst der Erste, der ausgesperrt wird.
  • Entscheide, was passiert, wenn eine Schutzschicht ausfällt: alles sperren oder durchlassen. Beides ist vertretbar, aber es sollte eine Entscheidung sein, kein Standardwert, den du nie angesehen hast.
  • Teste von außen. Vom eigenen Sofa aus sieht alles sicher aus, weil dein Heimnetz auf jeder Erlaubnisliste steht.

Alles hier hat bei mir zum Zeitpunkt des Schreibens funktioniert. Software ändert sich, und dein Setup ist anders: mach vorher ein Backup, teste vorsichtig und wende die Schritte auf eigenes Risiko an. Details im Impressum.