What you will set up
Every server on the internet is probed all day: password guesses over SSH, and scanners asking your web server for /.env, /wp-login.php and admin pages of software you do not run. CrowdSec reads your logs, recognises these patterns, and has the attacker's address blocked in the firewall for four hours. It is the same idea as Fail2ban, with ready-made detection rules from a shared collection, the Hub.
CrowdSec has two parts. The engine, crowdsec, reads logs and decides. A bouncer acts on its decisions; here, the firewall bouncer, which drops all traffic from a blocked address with nftables, on every port, over IPv4 and IPv6.
CrowdSec is developed by CrowdSec SAS, a French company in Montrouge, registered in Nanterre under number 880 140 496. The engine, the firewall bouncer and the detection rules on the Hub are open source under the MIT licence. The online console at app.crowdsec.net and the larger blocklists are CrowdSec's commercial services, with a free tier for the console. Although CrowdSec SAS is French, its online services rely on US companies. Its privacy policy names AWS, GCP, Firebase, Slack and ClickUp, all American, as recipients of data, and says transfers outside the EU rely on the European Commission's standard contractual clauses. When we checked, the Central API at api.crowdsec.net ran at Amazon Web Services in Ireland, the console at Vercel, and the Hub's downloads came through Amazon CloudFront and Cloudflare. CrowdSec's legal notice names Webflow, Inc. in San Francisco as the host of its website; that site answered from Vercel when we checked.
This guide has two setups. Pick one, and the steps below change to match:
- Local only: CrowdSec never registers with CrowdSec's Central API. It detects and blocks attacks on this server from this server's own logs, and sends nothing about them anywhere.
- Community blocklist: the server registers with the Central API. It reports the addresses that attacked it, and in return receives a blocklist of about 15,000 addresses that attacked other CrowdSec users, so they are blocked before they reach you.
Every step below was run on a fresh Melonslab VC-P Alloy (2 vCPU, 8 GB) with Debian 13, in both setups:
- Real attackers were blocked within three minutes of installing: an SSH password guesser from the internet, through the SSH rules, and a web scanner probing for sensitive files on Caddy.
- A burst of requests for missing pages, sent to the server's own IPv4 and IPv6 address, got the address blocked by the probing rule, and further requests timed out.
- From test locations elsewhere on the internet, a blocked address timed out on port 443 over IPv4 and IPv6 while others connected, and connected again after the block was lifted.
- ufw kept a port it had not opened closed while CrowdSec blocked addresses on the ports it had opened, and
ufw reloadleft CrowdSec's rules in place. - An address on the allowlist was never blocked, by a rule or by hand.
- In the Local only setup, a packet capture showed no connection to CrowdSec's services after installation, through a reboot.
- In the Community blocklist setup, a logging proxy recorded every request to the Central API; step 8 shows what they contained.
- Everything started again by itself after a reboot, with the blocks still in place.
CrowdSec used about 160 MB of memory and 0.15 % of one CPU core while idle, and the firewall bouncer about 26 MB. With the community blocklist loaded, that grew to about 205 MB and 37 MB. With Caddy, the whole server used 530 to 610 MB.
Before you start
You need:
- a Melonslab server with Debian 13;
- SSH keys, automatic security updates and ufw, set up as in steps 1 to 4 of the security guide, with ports 22, 80 and 443 open;
- for the web server part, a name such as
example.comwith an A and an AAAA record pointing at the server; - the addresses you connect from, for the allowlist in step 5. If your internet provider changes your address, you also need the console, see If you block yourself.
The examples use 203.0.113.10 and 2001:db8::10 for the server's addresses and 198.51.100.7 for your own. Replace them with your own throughout.
CrowdSec blocks addresses, and an attacker who controls many addresses still gets a few tries from each. It does not replace key-only SSH logins or updates: it makes the noise smaller and stops a scanner before it finds the one weak page.
1. Add CrowdSec's package repository
Debian 13 has its own crowdsec package, but it is version 1.4.6 from 2022, and the firewall bouncer is 0.0.25. Detection rules on the Hub are written for current versions, so this guide uses CrowdSec's repository, with version 1.8.1. The repository is hosted at packagecloud.io; on our test server, its downloads came from Amazon Web Services in California and Amazon CloudFront.
CrowdSec's documentation installs the repository with a script piped into the shell. These are the same steps by hand. As root:
apt update
apt install -y curl gpg
curl -fsSL https://packagecloud.io/crowdsec/crowdsec/gpgkey | gpg --dearmor -o /etc/apt/keyrings/crowdsec.gpg
Create /etc/apt/sources.list.d/crowdsec.sources:
Types: deb
URIs: https://packagecloud.io/crowdsec/crowdsec/any/
Suites: any
Components: main
Signed-By: /etc/apt/keyrings/crowdsec.gpg
Then:
apt update
apt-cache policy crowdsec
The candidate is 1.8.1 or newer, from packagecloud.io.
2. Install CrowdSec
With Local only
When it is installed, the package asks whether to register with the Central API, and registers if nobody answers. Answer no in advance:
echo "crowdsec crowdsec/capi boolean false" | debconf-set-selections
apt install -y crowdsec
The log confirms that the Central API is off:
grep "Central API" /var/log/crowdsec.log
level=info msg="push and pull to Central API disabled"
cscli capi status answers the Central API (CAPI) must be configured with 'cscli capi register'.
The package downloads the Hub's index and detection rules, looks at what runs on the server, and installs matching collections: on our test server, crowdsecurity/linux, crowdsecurity/sshd, and, because Caddy was already installed, crowdsecurity/caddy with the generic web rules. To see them:
cscli collections list
If you install Caddy after CrowdSec, add its collection yourself with cscli collections install crowdsecurity/caddy.
3. Read SSH logs from the journal
CrowdSec's setup points the SSH rules at /var/log/auth.log. Debian 13 writes SSH's log only to the systemd journal: the file exists, but stays empty, so CrowdSec would never see a password guess. Tell it to read the journal instead. Create /etc/crowdsec/acquis.d/sshd-journal.yaml:
# Debian 13 logs SSH to the journal only, so /var/log/auth.log stays empty
source: journalctl
journalctl_filter:
- "_SYSTEMD_UNIT=ssh.service"
labels:
type: syslog
Then reload CrowdSec:
systemctl reload crowdsec
grep journalctl /var/log/crowdsec.log | tail -1
level=info msg="Spawning process" command="journalctl --follow -n 0 _SYSTEMD_UNIT=ssh.service" ...
The setup's own files in /etc/crowdsec/acquis.d/ stay. They log warnings such as No matching files for pattern /var/log/syslog, which do no harm.
With a file of your own in that folder, package updates no longer rerun the automatic setup; they say so with The automatic setup will not proceed to avoid any conflict. When you add a service to the server later, install its collection yourself, as for Caddy in step 2.
4. Let CrowdSec read Caddy's log
Caddy writes no access log until you ask it to. Add a log block to your site in /etc/caddy/Caddyfile:
example.com {
root * /usr/share/caddy
file_server
log {
output file /var/log/caddy/access.log
}
}
Caddy writes one line of JSON per request, which is the format CrowdSec's Caddy rules expect. Apply it and make a request:
systemctl reload caddy
curl -sI https://example.com
tail -1 /var/log/caddy/access.log
There is one more step on Debian 13. Its Caddy is version 2.6.2, and the Caddy rules from the Hub read the visitor's address from the field client_ip, which Caddy only added in version 2.7. Version 2.6 writes it as remote_ip. Without the fix below, CrowdSec reads Caddy's log and recognises scanners, but cannot block them: its log shows scope is Ip but Meta[source_ip] doesn't exist every time a rule fires.
Create /etc/crowdsec/parsers/s02-enrich/00-caddy-remote-ip.yaml:
# Debian 13 has Caddy 2.6, whose logs have remote_ip but not client_ip,
# which the caddy-logs parser reads. Without this, nothing gets banned.
name: local/caddy-remote-ip
description: "Take the client address from remote_ip in Caddy 2.6 logs"
filter: "evt.Meta.log_type == 'http_access-log' && evt.Meta.source_ip == '' && evt.Unmarshaled.caddy.request.remote_ip != nil"
statics:
- parsed: remote_ip
expression: evt.Unmarshaled.caddy.request.remote_ip
- meta: source_ip
expression: evt.Unmarshaled.caddy.request.remote_ip
It only fills in the address when it is missing, so it does nothing once Debian ships a newer Caddy. Test it on the last line of the log, then reload:
tail -1 /var/log/caddy/access.log > /tmp/line.json
cscli explain --file /tmp/line.json --type caddy
systemctl reload crowdsec
Under s02-enrich, local/caddy-remote-ip is marked green, and the parser ends with parser success.
5. Put your own addresses on the allowlist
Do this before you install the bouncer. If you mistype your password a few times, or test CrowdSec from your own computer, it blocks you like anyone else. An allowlist keeps addresses from ever being blocked:
cscli allowlists create mine -d "My own addresses"
cscli allowlists add mine 198.51.100.7 -d "home"
Add every address you connect from, IPv6 as well; a range works too, such as 2001:db8:1234::/48. To check:
cscli allowlists inspect mine
cscli decisions add --ip 198.51.100.7 -d 1m -R test
The second command is refused:
Error: cscli decisions add: 198.51.100.7 is allowlisted by item 198.51.100.7 from mine (home), use --bypass-allowlist to add the decision anyway
When an address on the allowlist trips a rule, CrowdSec's log shows alert source 198.51.100.7 is allowlisted ... skipping, and nothing is blocked. Adding an address that is already blocked lifts the block. cscli allowlists remove mine 198.51.100.7 takes an address off again.
6. Install the firewall bouncer
apt install -y crowdsec-firewall-bouncer-nftables
The package registers the bouncer with CrowdSec on its own (API Key successfully created) and starts it. To check:
cscli bouncers list
nft list tables
The bouncer is listed with a recent Last API pull, and two tables have appeared next to ufw's: ip crowdsec and ip6 crowdsec6. Every 10 seconds the bouncer fetches new decisions and puts the addresses in a set in these tables. Its rule drops all traffic from those addresses, before ufw's rules are looked at.
7. Watch it block
Within minutes on our test server, real attackers had tripped the rules: an SSH password guesser and a scanner looking for sensitive files. To see what CrowdSec has detected:
cscli alerts list
cscli decisions list
| ID | Source | Scope:Value | Reason | Action | Country | AS | Events | expiration | Alert ID |
| 4 | crowdsec | Ip:192.0.2.38 | crowdsecurity/ssh-slow-bf | ban | GB | 209854 Cyberzone S.A.| 11 | 3h59m53s | 4 |
An alert is what a rule detected; a decision is the block that follows. cscli alerts inspect 4 shows more, such as the user names an SSH guesser tried.
To check that a ban really closes the server, block one address by hand. Take one you can test from, such as a second internet connection or a phone on mobile data, and not one on your allowlist:
cscli decisions add --ip 192.0.2.50 -d 30m -R "test ban"
Within 10 seconds, the address is in the firewall:
nft list set ip crowdsec crowdsec-blacklists-cscli
From that device, the website and SSH now time out. In our test, the blocked test location timed out on port 443 over IPv4, and a blocked IPv6 address timed out over IPv6, while every other location connected. Lift the block:
cscli decisions delete --ip 192.0.2.50
The device connects again within 10 seconds.
8. Check what the server sends to CrowdSec
With Local only
To see every name the server looks up, run this and leave it running while you restart CrowdSec and its bouncer from a second session:
tcpdump -l -n -i eth0 -Q out udp port 53
systemctl restart crowdsec crowdsec-firewall-bouncer
No crowdsec.net name appears. In our test, the server only looked up names of CrowdSec's services during installation, from packagecloud.io for the packages, and cdn-hub.crowdsec.net, hub-data.crowdsec.net and version.crowdsec.net for the Hub. After that, a packet capture through a reboot showed no connection to CrowdSec at all. The bouncer only talks to the engine on 127.0.0.1:8080.
Two things still contact CrowdSec, and send nothing about your server's attackers:
crowdsec-hubupdate.timerrunscscli hub updateandcscli hub upgradeonce a day, to fetch new versions of the detection rules from the Hub. The package does the same when it is updated. CrowdSec sees the server's address and its CrowdSec version. To stop it, runsystemctl disable --now crowdsec-hubupdate.timer, and update the rules by hand as in step 12.aptfetches updates from packagecloud.io.
CrowdSec also looks up the reverse DNS name of every address it blocks, through the server's normal resolver. That is part of its rules for recognising good bots, not a CrowdSec service.
If you installed CrowdSec with the Central API before reading this, switch it off. In /etc/crowdsec/config.yaml, put a # before the online_client: line and the credentials_path: line under it, then restart:
systemctl restart crowdsec
grep "Central API" /var/log/crowdsec.log | tail -1
The last line reads push and pull to Central API disabled. Blocks from the community blocklist stay until they expire, after up to a week. To remove them now:
cscli decisions delete --origin CAPI
systemctl restart crowdsec-firewall-bouncer
The bouncer only lets go of them after the restart.
9. ufw and CrowdSec together
ufw and the bouncer each have their own tables in nftables and do not touch each other's rules. CrowdSec's chains run at priority filter - 10, so a blocked address is dropped first, even on a port that ufw allows. Everything else then goes through ufw as before. To see both:
nft list tables
ufw status
table ip filter
table ip6 filter
table ip crowdsec
table ip6 crowdsec6
ip filter and ip6 filter are ufw's. In our test, with a block in place, ufw reload, ufw disable and ufw enable all left CrowdSec's tables and the blocked address in place. A test program on port 8081, which ufw had not opened, stayed closed from the internet, while unblocked addresses reached ports 22 and 443.
Two habits keep it that way. Leave the bouncer's deny_action: DROP as it is, and do not flush the whole ruleset with nft flush ruleset: that removes ufw's rules and CrowdSec's together, until both are restarted.
10. Everyday commands
cscli decisions list # who is blocked now, and why
cscli decisions list --origin crowdsec # only blocks from your own logs
cscli decisions delete --ip 192.0.2.38 # lift one block
cscli alerts list # what was detected
cscli alerts inspect 4 # details of one alert
cscli metrics # lines read, rules hit, packets dropped
cscli metrics starts with Acquisition Metrics: how many lines CrowdSec read from each log and how many matched its rules. If a log is missing there, or Lines read stays at zero while the service is in use, CrowdSec does not see that log. Successful SSH logins count as Lines unparsed, which is normal. Further down, Bouncer Metrics shows how much traffic the bouncer dropped, by where the block came from.
Blocks last four hours, set in /etc/crowdsec/profiles.yaml. Every alert, and its block, stays in CrowdSec's database in /var/lib/crowdsec/data/ after the block expires, and cscli alerts list keeps showing them.
11. If you block yourself
If you can no longer reach the server, and your address was not on the allowlist, use the console. In the client area, open your server, go to the Server tab and choose Open console. It shows the server's screen and works without the network. Log in as root with the root password, then:
cscli decisions list
cscli decisions delete --ip 198.51.100.7
cscli allowlists add mine 198.51.100.7 -d "home"
The bouncer opens the firewall for you within 10 seconds. If you do not know which address you have, cscli decisions list shows the ones blocked most recently, with the reason.
12. Updates
CrowdSec comes from its own repository, which the automatic updates from the security guide do not cover. To let them install CrowdSec's updates too, create /etc/apt/apt.conf.d/52unattended-upgrades-crowdsec:
Unattended-Upgrade::Origins-Pattern {
"origin=packagecloud.io/crowdsec/crowdsec,label=crowdsec";
};
To check:
unattended-upgrade --dry-run --debug 2>&1 | grep 'Allowed origins'
The line ends with origin=packagecloud.io/crowdsec/crowdsec,label=crowdsec. In our test, a run of unattended-upgrade updated CrowdSec from 1.8.0 to 1.8.1. The journal source, the Caddy fix, the allowlist and the blocks all stayed in place, and so did the setting from step 2.
The detection rules come from the Hub and update separately, once a day, through crowdsec-hubupdate.timer. To update them by hand:
cscli hub update
cscli hub upgrade
systemctl reload crowdsec
hub update fetches the list of what is new, and hub upgrade installs it. Files you made yourself, such as the Caddy fix from step 4, are left alone: hub upgrade reports local/caddy-remote-ip - not downloading local item.
Troubleshooting
SSH guesses fill the journal, but CrowdSec blocks nobody. CrowdSec is reading the empty /var/log/auth.log. Add the journal source from step 3. cscli metrics then lists journalctl:journalctl-_SYSTEMD_UNIT=ssh.service under Acquisition Metrics, with a count under Lines read after the next SSH login.
Scanners show up in Caddy's log, and cscli metrics shows rules overflowing, but nothing is blocked. CrowdSec's log shows scope is Ip but Meta[source_ip] doesn't exist. Caddy 2.6 writes the address in a field the Caddy rules do not read; add the parser from step 4.
Caddy's log stays empty in cscli metrics. The site has no log block, or it writes somewhere other than /var/log/caddy/, which is where /etc/crowdsec/acquis.d/setup.caddy.yaml looks.
cscli decisions add says the address is allowlisted. It is on your allowlist from step 5, which is the point. --bypass-allowlist adds the block anyway.
You blocked yourself. See step 11.