This is the multi-page printable view of this section.
Click here to print.
Return to the regular view of this page.
CrowdSec
CrowdSec is a three-part system; detection, coordination and mitigation.
Detection
The detection agent runs wherever you have log files worth looking at. It compares entries to known patterns of intrusion attempts and reports any events to the local hub. You can can also create your own patterns for custom services.
Coordination
This local hub coordinates these security events into a list that all participants can see. It submits these and pulls other known offenders from CrowdSec central. This keeps the local block list in sync with greater community. Putting the ‘Crowd’ in CrowdSec.
Mitigation
The mitigation agent is the final part of the solution. It runs on firewalls or servers and blocks any IPs associated with an event.
1 - Single Install
A basic deploy of CrowdSec is fairly straight-forward.
Installation
With Debian, you can simply add the repo via their script and install with a couple lines.
curl -s https://install.crowdsec.net | sudo sh
sudo apt install crowdsec
sudo apt install crowdsec-firewall-bouncer-nftables
This installs all three parts. The detection and coordination part (crowdsec), and the mitigation (crowdsec-firewall-bouncer) part.
The crowdsec binary will check in with the cloud, download a baseline list of known bad-actors, and begin watching the syslog for known patterns (like ssh login failures). The firewall-bouncer will create a nft drop table and keep it up to date with any changes from the hub.
# Check out the very long drop list
sudo nft list ruleset | less
Note - if the CrowdSec tables are empty, you may need to sudo systemctl restart nftables.service or possibly reboot (as I’ve found in testing)
Configuration
CrowdSec comes pre-configured to watch for the basics and seems to do some inspection of your system to add appropriate log monitoring. You can see what’s it monitoring with the command:
sudo cscli collections list
Sometimes, that’s all you need. But take a look at operations for more options.
2 - Network Install
Deploy a single hub that all agents connect to. This allows the whole network to share events.
Install a Local Sec Hub
Start with just the main CrowdSec binary.
curl -s https://install.crowdsec.net | sudo sh
sudo apt install crowdsec
Configure the hub on to listen on the network
sudo sed -i 's/listen_uri: 127.0.0.1:8080/listen_uri: 0.0.0.0:8080/' /etc/crowdsec/config.yaml
sudo systemctl restart crowdsec.service
Generate a Client API Key
To connect, clients should use individual API keys.
# For a host named 'www' in this case, change as desired.
sudo cscli machine add www --auto -f -
Machine 'www' successfully added to the local API.
url: http://0.0.0.0:8080
login: www
password: sadkljfhaslkdjhfalkwsuehfaliseudhbf00987
Install CrowdSec on the client.
curl -s https://install.crowdsec.net | sudo sh
sudo apt install crowdsec
Comment out the server block to prevent starting a hub here too.
sudo vi /etc/crowdsec/config.yaml
...
...
user: nobody # plugin process would be ran on behalf of this user
group: nogroup # plugin process would be ran on behalf of this group
api:
client:
insecure_skip_verify: false
credentials_path: /etc/crowdsec/local_api_credentials.yaml
# server:
# log_level: info
# listen_uri: 127.0.0.1:8080
# profiles_path: /etc/crowdsec/profiles.yaml
# console_path: /etc/crowdsec/console.yaml
# online_client: # Central API credentials (to push signals and receive bad IPs)
# credentials_path: /etc/crowdsec/online_api_credentials.yaml
# trusted_ips: # IP ranges, or IPs which can have admin API access
# - 127.0.0.1
# - ::1
# tls:
# cert_file: /etc/crowdsec/ssl/cert.pem
# key_file: /etc/crowdsec/ssl/key.pem
prometheus:
enabled: true
level: full
listen_addr: 127.0.0.1
listen_port: 6060
Supply the Sec Hub address and password.
sudo vi /etc/crowdsec/local_api_credentials.yaml
url: http://sechub.lan:8080
login: www
password: sadkljfhaslkdjhfalkwsuehfaliseudhbf00987
sudo systemctl restart crowdsec.service
sudo systemctl status crowdsec.service
The best place to stop attacks is at the border. If you’re running OpenWRT or a Linux Firewall there’s a handy netfilter ‘bouncer’ you can add.
Back on your SecHub, generate an API key for it.
sudo cscli bouncers add router-1-bouncer
API key for 'router-1-bouncer':
XXXXXXXXXX
Over on your firewall, install the bouncer and add the API details
curl -s https://install.crowdsec.net | sudo sh
sudo apt install crowdsec-firewall-bouncer-nftables
# Adjust 'SecHub' as needed and past in your key (or directly edit the file)
sudo sed -i 's#api_url: http://127.0.0.1:8080/#api_url: http://SecHub:8080/#' /etc/crowdsec/bouncers/crowdsec-firewall-bouncer.yaml
sudo sed -i 's#api_key: <API_KEY>#api_key: XXXXXX#' /etc/crowdsec/bouncers/crowdsec-firewall-bouncer.yaml
sudo systemctl restart crowdsec-firewall-bouncer.service
You can install the bouncer in other places as well if you need to establish defense in depth. Though you may need to look into how private ranges are whitelisted to protect different segments.
Confirmation
Is it working? Take a look at the decision list.
# On the hub, this will show the community block list.
sudo cscli decisions list --origin CAPI
# On the firewall, this will show the corresponding table contents from the list
sudo nft list table ip crowdsec
Troubleshooting
table ip crowdsec { … }
In some cases, the hub server will not download the community block list after installation without a reboot. Everything will seem to be working but no default list until you reboot.
3 - Operations
CrowdSec is most useful when you integrate local data sources. The easiest way to add local collections is to enable new collections.
3.1 - Add Local Collections
A collection is a bundle of config files that detail what to look for, and where to look. The install selects a few to start with based on it’s inspection of already already installed software.
sudo cscli collections list
If you want something else, take a look at CrowdSec’s collections to see what’s available and the more info link will show you how to install it. You can also do a quick look at the command line as well.
sudo cscli collections list -a
For example, to configure it to watch caddy:
sudo cscli collections install crowdsecurity/caddy
# It should add a file to the drop folder
cat /etc/crowdsec/acquis.d/caddy.yaml
# Reload crowdsec for these changes to take effect
sudo systemctl reload crowdsec
Collection Details
Inspecting the collection will tell you what parsers and scenarios it bundles together. As well as some metrics.
sudo cscli collections list
sudo cscli collections inspect crowdsecurity/linux
sudo cscli collections inspect crowdsecurity/sshd
To learn more a collection and it’s components, you can check out their page:
https://hub.crowdsec.net/author/crowdsecurity/collections/linux
The metrics are a bit confusing until you learn that the ‘Unparsed’ column doesn’t mean unparsed so much as it means a non-event. These are just normal log lines that don’t have a pattern a parser was looking for.
How Collections Work
A collection starts by putting a config file in the acquis.d folder. This is the acquisition drop folder and tells crowdsec where to acquire data - i.e. what log files to look at. If it’s a non-standard format it will also add a new parser under parsers and any patterns under scenarios. These are things like “5 failed login attempts in less than 30 seconds”.
The important thing to know is parsers are only successful on interesting events. A normal login is just recorded as a “failed” parse because it didn’t match anything interesting. The scenario then only considers actual events, reducing the contextual overhead.
Matching a scenario triggers an alert which is sent to the hub. These alerts usually (unless whitelisted) become decisions. The default decision is to add the IP added to the ban list. These have a configurable expiration, so that if you really guess wrong 10 times in a row, you’re not banned forever.
3.2 - View Activity
Is anyone currently attacking you? The decisions list shows you any current bad actors and the alerts list shows you a summary of past decisions. If you are just getting started this is probably none, but if you’re open to the internet this will grow quickly.
sudo cscli decisions list
sudo cscli alerts list
You’re also getting events from the cloud and you can check those with the -a option. You’ll notice that every 2 hours the community-blocklist is updated.
sudo cscli alerts list -a
After a while of this collection running, you’ll start to see these kinds of alerts
sudo cscli alerts list
╭────┬───────────────────┬───────────────────────────────────────────┬─────────┬────────────────────────┬───────────┬─────────────────────────────────────────╮
│ ID │ value │ reason │ country │ as │ decisions │ created_at │
├────┼───────────────────┼───────────────────────────────────────────┼─────────┼────────────────────────┼───────────┼─────────────────────────────────────────┤
│ 27 │ Ip:18.220.128.229 │ crowdsecurity/http-bad-user-agent │ US │ 16509 AMAZON-02 │ ban:1 │ 2023-03-02 13:12:27.948429492 +0000 UTC │
│ 26 │ Ip:18.220.128.229 │ crowdsecurity/http-path-traversal-probing │ US │ 16509 AMAZON-02 │ ban:1 │ 2023-03-02 13:12:27.979479713 +0000 UTC │
│ 25 │ Ip:18.220.128.229 │ crowdsecurity/http-probing │ US │ 16509 AMAZON-02 │ ban:1 │ 2023-03-02 13:12:27.9460075 +0000 UTC │
│ 24 │ Ip:18.220.128.229 │ crowdsecurity/http-sensitive-files │ US │ 16509 AMAZON-02 │ ban:1 │ 2023-03-02 13:12:27.945759433 +0000 UTC │
│ 16 │ Ip:159.223.78.147 │ crowdsecurity/http-probing │ SG │ 14061 DIGITALOCEAN-ASN │ ban:1 │ 2023-03-01 23:03:06.818512212 +0000 UTC │
│ 15 │ Ip:159.223.78.147 │ crowdsecurity/http-sensitive-files │ SG │ 14061 DIGITALOCEAN-ASN │ ban:1 │ 2023-03-01 23:03:05.814690037 +0000 UTC │
╰────┴───────────────────┴───────────────────────────────────────────┴─────────┴────────────────────────┴───────────┴─────────────────────────────────────────╯
You may even need to unblock yourself
sudo cscli decisions list
sudo cscli decision delete --id XXXXXXX
3.3 - View Details
Data comes in through the parsers. To see what they are doing, let’s take a look at the Acquisition and Parser metrics.
Most of the ‘Acquisition Metrics’ lines will be read and parsed, but not all. Some entries don’t have anything a scenario can use. For example, when reading the SSH logs, the parser quits early if there’s not even an IP address.
sudo cscli metrics show acquisition
╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Acquisition Metrics │
├────────────────────────────────────────┬────────────┬──────────────┬────────────────┬────────────────────────┬───────────────────┤
│ Source │ Lines read │ Lines parsed │ Lines unparsed │ Lines poured to bucket │ Lines whitelisted │
├────────────────────────────────────────┼────────────┼──────────────┼────────────────┼────────────────────────┼───────────────────┤
│ file:/var/log/caddy/access.log │ 1 │ 1 │ - │ 1 │ - │
│ file:/var/log/caddy/www.some.org.log │ 15 │ 15 │ - │ 18 │ - │
╰────────────────────────────────────────┴────────────┴──────────────┴────────────────┴────────────────────────┴───────────────────╯
“Lines poured to bucket” is the interesting column. If it matches a scenario, the parser ‘pours’ the event into a scenario’s ‘bucket’. Some get poured into multiple scenarios. Tha can make the “Lines poured” larger than the number of lines, like above.
Let’s take a look at what scenarios were matched.
sudo cscli metrics show scenarios
╭────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Scenario Metrics │
├──────────────────────────────────────┬───────────────┬───────────┬──────────────┬────────┬─────────┤
│ Scenario │ Current Count │ Overflows │ Instantiated │ Poured │ Expired │
├──────────────────────────────────────┼───────────────┼───────────┼──────────────┼────────┼─────────┤
│ crowdsecurity/http-crawl-non_statics │ 2 │ - │ 13 │ 14 │ 11 │
│ crowdsecurity/http-probing │ 2 │ - │ 5 │ 5 │ 3 │
╰──────────────────────────────────────┴───────────────┴───────────┴──────────────┴────────┴─────────╯
There are two scenarios matched. But while lines were poured in, nothing ‘Overflowed’. Sometimes people just type their password wrong or there’s a bad link in a web page. So the scenarios have threshold before action is taken.
You can take a look at the scenario to see what it’s looking for as well. Here’s the key lines:
# Let's look at the definition for http-probing
sudo cscli hub list
sudo cat /etc/crowdsec/hub/scenarios/crowdsecurity/http-probing.yaml
...
...
description: "Detect site scanning/probing from a single ip"
filter: "evt.Meta.service == 'http' && evt.Meta.http_status in ['404', '403', '400'] && evt.Parsed.static_ressource == 'false'"
...
capacity: 10
leakspeed: "10s"
...
It’s looking for HTTP events with 400’s that are not static resources. The capacity 10 means more than 10 events from a single IP triggers an alert. A leakspeed of 10s means to expire events that many seconds after they enter the bucket.
After a while, you’ll see something overflow and generate an alert and a decision.
╭────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Scenario Metrics │
├──────────────────────────────────────┬───────────────┬───────────┬──────────────┬────────┬─────────┤
│ Scenario │ Current Count │ Overflows │ Instantiated │ Poured │ Expired │
├──────────────────────────────────────┼───────────────┼───────────┼──────────────┼────────┼─────────┤
│ crowdsecurity/http-crawl-non_statics │ 2 │ - │ 65 │ 78 │ 63 │
│ crowdsecurity/http-generic-bf │ - │ 3 │ 5 │ 24 │ 2 │
│ crowdsecurity/http-probing │ 2 │ - │ 26 │ 26 │ 24 │
╰──────────────────────────────────────┴───────────────┴───────────┴──────────────┴────────┴─────────╯
sudo cscli alerts list
╭─────┬───────────────────┬───────────────────────────────┬─────────┬──────────────────────────────────┬───────────┬──────────────────────╮
│ ID │ value │ reason │ country │ as │ decisions │ created_at │
├─────┼───────────────────┼───────────────────────────────┼─────────┼──────────────────────────────────┼───────────┼──────────────────────┤
│ 117 │ Ip:20.114.185.119 │ crowdsecurity/http-generic-bf │ US │ 8075 MICROSOFT-CORP-MSN-AS-BLOCK │ ban:1 │ 2026-02-06T18:11:51Z │
╰─────┴───────────────────┴───────────────────────────────┴─────────┴──────────────────────────────────┴───────────┴──────────────────────╯
sudo cscli decision list
╭────────┬──────────┬───────────────────┬───────────────────────────────┬────────┬─────────┬──────────────────────────────────┬────────┬────────────┬──────────╮
│ ID │ Source │ Scope:Value │ Reason │ Action │ Country │ AS │ Events │ expiration │ Alert ID │
├────────┼──────────┼───────────────────┼───────────────────────────────┼────────┼─────────┼──────────────────────────────────┼────────┼────────────┼──────────┤
│ 904849 │ crowdsec │ Ip:20.114.185.119 │ crowdsecurity/http-generic-bf │ ban │ US │ 8075 MICROSOFT-CORP-MSN-AS-BLOCK │ 6 │ 3h39m30s │ 117 │
╰────────┴──────────┴───────────────────┴───────────────────────────────┴────────┴─────────┴──────────────────────────────────┴────────┴────────────┴──────────╯
An overflow generates an alert and a decision. That decision (to block) will stay in place for 4 hours until it expires but the alert will remain as a historical record.
Your bouncers will enforce the decisions and you can check them as well.
# Run this wherever your firewall bouncer is running
nft list ruleset | grep 20.114.185.119
elements = { 20.114.185.119 timeout 3h59m57s expires 3h33m25s132ms,
# If you're using the caddy bouncer
caddy crowdsec check 20.114.185.119
blocked: true
3.4 - Inspect Parsing
The reason why things parse or don’t parse is sometimes worth getting into and you can have Crowdsec explain it to you.
Parsed vs Unparsed
SSH authentication is interesting, because not all lines parse.
sudo cscli metrics show acquisition
╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Acquisition Metrics │
├─────────────────────────────────────────────────┬────────────┬──────────────┬────────────────┬────────────────────────┬───────────────────┤
│ Source │ Lines read │ Lines parsed │ Lines unparsed │ Lines poured to bucket │ Lines whitelisted │
├─────────────────────────────────────────────────┼────────────┼──────────────┼────────────────┼────────────────────────┼───────────────────┤
│ journalctl:journalctl-_SYSTEMD_UNIT=ssh.service │ 9 │ 5 │ 4 │ - │ 5 │
╰─────────────────────────────────────────────────┴────────────┴──────────────┴────────────────┴────────────────────────┴───────────────────╯
sudo cscli explain --dsn "journalctl://filters=_SYSTEMD_UNIT=ssh.service" --type syslog
# This line is just the server starting - it says 'success' but in the sense of 'successfully bailed out'
# before it completed parsing because it detected the 0.0.0.0 address. It will be counted as unparsed in the metrics.
line: Feb 02 15:04:22 www sshd[180]: Server listening on 0.0.0.0 port 22.
├ s00-raw
| ├ 🟢 crowdsecurity/syslog-logs (+12 ~8)
| └ 🔴 crowdsecurity/non-syslog
├ s01-parse
| ├ 🔴 crowdsecurity/caddy-logs
| ├ 🔴 crowdsecurity/sshd-logs
| └ 🔴 crowdsecurity/sshd-success-logs
└-------- parser success, ignored by whitelist (private ipv4/ipv6 ip/ranges) 🟢
# Here's me failing to log in. This was successfully parsed and made it to step 2. That's where crowdsec
# starts pulling out bits of the log message for further inspection.
#
# The numbers at the end of the line such as (+9 ~1) signify that the parser added 9 attributes
# (pieces of data) to the event, parsed from the raw log message, and updated 1.
#
# Now that it has more data, the step 2 'whitelists' get another go and see that I'm attacking
# from a private IP range. They flag it as such and it stops processing it before it gets further.
# Turns out private IPs are whitelisted by default so you can't lock yourself out.
#
# This entry is counted as parsed and whitelisted
line: Feb 06 18:12:30 www sshd-session[12643]: Failed password for allen from ::1 port 36082 ssh2
├ s00-raw
| ├ 🟢 crowdsecurity/syslog-logs (+12 ~8)
| └ 🔴 crowdsecurity/non-syslog
├ s01-parse
| ├ 🔴 crowdsecurity/caddy-logs
| ├ 🟢 crowdsecurity/sshd-logs (+9 ~1)
| └ 🔴 crowdsecurity/sshd-success-logs
├ s02-enrich
| ├ 🟢 crowdsecurity/dateparse-enrich (+2 ~1)
| ├ 🔴 crowdsecurity/geoip-enrich
| ├ 🔴 crowdsecurity/http-logs
| ├ 🟢 crowdsecurity/public-dns-allowlist (unchanged)
| └ 🟢 crowdsecurity/whitelists (~2 [whitelisted])
└-------- parser success, ignored by whitelist (private ipv4/ipv6 ip/ranges) 🟢
If that attempt was from the outside, a few of them would lead to an alert.
Parsing and Scenarios
Almost all HTTP access logs are parsed as they almost always have data interesting to CrowdSec. Take a look to see what a full parsing and scenario progression looks like.
sudo cscli metrics show acquisition
╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Acquisition Metrics │
├─────────────────────────────────────────────────┬────────────┬──────────────┬────────────────┬────────────────────────┬───────────────────┤
│ Source │ Lines read │ Lines parsed │ Lines unparsed │ Lines poured to bucket │ Lines whitelisted │
├─────────────────────────────────────────────────┼────────────┼──────────────┼────────────────┼────────────────────────┼───────────────────┤
│ file:/var/log/caddy/access.log │ 71 │ 71 │ - │ 63 │ - │
╰─────────────────────────────────────────────────┴────────────┴──────────────┴────────────────┴────────────────────────┴───────────────────╯
sudo tail -1 /var/log/caddy/access.log > ./logs
sudo cscli explain -v --file ./logs --type caddy
...
...
├ Scenarios
├ 🟢 crowdsecurity/http-crawl-non_statics
└ 🟢 crowdsecurity/http-generic-bf
# At the very bottom you'll see this line was feed to two different scenarios
3.5 - Custom Parsing
You may find entries aren’t being parsed even though they should be. Maybe the log format has changed or you’re logging additional data the author didn’t anticipate.
You can modify the existing parser, but best-practice it to add a custom parser.
Types of Parsers
There are several type of parsers and they are used in stages. Some are designed to work with the raw log entries while others are designed to take pre-parsed data and add or enrich it. This way you can do branching and not every parser needs to now how to read a syslog message.
Their Local Path will tell you what stage they kick in at. Use sudo cscli parsers list to display the details. s00-raw works with the ‘raw’ files while s01 and s02 work further down the pipeline. Currently, you can only create s00 and s01 level parsers.
Integrating with Scenarios
Useful parsers supply data that Scenarios are interested in. You can create a parser that watches the system logs for ‘FOOBAR’ entries, extracts the ‘FOOBAR-LEVEL`, and passes it on. But if nothing is looking for ‘FOOBARs’ then nothing will happen.
Let’s say you’ve added the Caddy collection. It’s pulled in a bunch of Scenarios you can view with sudo cscli scenarios list. If you look at one of the assicated files you’ll see a filter section where they look for ’evt.Meta.http_path’ and ’evt.Parsed.verb’. They are all different though, so how do you know what data to supply?
Your best bet is to take an existing parser and modify it.
Examples
Note - CrowdSec is pretty awesome and after talking in the discord they handled it. So these two examples are solved. I’m sure you’ll find new ones, though ;-)
It’s now also become an example of a prior version of the parser. I’ll update it as time permits.
A Web Example
Let’s say that you’ve installed the Caddy collection, but you’ve noticed basic auth login failures don’t trigger the parser. So let’s add a new file and edit it.
sudo cp /etc/crowdsec/parsers/s01-parse/caddy-logs.yaml /etc/crowdsec/parsers/s01-parse/caddy-logs-custom.yaml
You’ll notice two top level sections where the parsing happens; nodes and statics and some grok pattern matching going on.
Nodes allow you try multiple patterns and if any match, the whole section is considered successful. I.e. if the log could have either the standard HTTPDATE or a CUSTOMDATE, as long as it has one it’s good and the matching can move on. Statics just goes down the list extracting data. If any fail the whole event is considered a fail and dropped as unparseable.
All the pasrsed data gets attached to event as ’evt.Parsed.something’ and some of the statics are moving it to evt values the Senarios will be looking for Caddy logs are JSON formatted and so basically already parsed and this example makes use of the JsonExtract method quite a bit.
# We added the caddy logs in the acquis.yaml file with the label 'caddy' and so we use that as our filter here
filter: "evt.Parsed.program startsWith 'caddy'"
onsuccess: next_stage
# debug: true
name: caddy-logs-custom
description: "Parse custom caddy logs"
pattern_syntax:
CUSTOMDATE: '%{DAY:day}, %{MONTHDAY:monthday} %{MONTH:month} %{YEAR:year} %{TIME:time} %{WORD:tz}'
nodes:
- nodes:
- grok:
pattern: '%{NOTSPACE} %{NOTSPACE} %{NOTSPACE} \[%{HTTPDATE:timestamp}\]%{DATA}'
expression: JsonExtract(evt.Line.Raw, "common_log")
statics:
- target: evt.StrTime
expression: evt.Parsed.timestamp
- grok:
pattern: "%{CUSTOMDATE:timestamp}"
expression: JsonExtract(evt.Line.Raw, "resp_headers.Date[0]")
statics:
- target: evt.StrTime
expression: evt.Parsed.day + " " + evt.Parsed.month + " " + evt.Parsed.monthday + " " + evt.Parsed.time + ".000000" + " " + evt.Parsed.year
- grok:
pattern: '%{IPORHOST:remote_addr}:%{NUMBER}'
expression: JsonExtract(evt.Line.Raw, "request.remote_addr")
- grok:
pattern: '%{IPORHOST:remote_ip}'
expression: JsonExtract(evt.Line.Raw, "request.remote_ip")
- grok:
pattern: '\["%{NOTDQUOTE:http_user_agent}\"]'
expression: JsonExtract(evt.Line.Raw, "request.headers.User-Agent")
statics:
- meta: log_type
value: http_access-log
- meta: service
value: http
- meta: source_ip
expression: evt.Parsed.remote_addr
- meta: source_ip
expression: evt.Parsed.remote_ip
- meta: http_status
expression: JsonExtract(evt.Line.Raw, "status")
- meta: http_path
expression: JsonExtract(evt.Line.Raw, "request.uri")
- target: evt.Parsed.request #Add for http-logs enricher
expression: JsonExtract(evt.Line.Raw, "request.uri")
- parsed: verb
expression: JsonExtract(evt.Line.Raw, "request.method")
- meta: http_verb
expression: JsonExtract(evt.Line.Raw, "request.method")
- meta: http_user_agent
expression: evt.Parsed.http_user_agent
- meta: target_fqdn
expression: JsonExtract(evt.Line.Raw, "request.host")
- meta: sub_type
expression: "JsonExtract(evt.Line.Raw, 'status') == '401' && JsonExtract(evt.Line.Raw, 'request.headers.Authorization[0]') startsWith 'Basic ' ? 'auth_fail' : ''"
The very last line is where a status 401 is checked. It looks for a 401 and a request for Basic auth. However, this misses events where someone asks for a resource that is protected and the server responds telling you Basic is needed. I.e. when a bot is poking at URLs on your server ignoring the prompts to login. You can look at the log entries more easily with this command to follow the log and decode it while you recreate failed attempts.
sudo tail -f /var/log/caddy/access.log | jq
To change this, update the expression to also check the response header with an additional ? (or) condition.
expression: "JsonExtract(evt.Line.Raw, 'status') == '401' && JsonExtract(evt.Line.Raw, 'request.headers.Authorization[0]') startsWith 'Basic ' ? 'auth_fail' : ''"xtract(evt.Line.Raw, 'status') == '401' && JsonExtract(evt.Line.Raw, 'resp_headers.Www-Authenticate[0]') startsWith 'Basic ' ? 'auth_fail' : ''"
Syslog Example
Let’s say you’re using dropbear and failed logins are not being picked up by the ssh parser
To see what’s going on, you use the crowdsec command line interface. The shell command is cscli and you can ask it about it’s metrics to see how many lines it’s parsed and if any of them are suspicious. Since we just restarted, you may not have any syslog lines yet, so let’s add some and check.
ssh [email protected]
logger "This is an innocuous message"
cscli metrics
INFO[28-06-2022 02:41:33 PM] Acquisition Metrics:
+------------------------+------------+--------------+----------------+------------------------+
| SOURCE | LINES READ | LINES PARSED | LINES UNPARSED | LINES POURED TO BUCKET |
+------------------------+------------+--------------+----------------+------------------------+
| file:/var/log/messages | 1 | - | 1 | - |
+------------------------+------------+--------------+----------------+------------------------+
Notice that the line we just read is unparsed and that’s OK. That just means it wasn’t an entry the parser cared about. Let’s see if it responds to an actual failed login.
dbclient some.remote.host
# Enter some bad passwords and then exit with a Ctrl-C. Remember, localhost attempts are whitelisted so you must be remote.
[email protected]'s password:
[email protected]'s password:
cscli metrics
INFO[28-06-2022 02:49:51 PM] Acquisition Metrics:
+------------------------+------------+--------------+----------------+------------------------+
| SOURCE | LINES READ | LINES PARSED | LINES UNPARSED | LINES POURED TO BUCKET |
+------------------------+------------+--------------+----------------+------------------------+
| file:/var/log/messages | 7 | - | 7 | - |
+------------------------+------------+--------------+----------------+------------------------+
Well, no luck. We will need to adjust the parser
sudo cp /etc/crowdsec/parsers/s01-parse/sshd-logs.yaml /etc/crowdsec/parsers/s01-parse/sshd-logs-custom.yaml
Take a look at the logfile and copy an example line over to https://grokdebugger.com/. Use a pattern like
Bad PAM password attempt for '%{DATA:user}' from %{IP:source_ip}:%{INT:port}
Assuming you get the pattern worked out, you can then add a section to the bottom of the custom log file you created.
- grok:
name: "SSHD_AUTH_FAIL"
pattern: "Login attempt for nonexistent user from %{IP:source_ip}:%{INT:port}"
apply_on: message
3.6 - Whitelist
In the previous examples we’ve looked at the metrics and details of internal facing service like failed SSH logins. Those types aren’t prone to a lot of false positives. But other sources, like web access logs, can be.
False Positives
You’ll recall that when looking at metrics that a high number of ‘Lines unparsed’ is normal. They were simply entries that didn’t match any specific events the parser was looking for. Parsed lines however, are ‘poured’ to a bucket. A bucket being a potential attack type.
sudo cscli metrics
Acquisition Metrics:
╭────────────────────────────────┬────────────┬──────────────┬────────────────┬────────────────────────╮
│ Source │ Lines read │ Lines parsed │ Lines unparsed │ Lines poured to bucket │
├────────────────────────────────┼────────────┼──────────────┼────────────────┼────────────────────────┤
│ file:/var/log/auth.log │ 69 │ - │ 69 │ - │
│ file:/var/log/caddy/access.log │ 21 │ 21 │ - │ 32 │
│ file:/var/log/syslog │ 2 │ - │ 2 │ - │
╰────────────────────────────────┴────────────┴──────────────┴────────────────┴────────────────────────╯
Scenario Metrics:
╭──────────────────────────────────────┬───────────────┬───────────┬──────────────┬────────┬─────────╮
│ Scenario │ Current Count │ Overflows │ Instantiated │ Poured │ Expired │
├──────────────────────────────────────┼───────────────┼───────────┼──────────────┼────────┼─────────┤
│ crowdsecurity/http-crawl-non_statics │ - │ - │ 2 │ 17 │ 2 │
│ crowdsecurity/http-probing │ - │ 1 │ 2 │ 15 │ 1 │
╰──────────────────────────────────────┴───────────────┴───────────┴──────────────┴────────┴─────────╯
The 21 web requests where interesting to a couple scenarios. With 32 pours some where interesting to both.
The scenario http-crawl-non_statics is designed to allow some light web-crawling but http-probing is more sensitive and it overflowed into an alert.
Assuming this is related to a web application you’re trying to use, you just got blocked. Let’s take a look at the alert
allen@www:~$ sudo cscli alert list
╭─────┬────────────────────┬────────────────────────────────────────────┬─────────┬──────────────────────────────────┬───────────┬──────────────────────╮
│ ID │ value │ reason │ country │ as │ decisions │ created_at │
├─────┼────────────────────┼────────────────────────────────────────────┼─────────┼──────────────────────────────────┼───────────┼──────────────────────┤
│ 129 │ Ip:20.42.209.0 │ crowdsecurity/http-probing │ AU │ 8075 MICROSOFT-CORP-MSN-AS-BLOCK │ ban:1 │ 2026-02-06T20:33:26Z │
sudo cscli alerts inspect -d 129
...
...
- Context :
╭────────────┬───────────────────────────────────────────────────╮
│ Key │ Value │
├────────────┼───────────────────────────────────────────────────┤
│ method │ GET │
│ status │ 404 │
│ target_uri │ /0/icon/John │
│ target_uri │ /0/icon/Smith │
│ target_uri │ /0/icon/Adam │
│ target_uri │ /0/icon/Smith │
╰────────────┴───────────────────────────────────────────────────╯
...
...
target_fqdn │ app.some.org
You can see the application in question is trying to load some missing icons and just fails silently, That may be fine for the app, but it looks like someone crawling or probing the server. To fix it we’ll need to create a whitelist definition for the app.
Whitelist
To whitelist an app, we create a file with an expression that matches the behavior we see above, such as the apps attempts to load a file that doesn’t exist, and exempts it. You can only add these to the s02 stage folder and the name element but be unique for each.
sudo vi /etc/crowdsec/parsers/s02-enrich/some-app-whitelist.yaml
This example uses the startsWith expression and assumes that all requests start the same
name: you/some-app
description: "Whitelist 404s for icon requests"
whitelist:
reason: "icon request"
expression:
- evt.Parsed.request startsWith '/0/icon/'
If it’s less predictable, you can use a regular expression instead and combine with other expressions like a site match. In general, the more specific the better.
name: you/some-app-whitelist
description: "Whitelist 404s for icon requests"
whitelist:
reason: "icon request"
expression:
- evt.Parsed.request matches '^/[0-9]/icon/.*' && evt.Meta.target_fqdn == "some-app.you.org"
Now you can reload crowdsec and test
sudo systemctl restart crowdsec.service
sudo cscli explain -v --file ./test.log --type caddy
├ s00-raw
| ├ 🔴 crowdsecurity/syslog-logs
| └ 🟢 crowdsecurity/non-syslog (+5 ~8)
| └ update evt.ExpectMode : %!s(int=0) -> 1
| └ update evt.Stage : -> s01-parse
...
├ s02-enrich
| ├ 🟢 you/some-app-whitelist (~2 [whitelisted])
| ├ update evt.Whitelisted : %!s(bool=false) -> true
| ├ update evt.WhitelistReason : -> some icon request
| ├ 🟢 crowdsecurity/dateparse-enrich (+2 ~2)
...
...
| ├ 🟢 crowdsecurity/http-logs (+7)
| └ 🟢 crowdsecurity/whitelists (unchanged)
└-------- parser success, ignored by whitelist (icon request) 🟢
You’ll see in the above example, we successfully parsed the entry, but it was ‘ignored’ and didn’t go on to the Scenario section.
Troubleshooting
New Whitelist Has No Effect
If you have more than one whitelist, check the name you gave it on the first line. If that’s not unique, the whole thing will be silently ignored.
Regular Expression Isn’t Matching
CrowdSec uses the go-centric expr-lang. You may be used to unix regex where you’d escape slashes, for example. A tool like https://www.akto.io/tools/regex-tester is helpful.
3.7 - Custom Scenario
The parsing is almost always right, but you may need to add a scenario that is specific to your environment. A great example is when you need to pay attention to other HTTP Status codes.
Take a look at the On Caddy example for that.
3.8 - On Caddy
There are two things you need to look at; detecting aborted connections and trusted proxies. Connections that you abort aren’t an established HTTP Status code, so you’ll need to tell CrowdSec about it. And trusted proxies require blocking at a higher level in the stack.
Detecting Aborted Connections From Probes
If you’ve secured caddy by aborting invalid requests they will be recorded in the log as “status 0”.
grep '"status":0' /var/log/caddy/access.log | tail -1 > aborts.log
sudo cscli explain -v --file ./aborts.log --type caddy
├ s01-parse
| └ 🟢 crowdsecurity/caddy-logs (+14 ~2)
| └ update evt.Stage : s01-parse -> s02-enrich
| └ ...
| └ ...
| └ create evt.Meta.http_status : 0
Crowdsec is parsing correctly, but none of the scenarios are looking for that status code.
sudo grep evt.Meta.http_status /etc/crowdsec/scenarios/*
...
...
# a bunch of thing looking for 401 and such
We just need to let CrowdSec know we’re doing this by adding a scenario.
sudo vi /etc/crowdsec/scenarios/custom-http-probe-aborted.yaml
type: leaky
name: custom/http-probe-aborted
description: "Detect http probes that caddy aborted"
filter: "evt.Meta.log_type == 'http_access-log' && evt.Meta.http_status == '0'"
groupby: evt.Meta.source_ip
leakspeed: 10s
capacity: 5
blackhole: 1m
labels:
remediation: true
classification:
- attack.T1595
behavior: "http:scan"
label: "HTTP Probing"
service: http
sudo systemctl restart crowdsec
Make a request and check metrics to see it.
# Make a request for just the IP address so it doesn't match a domain
curl -D - http://123.123.123.123
curl: (52) Empty reply from server
# Check metrics to see if it poured (matched a scenario)
sudo cscli metrics
...
...
╭────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Scenario Metrics │
├──────────────────────────────────────┬───────────────┬───────────┬──────────────┬────────┬─────────┤
│ Scenario │ Current Count │ Overflows │ Instantiated │ Poured │ Expired │
├──────────────────────────────────────┼───────────────┼───────────┼──────────────┼────────┼─────────┤
│ crowdsecurity/http-crawl-non_statics │ 1 │ - │ 1 │ 1 │ - │
│ custom/http-probe-aborted │ 1 │ - │ 1 │ 1 │ - │
╰──────────────────────────────────────┴───────────────┴───────────┴──────────────┴────────┴─────────╯
# Check alerts - there won't be any with just one hit so far
sudo cscli alerts list
No active alerts
# Run a series of probes
for X in {1..10}; do curl -D - http://123.123.123.123/$X;done
sudo cscli alert list
╭────┬──────────────────┬───────────────────────────┬─────────┬────────────────────┬───────────┬──────────────────────╮
│ ID │ value │ reason │ country │ as │ decisions │ created_at │
├────┼──────────────────┼───────────────────────────┼─────────┼────────────────────┼───────────┼──────────────────────┤
│ 64 │ Ip:some.ip.addr │ custom/http-probe-aborted │ US │ 1234 SOME NAME │ ban:1 │ 2026-02-05T15:04:11Z │
╰────┴──────────────────┴───────────────────────────┴─────────┴────────────────────┴───────────┴──────────────────────╯
sudo cscli decision list
╭────────┬──────────┬──────────────────┬───────────────────────────┬────────┬─────────┬────────────────────┬────────┬────────────┬──────────╮
│ ID │ Source │ Scope:Value │ Reason │ Action │ Country │ AS │ Events │ expiration │ Alert ID │
├────────┼──────────┼──────────────────┼───────────────────────────┼────────┼─────────┼────────────────────┼────────┼────────────┼──────────┤
│ 709809 │ crowdsec │ Ip:some.ip.addr │ custom/http-probe-aborted │ ban │ US │ 1234 SOME NAME │ 6 │ 3h59m40s │ 64 │
╰────────┴──────────┴──────────────────┴───────────────────────────┴────────┴─────────┴────────────────────┴────────┴────────────┴──────────╯
# And un-ban yourself
sudo cscli decision delete --id 709809
Trusted Proxy
If you’re using a proxy, you’ve probably already configured caddy to log the correct end-user IP address. This will show up in your logs as the difference between remote_ip and client_ip. This is a good time to do that if you haven’t already.
sudo tail -1 /var/log/caddy/site.some.org.log | jq '.request'
# Just the relevant bits pasted in - the first two should be different if your trusted proxy setup is working
"remote_ip": "172.68.70.128",
"client_ip": "74.7.175.138",
"X-Forwarded-For": [
"74.7.175.138"
],
"Cf-Connecting-Ip": [
"74.7.175.138"
],
Cloudflare filters most bad actors for you, but many make it though. Here’s a sample of what it looks like if you haven’t set up trusted proxies. You can see that CrowdSec took action, but it’s a broad brush. It’s blocking the Cloudflare exit node and so removed everyone’s access, not just the attacker.
sudo cscli alert list
╭─────┬────────────────────┬───────────────────────────────────┬─────────┬────────────────────────┬───────────┬─────────────────────────────────────────╮
│ ID │ value │ reason │ country │ as │ decisions │ created_at │
├─────┼────────────────────┼───────────────────────────────────┼─────────┼────────────────────────┼───────────┼─────────────────────────────────────────┤
│ 221 │ Ip:162.158.49.136 │ crowdsecurity/jira_cve-2021-26086 │ IE │ 13335 CLOUDFLARENET │ ban:1 │ 2025-01-22 15:14:34.554328601 +0000 UTC │
│ 187 │ Ip:128.199.182.152 │ crowdsecurity/jira_cve-2021-26086 │ SG │ 14061 DIGITALOCEAN-ASN │ ban:1 │ 2025-01-19 20:50:45.822199509 +0000 UTC │
│ 186 │ Ip:46.101.1.225 │ crowdsecurity/jira_cve-2021-26086 │ GB │ 14061 DIGITALOCEAN-ASN │ ban:1 │ 2025-01-19 20:50:41.699518104 +0000 UTC │
│ 181 │ Ip:162.158.108.104 │ crowdsecurity/http-bad-user-agent │ SG │ 13335 CLOUDFLARENET │ ban:1 │ 2025-01-19 12:39:20.468268327 +0000 UTC │
│ 180 │ Ip:172.70.208.61 │ crowdsecurity/http-bad-user-agent │ SG │ 13335 CLOUDFLARENET │ ban:1 │ 2025-01-19 12:38:36.664997131 +0000 UTC │
╰─────┴────────────────────┴───────────────────────────────────┴─────────┴────────────────────────┴───────────┴─────────────────────────────────────────╯
But even though caddy identifies the correct client_ip, still have a problem. Crowdsec dutifully adds the client_ip to the block list, but their attacks continue. The bouncer can’t separate the good traffic from the bad because it’s layer 4 and only sees the Cloudflare IP.
for X in {1..100}; do curl -D - https://proxied.your.org/$X; done
allen@www:~$ sudo cscli alert list
# Even though you're listed, you can continue probing
╭────┬───────────────────┬───────────────────────────────────┬─────────┬──────────────────────────────┬───────────┬──────────────────────╮
│ ID │ value │ reason │ country │ as │ decisions │ created_at │
├────┼───────────────────┼───────────────────────────────────┼─────────┼──────────────────────────────┼───────────┼──────────────────────┤
│ 66 │ Ip:212.56.42.20 │ crowdsecurity/http-cve-2021-41773 │ US │ 40021 CONTABO-40021 │ ban:1 │ 2026-02-05T16:02:43Z │
│ 65 │ Ip:34.133.180.181 │ crowdsecurity/http-probing │ US │ 396982 GOOGLE-CLOUD-PLATFORM │ ban:1 │ 2026-02-05T15:38:37Z │
│ 64 │ Ip:some.ip.addr │ custom/http-probe-aborted │ US │ 1234 SOME NAME │ ban:1 │ 2026-02-05T15:04:11Z │
╰────┴───────────────────┴───────────────────────────────────┴─────────┴──────────────────────────────┴───────────┴──────────────────────╯
The ideal approach is to tell Cloudflare to stop forwarding traffic from the bad actors. There is a cloudflare-bouncer to do just that. It’s rate limited however, and only suitable for premium clients. There is also the CrowdSec Cloudflare Worker. It’s better, but still suffers from limits for non-premium clients.
Caddy Bouncer
Instead, we’ll use the caddy-crowdsec-bouncer. This is a layer 7 (protocol level) bouncer. It works inside Caddy and will block IPs based on the client_ip from the HTTP request.
The fist step is to tell the Local API that we’re going to add a bouncer and generate a key for it. If you’re running the LAPI on your router, you’d do this there.
# I named this after the hostname 'www' but you can use anything you like
sudo cscli bouncers add www-bouncer
Add the module to Caddy
sudo caddy add-package github.com/hslatman/caddy-crowdsec-bouncer
Configure Caddy
#
# Global Options Block
#
{
# Format like this or the syntax will fail.
crowdsec {
api_key 2WXxY440sLxjapyQKlvp6ILdLA/hn/dhoG1ML82+xiw
# Only needed if your LAPI is remote, like on your router
api_url http://192.168.1.1:8080
}
order crowdsec first
}
www.some.org {
crowdsec
root * /var/www/www.some.org
file_server
}
And restart. Make sure you restart as a reload will fail.
sudo systemctl restart caddy.service
#You should see something like this is your system journal
sudo journalctl | grep caddy | grep crowdsec
Feb 05 16:42:23 www caddy[10724]: {"level":"info","ts":1770309743.418141,"logger":"crowdsec","msg":"started","instance_id":"155c21c7"}
Testing Mitigation
Let’s test that probe again. Initially, you’ll get a 404 (not found) but after while of that, it should switch to 403 (access denied)
for X in {1..100}; do curl -D - --silent https://proxied.your.org/$X | grep HTTP; done
HTTP/2 404
HTTP/2 404
...
...
HTTP/2 403
HTTP/2 403
Conclusion
Congrats! after much work you’ve traded 404s for 403s. Was it worth it? Probably. If an adversary’s probe had a chance to find something, it has less of a chance now.
Bonus Spoofing Section
Back when we set up a trusted proxy for caddy we explicitly used the CF-Connecting-IP header. This is because the X-Forwarded-For header is easily spoofed. If you want to give that a test, comment out that line in your caddy config and reload.
sudo vi /etc/caddy/Caddyfile
# Comment out 'client_ip_headers CF-Connecting-IP' in the servers block
sudo systemctl reload caddy
for X in {1..100}; do curl -D - --silent "X-Forwarded-For: 192.168.0.2" https://www.some.org/$X | grep HTTP;done
HTTP/2 404
HTTP/2 404
...
...
HTTP/2 404
HTTP/2 404
No remediation happens. Turns out Cloudflare appends by default, giving you:
sudo tail -f /var/log/caddy/www.some.org.log | jq
"client_ip": "192.168.0.2",
"X-Forwarded-For": [
"192.168.0.2,109.206.128.45"
],
Caddy only uses the X-Forwarded-For when it trusts the upstream proxy, but it takes the first value, which is rather trusting but canonically correct. That’s why we use the CF-Connecting-IP header as francislavoie suggests, as it will always be replaced and not appended.
Adjusting Cloudflare
You don’t need to, but you can configure Cloudflare to “Remove visitor IP headers”. This is counterintuitive, but the notes say “…Cloudflare will only keep the IP address of the last proxy”. In testing, it keeps the last value in the X-Forwarded-For string, and that’s what we’re after. It works for normal and forged headers.
This adds an extra layer of protection - but it’s not clear if it’s worth it. It may add to the proxy time, so use at your own discretion.
- Log in to the Cloudflare dashboard and select your website
- Go to Rules > Overview
- Select “Manage Request Header Transform Rules”
- Select “Managed Transforms”
- Enable Remove visitor IP headers
The Overview page may look different depending on your plan, so you may have to hunt around for this setting.
Now when you test, you’ll get access denied regardless of your header
for X in {1..100}; do curl -D - --silent "X-Forwarded-For: 192.168.0.2" https://www.some.org/$X | grep HTTP;done
HTTP/2 404
HTTP/2 404
...
...
HTTP/2 403
HTTP/2 403
Troubleshooting
If you’re generating lots of overflows but no alerts, check your caddy logs for an error like this
level=error msg=“while pushing to api : failed sending alert to LAPI: API error: invalid character ‘\x1f’ looking for beginning of value”
This happens when your client is 1.7 but your LAPI is 1.6, as is the situation right now with OpenWrt if that’s what you’re using as the local hub. You can reverse the roles and use just a bouncer on the router, or install a true multi-server setup with a dedicated hub. You can also just copy the binaries around.
# On OpenWrt
mv /usr/bin/crowdsec /usr/bin/crowdsec.orig
mv /usr/bin/crowdsec-cli /usr/bin/crowdsec-cli
scp -T [email protected]:'/usr/bin/crowdsec /usr/bin/cscli' /usr/bin/
service crowdsec restart
3.9 - On OpenWrt
It’s better to block bad-guys at the boarder and CrowdSec on OpenWrt allows you do do that. It’s also more efficient to do it there than on every instance you want to protect.
Installation
There’s two parts; the ‘hub’ that downloads and maintains the list of baddies, and the ‘bouncer’ that checks the list every so often and updates firewall tables to block them.
OpenWRT is best served by installing just the bouncer. But if your install is very small, you can run the hub (crowdsec) there as well.
# At the OpenWrt command line
apk update
apk install crowdsec crowdsec-firewall-bouncer luci-app-crowdsec-firewall-bouncer
There will be some error messages as the packages disagree on file ownership, but this seems OK as they don’t use the .yaml files you expect.
The bouncer won’t be running after installation like it would with a normal linux install. You must register the bouncer with the hub and start it like so:
# Note the API key this command generates
cscli bouncers add openwrt
# Edit OpenWRT's config file for the service and configure the bouncer section.
vi /etc/config/crowdsec
config bouncer
option enabled '1'
option ipv4 '1'
option ipv6 '1'
option api_url 'http://localhost:8080/'
option api_key 'XXXXXXXXXXXXXXX'
option deny_action 'drop'
option deny_log '0'
option log_prefix 'crowdsec: '
option log_level 'info'
option filter_input '1'
option filter_forward '1'
list interface 'eth0'
And then restart and check.
service crowdsec restart
service crowdsec-firewall-bouncer restart
# This should now have a long list of rules at the end under "crowdsec-blacklists-CAPI"
nft list ruleset
We installed the luci-gui package that lets you access the bouncer config in the GUI at network -> firewall -> crowdsec-firewall-bouncer. Though beyond being a curiosity, I’ve not found a use for it.
Sources
https://docs.crowdsec.net/u/user_guides/multiserver_setup/
https://kroon.email/site/en/posts/2025/10/openwrt-crowdsec/
https://openwrt.org/docs/guide-user/services/crowdsec
3.10 - On Alpine
These notes are very old at this point, but I kept them in case I ever need to refer back to them.
Install
There are some packages available, but (as of 2022) they are a bit behind and don’t include the config and service files. So let’s download the latest binaries from Crowsec and create our own.
Download the current release
Note: Download the static versions. Alpine uses a differnt libc than other distros.
cd /tmp
wget https://github.com/crowdsecurity/crowdsec/releases/latest/download/crowdsec-release-static.tgz
wget https://github.com/crowdsecurity/cs-firewall-bouncer/releases/latest/download/crowdsec-firewall-bouncer.tgz
tar xzf crowdsec-firewall*
tar xzf crowdsec-release*
rm *.tgz
Install Crowdsec and Register with The Central API
You cannot use the wizard as it expects systemd and doesn’t support OpenRC. Follow the Binary Install steps from CrowdSec’s binary instrcutions.
sudo apk add bash newt envsubst
cd /tmp/crowdsec-v*
# Docker mode skips configuring systemd
sudo ./wizard.sh --docker-mode
sudo cscli hub update
sudo cscli machines add -a
sudo cscli capi register
# A collection is just a bunch of parsers and scenarios bundled together for convienence
sudo cscli collections install crowdsecurity/linux
Install The Firewall Bouncer
We need a netfilter tool so install nftables. If you already have iptables installed you can skip this step and set FW_BACKEND to that below when generating the API keys.
Now we install the firewall bouncer. There is no static build of the firewall bouncer yet from CrowdSec, but you can get one from Alpine testing (if you don’t want to compile it yourself)
# Change from 'edge' to other versions a needed
echo "http://dl-cdn.alpinelinux.org/alpine/edge/testing" >> /etc/apk/repositories
apk update
apk add cs-firewall-bouncer
Now configure the bouncer. We will once again do this manually becase there is not support for non-systemd linuxes with the install script. But cribbing from their install script, we see we can:
cd /tmp/crowdsec-firewall*
BIN_PATH_INSTALLED="/usr/local/bin/crowdsec-firewall-bouncer"
BIN_PATH="./crowdsec-firewall-bouncer"
sudo install -v -m 755 -D "${BIN_PATH}" "${BIN_PATH_INSTALLED}"
CONFIG_DIR="/etc/crowdsec/bouncers/"
sudo mkdir -p "${CONFIG_DIR}"
sudo install -m 0600 "./config/crowdsec-firewall-bouncer.yaml" "${CONFIG_DIR}crowdsec-firewall-bouncer.yaml"
Generate The API Keys
Note: If you used the APK, just do the first two lines to get the API_KEY (echo $API_KEY) and manually edit the file (vim /etc/crowdsec/bouncers/crowdsec-firewall-bouncer.yaml)
cd /tmp/crowdsec-firewall*
CONFIG_DIR="/etc/crowdsec/bouncers/"
SUFFIX=`tr -dc A-Za-z0-9 </dev/urandom | head -c 8`
API_KEY=`sudo cscli bouncers add cs-firewall-bouncer-${SUFFIX} -o raw`
FW_BACKEND="nftables"
API_KEY=${API_KEY} BACKEND=${FW_BACKEND} envsubst < ./config/crowdsec-firewall-bouncer.yaml | sudo install -m 0600 /dev/stdin "${CONFIG_DIR}crowdsec-firewall-bouncer.yaml"
Create RC Service Files
sudo touch /etc/init.d/crowdsec
sudo chmod +x /etc/init.d/crowdsec
sudo rc-update add crowdsec
sudo vim /etc/init.d/crowdsec
#!/sbin/openrc-run
command=/usr/local/bin/crowdsec
command_background=true
pidfile="/run/${RC_SVCNAME}.pid"
depend() {
need localmount
need net
}
Note: If you used the package from Alpine testing above it came with a service file. Just rc-update add cs-firewall-bouncer and skip this next step.
sudo touch /etc/init.d/cs-firewall-bouncer
sudo chmod +x /etc/init.d/cs-firewall-bouncer
sudo rc-update add cs-firewall-bouncer
sudo vim /etc/init.d/cs-firewall-bouncer
#!/sbin/openrc-run
command=/usr/local/bin/crowdsec-firewall-bouncer
command_args="-c /etc/crowdsec/bouncers/crowdsec-firewall-bouncer.yaml"
pidfile="/run/${RC_SVCNAME}.pid"
command_background=true
depend() {
after firewall
}
Start The Services and Observe The Results
Start up the services and view the logs to see that everything started properly
sudo service start crowdsec
sudo service cs-firewall-bouncer status
sudo tail /var/log/crowdsec.log
sudo tail /var/log/crowdsec-firewall-bouncer.log
# The firewall bouncer should tell you about how it's inserting decisions it got from the hub
sudo cat /var/log/crowdsec-firewall-bouncer.log
time="28-06-2022 13:10:05" level=info msg="backend type : nftables"
time="28-06-2022 13:10:05" level=info msg="nftables initiated"
time="28-06-2022 13:10:05" level=info msg="Processing new and deleted decisions . . ."
time="28-06-2022 14:35:35" level=info msg="100 decisions added"
time="28-06-2022 14:35:45" level=info msg="1150 decisions added"
...
...
# If you are curious about what it's blocking
sudo nft list table crowdsec
...