Skip to content

CrowdSec

Optional, and off until switched on. CrowdSec decides who is banned from what its agents see across the fleet; HAProxy asks about each visitor through a small local agent, the SPOA bouncer (crowdsec-haproxy-spoa-bouncer, from CrowdSec's own repository), and answers a banned address with a 403 page — or a captcha first, for hCaptcha, reCAPTCHA or Cloudflare Turnstile. The bouncer runs on every node and polls your CrowdSec LAPI for decisions; one API key (cscli bouncers add haproxy-manager on the CrowdSec host) serves every node. This page is about the setting; the LAPI and its agents are yours to run.

The settings

Access › CrowdSec. The header carries a pill, saved: on or saved: off, and a status line for this node; the fields, in order:

Field Notes
LAPI URL your CrowdSec local API, http://192.168.1.5:8080/ — the machine running the agent
API key from cscli bouncers add; left empty, the stored key is kept
Skip TLS verification for a LAPI on https:// with a certificate this node does not trust
What a banned visitor gets ban (a 403 page) or captcha (a challenge first; solving it lets the visitor through)
Captcha provider, site key, secret key captcha mode only — hCaptcha, reCAPTCHA or Cloudflare Turnstile, with the keys from the provider's dashboard; an empty secret keeps the stored one
Contact URL on the ban page optional — somewhere a wrongly banned visitor can reach you; a form rather than a mailto:, which crawlers harvest
Never ask about address lists CrowdSec is not consulted for — the LAN, typically; everyone else is
Enable CrowdSec last, so the rest is filled in before it is turned on

Two buttons. Save stores the settings and answers Saved. Apply to make it so on this node; the other nodes follow. — the settings are shared, and they reach every node with the next Apply like the rest of the configuration. Test the LAPI connection asks the LAPI one question with the credentials in the form and says what came back: the LAPI answers and accepts the key, refused the key (403), or did not answer. One rule governs it: the stored key is only ever sent to the stored URL. Testing a different URL needs a key typed into the form, so a mistyped address cannot hand your key to whatever answers there.

Save checks the result before storing it: the configuration these settings render to is run through haproxy -c against staged copies of the SPOE engine and the error pages, in a temporary directory, so a setting HAProxy would refuse is never stored and nothing under /etc/haproxy changes until Apply.

The 403 page is the one under Error Pages for 403 when there is one, else the bouncer's own, with a contact link when a contact URL is set. Fail-open, by design: a bouncer that is down or slow leaves the request alone rather than taking the service down with it. It is a filter on top of your services, not a gate they depend on.

What Apply does

Each Apply, in this order: haproxy -c over the rendered configuration against staged copies of the files it names; then the error pages and the CrowdSec files are written for real — the bouncer's YAML and the SPOE engine HAProxy loads (/etc/haproxy/crowdsec.cfg, HAM_CROWDSEC_SPOE); the bouncer is started, restarted when its configuration changed, or stopped when CrowdSec is off; and only then haproxy.cfg is installed and HAProxy reloaded, so the first request after the reload finds a bouncer already listening.

With CrowdSec on, every HTTP listener asks the bouncer before anything else is done for a request; a TCP listener is left alone, since the answer is a page.

The bouncer on each node

The bouncer is a package from CrowdSec's repository, not the distribution's, and each install path handles that differently:

Install What you get
Docker image the bouncer is in the image
install.sh, Proxmox script CrowdSec's apt repository and the package are added, and the service left stopped
.deb / .rpm the package only recommends the bouncer; the app fetches it itself when needed

The app installs the bouncer only when CrowdSec is switched on and the binary is missing — at startup and at Apply. A node that never enabled CrowdSec is never given a third-party repository. The install adds CrowdSec's packagecloud repository (on the Debian family the bookworm suite, whose build is a static binary; on the RPM family a .repo file for el/<major> or fedora/<version>), fetches the repository's signing key over HTTPS and checks it against the fingerprint 6A89E3C2303A901A889971D3376ED5326E93CD0C before anything is installed. The same fingerprint is pinned on every path — the image build, install.sh, the Proxmox script and the app — so a key that does not match stops the install rather than being trusted. A failed install is not retried for six hours (the marker /var/lib/haproxy-manager/crowdsec-install-tried holds the time), and the node serves everything else meanwhile.

A node that must not carry the bouncer is installed with install.sh --without-crowdsec, which leaves the marker /var/lib/haproxy-manager/crowdsec-not-wanted; the app honours it, and it survives install.sh --update — which is what the one-click updater runs. The status line on the CrowdSec page shows the bouncer's version and service state, or bouncer not installed — fetched at the next Apply once CrowdSec is on, or now: with an Install on this node button, which clears both markers and installs at once. The install call is node-local, so the button works on a passive node too — it is the one thing on the page a read-only node may do.

In the container the bouncer runs under supervisord as the crowdsec-spoa user through /usr/local/bin/crowdsec-run, a loop that starts it again every 10 s while the LAPI cannot be reached (the bouncer halts when the LAPI is down at startup, and supervisord would otherwise give up on it). The status the page reports is the bouncer process itself, not the loop around it. It is not started with the container: the app starts it once CrowdSec is on, and stops it when CrowdSec is switched off.

The bouncer's own log is not on the Logs page: journalctl -u crowdsec-spoa-bouncer on a server, docker logs (its standard output) in the container.

What it looks like on disk

Path What Written
config.json section crowdsec the settings, the LAPI key, the captcha secret and the signing key — mode 0600 like the rest on Save
/etc/crowdsec/bouncers/crowdsec-spoa-bouncer.yaml (HAM_CROWDSEC_BOUNCER_CFG) the bouncer's configuration, with the key — mode 0640, root:crowdsec-spoa, via a temporary file and rename at Apply
/etc/haproxy/crowdsec.cfg (HAM_CROWDSEC_SPOE) the SPOE engine HAProxy loads at Apply
/var/lib/haproxy-manager/crowdsec-not-wanted this node is not to get the bouncer by --without-crowdsec
/var/lib/haproxy-manager/crowdsec-install-tried when an install last failed by a failed install

The CrowdSec settings are shared, so they are in every history snapshot and a history restore puts them back with the rest. They are not in the backup file from Settings › Backup & Export, which carries only the HAProxy, ACME and access sections; a restore leaves CrowdSec as the node has it. The markers are each node's own.

Mixed versions

Upgrade every node before editing rules or services

The crowdsec section is part of the shared fingerprint, so during a rolling upgrade from 1.x the header says configuration differs until every node runs 2.0 — expected, and it clears by itself. More important: a 1.x node reads only the old rule shape, so a rule saved on a 2.0 node with both any of these and all of these filled would apply there without its conditions. The Cluster page names the nodes still on 1.x; upgrade them before editing rules or services on a 2.0 node.