GuidesNetwork and VPNBlock attacks with CrowdSec

Block attackers with CrowdSec

CrowdSec on Debian 13 reads your SSH and Caddy logs, spots password guessing and scanners, and blocks them in the firewall next to ufw. Run it fully local, or share signals for the community blocklist.

Tested on CrowdSec 1.8.1 and crowdsec-firewall-bouncer 0.0.36 on Debian 13 (trixie) on a Melonslab server Updated October 2, 2026

Recommended server for this guide

VC-S Micro · 2 vCPU · 8 GB Memory · 250 GB Storage

Month to month, no lock-in 7-day money-back guarantee

€7.99/mo

Deploy now
On this page

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.
Setup

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 reload left 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.com with 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'.

With Community blocklist

apt install -y crowdsec

The package registers the server with the Central API on its own. To check:

cscli capi status
You can successfully interact with Central API (CAPI)
Sharing signals is enabled
Pulling community blocklist is enabled
Pulling blocklists from the console is enabled

Registration creates a random machine name and password in /etc/crowdsec/online_api_credentials.yaml. It is not linked to you, your email or a CrowdSec account, though CrowdSec sees the server's address in every request. Linking it to an account, called enrolling, is a separate step in the console that this guide does not use.

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.timer runs cscli hub update and cscli hub upgrade once 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, run systemctl disable --now crowdsec-hubupdate.timer, and update the rules by hand as in step 12.
  • apt fetches 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.

With Community blocklist

The engine talks to api.crowdsec.net over HTTPS. We sent its requests through a logging proxy on the test server to see exactly what they contained. To get a signal without reporting anyone, we tripped the rules from the server's own address and held that signal back at the proxy:

  • At every start, and when its session has expired: a login with the machine name and password from step 2, plus the names of all detection rules installed, so the blocklist can match them.
  • Every two hours: a download of the community blocklist. Our server received 15,000 addresses at the first download, with the reason for each, such as http:bruteforce or ssh:bruteforce.
  • Every 30 minutes: the versions of CrowdSec and its bouncer, the bouncer's name, and when each last checked in.
  • For each attack it detects, within 10 seconds: a signal. It holds the attacker's address with its network range, network operator (AS), country and approximate coordinates; the rule's name, version and hash; when the attack started and ended, and how many log lines it took; the block your server applied, with its length; and the server's local machine name. Nothing else from the logs was in it: no paths, user agents or user names.

CrowdSec's documentation says the same, in what is sent to the Central API: signals are only sent for unmodified rules from the Hub. Blocks you add by hand with cscli, and alerts from rules you changed or wrote, are not sent unless you enrol the server in the console. In our test, none of the blocks we added by hand appeared in the requests.

According to CrowdSec's privacy policy, it keeps attacking addresses in full for three months, then less precisely: after three months, the address is narrowed to one of 16 and the time to a 6-hour window; after six months, to one of 256 and a day.

To see the community blocklist on your server:

cscli decisions list --origin CAPI -o raw | cut -d, -f4 | sort | uniq -c | sort -rn
   7659 http:bruteforce
   2371 generic:scan
   2139 http:exploit
   1735 ssh:bruteforce
    592 http:scan
    414 ssh:exploit
     85 vm-management:exploit
      5 http:crawl

To keep receiving the blocklist but stop sending signals, add sharing: false under online_client: in /etc/crowdsec/config.yaml, with the same indentation as credentials_path:, and restart CrowdSec. cscli capi status then says Sharing signals is disabled, and in our test no signal went out for a new attack. CrowdSec's documentation says that servers that do not contribute regularly get the smaller Lite list of 3,000 addresses instead; we did not test how long that takes.

To leave the Central API altogether, put a # before the online_client: line and the credentials_path: line under it in the same file, then run systemctl restart crowdsec. The log then reads push and pull to Central API disabled, and the proxy saw no further requests. The community blocklist stays until it expires; the Local only setup shows how to remove it.

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.

Run it on your own server

VC-S Micro

€7.99/mo

vCPU
2
Memory
8 GB
Storage
250 GB
Transfer
10 TB
Standard
HDD · RAID 10
  • Full root access
  • Native /64 IPv6
  • RAID-protected storage
  • Malmö, Sweden
  • Month to month, no lock-in
  • 7-day money-back guarantee
All guides