CrowdSec on CloudPanel: Mind the Log Format

[drop_cap]I[/drop_cap]’ve written before about the background radiation of the internet: the endless automated noise crawling over every reachable IP. Besides creative filtering on the edge, my preferred tooling answer to that noise is now CrowdSec: an open-source security engine that reads your logs, detects bad patterns, and drops bans straight into your firewall. Fail2ban’s bigger sibling, with a crowd-sourced blocklist to boot.
It works great on most systems, but there are a few things to know when deploying it on CloudPanel. The two important ones are:
- CloudPanel’s nginx access-log format is not the standard nginx
combinedformat. This matters especially when traffic passes through Cloudflare. - On the CrowdSec
v1.8.1Debian pragmatic arm64 build tested here, the usualmessageparser field was empty. Custom parsers need to apply directly toLine.Raw.
Here’s the full walkthrough of how you can get CrowdSec working on your CloudPanel box:
Install CrowdSec
Add the CrowdSec repository and install the agent:
curl -s https://install.crowdsec.net | sudo sh
apt install crowdsecSome CrowdSec versions offer a guided cscli setup step during or after installation. For this tutorial, leave unrelated sources such as syslog, SSH, SQL, Docker, and application logs unchecked. The nginx collection and acquisition configuration are installed explicitly below so the setup stays small and predictable.
Fixing Port Conflicts
On many CloudPanel installs, port 8080 may already be occupied, so you can avoid that conflict by moving to a different one, such as 127.0.0.1:8081.
In /etc/crowdsec/config.yaml:
listen_uri: 127.0.0.1:8081The same port needs to appear in:
/etc/crowdsec/local_api_credentials.yaml
/etc/crowdsec/bouncers/crowdsec-firewall-bouncer.yamlThe bouncer’s API URL should be:
api_url: http://127.0.0.1:8081/Restart the agent after changing the configuration:
systemctl restart crowdsec
systemctl is-active crowdsec
cscli lapi statusAt this point the agent should be running and the Local API should authenticate successfully. Next, we are going to setup Nginx parsing.
Install the Nginx Collection
Install the collection that provides the stock nginx parser and HTTP scenarios:
cscli collections install crowdsecurity/nginxThe custom parser below handles CloudPanel’s format; the collection still supplies the scenarios that decide when parsed requests should be banned.
Acquire CloudPanel Logs
CloudPanel doesn’t keep nginx in the default /var/log/nginx/. It keeps each site’s nginx logs under its home directory. To make CrowdSec aware of your site-specific logs add this to /etc/crowdsec/acquis.yaml:
filenames:
- /home/*/logs/nginx/access.log
labels:
type: nginxThis covers the NGINX bit only. You can add syslog and others, but for this tutorial we focus only on nginx since this is where the difficult parts are with CloudPanel.
The CloudPanel Parser
Stock CrowdSec’s nginx parser expects a normal combined log line:
1.2.3.4 - - [07/Sep/2026:10:00:00 +0300] "GET / HTTP/1.1" 200 123 "-" "curl/8.0"CloudPanel appends $http_x_forwarded_for:
1.2.3.4 - - [07/Sep/2026:10:00:00 +0300] "GET / HTTP/1.1" 200 123 "-" "curl/8.0" "1.2.3.4"That final field is enough to make the stock parser choke. A strict regex that anchors both ends of the line sees one extra field and drops it.
The solution? Create a local parser at:
/etc/crowdsec/parsers/s01-parse/cloudpanel-nginx.yamlThe important parts are the trailing XFF field and apply_on: Line.Raw. For this tutorial, we assume all traffic is proxied through Cloudflare, so the parser uses the Cloudflare-aware source_ip expression directly. Give the local parser a name: field. CrowdSec may ignore an unnamed parser file.
name: example/cloudpanel-nginx
onsuccess: next_stage
pattern_syntax:
CP_NOTDQUOTE: '[^"]*'
CP_USER: '[a-zA-Z0-9.@-+_%]+'
nodes:
- grok:
apply_on: Line.Raw
pattern: '(%{IPORHOST:target_fqdn}(:%{INT:port})? )?%{IPORHOST:remote_addr} - (%{CP_USER:remote_user} )?[%{HTTPDATE:time_local}] "%{WORD:verb} %{DATA:request} HTTP/%{NUMBER:http_version}" %{NUMBER:status} %{NUMBER:body_bytes_sent} "%{CP_NOTDQUOTE:http_referer}" "%{CP_NOTDQUOTE:http_user_agent}" "%{CP_NOTDQUOTE:http_x_forwarded_for}"'
statics:
- meta: log_type
value: http_access-log
- target: evt.StrTime
expression: evt.Parsed.time_local
statics:
- meta: service
value: http
- meta: source_ip
expression: >-
(IpInRange(evt.Parsed.remote_addr, '173.245.48.0/20') ||
IpInRange(evt.Parsed.remote_addr, '103.21.244.0/22') ||
IpInRange(evt.Parsed.remote_addr, '103.22.200.0/22') ||
IpInRange(evt.Parsed.remote_addr, '103.31.4.0/22') ||
IpInRange(evt.Parsed.remote_addr, '141.101.64.0/18') ||
IpInRange(evt.Parsed.remote_addr, '108.162.192.0/18') ||
IpInRange(evt.Parsed.remote_addr, '190.93.240.0/20') ||
IpInRange(evt.Parsed.remote_addr, '188.114.96.0/20') ||
IpInRange(evt.Parsed.remote_addr, '197.234.240.0/22') ||
IpInRange(evt.Parsed.remote_addr, '198.41.128.0/17') ||
IpInRange(evt.Parsed.remote_addr, '162.158.0.0/15') ||
IpInRange(evt.Parsed.remote_addr, '104.16.0.0/13') ||
IpInRange(evt.Parsed.remote_addr, '104.24.0.0/14') ||
IpInRange(evt.Parsed.remote_addr, '172.64.0.0/13') ||
IpInRange(evt.Parsed.remote_addr, '131.0.72.0/22') ||
IpInRange(evt.Parsed.remote_addr, '2400:cb00::/32') ||
IpInRange(evt.Parsed.remote_addr, '2606:4700::/32') ||
IpInRange(evt.Parsed.remote_addr, '2803:f800::/32') ||
IpInRange(evt.Parsed.remote_addr, '2405:b500::/32') ||
IpInRange(evt.Parsed.remote_addr, '2405:8100::/32') ||
IpInRange(evt.Parsed.remote_addr, '2a06:98c0::/29') ||
IpInRange(evt.Parsed.remote_addr, '2c0f:f248::/32')) &&
evt.Parsed.http_x_forwarded_for matches '^[0-9a-fA-F.:]{7,45}