⚠️ Warning: This role modifies firewall rules and security services. Misconfiguration can lock you out of your server. Ensure you have out-of-band access (console, KVM, recovery mode) before deploying. Test on staging first. The author assumes no responsibility for lost access, downtime, or any damages resulting from use of this role.
Ansible role to install and configure CrowdSec Security Engine for Trellis WordPress deployments.
Replaces fail2ban and ferm with a modern, community-driven intrusion detection and prevention system.
| fail2ban + ferm | CrowdSec | |
|---|---|---|
| Threat intelligence | Local only - learns from attacks on your server | Community blocklists - benefit from attacks seen across 200k+ servers |
| Detection | Regex-based jail configs | Behavioral analysis with parsers and scenarios |
| Firewall | iptables (legacy) | nftables (modern, faster, better syntax) |
| False positives | Common with aggressive configs | Reputation-aware, whitelists good actors (Googlebot, etc.) |
| Updates | Manual filter maintenance | Auto-updated Hub with community scenarios |
| Dashboard | CLI only | Free web console for multi-server monitoring |
- Ansible 2.10+
- Ubuntu 20.04/22.04/24.04 or Debian 11/12
- Trellis-based WordPress deployment (or compatible nginx setup)
# galaxy.yml
roles:
- name: trellis-crowdsec
src: https://github.com/AltanS/trellis-crowdsec
version: v0.3.0 # or 'main' for latestThen install:
ansible-galaxy install -r galaxy.yml# server.yml
roles:
- { role: common, tags: [common] }
- { role: trellis-crowdsec, tags: [crowdsec, security] }
# Comment out legacy roles:
# - { role: fail2ban, tags: [fail2ban] }
# - { role: ferm, tags: [ferm] }When you run provisioning, this role automatically handles the migration:
-
Stops and disables fail2ban - If
/etc/fail2banexists, the fail2ban service is stopped and disabled. The package is not removed. -
Stops and disables ferm - If
/etc/fermexists, the ferm service is stopped and disabled. The package is not removed. -
Installs CrowdSec - Adds the official CrowdSec repository and installs the Security Engine.
-
Installs nftables firewall bouncer - Registers with the CrowdSec LAPI to enforce blocking decisions.
-
Configures log acquisition - Points CrowdSec at Trellis log paths (
/srv/www/*/logs/*.log). -
Installs Hub collections - WordPress, nginx, SSH, and HTTP security scenarios.
Note: iptables rules from ferm remain active until you explicitly flush them (see below).
ansible-playbook server.yml -e env=production --tags=crowdsec# SSH into server
sudo cscli metrics # Should show log processing
sudo cscli decisions list # Active bans
sudo cscli bouncers list # Should show firewall-bouncerAfter confirming CrowdSec is working, you can flush the old iptables rules from ferm:
# group_vars/all/security.yml
crowdsec_flush_legacy_rules: trueThen re-provision. This runs iptables -F to clear legacy rules. Only do this after confirming CrowdSec's nftables bouncer is active.
| Variable | Default | Description |
|---|---|---|
crowdsec_disable_legacy |
true |
Stop and disable fail2ban/ferm services |
crowdsec_flush_legacy_rules |
false |
Flush iptables rules (run iptables -F) |
To remove CrowdSec and restore fail2ban/ferm:
roles:
- { role: common, tags: [common] }
# - { role: trellis-crowdsec, tags: [crowdsec, security] }
- { role: fail2ban, tags: [fail2ban] }
- { role: ferm, tags: [ferm] }# SSH into your server
sudo systemctl stop crowdsec crowdsec-firewall-bouncer
sudo systemctl disable crowdsec crowdsec-firewall-bouncer
sudo apt purge crowdsec crowdsec-firewall-bouncer-nftables
sudo nft flush ruleset # Clear nftables rulesansible-playbook server.yml -e env=production --tags=fail2ban,fermsudo fail2ban-client status
sudo ferm --check /etc/ferm/ferm.conf- Installs CrowdSec Security Engine and nftables Firewall Bouncer
- Trellis-aware log acquisition (per-site nginx logs)
- Automatic migration from fail2ban/ferm
- IP whitelists (integrates with Trellis
ip_whitelist) - Import existing blocked IPs as CrowdSec decisions
- WordPress-specific attack detection via Hub collections
- Built-in scenarios for common attack patterns (enabled by default)
- Custom scenario support
Zero configuration required - just add the role to server.yml and run provisioning. No changes to group_vars needed.
Out of the box, you get:
- WordPress, nginx, and SSH protection via CrowdSec Hub collections
- Eleven built-in scenarios: actuator probes, debug fuzzing, scanner user agents, SSRF callbacks, encoded attack payloads, XSS patterns, cache-buster bot detection, open redirect fuzzing, parameter stuffing, aggressive crawl detection, and sustained crawl detection
- Trellis log paths (
/srv/www/*/logs/*.log) - nftables firewall bouncer
- Localhost whitelisted (
127.0.0.0/8)
Add to group_vars/all/security.yml only if you need to customize:
# Whitelist additional IPs (extends the default 127.0.0.0/8)
ip_whitelist:
- 127.0.0.0/8
- 203.0.113.50 # Office IP
- 198.51.100.0/24 # VPN range
# Block specific IPs permanently
ip_blocklist:
- 192.0.2.100
- 198.51.100.50
# Enroll in CrowdSec Console for centralized dashboard
crowdsec_console_token: "your-enrollment-token"
# Disable CrowdSec without removing the role
crowdsec_enabled: falseCrowdSec Hub provides community-maintained collections that bundle parsers and scenarios for specific applications. This role installs a curated set by default.
Default collections:
| Collection | Description |
|---|---|
crowdsecurity/linux |
Linux system log parsing |
crowdsecurity/nginx |
Nginx access/error log parsing |
crowdsecurity/sshd |
SSH brute-force detection |
crowdsecurity/base-http-scenarios |
HTTP probing, crawling, bad user-agents |
crowdsecurity/http-cve |
Known CVE exploitation attempts |
crowdsecurity/wordpress |
WordPress-specific attacks (xmlrpc, wp-login brute-force) |
crowdsecurity/whitelist-good-actors |
Whitelist Googlebot, Bingbot, etc. |
Adding community collections:
Browse available collections at CrowdSec Hub. To add collections, override crowdsec_collections in group_vars/all/security.yml:
# Add to your existing defaults
crowdsec_collections:
# Default collections (keep these)
- crowdsecurity/linux
- crowdsecurity/nginx
- crowdsecurity/sshd
- crowdsecurity/base-http-scenarios
- crowdsecurity/http-cve
- crowdsecurity/wordpress
- crowdsecurity/whitelist-good-actors
# Additional collections
- crowdsecurity/postfix # Mail server protection
- crowdsecurity/mysql # MySQL/MariaDB protection
- crowdsecurity/iptables # If using iptables logsRemoving collections:
To remove a default collection, override the list without it:
crowdsec_collections:
- crowdsecurity/linux
- crowdsecurity/nginx
- crowdsecurity/sshd
- crowdsecurity/base-http-scenarios
- crowdsecurity/http-cve
# Removed: crowdsecurity/wordpress (not using WordPress)
- crowdsecurity/whitelist-good-actorsAdding individual scenarios or parsers:
If you only need specific scenarios/parsers (not full collections):
# Add individual scenarios
crowdsec_scenarios:
- crowdsecurity/http-sqli # SQL injection detection
# Add individual parsers
crowdsec_parsers:
- crowdsecurity/geoip-enrich # Add geolocation to alertsEnroll in CrowdSec Console for a centralized dashboard to monitor all your servers.
Port fail2ban filters or add custom detection:
crowdsec_custom_scenarios:
- name: wordpress-admin-exploit
description: "Detect WordPress admin exploitation"
type: leaky
filter: "evt.Meta.http_path contains '/wp-login.php' and evt.Meta.http_args contains 'lostpassword'"
groupby: evt.Meta.source_ip
capacity: 1
leakspeed: 24h
blackhole: 24h
labels:
service: wordpress
type: exploit
remediation: trueThe http-probing scenario bans IPs that hit 10+ paths returning 404/403/400. This causes false positives on WordPress REST APIs where 404 means "resource not found" (normal behavior).
Option 1: Exclude 404s (Recommended)
Keep probing detection but exclude 404 responses:
# Detect probing on 403/400 only, ignore 404s
crowdsec_http_probing_exclude_404: trueOption 2: Disable http-probing entirely
crowdsec_scenarios_remove:
- crowdsecurity/http-probingThis role includes eleven built-in scenarios that are enabled by default to protect against common attack patterns. All parameters are configurable.
| Parameter | Description |
|---|---|
capacity |
Number of events allowed before triggering (bucket size). Lower = more sensitive. |
leakspeed |
How fast the bucket drains. 1m = 1 event drains per minute. Faster = more tolerant. |
blackhole |
Cooldown after triggering before the scenario can fire again for the same IP. |
ban |
How long the IP is banned when the scenario triggers. |
Detects probing for Spring Boot actuator endpoints (/actuator, /heapdump) which expose sensitive application data.
| Variable | Default | Description |
|---|---|---|
crowdsec_scenario_actuator_probe |
true |
Enable/disable scenario |
crowdsec_scenario_actuator_probe_capacity |
3 |
Requests before triggering |
crowdsec_scenario_actuator_probe_leakspeed |
1m |
Drain rate |
crowdsec_scenario_actuator_probe_blackhole |
5m |
Cooldown between alerts |
crowdsec_scenario_actuator_probe_ban |
24h |
Ban duration |
Detects probing for debug endpoints and error parameters (/debug, ?error=, ?stacktrace=, ?exception=).
| Variable | Default | Description |
|---|---|---|
crowdsec_scenario_debug_fuzzing |
true |
Enable/disable scenario |
crowdsec_scenario_debug_fuzzing_capacity |
5 |
Requests before triggering |
crowdsec_scenario_debug_fuzzing_leakspeed |
30s |
Drain rate |
crowdsec_scenario_debug_fuzzing_blackhole |
10m |
Cooldown between alerts |
crowdsec_scenario_debug_fuzzing_ban |
12h |
Ban duration |
Supplements the Hub's http-bad-user-agent scenario (500+ auto-updated patterns) with immediate blocking for the most egregious scanner user agents. The Hub scenario triggers after 2 requests; this triggers immediately on first match.
Detected agents: ffuf, sqlmap, nikto, nuclei, gobuster, dirbuster, wpscan, Team Anon Force, Mozlila, Moz111a, depconf_deep_scanner, getodin.com, cypex.ai/scanning, onlyscans.com
| Variable | Default | Description |
|---|---|---|
crowdsec_scenario_custom_bad_user_agent |
true |
Enable/disable scenario |
crowdsec_scenario_custom_bad_user_agent_ban |
168h |
Ban duration (7 days) |
Detects Server-Side Request Forgery (SSRF) attempts using callback domains to exfiltrate data or confirm vulnerabilities. These domains are used by security testing tools for out-of-band detection.
Detected domains: burpcollaborator.net, oastify.com, interact.sh, canarytokens.com, requestbin.net, webhook.site, evil.com, .oast.
| Variable | Default | Description |
|---|---|---|
crowdsec_scenario_ssrf_callback |
true |
Enable/disable scenario |
crowdsec_scenario_ssrf_callback_ban |
168h |
Ban duration (7 days) |
Detects double-encoded attack characters and scanner quote probe patterns in URLs. These encoding techniques (%253C = double-encoded <, %27%22 = URL-encoded '") are used to evade WAF/IDS pattern matching. Legitimate traffic never uses these patterns.
| Variable | Default | Description |
|---|---|---|
crowdsec_scenario_encoded_attack_payload |
true |
Enable/disable scenario |
crowdsec_scenario_encoded_attack_payload_ban |
168h |
Ban duration (7 days) |
Supplements the Hub's http-xss-probbing scenario with XSS patterns it does not cover: event handler attributes (onerror=, onload=, onfocus=, onmouseover=) and DOM property access (document.cookie, document.domain).
| Variable | Default | Description |
|---|---|---|
crowdsec_scenario_xss_extended |
true |
Enable/disable scenario |
crowdsec_scenario_xss_extended_capacity |
5 |
Unique payloads before triggering |
crowdsec_scenario_xss_extended_leakspeed |
30s |
Drain rate |
crowdsec_scenario_xss_extended_blackhole |
5m |
Cooldown between alerts |
crowdsec_scenario_xss_extended_ban |
4h |
Ban duration |
Detects bot networks using cache-busting query parameters to probe WordPress sites. Matches requests where the entire query string is a single parameter with a 2-8 character alphanumeric key and a 4+ digit numeric value (e.g. ?cb=12345, ?ddd=67890). Excludes static resources and WordPress parameters with underscores.
| Variable | Default | Description |
|---|---|---|
crowdsec_scenario_cache_buster_probe |
true |
Enable/disable scenario |
crowdsec_scenario_cache_buster_probe_capacity |
8 |
Requests before triggering |
crowdsec_scenario_cache_buster_probe_leakspeed |
2m |
Drain rate |
crowdsec_scenario_cache_buster_probe_blackhole |
5m |
Cooldown between alerts |
crowdsec_scenario_cache_buster_probe_ban |
168h |
Ban duration (7 days) |
Detects redirect parameter fuzzing -- bots trying many different redirect-like parameter names (redirect=, goto=, next=, returnTo=, etc.) with external URL targets. Requires both a redirect parameter name AND an external URL value (http://, https://). WordPress internal redirects (?redirect_to=/my-account/) do not match. Uses distinct counting so only IPs sending many unique query string combinations trigger.
| Variable | Default | Description |
|---|---|---|
crowdsec_scenario_open_redirect_probe |
true |
Enable/disable scenario |
crowdsec_scenario_open_redirect_probe_capacity |
5 |
Unique query strings before triggering |
crowdsec_scenario_open_redirect_probe_leakspeed |
30s |
Drain rate |
crowdsec_scenario_open_redirect_probe_blackhole |
5m |
Cooldown between alerts |
crowdsec_scenario_open_redirect_probe_ban |
168h |
Ban duration (7 days) |
Detects requests with 20+ query parameters, a strong indicator of parameter fuzzing. No legitimate WordPress/WooCommerce request sends 20+ parameters in the query string. WordPress bulk admin operations use POST body.
| Variable | Default | Description |
|---|---|---|
crowdsec_scenario_param_stuffing |
true |
Enable/disable scenario |
crowdsec_scenario_param_stuffing_ban |
168h |
Ban duration (7 days) |
WordPress-compatible replacement for the Hub's http-crawl-non_statics scenario, which is broken on WordPress sites using pretty permalinks. The Hub scenario uses distinct: "evt.Parsed.file_name", but WordPress URLs end with / (e.g. /bonus/, /tipps/match-prognose/), making file_name always empty and collapsing all requests into one bucket entry. This scenario uses distinct: "evt.Meta.http_path" instead, counting each unique URL path separately. Catches fast scrapers (~9 req/sec) in ~5 seconds and slow persistent scrapers (~0.5 req/sec) in ~2 minutes. Excludes Ahrefs crawlers.
| Variable | Default | Description |
|---|---|---|
crowdsec_scenario_aggressive_crawl |
true |
Enable/disable scenario |
crowdsec_scenario_aggressive_crawl_capacity |
40 |
Unique paths before triggering |
crowdsec_scenario_aggressive_crawl_leakspeed |
5s |
Drain rate |
crowdsec_scenario_aggressive_crawl_blackhole |
5m |
Cooldown between alerts |
crowdsec_scenario_aggressive_crawl_ban |
168h |
Ban duration (7 days) |
Complements aggressive-crawl by catching slow-but-persistent scrapers that evade unique-path-based detection. Uses raw request count (no distinct filter) with higher capacity and slower drain. Targets coordinated scraper clusters using multiple IPs at 25-40 non-static requests/min each. A legitimate user at 10 pages/min would need 28+ minutes sustained to trigger. Excludes Ahrefs crawlers.
| Variable | Default | Description |
|---|---|---|
crowdsec_scenario_sustained_crawl |
true |
Enable/disable scenario |
crowdsec_scenario_sustained_crawl_capacity |
120 |
Requests before triggering |
crowdsec_scenario_sustained_crawl_leakspeed |
10s |
Drain rate |
crowdsec_scenario_sustained_crawl_blackhole |
5m |
Cooldown between alerts |
crowdsec_scenario_sustained_crawl_ban |
168h |
Ban duration (7 days) |
# Disable specific built-in scenarios
crowdsec_scenario_actuator_probe: false
crowdsec_scenario_debug_fuzzing: false
crowdsec_scenario_custom_bad_user_agent: false
crowdsec_scenario_ssrf_callback: false
crowdsec_scenario_encoded_attack_payload: false
crowdsec_scenario_xss_extended: false
crowdsec_scenario_cache_buster_probe: false
crowdsec_scenario_open_redirect_probe: false
crowdsec_scenario_param_stuffing: false
crowdsec_scenario_aggressive_crawl: false
crowdsec_scenario_sustained_crawl: falseBan durations are configured via CrowdSec profiles. This role deploys a custom profiles.yaml with sensible defaults.
# Default ban duration for CrowdSec Hub scenarios (ssh-bf, http-probing, etc.)
crowdsec_ban_duration_default: "24h"
# Override built-in scenario ban durations
crowdsec_scenario_actuator_probe_ban: "24h"
crowdsec_scenario_debug_fuzzing_ban: "12h"
crowdsec_scenario_custom_bad_user_agent_ban: "168h" # 7 days
crowdsec_scenario_ssrf_callback_ban: "168h" # 7 days
crowdsec_scenario_encoded_attack_payload_ban: "168h" # 7 days
crowdsec_scenario_xss_extended_ban: "4h"
crowdsec_scenario_cache_buster_probe_ban: "168h" # 7 days
crowdsec_scenario_open_redirect_probe_ban: "168h" # 7 days
crowdsec_scenario_param_stuffing_ban: "168h" # 7 days
crowdsec_scenario_aggressive_crawl_ban: "168h" # 7 days
crowdsec_scenario_sustained_crawl_ban: "168h" # 7 daysDuration format: 30m (minutes), 4h (hours), 7d (days)
| Variable | Default | Description |
|---|---|---|
ip_whitelist |
[127.0.0.0/8] |
IPs to never block |
ip_blocklist |
[] |
IPs to permanently block |
| Variable | Default | Description |
|---|---|---|
crowdsec_enabled |
true |
Master switch |
crowdsec_disable_legacy |
true |
Stop fail2ban/ferm |
crowdsec_flush_legacy_rules |
false |
Flush iptables (use with caution) |
crowdsec_collections |
See defaults | Hub collections to install |
crowdsec_parsers |
[] |
Additional parsers |
crowdsec_scenarios |
[] |
Additional scenarios |
crowdsec_scenarios_remove |
[] |
Scenarios to remove |
crowdsec_http_probing_exclude_404 |
false |
Exclude 404s from http-probing |
crowdsec_ban_duration_default |
24h |
Default ban for Hub scenarios |
crowdsec_acquisition |
Trellis paths | Log file acquisition |
crowdsec_firewall_bouncer_enabled |
true |
Install firewall bouncer |
crowdsec_ip_blocklist_duration |
87600h |
Block duration (10 years) |
crowdsec_console_token |
"" |
Console enrollment token |
crowdsec_custom_scenarios |
[] |
Custom scenario definitions |
# Service status
cscli metrics
cscli alerts list
cscli decisions list
# Check log acquisition
cscli metrics show acquisition
# View active bans
cscli decisions list --type ban
# Manually ban an IP
cscli decisions add --ip 1.2.3.4 --duration 24h --reason "Manual ban"
# Unban an IP
cscli decisions delete --ip 1.2.3.4
# Unban all IPs banned by a specific scenario
cscli decisions delete --scenario crowdsecurity/http-probing
# Check collections
cscli collections list
cscli hub listMIT