Fachartikel · Infrastruktur
Ember + CrowdSec:
Ein TUI-Plugin gegen die 50.000-Decisions-Wand
Wie ein Caddy-TUI-Plugin lokale CrowdSec-Decisions vom Threat-Intel-Feed trennt — und nebenbei die LAPI-Auth-Falle erklaert.
parallel
Bouncer-Key fuer Read-Decisions, Machine-JWT fuer Alerts und Write-Ops
in Ember
Live-View ueber Decisions und Alerts mit StatusCount-Badge
Audit-Log
Append-only, mode 0600, jede Schreiboperation journalisiert
Mein Caddy + CrowdSec-Cluster wirft 50.000 aktive Decisions an mich. Welche davon stammen aus meinem Logfile? Welche aus dem Threat-Intel-Feed der Community? Und welche IP wartet eigentlich gerade darauf, von mir whitelistet zu werden?
Genau die Frage stand auf meinem Bildschirm, als ich auf einer produktiven Caddy-LXC einen Smoke-Test gegen die lokale CrowdSec-LAPI gefahren habe. Die Liste war so lang, dass die Operatorfrage "welcher Ban gehoert eigentlich mir?" in der Mauer aus Community-Bans verschwand.
Die Antwort ist ember-crowdsec: ein Plugin fuer Ember, den Caddy-TUI-Inspector. Ein zusaetzlicher Tab in der TUI, in dem die Decisions geordnet, filterbar, mit Live-Status und zwei Hotkeys fuer Unban und Whitelist auftauchen — und der nebenbei eine bemerkenswerte Auth-Falle der CrowdSec-LAPI sichtbar macht.
Was das Plugin liefert
Live-Decisions + Alerts
Tick-basiertes Polling, parallele Goroutinen, Sortierung nach Restlaufzeit und Created-At.
Origin-Filter mit Hotkey
Default zeigt nur eigene Bans. Hotkey c blendet den Community-Feed dazu — auf Wunsch.
Unban + Whitelist
Confirm-Dialog mit IP, Origin und Scenario. Tastatur wird waehrend des Dialogs gelockt.
Audit-Log
JSON-Lines, mode 0600, append-only. Erfolge und Fehler beide journalisiert.
Werkzeug
Kurzer Caddy-Bezug: Was ist Ember?
Caddy hat eine sehr saubere Admin-API auf localhost:2019. Wer den Live-State eines Caddy-Hosts pruefen will — welche Hosts gerade serven, welche Upstreams die Reverse-Proxies haben, welche Zertifikate ablaufen — kann das per curl /config/ + jq tun. Das ist praezise, aber muehsam, sobald man drei oder vier davon nebeneinander hat.
Ember ist die Antwort darauf: ein Open-Source Terminal-UI fuer Caddy von Alexandre Daubois (Symfony-Maintainer), in Go geschrieben mit Bubble Tea und Lipgloss. Statt API-Calls sieht man eine Live-TUI mit Tabs fuer Hosts, Upstreams, Caddy-Config, Certificates und Logs. FrankenPHP-Setups bekommen on top per-Thread-Introspection und Worker-Memory-Tracking. Ein Helper-Tool — keine eigene Konfigurations-Schicht, keine Schreibrechte am Caddy, reines Inspector-Werkzeug.
Seit v1.3.0 hat Ember zusaetzlich eine Plugin-API. Eigene Tabs lassen sich compile-time per blank-import in ein Custom-Binary einbauen — derselbe Mechanismus, mit dem Caddy selbst seit Jahren via xcaddy seine Plugins zieht. Genau dieser Hebel ist die Eintrittstuer fuer ein CrowdSec-Tab: ein Plugin-Modul, das die LAPI-Daten in Embers TUI hineinrendert, ohne dass Ember selbst etwas von CrowdSec wissen muss.
Plattform
Plugin-System — der xcaddy-Hebel fuer TUIs
Plugins werden compile-time per blank-import in ein Custom-Ember-Binary dazugelinkt. Das angereicherte Binary hat alle Tabs des Originals plus den zusaetzlichen CrowdSec-Tab — kein Fork, kein Maintainer-Coordination-Aufwand.
Distribution
Eine winzige cmd/ember-custom/main.go importiert das Plugin-Paket mit _-Praefix, ruft ember.Run() auf — fertig. Der Build erzeugt ein angereichertes Ember-Binary mit allen Tabs des Originals plus dem zusaetzlichen CrowdSec-Tab.
EXPERIMENTAL — pinning Pflicht
Embers Plugin-API ist als experimentell markiert. Ich pinne deshalb auf Version v1.3.0. Wer ein eigenes Plugin schreiben will, weiss damit zumindest wo der Hebel ansetzt: Plugin als Pflichtschnittstelle, dazu komponierbare Fetcher und Renderer.
Stolperstein
Die Auth-Falle: Bouncer-Key oder JWT — beides nie
Erste Implementation, JWT-only: GET /v1/decisions antwortet mit 401 token rejected twice. Was nach einem buggy Token aussah, ist Architektur.
Im Quellcode crowdsecurity/crowdsec/pkg/apiserver/controllers/controller.go Zeilen 113 bis 145 sind zwei separate Gin-Routergruppen unter /v1 definiert: eine fuer Bouncer (X-Api-Key), eine fuer Machines (JWT-Bearer). Endpoints sind exklusiv genau einer Gruppe zugeordnet — kein eitherAuth-Mittelweg.
Der Sinn dahinter ist Least-Privilege: Bouncer (Caddy, Traefik) sollen lesen, nicht schreiben. Konsole und Machine-Accounts sollen schreiben — sich aber an einem anderen Endpoint vorbei am Bouncer-Pfad authentifizieren. Wer wie diese TUI beides will, faehrt beide Auth-Stacks parallel.
Auth-Matrix CrowdSec LAPI
| Endpoint | Bouncer-Key | Machine-JWT |
|---|---|---|
| POST /v1/watchers/login | — | Login (liefert JWT) |
| GET /v1/decisions | JA | NEIN (401) |
| GET /v1/decisions/stream | JA | NEIN |
| GET /v1/alerts | NEIN | JA |
| DELETE /v1/decisions/{id} | NEIN | JA |
| POST /v1/alerts (Whitelist) | NEIN | JA |
Setup auf der LAPI-Seite: zwei Befehle, zwei Credentials.
# Machine-Account fuer JWT-Login (Alerts + Schreibzugriffe):
cscli machines add ember-tui --auto -f -
# Bouncer-Key fuer GET /v1/decisions:
cscli bouncers add ember-tui-bouncer --auto -f - auf machines add umgeht den Abbruch, wenn /etc/crowdsec/local_api_credentials.yaml bereits existiert.
Datenmodell
Decisions vs. Alerts — ein subtiler Unterschied
Der Unterschied klingt klein, ist aber konsequent durchgezogen: Eine Decision ist die aktive Konsequenz (Ban, Captcha, Throttle) auf eine IP. Ein Alert ist das ausloesende Ereignis ("nginx-bf-wordpress-enum auf 192.0.2.1, 12 Hits"). 1:n — ein Alert kann mehrere Decisions tragen, eine Decision lebt aber auch ohne dauerhaften Alert-Verweis weiter.
cscli
Sieht beide Welten — Machine-Account-Zugriff. cscli decisions list und cscli alerts list sind zwei verschiedene Befehle, weil es zwei verschiedene Tabellen sind.
Bouncer
Sieht nur Decisions. X-Api-Key gilt ausschliesslich auf /v1/decisions. Eine Caddy-Reverse-Proxy-Integration kommt mit Decisions aus — und nur deshalb war "JWT funktioniert auch fuer Decisions" jahrelang ein verbreiteter Irrglaube in Foren.
Origin-Filter
Der "die Liste explodiert"-Moment
CrowdSec liefert auf GET /v1/decisions ohne Filter alle aktiven Bans — eigene Engine, manuelle cscli-Eintraege, der CAPI-Threat-Intel-Feed und alle abonnierten Community-Listen (firehol, tor, ...). Auf einem aktiven Knoten sind das schnell 50.000+ Eintraege.
Default vs. CAPI an
CrowdSec — 12 decisions, 4 alerts (filter: local+manual, c: include CAPI)
CrowdSec — 50217 decisions, 4 alerts (filter: ALL, c: hide CAPI) Das Plugin filtert serverseitig per Default mit ?origins=crowdsec,cscli — also nur was die lokale Engine produziert hat plus manuelle Eintraege. Hotkey c schaltet den Threat-Intel-Feed dazu, der naechste Tick der Polling-Schleife liefert die volle Liste. Ein Atomic-Bool im Fetcher haelt den Toggle-State.
Konsequenz fuer das Killer-Feature: d (Unban) ist nur auf crowdsec- und cscli-Origins erlaubt. Eine CAPI-Decision lokal zu loeschen geht — aber der naechste CAPI-Sync zieht sie sofort wieder rein. Die TUI blockiert den Hotkey explizit mit Status-Hinweis und schlaegt den richtigen Weg vor:
Cannot unban CAPI decision (will re-pull). Use w (whitelist) instead. Schreibzugriffe
Unban, Whitelist, Audit — Schreiben aus der TUI
Lesen ist die haelfte. Wer beim Vorbeischauen sieht, dass die IP eines Kunden an einer Brute-Force-Liste klebt, will nicht erst in eine andere Shell wechseln, um cscli decisions delete zu tippen. Hotkey d, Confirm, fertig.
Hotkey-Mapping
| Taste | Modus | Effekt |
|---|---|---|
| c | normal | CAPI / Threat-Intel ein- bzw. ausblenden |
| d | normal | Confirm-Unban — nur auf eigenen (crowdsec/cscli) Decisions |
| w | normal | Whitelist mit Default-Dauer 24h, auf jeder Origin erlaubt |
| y / Y | confirm | Aktion ausfuehren |
| n / Esc | confirm | Abbrechen |
Jeder Confirm-Dialog blendet IP, Origin und Scenario inline ein — versehentliche Treffer sind unwahrscheinlich. Solange ein Confirm-Dialog offen ist, frisst das Plugin jeden Tastendruck und reicht keinen weiter. Andere Tabs und globale Shortcuts loesen also nicht aus, waehrend man den Cursor auf y hat.
Whitelist ist gegenintuitiv: nicht POST /v1/decisions, sondern POST /v1/alerts mit einem Array. Ein Alert-Wrapper traegt eine einzelne Decision vom Typ whitelist. Genau so macht es cscli decisions add --type whitelist — verifiziert gegen cmd/crowdsec-cli/clidecision/decisions.go Z. 256–388.
Jeder Schreibversuch — Erfolg und Fehler — landet als JSON-Line im Audit-Log. Das File wird mit O_NOFOLLOW + Mode 0600 geoeffnet, damit ein nachtraeglich angelegter Symlink den Schreibpfad nicht in eine andere Datei umleiten kann.
{"ts":"2026-05-07T12:34:56Z","action":"unban","ip":"192.0.2.1","decision_id":12345,"status":"ok"}
{"ts":"2026-05-07T12:35:08Z","action":"whitelist","ip":"192.0.2.7","duration":"24h","reason":"manual whitelist via ember-tui","status":"ok"}
{"ts":"2026-05-07T12:36:11Z","action":"unban","ip":"192.0.2.9","decision_id":9999,"status":"error","error":"delete decision 9999: status 500: boom"} Auth-Header-Split
Zwei Methoden, zwei Header
Im Fetcher leben beide Auth-Pfade nebeneinander. bouncerGet setzt X-Api-Key, authedGet setzt Authorization: Bearer und reagiert auf 401 mit Token-Invalidate plus Retry — exakt einmal.
// Decisions: statischer Bouncer-Key, kein Refresh.
req.Header.Set("X-Api-Key", f.bouncerKey)
// Alerts + Write-Ops: JWT mit One-Shot-Retry.
req.Header.Set("Authorization", "Bearer "+token)
if resp.StatusCode == 401 {
f.auth.Invalidate()
continue // retry once with fresh token
} Konfiguration laeuft komplett ueber Environment-Variablen mit dem Praefix EMBER_PLUGIN_CROWDSEC_. Ember strippt den Praefix und reicht den Rest lowercase als PluginConfig.Options-Map ans Plugin durch.
EMBER_PLUGIN_CROWDSEC_LAPI_URL=http://127.0.0.1:8080
EMBER_PLUGIN_CROWDSEC_MACHINE_ID=ember-tui
EMBER_PLUGIN_CROWDSEC_MACHINE_PASSWORD=<...>
EMBER_PLUGIN_CROWDSEC_BOUNCER_KEY=<...> Dieses Plugin ist fuer Localhost-LAPI ausgelegt. Wer eine entfernte LAPI ansprechen muss, tunnelt sie ueber WireGuard oder NetBird und laesst das Plugin weiter http://127.0.0.1 sehen. Plaintext-HTTP gegen einen Off-Host wuerde Bouncer-Key und Machine-Passwort im Klartext ueber die Leitung schicken — explizit aus Scope.
Repo
Adoption
Das Plugin liegt auf GitHub unter github.com/rewulff/ember-crowdsec, MIT-Lizenz. Quickstart im einen Befehl: go install github.com/rewulff/ember-crowdsec/cmd/ember-custom@latest. Vor dem ersten Lauf zwei cscli-Befehle plus eine Env-Datei mit den Credentials — alles im README. Wer noch keinen Caddy + CrowdSec-Stack laufen hat, findet im Repo unter docs/CADDY-CROWDSEC-SETUP.md den vollstaendigen Setup-Guide.
Fazit
Ember ist ein angenehmer Caddy-TUI-Inspector. Mit einem CrowdSec-Tab daneben sieht man auf einen Blick, was der eigene Engine produziert — und kann Bans und Whitelists ohne Shell-Wechsel direkt aus der Operator-Sicht setzen. Der Aufwand dafuer ist ein paar hundert Zeilen Go, eine Auth-Matrix, die man einmal verstanden hat, und die Disziplin, jede Schreibaktion in ein Audit-Log zu spiegeln.
Eigenes CrowdSec- oder Caddy-Setup?
Operator-Tooling fuer Ihre Infrastruktur
Ich baue passgenaue TUIs, Plugins und Operator-Workflows fuer CrowdSec, Caddy und Reverse-Proxy-Setups — Open-Source-first, ohne Cloud-Abhaengigkeit.