Zum Inhalt springen
Open Beta Hansestack ist in der aktuellen Phase komplett kostenlos. 🐙 Feedback auf GitHub einreichen

Caddy v2 Plugin (Zero-Code)

Das offizielle Caddy v2 Plugin für Hansestack ermöglicht Decoupled Security: Du hängst den k-Anonymity Leak-Check direkt in deinen Reverse Proxy, ohne auch nur eine Zeile Code in deinem Backend anfassen zu müssen.

Das Plugin fängt Requests ab, prüft enthaltene Passwörter datenschutzkonform und reichert den Request oder die Response mit dem Ergebnis an. Dein Backend benötigt keinen eigenen API-Client.

Ressourcen & Hands-on

  • 🐙 Source Code: Quellcode, Releases und den Issue-Tracker findest du im caddy-hansestack Repository.
  • 👾 Sandbox testen: Willst du das Plugin in Aktion sehen? Clone unser Demo-Repo, tippe docker-compose up und verfolge im mitgelieferten Grafana-Dashboard live, wie der Leak-Check (und das Fail-Open) unter Last reagiert.

Installation

Es gibt zwei Wege, das Plugin in deiner Infrastruktur zu nutzen.

Option A: Build mit xcaddy (Empfohlen)

Wenn du deine Caddy-Binaries selbst baust, nutze xcaddy:

xcaddy build --with github.com/hansestack/caddy-hansestack

Option B: Pre-built Docker Image

Wir stellen ein fertiges Docker-Image (basierend auf dem offiziellen Caddy-Image) zur Verfügung:

# docker-compose.yml
services:
  caddy:
    image: ghcr.io/hansestack/caddy-hansestack:latest
    ports:
      - "80:80"
      - "443:443"

(Tipp: Pinne in Produktion eine feste Version statt :latest, z.B. :v1.0.0)

Konfiguration (Caddyfile)

Die Konfiguration erfolgt nativ im Caddyfile. Das Plugin registriert sich automatisch so, dass es in der Aufrufreihenfolge vor dem reverse_proxy ausgeführt wird.

:443 {
    hansestack /login* leakcheck {
        # Optional: Route den Traffic zu einem selbst gehosteten On-Prem-Deployment
        # (z.B. einem lokalen Sidecar-Container) statt zur öffentlichen SaaS-API.
        # Ohne Angabe wird der öffentliche Hansestack-Endpunkt genutzt.
        endpoint "http://localhost:8081"

        # Pflicht bei der öffentlichen SaaS-API. Optional, wenn "endpoint" auf ein
        # vertrauenswürdiges On-Prem-/Sidecar-Deployment ohne Auth zeigt.
        api_key {$HANSESTACK_API_KEY}
        mode enrich_response
        password_field "password"
        
        # Optionale Circuit Breaker Konfiguration für hohe Resilienz
        timeout 500ms
        circuit_breaker_threshold 5
        circuit_breaker_cooldown 30s
        max_idle_conns 200
    }

    reverse_proxy backend:8080
}

Konfigurations-Parameter

DirektiveStandardwertBeschreibung
endpoint(öffentliche SaaS-API)Überschreibt die Basis-URL der Hansestack API. Zeige damit auf ein selbst gehostetes On-Prem-Deployment – z.B. einen Sidecar-Container im selben Kubernetes Pod – für Datensouveränität oder um Egress-Bandbreitenlimits zu umgehen. Ohne Angabe wird der öffentliche SaaS-Endpunkt genutzt.
api_key(Pflichtfeld für die öffentliche SaaS-API)Dein Hansestack API-Key. (Nutze {$ENV_VAR} für Umgebungsvariablen). Optional, wenn endpoint auf ein vertrauenswürdiges On-Prem-/Sidecar-Deployment zeigt, das ohne Authentifizierung läuft – der API-Key-Header wird dann von hansestack-go komplett weggelassen statt leer gesendet.
modeenrich_requestModus: observe, enrich_request, enrich_response oder block.
password_fieldpasswordDer Key im JSON oder Form-Data, der das Passwort enthält.
header_leakedX-Hansestack-LeakedHeader, der das Ergebnis (true/false) enthält.
header_countX-Hansestack-Leak-CountHeader mit der Anzahl der gefundenen Leaks.
block_status401HTTP Status-Code, wenn mode=block greift.
timeout500msMaximale Wartezeit für den API-Aufruf (Fail-Open greift danach).
circuit_breaker_thresholddeaktiviert (0)Anzahl aufeinanderfolgender Fehler, bevor der Breaker auslöst.
circuit_breaker_cooldown30sDauer, für die der Breaker neue Requests lokal sofort durchwinkt.
max_idle_conns200Begrenzt den Idle-Connection-Pool (Keep-Alive) des zugrunde liegenden HTTP-Transports – gilt für MaxIdleConns und MaxIdleConnsPerHost gleichermaßen, da in der Regel nur ein Upstream existiert (die SaaS-API oder ein Sidecar). Schützt vor TCP-Socket-Erschöpfung bei massiven Lastspitzen.

Operations-Modi

Je nach Architektur deines Auth-Flows bietet das Plugin vier verschiedene Modi:

  1. observe (Zero-Risk Observability): Asynchron. Läuft den Leak-Check exakt wie enrich_response, injiziert aber niemals header_leaked/header_count in Request oder Response – weder für den Client noch für dein Backend sichtbar oder veränderbar. Dieser Modus existiert einzig, um die Indicator-of-Compromise-Korrelationsmetrik hansestack_leakcheck_responses_total zu befüllen (siehe Metriken unten). Wähle observe, wenn du zunächst nur beobachten willst, ob geleakte Passwörter zu erfolgreichen Logins führen, ohne auch nur ein Byte an Request oder Response zu verändern – ideal für den ersten Rollout in Produktion, bevor du dich für einen aktiveren Modus entscheidest.
  2. enrich_response (Empfohlen): Asynchron. Der Leak-Check läuft parallel zur Verarbeitung deines Backends. Es fügt absolut keine spürbare Latenz zum Login hinzu. Die Leak-Header werden der ausgehenden Response (auf dem Weg zum Client) hinzugefügt.
    sequenceDiagram
    participant C as Client
    participant P as Caddy (Plugin)
    participant B as Dein Backend
    participant H as Hansestack API

    C->>P: POST /login (Credentials)
    
    par Asynchroner Leak-Check
        P->>H: Sende Hash-Präfix
        H-->>P: Suffix-Liste (Kein Leak)
    and Normaler Login-Flow
        P->>B: POST /login (Original Body)
        Note over B: Backend verifiziert Login
        B-->>P: HTTP 201 (Session Token)
    end
    
    P-->>C: HTTP 201 + X-Hansestack-Leaked Header
  
  1. enrich_request: Synchron. Wartet auf die API (max. timeout) und fügt die Leak-Header dem eingehenden Request hinzu. Dein Backend kann diese Header lesen und selbst entscheiden, was zu tun ist.
  2. block: Synchron. Die einzige Methode, die den Request abweisen kann. Nur wenn ein Leak zweifelsfrei bestätigt wurde, wird der Request direkt vom Caddy mit z.B. HTTP 401 abgelehnt. Dein Backend wird in diesem Fall nie aufgerufen.
Fail-Open by Design
In allen Modi gilt: Bei API-Timeouts, Rate-Limits oder Netzwerkfehlern wird der Request immer unverändert durchgelassen. Der Login-Flow deiner echten Nutzer wird durch einen Ausfall des Leak-Checks niemals blockiert.

Security & Performance

  • multipart/form-data (File-Uploads) wird komplett ignoriert. Das Plugin prüft den Content-Type-Header, bevor auch nur ein einziges Byte des Bodies gelesen wird. Nur application/json und application/x-www-form-urlencoded werden überhaupt inspiziert – alle anderen Content-Types, allen voran multipart/form-data, laufen vollkommen unangetastet durch. r.Body wird in diesem Fall niemals gewrappt, gelesen oder ersetzt. Große Datei-Uploads landen also nie im Arbeitsspeicher, nur weil dieses Plugin vor dem Endpunkt installiert ist.
  • Für die beiden inspizierten Content-Types wird der Body über einen io.LimitReader (1 MiB Cap) gelesen, um den Speicherverbrauch gegen übergroße oder böswillige Payloads zu begrenzen. Bodies über diesem Limit werden gar nicht erst geparst (kein Passwort-Check läuft), aber trotzdem unverändert an dein Backend weitergereicht.
  • Das Klartext-Passwort wird ausschließlich in-process an die CheckPassword-Methode des offiziellen hansestack-go-Clients übergeben, für die Dauer eines einzigen k-Anonymity-Lookups. Es wird von diesem Plugin niemals geloggt, gecacht oder auf Platte geschrieben.
  • Nutze zusätzlich Path-Scoping (die /login*-Matcher-Syntax oben), um den Leak-Check gezielt auf deine Auth-Endpunkte zu beschränken – Defense in Depth, auch wenn multipart/form-data bereits automatisch übersprungen wird.

Metriken (Prometheus)

Wenn du Caddys globale metrics-Option aktivierst, stellt das Plugin eigene Prometheus-Counter am Admin-Endpunkt (/metrics) bereit, unter einem eigenen hansestack_leakcheck_*-Namespace (getrennt von Caddys eigenen caddy_http_*-Metriken, da diese ein API-Ergebnis beschreiben, keine HTTP-Server-Statistik):

MetrikTypBeschreibung
hansestack_leakcheck_checks_total{result}CounterGesamtzahl geprüfter Passwörter, gelabelt mit result="leaked" oder result="not_leaked".
hansestack_leakcheck_check_outcomes_total{outcome}CounterDie Begründung hinter jedem Check: outcome="checked", "skipped_timeout", "skipped_rate_limited", "skipped_circuit_open", "skipped_error" oder "skipped_canceled". Unter Fail-Open zählt ein übersprungener Check ebenso wie ein echtes "not leaked" oben als result="not_leaked" – diese Metrik zeigt dir den Unterschied.
hansestack_leakcheck_check_errors_totalCounterGesamtzahl der Checks, die wegen eines Client-Fehlers auf Fail-Open zurückgefallen sind. In der Standardkonfiguration sollte dieser Wert bei null bleiben – eine Rate ungleich null deutet auf eine Fehlkonfiguration hin (z.B. einen ungültigen API-Key), ohne dass ein Endnutzer betroffen war.
hansestack_leakcheck_responses_total{result, status_class}CounterIndicator-of-Compromise-Korrelationsmetrik. Verknüpft das Leak-Check-Ergebnis mit der finalen HTTP-Statusklasse deines Backends. result ist "leaked", "not_leaked" oder "skipped" (jeder Fail-Open-Grund gebündelt in einen Bucket). status_class ist auf "2xx"/"3xx"/"4xx"/"5xx"/"unknown" gebucketed (nie ein roher Status-Code), um die Kardinalität begrenzt zu halten. Wird in jedem Modus erfasst, inklusive observe, sogar wenn das Backend paniert.
🚨 Der wichtigste Wert im gesamten Dashboard: result="leaked", status_class="2xx"
hansestack_leakcheck_responses_total{result="leaked", status_class="2xx"} ist die aussagekräftigste Zeitreihe, die dieses Plugin liefert: Sie bedeutet, dass ein Request mit einem bestätigt geleakten Passwort trotzdem beim Backend ankam und eine erfolgreiche Antwort erhielt – also, dass sich jemand erfolgreich mit einem bekannt kompromittierten Passwort angemeldet hat. Das ist ein bestätigter Account-Takeover (ATO). Diese Metrik wird in jedem Modus erfasst, nicht nur in block, denn observe/enrich_request/enrich_response leiten den Request unabhängig vom Check-Ergebnis immer weiter. Erfassung erfolgt per defer in jedem Modus – eine Beobachtung geht also nie verloren, weder bei einem frühen Return noch wenn das Backend paniert (dann als status_class="unknown" erfasst).

Beispiel-Queries (PromQL)

Leak-Rate der letzten 5 Minuten:

sum(rate(hansestack_leakcheck_checks_total{result="leaked"}[5m]))
/
sum(rate(hansestack_leakcheck_checks_total[5m]))

Anteil der Checks, die tatsächlich von der API beantwortet wurden (statt unter Fail-Open übersprungen zu werden):

sum(rate(hansestack_leakcheck_check_outcomes_total{outcome="checked"}[5m]))
/
sum(rate(hansestack_leakcheck_check_outcomes_total[5m]))

Ein anhaltender Abfall unter 1.0 – besonders bei steigender outcome="skipped_circuit_open"- oder "skipped_timeout"-Rate – bedeutet, dass die Leak-Check-API beeinträchtigt oder nicht erreichbar ist. Endnutzer sind davon nicht betroffen (jeder Modus außer block lässt Requests weiterhin durch), aber der Leak-Check selbst läuft nicht.

Indicator-of-Compromise: Geleakte Passwörter mit erfolgreichem Login der letzten Stunde:

sum(increase(hansestack_leakcheck_responses_total{result="leaked", status_class="2xx"}[1h]))

Ein von null verschiedener, anhaltender Wert hier ist unabhängig vom konfigurierten Modus einen Alert wert: Er bedeutet, dass ein Request mit einem bestätigt geleakten Passwort an dein Backend weitergeleitet (oder in enrich_request/enrich_response sogar verarbeitet) wurde und eine 2xx-Antwort zurückbekam – jemand hat sich also erfolgreich mit einem bekannt geleakten Passwort authentifiziert.