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 upund 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-hansestackOption 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
| Direktive | Standardwert | Beschreibung |
|---|---|---|
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. |
mode | enrich_request | Modus: observe, enrich_request, enrich_response oder block. |
password_field | password | Der Key im JSON oder Form-Data, der das Passwort enthält. |
header_leaked | X-Hansestack-Leaked | Header, der das Ergebnis (true/false) enthält. |
header_count | X-Hansestack-Leak-Count | Header mit der Anzahl der gefundenen Leaks. |
block_status | 401 | HTTP Status-Code, wenn mode=block greift. |
timeout | 500ms | Maximale Wartezeit für den API-Aufruf (Fail-Open greift danach). |
circuit_breaker_threshold | deaktiviert (0) | Anzahl aufeinanderfolgender Fehler, bevor der Breaker auslöst. |
circuit_breaker_cooldown | 30s | Dauer, für die der Breaker neue Requests lokal sofort durchwinkt. |
max_idle_conns | 200 | Begrenzt 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:
observe(Zero-Risk Observability): Asynchron. Läuft den Leak-Check exakt wieenrich_response, injiziert aber niemalsheader_leaked/header_countin 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-Korrelationsmetrikhansestack_leakcheck_responses_totalzu befüllen (siehe Metriken unten). Wähleobserve, 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.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
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.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.
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 denContent-Type-Header, bevor auch nur ein einziges Byte des Bodies gelesen wird. Nurapplication/jsonundapplication/x-www-form-urlencodedwerden überhaupt inspiziert – alle anderen Content-Types, allen voranmultipart/form-data, laufen vollkommen unangetastet durch.r.Bodywird 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 offiziellenhansestack-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 wennmultipart/form-databereits 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):
| Metrik | Typ | Beschreibung |
|---|---|---|
hansestack_leakcheck_checks_total{result} | Counter | Gesamtzahl geprüfter Passwörter, gelabelt mit result="leaked" oder result="not_leaked". |
hansestack_leakcheck_check_outcomes_total{outcome} | Counter | Die 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_total | Counter | Gesamtzahl 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} | Counter | Indicator-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. |
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.