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.

Mai 2026 CrowdSec · Caddy · Bubble Tea · Go
Terminal-Fenster mit zwei Spalten Decisions und Alerts, Confirm-Dialog im Vordergrund
2 Auth-Stacks

parallel

Bouncer-Key fuer Read-Decisions, Machine-JWT fuer Alerts und Write-Ops

1 Tab

in Ember

Live-View ueber Decisions und Alerts mit StatusCount-Badge

JSON-Lines

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.