After cleaning a server compromised through a web shell, the next step was to make sure it wouldn't happen again. We put an nginx reverse proxy with TLS and hardening in front of the apps, a Suricata that sees all traffic, and a CrowdSec that reads the logs and auto-blocks hostile sources. Three layers that cover each other.
The requirement: defense in depth, not a single wall
A single defense layer fails at the first mistake. We wanted three: a proxy that hides and filters the app, an IDS that sees attacks at the network level, and an engine that learns from logs and blocks automatically. If one misses, the other catches.
In short: the same attacker is seen by the proxy, the IDS and the reputation engine; we block it once, automatically, before it does damage.
Architecture
The internet enters through nginx (reverse proxy + TLS), which forwards to the PHP apps behind. Suricata inspects all traffic (IDS), and CrowdSec reads the nginx logs, applies scenarios and puts bad IPs into a firewall bouncer.
nginx: reverse proxy, TLS and hardening
nginx hides the app, terminates TLS and applies the basic limits (rate-limit, security headers). The PHP app is no longer directly exposed.
# /etc/nginx/sites-available/app
limit_req_zone $binary_remote_addr zone=app:10m rate=10r/s;
server {
listen 443 ssl http2;
server_name app.example.com;
ssl_certificate /etc/letsencrypt/live/app.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/app.example.com/privkey.pem;
add_header X-Content-Type-Options nosniff;
add_header X-Frame-Options SAMEORIGIN;
location / {
limit_req zone=app burst=20 nodelay;
proxy_pass http://127.0.0.1:8080;
proxy_set_header X-Real-IP $remote_addr;
}
}nginx -t && systemctl reload nginx
Suricata: IDS on all traffic
Suricata sees the packets and raises alerts on signatures (scans, exploits, web shells). You put it on the interface and load the Emerging Threats rules.
apt -y install suricata
suricata-update # download ET open rules
# set the listening interface (af-packet) in /etc/suricata/suricata.yaml
systemctl enable --now suricata
# watch the alerts:
tail -f /var/log/suricata/fast.logIn short: Suricata doesn't block on its own here (it's an IDS), but it gives eyes on the network: you see the scan or exploit as it happens.
CrowdSec: reads the logs and blocks
CrowdSec reads the nginx logs (and optionally Suricata's eve.json), recognizes attack patterns and puts the bad IP into a firewall bouncer. Plus community reputation.
curl -s https://install.crowdsec.net | sudo sh
apt -y install crowdsec
cscli collections install crowdsecurity/nginx crowdsecurity/base-http-scenarios
# firewall bouncer (nftables) -> drops the IPs
apt -y install crowdsec-firewall-bouncer-nftables# what it collects + what it blocked
cscli metrics
cscli decisions list
cscli alerts list --since 24h
cscli console enroll <KEY> # dashboard + CTI blocklists
Result
The PHP app is no longer directly exposed: nginx filters and limits, Suricata gives visibility on attacks, and CrowdSec auto-blocks hostile sources before they persist. A scanner looking for web shells is caught and blocked at the firewall in seconds, and community reputation stops many attacks before they start. Everything goes into 24/7 monitoring.