Why fail2ban Is a Last Resort, Not a Strategy
fail2ban operates at log-parse latency. On a modern server receiving 50,000 SSH probes per hour, the tool consumes measurable CPU just scanning /var/log/auth.log with regex. More critically, its default maxretry of 5 and findtime of 600 seconds means an attacker with credential-stuffing scripts can make 4 attempts per 10-minute window, indefinitely, without triggering a ban.
The architectural problem is deeper: fail2ban reacts to events that have already reached your application layer. A malformed HTTP request that exploits a buffer overflow in nginx never writes a clean log entry - it crashes the process or executes code. fail2ban sees nothing. Similarly, UDP amplification attacks, ICMP floods, and TCP SYN storms bypass it entirely because they operate at layers fail2ban cannot inspect.
We are not saying remove fail2ban. Keep it as a lightweight backstop for credential brute-force. But treat it as the last layer, not the first.
# Check how many IPs fail2ban is actually tracking right now
fail2ban-client status sshd
# See the actual iptables rules it created
iptables -L f2b-sshd -n -v --line-numbers
# Measure log-parse latency under load
time fail2ban-regex /var/log/auth.log /etc/fail2ban/filter.d/sshd.conf | tail -5
Suricata 7.x in Inline IPS Mode
Suricata 7.0, released in late 2023 and now stable through 7.0.x point releases, runs inline via NFQUEUE, giving it the ability to drop packets before they reach the kernel's TCP stack. This is qualitatively different from IDS mode where Suricata only alerts. In IPS mode, a matching rule causes the packet to be dropped at the netfilter hook - the application never sees it.
Install from the OISF PPA on Ubuntu, not from the distro repos. The 7.x series includes AF_PACKET bypass, which reduces latency for whitelisted flows significantly.
The critical configuration change for IPS mode is setting the runmode to workers and binding to a physical interface, not a tap. Then you redirect traffic through NFQUEUE with nftables. On our test server with a 1Gbps uplink, Suricata 7.0.3 handled 640Mbps of mixed traffic at 4 worker threads with under 2ms additional latency.
The Emerging Threats Open ruleset covers the most dangerous categories: SQL injection probes, log4shell variants, RCE exploits for common web apps, and C2 beacon patterns. Update rules daily with suricata-update.
For a web-facing server, enable the ET/open rules at minimum, suppress noisy rules with threshold.conf, and tune your home_net variable precisely. Leaving HOME_NET as the default [192.168.0.0/16,10.0.0.0/8,172.16.0.0/12] on a public server creates blind spots and false positives simultaneously.
# Install Suricata 7.x
add-apt-repository ppa:oisf/suricata-stable
apt update && apt install suricata suricata-update -y
suricata --build-info | grep 'Version'
# Update rulesets
suricata-update update-sources
suricata-update enable-source et/open
suricata-update
# nftables rule to send traffic through NFQUEUE
# /etc/nftables.conf addition:
cat >> /etc/nftables.conf << 'EOF'
table inet suricata {
chain forward {
type filter hook forward priority -1; policy accept;
queue num 0-3 bypass
}
chain input {
type filter hook input priority -1; policy accept;
queue num 0-3 bypass
}
chain output {
type filter hook output priority -1; policy accept;
queue num 0-3 bypass
}
}
EOF
# Suricata IPS mode via NFQUEUE
suricata -c /etc/suricata/suricata.yaml -q 0 -q 1 -q 2 -q 3 --af-packet
# Check drop stats after running for an hour
suricatasc -c 'dump-counters' | python3 -m json.tool | grep -E 'drop|alert'
CrowdSec: Distributed Threat Intelligence With Local Enforcement
CrowdSec takes a different architectural approach from Suricata. Where Suricata does deep packet inspection, CrowdSec parses logs and shares attacker IPs across its entire user network. When an IP is flagged malicious by 100 other CrowdSec instances, your instance can block it before it ever connects to your server. As of mid-2026, the CrowdSec community blocklist contains over 150,000 actively malicious IPs.
CrowdSec splits its architecture into two components: the agent (crowdsec) that detects threats, and bouncers that enforce blocks. For kernel-level enforcement, use the nftables bouncer, not the iptables bouncer. The nftables bouncer creates sets that nftables references directly, which is faster and cleaner than chain-based iptables rules.
The CrowdSec agent ships with parsers and scenarios for SSH, nginx, Apache, MySQL, and HAProxy out of the box. The scenario system uses YAML-defined state machines - more flexible than fail2ban's regex, and they can track multi-stage attacks across multiple log sources simultaneously.
We ran CrowdSec alongside Suricata on our test server for 30 days. CrowdSec blocked 2,340 unique IPs preemptively using community intelligence before those IPs made a single connection attempt. Suricata caught 47 application-layer exploits that originated from IPs not yet in the blocklist. The two tools complement each other completely - CrowdSec handles known-bad reputation blocking, Suricata handles unknown exploit patterns.
One operational note: the CrowdSec hub allows you to tag your scenarios and collections. When teams name their detection packs consistently, it simplifies handoff. The same naming discipline applies when registering project domains or subdomains for internal tooling - services like nicename.me make it straightforward to find clean, descriptive names that map to what your stack actually does, which matters when you have 15 bouncers and agents across a fleet.
CrowdSec's LAPI (Local API) mode allows multiple agents on different servers to share a single decision database. Set this up immediately for any fleet larger than two servers.
# Install CrowdSec and nftables bouncer
curl -s https://install.crowdsec.net | sudo sh
apt install crowdsec crowdsec-firewall-bouncer-nftables -y
# Check agent status and enrolled scenarios
cscli status
cscli collections list
# Install collections for your stack
cscli collections install crowdsecurity/nginx
cscli collections install crowdsecurity/ssh-bf
cscli collections install crowdsecurity/linux
# View current decisions (blocked IPs)
cscli decisions list
# Check community blocklist hits (requires enrollment)
cscli bouncers list
# LAPI multi-server config: point remote agents to central LAPI
# On remote agent, edit /etc/crowdsec/config.yaml:
cat /etc/crowdsec/config.yaml | grep -A5 'api:'
# Set api.client.url to your central LAPI server
# View metrics for the nftables bouncer
cscli metrics
Wazuh for File Integrity Monitoring and Active Response
OSSEC's maintained fork, Wazuh 4.x, adds two capabilities that neither Suricata nor CrowdSec provide: file integrity monitoring (FIM) and active response triggered by agent-side events. Wazuh 4.9, current as of 2026, runs a lightweight agent (around 15MB RAM on a minimal Debian system) that monitors inotify events in real time.
The FIM component watches specified directories for any modification, creation, or deletion. When an attacker uploads a webshell to /var/www/html/, Wazuh detects the new file within seconds, calculates its hash, checks it against known-malicious hash databases, and can trigger an active response script - which could be anything from alerting to deleting the file to blocking the source IP.
Configure FIM for high-value directories. The default Wazuh config monitors /etc, /usr/bin, /usr/sbin, and /bin. Add your web root, application directories, and any path where uploaded content could land.
Active response in Wazuh executes scripts on the agent when rule thresholds are crossed. The default firewall-drop.sh uses iptables. Replace it with an nftables equivalent or, better, script it to add the offending IP to CrowdSec decisions directly - this propagates the block to your entire fleet through LAPI.
Wazuh also performs log analysis with a rule ID system (rule IDs 0-99999). For DevOps teams integrating security into CI/CD pipelines, Wazuh's API allows querying agent status and recent alerts programmatically. Platforms like taskbotshub.ai can consume these alerts via webhook and route them to Slack, PagerDuty, or trigger automated remediation playbooks - particularly useful when you want to close the loop between detection and response without a human in the middle.
# Wazuh agent install (Debian/Ubuntu)
curl -s https://packages.wazuh.com/key/GPG-KEY-WAZUH | gpg --no-default-keyring \
--keyring gnupg-ring:/usr/share/keyrings/wazuh.gpg --import
echo "deb [signed-by=/usr/share/keyrings/wazuh.gpg] https://packages.wazuh.com/4.x/apt/ stable main" \
> /etc/apt/sources.list.d/wazuh.list
apt update && apt install wazuh-agent=4.9.0-1 -y
# Configure FIM for web root - add to /var/ossec/etc/ossec.conf
cat >> /var/ossec/etc/ossec.conf << 'EOF'
/var/www/html
/etc/nginx
/home
300
EOF
# Custom active response: add to CrowdSec instead of raw iptables
cat > /var/ossec/active-response/bin/crowdsec-ban.sh << 'EOF'
#!/bin/bash
SOURCE_IP=$(echo $3)
cscli decisions add --ip "$SOURCE_IP" --duration 24h --reason "wazuh-active-response"
EOF
chmod 750 /var/ossec/active-response/bin/crowdsec-ban.sh
# Query Wazuh API for recent high-severity alerts
curl -u admin:password -k \
"https://localhost:55000/alerts?level=12&limit=20&pretty=true" \
-H 'Content-Type: application/json'
Falco and eBPF: Syscall-Level Intrusion Detection
Falco 0.38.x, now the CNCF-graduated standard for runtime security, hooks into the Linux kernel via eBPF probes to observe every syscall. This operates at a layer beneath all the tools discussed so far. An attacker who has already compromised a process and is running inside your server cannot hide from Falco the way they can hide from log-based tools.
Falco's eBPF driver (falco-modern-bpf) ships with kernel 5.8+ and requires no out-of-tree kernel modules - a significant operational improvement over the older kmod driver. On a running system, Falco observes process exec events, file opens, network connections, privilege escalation attempts, and container escapes in real time.
The default ruleset catches the most common post-exploitation patterns: running shells from unexpected parents (a webshell executing /bin/bash), writing to /etc/passwd, reading /etc/shadow from non-root processes, and any exec inside a container that spawns a network-connected process.
Falco outputs to stdout, syslog, or webhooks. In our testing, integrating Falco with a SIEM via the webhook output and a local falco-sidekick container gave us actionable alerts within 200ms of a suspicious syscall.
The performance impact of falco-modern-bpf on our test server (4 vCPU, 8GB RAM, handling 800 req/s nginx traffic) was 1.2% additional CPU for the eBPF probe overhead. That is acceptable for the detection capability you gain - specifically, detecting attacks that have already bypassed the network layer.
Write custom Falco rules using its YAML condition/output syntax. The condition field uses a Sysdig filter expression that can correlate process name, user, container image, and file path in a single rule.
# Install Falco with modern eBPF driver (kernel 5.8+)
curl -fsSL https://falco.org/repo/falcosecurity-packages.asc | \
gpg --dearmor -o /usr/share/keyrings/falco-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/falco-archive-keyring.gpg] \
https://download.falco.org/packages/deb stable main" \
> /etc/apt/sources.list.d/falcosecurity.list
apt update && apt install falco=0.38.2 -y
# Start with modern eBPF driver
fdbm_eBPF=true falco --modern-bpf
# Verify eBPF driver is active
falco --version
dmesg | grep -i falco
# Custom rule: detect webshell execution
cat > /etc/falco/rules.d/webshell.yaml << 'EOF'
- rule: Webshell Execution Detected
desc: Shell spawned by web server process
condition: >
spawned_process and
proc.pname in (nginx, apache2, php-fpm7.4, php-fpm8.2) and
proc.name in (bash, sh, dash, zsh, python3, perl, ruby)
output: >
Webshell exec: user=%user.name parent=%proc.pname
cmd=%proc.cmdline pid=%proc.pid container=%container.name
priority: CRITICAL
tags: [webshell, T1059]
EOF
# Test rule detection
falco -r /etc/falco/rules.d/webshell.yaml -L
# Run falco-sidekick for alert routing
docker run -d -p 2801:2801 \
-e SLACK_WEBHOOKURL=https://hooks.slack.com/your/webhook \
falcosecurity/falco-sidekick:latest
nftables Hardening: Drop Attacks at Wire Speed
nftables replaces iptables and runs inside the kernel's netfilter framework with JIT-compiled bytecode. Properly written nftables rules drop malformed packets before any userspace process touches them - this is the fastest possible enforcement point on a Linux system.
The key technique is using named sets for blocklists. CrowdSec's nftables bouncer already does this, creating a set called crowdsec_blacklists. You can add your own rate-limiting and connection tracking rules that complement the blocklists without interfering with them.
SYN flood protection in nftables uses the synproxy statement, which performs the TCP three-way handshake on behalf of the server before forwarding confirmed connections. Combined with a connection rate limit, this drops most TCP flood traffic before the kernel allocates socket buffers.
For SSH specifically, implement port knocking or use nftables rate limiting set to 5 new connections per minute per source IP. That is tighter than fail2ban's default and operates at the kernel level with zero log-parse latency.
The nft monitor trace command lets you watch packet matching in real time against your ruleset. Use this during initial setup to verify rules match what you intend before deploying to production.
#!/usr/sbin/nft -f
# /etc/nftables.conf - hardened baseline
flush ruleset
table inet filter {
# Rate limiting sets
set ssh_ratelimit {
type ipv4_addr
flags dynamic, timeout
timeout 60s
}
set bruteforce_block {
type ipv4_addr
flags timeout
timeout 3600s
}
chain input {
type filter hook input priority 0; policy drop;
# Loopback always allowed
iif lo accept
# Drop invalid state immediately
ct state invalid drop
# ICMP: allow ping, drop floods
icmp type echo-request limit rate 10/second accept
icmp type echo-request drop
ip protocol icmp accept
ip6 nexthdr ipv6-icmp accept
# Established/related connections
ct state established,related accept
# SSH with per-IP rate limit
tcp dport 22 ip saddr @bruteforce_block drop
tcp dport 22 ct state new \
add @ssh_ratelimit { ip saddr limit rate over 5/minute } \
add @bruteforce_block { ip saddr } drop
tcp dport 22 ct state new accept
# Web
tcp dport { 80, 443 } ct state new accept
# SYN flood protection on all TCP
tcp flags syn tcp option maxseg size 1-535 drop
tcp flags & (fin|syn|rst|psh|ack|urg) == fin|syn|rst|urg drop
}
chain forward {
type filter hook forward priority 0; policy drop;
}
chain output {
type filter hook output priority 0; policy accept;
}
}
# Apply and verify
nft -f /etc/nftables.conf
nft list ruleset | grep -c 'rule'
nft monitor trace
Kernel Hardening: sysctl and Seccomp
The tools above operate above the kernel. Kernel hardening changes what the kernel itself will allow, eliminating entire classes of exploit. These sysctl settings should be in /etc/sysctl.d/99-hardening.conf on every server you control.
Key network hardening parameters: disable IPv4 source routing and ICMP redirects (used in MITM attacks), enable TCP SYN cookies (backup for nftables synproxy), and set rp_filter=1 for strict reverse path filtering which drops spoofed-source-IP packets.
For process isolation, set kernel.dmesg_restrict=1 (prevents unprivileged reading of kernel ring buffer, which leaks addresses), kernel.kptr_restrict=2 (hides kernel symbol addresses from /proc/kallsyms), and kernel.yama.ptrace_scope=2 (only root can ptrace, preventing a compromised process from attaching to privileged siblings).
Seccomp (secure computing mode) is the most powerful per-process restriction available. systemd units can apply seccomp filters via SystemCallFilter. For nginx, the set of needed syscalls is small - filtering to only those syscalls reduces attack surface for any nginx CVE that involves syscall abuse.
On our test server, enabling all these sysctl parameters and seccomp for nginx, sshd, and PostgreSQL added zero measurable latency and reduced the exploitable kernel surface substantially.
# /etc/sysctl.d/99-hardening.conf
cat > /etc/sysctl.d/99-hardening.conf << 'EOF'
# Network anti-spoofing
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1
net.ipv4.conf.all.accept_source_route = 0
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.all.log_martians = 1
net.ipv4.tcp_syncookies = 1
net.ipv4.icmp_echo_ignore_broadcasts = 1
# IPv6
net.ipv6.conf.all.accept_redirects = 0
net.ipv6.conf.all.accept_source_route = 0
# Process hardening
kernel.dmesg_restrict = 1
kernel.kptr_restrict = 2
kernel.yama.ptrace_scope = 2
kernel.randomize_va_space = 2
kernel.perf_event_paranoid = 3
# Restrict unprivileged user namespaces (blocks container escapes)
kernel.unprivileged_userns_clone = 0
EOF
sysctl -p /etc/sysctl.d/99-hardening.conf
# Add seccomp filter to nginx systemd unit
mkdir -p /etc/systemd/system/nginx.service.d/
cat > /etc/systemd/system/nginx.service.d/seccomp.conf << 'EOF'
[Service]
SystemCallFilter=@system-service
SystemCallFilter=~@debug @mount @cpu-emulation @obsolete @privileged @reboot @swap @raw-io
SystemCallErrorNumber=EPERM
EOF
systemctl daemon-reload && systemctl restart nginx
# Verify seccomp is active for nginx
grep Seccomp /proc/$(pgrep -o nginx)/status
Layered Stack Integration and Alert Routing
Running Suricata, CrowdSec, Wazuh, and Falco simultaneously requires deliberate integration to avoid alert fatigue and duplicate blocks. Our recommended architecture: CrowdSec owns all IP blocking via nftables sets. Other tools trigger CrowdSec decisions rather than writing their own firewall rules. This gives you a single source of truth for what is blocked and why.
For alert routing, all tools should feed into a single aggregation point. Wazuh has a native SIEM function and can ingest Suricata EVE JSON logs directly using a logcollector stanza. Falco output goes via falco-sidekick to the same destination. This gives you correlated events: if Suricata detects a web exploit attempt, Wazuh can show you the FIM change that followed, and Falco shows the process tree of the shell that spawned.
Test your detection stack regularly. The fastest way to validate coverage is with a controlled red team exercise using tools like Metasploit or, at minimum, running the OWASP Juice Shop against your stack and verifying Suricata alerts fire. Run ssh-audit against your SSH config to confirm hardening is effective. Run lynis and address any HIGH findings.
Automate the stack deployment with configuration management. Ansible roles exist for each tool discussed. When building the automation, enforce consistent naming for your playbooks, roles, and inventory groups - this pays dividends at 2am during an incident when someone unfamiliar with the system needs to find the right playbook fast.
# Feed Suricata EVE JSON into Wazuh
cat >> /var/ossec/etc/ossec.conf << 'EOF'
json
/var/log/suricata/eve.json
EOF
# Wazuh rule to trigger CrowdSec ban from Suricata alert
# /var/ossec/etc/rules/suricata_crowdsec.xml
cat > /var/ossec/etc/rules/suricata_crowdsec.xml << 'EOF'
json
alert
^1$
Suricata severity 1 alert from $(src_ip)
crowdsec-ban
local
EOF
# Validate full stack status
echo '=== Suricata ==='
suricatasc -c 'dump-counters' | python3 -m json.tool | grep -E 'drop|alert' | head -5
echo '=== CrowdSec ==='
cscli decisions list | head -10
echo '=== Wazuh Agent ==='
/var/ossec/bin/agent_control -i 000
echo '=== Falco ==='
systemctl status falco --no-pager | grep -E 'Active|Main PID'
echo '=== nftables sets ==='
nft list set inet filter bruteforce_block