Automatically Banning Malicious IPs on a Debian Server with CrowdSec
Public-facing servers face all kinds of automated scanning every single day.
Even when you never advertise a service address, your SSH port still routinely receives login attempts against usernames like root, admin and test. Web services may get probed for paths such as .env, wp-admin and phpinfo.php.
Most of these requests come from automated scanners. They may not amount to an actual intrusion, but they generate a huge amount of pointless log noise and widen the exposed attack surface.
Relying on static firewall rules alone makes it hard to adjust the ban list dynamically based on actual attack behaviour.
That’s why I chose to try CrowdSec: identify malicious behaviour from server logs, then work with nftables to automatically ban the source of the attack.
This article records the process of deploying CrowdSec on Debian 12, and how it decides whether an IP should be banned.
1. What Is CrowdSec?
CrowdSec is an open-source intrusion detection and security response system.
It analyses the logs produced by services such as SSH and web servers to detect brute-force attacks, vulnerability scanning and malicious crawlers.
Unlike a traditional static IP blacklist, CrowdSec mainly decides whether to ban based on behavioural characteristics, and it can also draw on community threat intelligence.
CrowdSec’s work splits into two parts:
- Security Engine: reads the logs, identifies attacks and produces ban decisions.
- Remediation Component (Bouncer): carries out those ban decisions, for example by modifying firewall rules.
The two are independent of each other.
In other words, installing only the CrowdSec Security Engine will not automatically block network connections.
1.1 How It Works
SSH / Caddy / Nginx
|
v
Application Logs
|
v
CrowdSec Parser
|
v
Detection Scenarios
|
v
Security Alerts
|
v
Decision Profiles
|
v
Local API
|
v
Firewall Bouncer
|
v
nftables
|
v
Block IP
By decoupling log analysis from firewall enforcement, CrowdSec works just as well on a single server as it does across a fleet of machines, where a central API dispatches ban decisions to many hosts.
1.2 CrowdSec vs Fail2ban
Fail2ban is a common automatic banning tool on Linux.
Both can detect SSH brute-force attempts from logs, but the design philosophies differ.
| Feature | CrowdSec | Fail2ban |
|---|---|---|
| SSH brute-force detection | Supported | Supported |
| Web attack detection | Supported | Supported, usually needs a matching filter |
| Community threat intelligence | Supported | Usually needs extra integration |
| Distributed deployment | Native support | Usually requires your own integration |
| Ban mechanism | Bouncer | Action |
| Detection rules | Parser + Scenario | Filter + Jail |
| Configuration complexity | Relatively high | Relatively low |
If all you need is to ban IPs that fail SSH logins, Fail2ban is already good enough.
If you want to protect multiple web services, SSH and other network services at once, CrowdSec’s modular architecture leaves more room to scale.
2. Deployment Environment
This article uses the following environment:
| Item | Configuration |
|---|---|
| Operating system | Debian 12 Bookworm |
| CrowdSec | 1.8.x |
| Firewall | nftables |
| Installation method | APT |
| Detection targets | SSH, Caddy, Nginx |
| Ban mechanism | Firewall Bouncer |
I use a native installation rather than Docker.
The main reason is that CrowdSec needs to read the host’s systemd journal and, through the Bouncer, operate the system firewall. A native install avoids extra permission, network and mount configuration.
3. Installing CrowdSec
3.1 Add the Official APT Repository
First install the dependencies:
sudo apt update
sudo apt install -y \
debian-archive-keyring \
ca-certificates \
curl \
gnupg
Create the keyring directory:
sudo install -d -m 0755 /etc/apt/keyrings
Import the signing key for the CrowdSec repository:
curl -fsSL \
https://packagecloud.io/crowdsec/crowdsec/gpgkey |
sudo gpg --dearmor \
-o /etc/apt/keyrings/crowdsec.gpg
Add the repository:
echo \
"deb [signed-by=/etc/apt/keyrings/crowdsec.gpg] https://packagecloud.io/crowdsec/crowdsec/any any main" |
sudo tee /etc/apt/sources.list.d/crowdsec.list
Refresh the APT cache:
sudo apt update
Check where the package comes from:
apt-cache policy crowdsec
Once you’ve confirmed you’re using the official repository, install it:
sudo apt install -y crowdsec
Check the version:
sudo cscli version
Look at the service:
sudo systemctl status crowdsec
3.2 Install the Firewall Bouncer
CrowdSec itself handles detection but does not enforce firewall bans directly.
So you need to install a Firewall Bouncer as well.
This article uses nftables:
sudo apt install -y \
crowdsec-firewall-bouncer-nftables
Enable the service at boot:
sudo systemctl enable --now \
crowdsec-firewall-bouncer
Check its status:
sudo systemctl status \
crowdsec-firewall-bouncer
Check that the Bouncer has registered with CrowdSec:
sudo cscli bouncers list
Normally you should see:
Name Valid Type
firewall-bouncer true crowdsec-firewall-bouncer
The name is just an example — the real one may carry an auto-generated suffix from the installation.
The Bouncer configuration file is usually located at:
/etc/crowdsec/bouncers/crowdsec-firewall-bouncer.yaml
The parts worth paying attention to are:
mode: nftables
api_url: http://127.0.0.1:8080/
It also contains the API key.
The APT install normally handles API registration automatically, so overwriting the entire configuration file is not recommended.
4. Configuring SSH Attack Detection
SSH is one of the most common entry points for attacks against public-facing servers.
Even with password login disabled, you can still receive a large volume of invalid-username probes and public-key authentication attempts.
4.1 Install the SSH Collection
The CrowdSec Hub provides predefined SSH detection rules.
Install it:
sudo cscli collections install \
crowdsecurity/sshd
View it:
sudo cscli collections list
A Collection is a set of Parsers and Scenarios, where:
- The Parser is responsible for understanding SSH logs.
- The Scenario is responsible for deciding whether something counts as an attack.
4.2 Configure journald
Debian 12 uses systemd by default, so SSH logs can be read straight from journald.
Check first:
sudo journalctl -u ssh.service \
-n 20 --no-pager
If you can see SSH login records, you can configure CrowdSec to read the matching logs.
Create the file:
/etc/crowdsec/acquis.d/ssh.yaml
Write:
source: journalctl
journalctl_filter:
- "_SYSTEMD_UNIT=ssh.service"
labels:
type: syslog
Two details matter here.
First, journalctl_filter must match the actual log fields.
Second, type: syslog tells CrowdSec to use the corresponding log parsing pipeline — it should not be casually changed to sshd.
If the auto-discovered log source installed at setup time already covers SSH, avoid collecting the same data twice.
Validate the configuration:
sudo crowdsec -t
Restart the service:
sudo systemctl restart crowdsec
Check acquisition:
sudo cscli metrics show acquisition
Once new SSH log events appear, the corresponding source should show up in the acquisition metrics.
4.3 Verify SSH Log Parsing
You can use cscli explain to test the log parser.
The example below uses the documentation-reserved IP, so it doesn’t expose a real attack source:
sudo cscli explain \
--log 'Oct 19 18:48:40 server sshd[1234]: Invalid user test from 203.0.113.10 port 51878' \
--type syslog
If parsing succeeds, you should see the corresponding syslog and SSH parsing pipeline.
Note: cscli explain is a log-parsing diagnostic tool — a single test record like this will not get an IP banned.
5. Configuring Caddy Log Detection
Besides SSH, CrowdSec can also identify malicious HTTP requests from a reverse proxy’s access logs.
Caddy is used as the example here.
5.1 Install the Caddy Collection
sudo cscli collections install \
crowdsecurity/caddy
If you also run Nginx, install this as well:
sudo cscli collections install \
crowdsecurity/nginx
5.2 Configure Log Acquisition
Suppose Caddy’s access logs are stored in:
/var/log/caddy/
and use JSON format.
Create:
/etc/crowdsec/acquis.d/caddy.yaml
Contents:
filenames:
- /var/log/caddy/*.log
labels:
type: caddy
Restart CrowdSec:
sudo crowdsec -t
sudo systemctl restart crowdsec
Check:
sudo cscli metrics show acquisition
If it shows logs being read and parsed, the acquisitor and the parser are working.
That said, it does not mean malicious requests have been detected yet.
5.3 How Does CrowdSec Judge HTTP Attacks?
CrowdSec judges based on different Scenarios, asking questions such as:
- Whether sensitive files are being accessed repeatedly in a row.
- Whether a large number of non-existent paths are being probed.
- Whether known vulnerability-probing behaviour is present.
- Whether the HTTP request rate is abnormal.
- Whether the request matches the behaviour signature of known malicious HTTP clients.
For example, requests to these paths:
/.env
/wp-admin/
/phpinfo.php
/.git/config
These paths show up in automated scans all the time.
But a single visit does not necessarily trigger a ban.
CrowdSec evaluates the rules of the specific Scenario, so that normal traffic isn’t misjudged as an attack.
6. When Does CrowdSec Actually Ban an IP?
This is the single most important thing to understand when using CrowdSec.
CrowdSec does not ban an IP the moment it sees one failed SSH login. It generates alerts and ban decisions once the behaviour meets a Scenario’s detection conditions.
6.1 The Leaky Bucket Algorithm
Many CrowdSec Scenarios use the Leaky Bucket algorithm.
You can think of each IP as an individual bucket:
- Every qualifying attack event adds one unit to the bucket.
- The bucket leaks continuously at a configured rate.
- If events accumulate faster than they leak, the bucket overflows and an attack alert is triggered.
For example, a hypothetical rule:
type: leaky
capacity: 5
leakspeed: "10s"
This means:
- The bucket has a capacity of 5.
- One event leaks out every 10 seconds.
- An overflow is triggered when accumulated events exceed the capacity.
If an attacker keeps trying to log in over a short period, a ban may be triggered.
If attempts happen only once every few minutes, the events may have leaked away before reaching the threshold.
This is also why some IPs that scanned through multiple usernames in a row were not banned immediately.
6.2 Inspect the Real Detection Thresholds
Don’t guess at CrowdSec’s default thresholds from memory.
Different Scenarios, Hub versions and local custom rules may use different parameters.
Inspect the SSH brute-force rule:
sudo cscli scenarios inspect \
crowdsecurity/ssh-bf
You can also read the installed rule file directly:
cat /etc/crowdsec/scenarios/ssh-bf.yaml
Focus on:
capacity:
leakspeed:
groupby:
distinct:
Where:
capacitydetermines the bucket size.leakspeeddetermines how fast events decay.groupbyusually groups by source IP.distinctprevents repeated events from being counted over and over.
So CrowdSec should not be reduced to “5 failures within 10 minutes and you get banned.”
The real trigger condition is defined by the Scenario’s complete rule set.
6.3 How Long Does the Ban Last?
After producing an Alert, CrowdSec uses a Profile to decide whether to generate a ban Decision.
Check it:
cat /etc/crowdsec/profiles.yaml
A common IP ban policy looks like this:
name: default_ip_remediation
filters:
- Alert.Remediation == true && Alert.GetScope() == "Ip"
decisions:
- type: ban
duration: 4h
on_success: break
Where:
duration: 4h
means the ban lasts 4 hours.
The Profile that is actually installed may contain other conditions or policies, so check the full configuration before editing — and avoid defining a rule with the same name twice.
6.4 From Alert to an Actual Ban
Even after a Scenario has fired, several further conditions must be met:
- The detection scenario produces an Alert.
- The Profile converts the Alert into a Decision.
- The Firewall Bouncer pulls the Decision.
- nftables adds the source IP to the ban set.
- An actual firewall rule matches that source IP.
If any one of these steps fails, you can end up in a situation where CrowdSec shows attack alerts but the network connections are not actually blocked.
7. Verifying That Automatic Bans Work
A successful installation does not mean the whole ban chain has been verified.
It makes sense to split this into two parts: first verify firewall enforcement, then verify attack detection.
7.1 Check the Bouncer
sudo cscli bouncers list
Confirm that:
- The Bouncer is registered.
- API authentication is valid.
- It has been pulling Decisions recently.
7.2 Create a Test Ban
Using an RFC 5737 documentation example IP:
sudo cscli decisions add \
--ip 203.0.113.10 \
--type ban \
--duration 5m \
--reason manual-test
List the decisions:
sudo cscli decisions list
Then check nftables:
sudo nft list ruleset | grep -i crowdsec
Confirm that the banned IP has entered the corresponding set:
sudo nft list ruleset | grep '203.0.113.10'
If you can see the test IP, and the set is referenced by an effective firewall rule, the Bouncer has synced the ban decision to the firewall.
Release it once the test is done:
sudo cscli decisions delete \
--ip 203.0.113.10
Note that 203.0.113.10 is a documentation test address: it’s fine for verifying decision sync, but not for testing whether a real network connection gets blocked.
7.3 Look at Real Attack Alerts
sudo cscli alerts list
View the ban decisions:
sudo cscli decisions list
View Scenario metrics:
sudo cscli metrics show scenarios
If a Scenario’s Overflow counter increases, the attack detection rule has fired.
But whether a ban was actually produced has to be judged together with the Profile, Simulation and Decision.
Check the Simulation status:
sudo cscli simulation status
If a Scenario is in simulation mode, it may only produce detection results without enforcing the ban.
7.4 Avoid Locking Out Your Own Management IP
When testing bans on a public-facing server, the most important thing is not to lock out your only way in.
Recommendations:
- Have the cloud provider’s console ready in advance.
- Never directly ban the egress IP of your current SSH client.
- Set up the necessary AllowList for trusted, fixed management addresses.
- Confirm nftables rule priority before changing any policy.
- Use another controlled device for real connection tests.
8. Does CrowdSec Look at Old Logs?
By default, CrowdSec mainly focuses on real-time log analysis, and it does not guarantee that it will automatically scan all historical logs from before the installation at startup.
For example, before CrowdSec was deployed, the SSH log may already contain several hundred failed login attempts.
Those events will not all turn into current ban decisions merely because CrowdSec was installed.
8.1 Normal Operation
| Situation | Handling |
|---|---|
| Newly generated SSH logs | Real-time detection |
| Newly generated Caddy logs | Real-time detection |
| Historical logs from before installation | Usually not backfilled automatically |
| Need to re-analyse old logs | Use Replay Mode |
The actual starting point for reading may still depend on the data source and its configuration.
8.2 Export Historical SSH Logs
For example, export the past 24 hours:
sudo journalctl -u ssh.service \
--since "24 hours ago" \
--output=short \
--no-pager \
> /tmp/ssh-history.log
CrowdSec can analyse existing log files through Replay Mode.
It replays events according to the original timestamps in the log, and judges whether the detection threshold was reached at that time.
Be aware, however, that Replay may submit alerts to the API, so you shouldn’t casually replay large amounts of historical attack logs directly on a production environment.
A safer approach is to use a separate configuration and a test environment, so that real ban decisions aren’t affected.
Official documentation:
https://docs.crowdsec.net/u/user_guides/replay_mode/
9. Day-to-Day Management
CrowdSec provides the cscli command-line tool, and most routine maintenance can be done from the terminal.
9.1 Check Service Status
systemctl status crowdsec
systemctl status crowdsec-firewall-bouncer
9.2 Check Log Acquisition
sudo cscli metrics show acquisition
9.3 Check Parser Metrics
sudo cscli metrics show parsers
9.4 Check Scenario Metrics
sudo cscli metrics show scenarios
9.5 View Banned IPs
sudo cscli decisions list
9.6 Ban Manually
sudo cscli decisions add \
--ip 203.0.113.10 \
--duration 4h \
--reason manual-ban
9.7 Unban Manually
sudo cscli decisions delete \
--ip 203.0.113.10
9.8 View CrowdSec Logs
sudo journalctl -u crowdsec -f
View the Bouncer logs:
sudo journalctl \
-u crowdsec-firewall-bouncer -f
9.9 Update the Rules
Rules from the CrowdSec Hub can be updated independently:
sudo cscli hub update
sudo cscli hub upgrade
Updating detection rules is not the same as upgrading the CrowdSec software itself. The software update is still managed through APT.
10. Frequently Asked Questions
CrowdSec Is Running but Nothing Is Being Banned
First check log acquisition:
sudo cscli metrics show acquisition
Then check the parsers:
sudo cscli metrics show parsers
Then check:
sudo cscli metrics show scenarios
sudo cscli alerts list
sudo cscli decisions list
Finally check the Bouncer and nftables:
sudo cscli bouncers list
sudo nft list ruleset
The correct troubleshooting order is:
Log Acquisition
↓
Parser
↓
Scenario
↓
Alert
↓
Decision
↓
Bouncer
↓
Firewall
Don’t rush to change the ban thresholds before you’ve confirmed that the logs are actually being read.
Why Are There Lots of Attack Logs but No Bans?
Because the number of log lines isn’t the same as the number of events that meet the detection conditions.
Some logs may not have been parsed correctly, and some may simply not match the Scenario’s filter.
Also, even if the same IP fails to log in many times, if the interval between two events is long enough, the Leaky Bucket may never overflow.
It Says I’ve Been Banned, but I Can Still Get In
Possible causes include:
- The Bouncer hasn’t synced the Decision yet.
- The nftables rule isn’t hooked into the point where traffic actually passes.
- The firewall rules are in the wrong order.
- IPv4 and IPv6 aren’t both covered.
- The traffic actually goes through a reverse proxy, so the IP in the log isn’t the real client address.
- The blocked connection and the connection you’re testing with come from different sources.
So cscli decisions list only proves that CrowdSec produced a decision — on its own it cannot prove that the network layer blocked the traffic.
Does CrowdSec Automatically Unban?
Yes.
Once a ban Decision expires, the Bouncer should sync the firewall set and remove the corresponding IP.
However, if the same IP is also blocked by some other firewall rule, CrowdSec unbanning it will not automatically remove those independent rules.
11. Conclusion
The real value of CrowdSec isn’t just automatic IP banning — it’s splitting log parsing, behaviour detection and ban enforcement into independent components.
That lets you start from the most basic SSH brute-force protection and expand step by step to web services, reverse proxies and centralised multi-server protection.
In a real deployment, there are three things worth understanding:
First, CrowdSec does not ban based on a fixed number of failures. It uses Scenarios to judge whether behaviour is abnormal, and many rules use the Leaky Bucket algorithm, so both the frequency and the duration of an attack affect the detection result.
Second, successful detection is not the same as a successful ban. Alerts, Decisions, Bouncers and firewall rules are different stages — when troubleshooting, confirm them layer by layer.
Third, CrowdSec suits continuous monitoring of newly generated security events. If you need to analyse historical attacks, Replay Mode is available, but you should avoid letting historical alerts accidentally affect production bans.
For servers already running Debian, systemd, Caddy or Nginx, CrowdSec together with nftables is an automated security-protection option worth considering.
It cannot replace timely software updates, secure configuration, SSH public-key authentication and the principle of least privilege — but it can cut down a large volume of repetitive automated scanning and brute-force requests.