CrowdSec & Fail2Ban: Real-Time Threat Defense for SWAG
Throughout my 20+ years in IT, I've consistently advocated for a layered and dynamic security approach โ a strategy where defences are built in parallel and depth, creating a robust and resilient system. Think of your public IP address as the drawbridge to your digital castle. It's the link to the outside world and as such, needs to be keenly protected.
In Part 2, we got a bare SWAG container running: valid certs, one app proxied, nothing fancy. That's a working reverse proxy, but it's not yet a secure one. Today we fix that: harden the TLS and header configuration to earn an A+ SSL Labs score, arm Fail2Ban's bot traps properly, and bring CrowdSec online as real-time, crowd-sourced threat intelligence.
๐น Hardening TL;DR
- SWAG = entry point (done, Part 2)
- Fail2Ban & CrowdSec = threat blocking (today)
- Authelia = identity (next post)
We aren't just opening ports; we're building a siege-ready fortress.
Mastering Reverse Proxies with SWAG
Learn the concepts, deploy SWAG, secure your services, understand the request flow, and confidently migrate from other reverse proxy solutions.
๐ฐ What is the "Digital Castle" Architecture?
We are moving beyond simple "Firewalls." We are building Defence in Depth.
- Layer 1 (The Moat): Your Router/Firewall (OPNsense/Unifi) blocking all ports except 443. Already in place. If not, see my complete OPNsense deployment series!
- Layer 2 (The Traps): Fail2Ban (Integrated into SWAG). The classic "Three strikes and you're out" defender. Today.
- Layer 3 (The Watchman): CrowdSec reads the logs. If an IP acts suspicious, it's banned instantly - and shared with the global community. Today.
- Layer 4 (The Gatekeeper): Authelia intercepts valid traffic and demands your 2FA token. Next post.
Part 1: Implementing CrowdSec for Real-Time Threat Intelligence๐ก๏ธ
Prerequisite Steps - Building on your existing part 2 Deployment
This post assumes you've completed Part 2: Installing SWAG & Publishing Your First Service - a working SWAG container with a valid wildcard certificate and at least one app proxied. Everything below extends that same compose file. โ
We're not reinstalling anything, we're extending the same docker-compose.yml with one new service, CrowdSec. Fail2Ban needs no new container at all; it's already built into SWAG, we're just switching on and tuning its additional jails.
New Folder for CrowdSec
You already have a swag/ folder from Part 2. Add one more alongside it:
your-existing-project/
โโโ docker-compose.yml <- the same file from Part 2, we're extending it
โโโ .env <- new โ for secrets like your CrowdSec API key
โโโ swag/ <- already exists from Part 2
โโโ crowdsec/ <- new๐ Don't forget to set permissions on the new folder, with whatever user you decided to run crowdsec container with. Same one was SWAG maybe? chown -R 1000:1000 /yourpath/to/crowdsec and chmod 644 /yourpath/to/crowdsec as well.
Adding CrowdSec to Your Compose File
Add these lines to your existing swag service's environment: block:
- DOCKER_MODS=linuxserver/mods:swag-crowdsec # append with a | if you already have other mods, e.g. linuxserver/mods:swag-dashboard|linuxserver/mods:swag-crowdsec
- CROWDSEC_API_KEY=${CROWDSEC_API_KEY}
- CROWDSEC_LAPI_URL=http://crowdsec:8080Then add the new crowdsec service alongside it:
crowdsec:
container_name: crowdsec
image: crowdsecurity/crowdsec:latest
depends_on:
- swag
environment:
- COLLECTIONS=crowdsecurity/nginx crowdsecurity/base-http-scenarios crowdsecurity/appsec-virtual-patching crowdsecurity/whitelist-good-actors
- CUSTOM_HOSTNAME=crowdsec
- PID=1000
- GID=1000
- BOUNCER_KEY_SWAG=${CROWDSEC_API_KEY}
volumes:
- /yourpath/DOCKERS/swag/log/nginx:/var/log/swag:ro
- /yourpath/DOCKERS/crowdsec/data:/var/lib/crowdsec/data:rw
- /yourpath/DOCKERS/crowdsec/config:/etc/crowdsec:rw
- /var/log:/var/log/host:ro
security_opt:
- no-new-privileges=true
restart: unless-stoppedCROWDSEC_API_KEY is a variable, not a literal value - it's pulled from a .env file, not hardcoded into the compose file. We generate the actual key further down.
Starting CrowdSec Up
- Start the stack once -
docker compose up -d- just to let CrowdSec generate its default folders, thendocker compose down. - Edit
acquis.yamlto add the proper log sources (shown below).
CrowdSec Bouncer Setup / Bouncer API Key
This key is what lets SWAG (the bouncer) actually execute the bans CrowdSec (the brain) decides on:
docker compose up -d
docker exec -t crowdsec cscli bouncers add swag.env file# .env
CROWDSEC_API_KEY=the_key_you_just_copiedThis is why the compose file references ${CROWDSEC_API_KEY} instead of a literal string - Docker Compose pulls it from .env automatically, keeping the secret out of a file you might paste into a support forum or commit to git by accident.
Telling CrowdSec What to Read
Edit crowdsec/acquis.yaml which is buried wherever you put crowdsec, nano /opt/DOCKERS/crowdsec/acquis.yml as an example:
filenames:
- /var/log/swag/*
labels:
type: nginx
---
filenames:
- /var/log/auth.log*
- /var/log/syslog
labels:
type: syslogThe type: nginx label matters more than it looks - without it, CrowdSec has no idea which parser to apply to those logs, and it'll sit there technically running while catching nothing.
Confirmation & Testing CrowdSec Function
Now you can drop yourself into the crowdsec docker to check things out quickly. Type: docker exec -it crowdsec /bin/sh
You'll get dropped into the crowdsec's shell. This means the container is running and alive. You can try cscli metrics and will see some output. This is good. Type exit until it drops you back to normal console/command prompt on your host again.
Now check their logs one at a time, CTRL-C to drop out.
docker logs -f swag or docker logs -f crowdsec and then CTRL-C to cancel.
Now you check to see if the bouncer is live, talking to the CrowdSec LAPI and all is ok!
docker exec -t crowdsec cscli bouncers listYou're looking for your SWAG entry with a checkmark and a recent timestamp under "Last API pull":
docker exec -t crowdsec cscli bouncers list
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Name IP Address Valid Last API pull Type Version Auth Type
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
SWAG 10.0.0.4 โ๏ธ 2026-04-12T03:06:26Z crowdsec-nginx-bouncer v1.1.3 api-key
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโSo long as you see there's a DTG in the "Last API pull" and a check mark, you're GTG! โ
Output of docker exec -t crowdsec cscli metrics
โญโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฎ
โ Acquisition Metrics โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโค
โ Source โ Lines read โ Lines parsed โ Lines unparsed โ Lines poured to bucket โ Lines whitelisted โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโผโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโค
โ : โ - โ - โ - โ - โ 202 โ
โ file:/var/log/swag/access.log โ 67.98k โ 67.98k โ - โ 41.17k โ 6.75k โ
โ file:/var/log/swag/error.log โ 24 โ 24 โ - โ 13 โ - โ
โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโดโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโฏ
โญโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฎ
โ Local API Alerts โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโค
โ Reason โ Count โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโค
โ LePresidente/http-generic-401-bf โ 1 โ
โ LePresidente/http-generic-403-bf โ 7 โ
โ crowdsecurity/http-crawl-non_statics โ 1 โ
โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโฏ
โญโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฎ
โ Local API Decisions โ
โโโโโโโโโโโโโโโโโโโฌโโโโโโโโโฌโโโโโโโโโฌโโโโโโโโค
โ Reason โ Origin โ Action โ Count โ
โโโโโโโโโโโโโโโโโโโผโโโโโโโโโผโโโโโโโโโผโโโโโโโโค
โ http:bruteforce โ CAPI โ ban โ 24809 โ
โ http:crawl โ CAPI โ ban โ 2554 โ
โฐโโโโโโโโโโโโโโโโโโดโโโโโโโโโดโโโโโโโโโดโโโโโโโโฏ
โญโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฎ
โ Local API Metrics โ
โโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโฌโโโโโโโโค
โ Route โ Method โ Hits โ
โโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโผโโโโโโโโค
โ /v1/decisions โ GET โ 56288 โ
โ /v1/heartbeat โ GET โ 5497 โ
โ /v1/usage-metrics โ POST โ 550 โ
โ /v1/watchers/login โ POST โ 94 โ
โฐโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโดโโโโโโโโฏ
โญโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฎ
โ Local API Bouncers Metrics โ
โโโโโโโโโโโฌโโโโโโโโโโโโโโโโฌโโโโโโโโโฌโโโโโโโโค
โ Bouncer โ Route โ Method โ Hits โ
โโโโโโโโโโโผโโโโโโโโโโโโโโโโผโโโโโโโโโผโโโโโโโโค
โ SWAG โ /v1/decisions โ GET โ 56287 โ
โฐโโโโโโโโโโดโโโโโโโโโโโโโโโโดโโโโโโโโโดโโโโโโโโฏ
โญโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฎ
โ Local API Bouncers Decisions โ
โโโโโโโโโโโฌโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโค
โ Bouncer โ Empty answers โ Non-empty answers โ
โโโโโโโโโโโผโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโค
โ SWAG โ 56287 โ 0 โ
โฐโโโโโโโโโโดโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโฏ
โญโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฎ
โ Local API Machines Metrics โ
โโโโโโโโโโโโฌโโโโโโโโโโโโโโโโฌโโโโโโโโโฌโโโโโโโค
โ Machine โ Route โ Method โ Hits โ
โโโโโโโโโโโโผโโโโโโโโโโโโโโโโผโโโโโโโโโผโโโโโโโค
โ crowdsec โ /v1/heartbeat โ GET โ 5497 โ
โฐโโโโโโโโโโโดโโโโโโโโโโโโโโโโดโโโโโโโโโดโโโโโโโฏ
โญโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฎ
โ Parser Metrics โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโฌโโโโโโโโโโฌโโโโโโโโโโโค
โ Parsers โ Hits โ Parsed โ Unparsed โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโผโโโโโโโโโโผโโโโโโโโโโโค
โ child-crowdsecurity/http-logs โ 204.01k โ 141.45k โ 62.55k โ
โ child-crowdsecurity/nginx-logs โ 68.03k โ 68.00k โ 24 โ
โ crowdsecurity/cdn-whitelist โ 202 โ 202 โ - โ
โ crowdsecurity/dateparse-enrich โ 68.00k โ 68.00k โ - โ
โ crowdsecurity/geoip-enrich โ 61.25k โ 61.25k โ - โ
โ crowdsecurity/http-logs โ 68.00k โ 68.00k โ - โ
โ crowdsecurity/nginx-logs โ 68.00k โ 68.00k โ - โ
โ crowdsecurity/non-syslog โ 68.00k โ 68.00k โ - โ
โ crowdsecurity/overseerr-whitelist โ 68.00k โ 68.00k โ - โ
โ crowdsecurity/public-dns-allowlist โ 68.00k โ 68.00k โ - โ
โ crowdsecurity/rdns โ 202 โ 202 โ - โ
โ crowdsecurity/seo-bots-whitelist โ 202 โ 202 โ - โ
โ crowdsecurity/whitelists โ 68.00k โ 68.00k โ - โ
โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโดโโโโโโโโโโดโโโโโโโโโโโฏ
โญโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฎ
โ Scenario Metrics โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโฌโโโโโโโโโโโโโโโฌโโโโโโโโโฌโโโโโโโโโโค
โ Scenario โ Current Count โ Overflows โ Instantiated โ Poured โ Expired โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโผโโโโโโโโโโโโผโโโโโโโโโโโโโโโผโโโโโโโโโผโโโโโโโโโโค
โ LePresidente/http-generic-401-bf โ - โ - โ 12 โ 12 โ 12 โ
โ LePresidente/http-generic-403-bf โ - โ - โ 6 โ 6 โ 6 โ
โ crowdsecurity/http-admin-interface-probing โ - โ 45 โ 407 โ 554 โ 362 โ
โ crowdsecurity/http-backdoors-attempts โ - โ 11 โ 98 โ 109 โ 87 โ
โ crowdsecurity/http-bad-user-agent โ - โ - โ 86 โ 86 โ 86 โ
โ crowdsecurity/http-crawl-non_statics โ 2 โ - โ 24.79k โ 29.63k โ 24.79k โ
โ crowdsecurity/http-path-traversal-probing โ - โ - โ 7 โ 8 โ 7 โ
โ crowdsecurity/http-probing โ - โ 161 โ 4.53k โ 9.82k โ 4.37k โ
โ crowdsecurity/http-sensitive-files โ - โ - โ 276 โ 288 โ 276 โ
โ crowdsecurity/http-wordpress-scan โ - โ 95 โ 333 โ 675 โ 238 โ
โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโดโโโโโโโโโโโโดโโโโโโโโโโโโโโโดโโโโโโโโโดโโโโโโโโโโฏ
โญโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฎ
โ Whitelist Metrics โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโฌโโโโโโโโโโโโโโค
โ Whitelist โ Reason โ Hits โ Whitelisted โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโผโโโโโโโโโโโโโโค
โ crowdsecurity/cdn-whitelist โ CDN provider โ 202 โ 202 โ
โ crowdsecurity/overseerr-whitelist โ Overseerr whitelist โ 68002 โ - โ
โ crowdsecurity/public-dns-allowlist โ public DNS server โ 68002 โ - โ
โ crowdsecurity/seo-bots-whitelist โ good bots (search engine crawlers) โ 202 โ - โ
โ crowdsecurity/whitelists โ private ipv4/ipv6 ip/ranges โ 68002 โ 6752 โ
โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโดโโโโโโโโโโโโโโฏ
There's another post you can dig through once you've finished with this series, to really dig in๐ and see if CrowdSec is blocking actively for you.
Why CrowdSec Beats Fail2Ban Alone
Fail2Ban blocks based on your own logs. CrowdSec shares and receives ban intelligence with every other CrowdSec user - if an IP got flagged attacking someone else's server an hour ago, it can be blocked on yours before it tries anything. This is crowd-sourced threat intelligence - the kind enterprise security hardware charges real money for.
Here's my complete SWAG & CrowdSec Compose for reference:
๐ View Full YAML Configuration (Click to Expand)
networks:
vlan7_home:
driver: macvlan
driver_opts:
parent: enp40s0f1 # Use your actual parent interface
ipam:
config:
- subnet: 10.0.0.0/24 # Any class C / RFC1918 range
gateway: 10.0.0.1 # Gateway of this subnet/vlan/network
backend:
name: dockernet
driver: bridge
### SWAG config begin
swag:
image: lscr.io/linuxserver/swag
container_name: swag
networks:
docker_vlan:
ipv4_address: X.X.X.X
cap_add:
- NET_ADMIN
environment:
- PUID=1000
- PGID=1000
- TZ=America/Toronto
- URL=corelab.tech
- VALIDATION=dns #can be http or dns
- SUBDOMAINS=wildcard #optional
# - CERTPROVIDER= #optional
- DNSPLUGIN=cloudflare
- DOCKER_MODS=linuxserver/mods:swag-crowdsec|linuxserver/mods:swag-dashboard|linuxserver/mods:swag-cloudflare-real-ip
# - PROPAGATION= #optional
# - EMAIL= #optional
- ONLY_SUBDOMAINS=false #optional, set as false will have swag fetch certs for my extra domains as well!
- EXTRA_DOMAINS=corelab.tech #optional
# - STAGING=false #optional
# - DISABLE_F2B= #optional
# - SWAG_AUTORELOAD= #optional
# - SWAG_AUTORELOAD_WATCHLIST= #optional
- CROWDSEC_API_KEY=${CROWDSEC_API_KEY} | OR put your API key here directly
- CROWDSEC_LAPI_URL=http://crowdsec:8080 <--- Can be IP or name of your crowdsec below
volumes:
- /brain/DOCKERS/swag:/config
ports:
- 443:443 # Mandatory, required for functioning
- 80:80 # Optional
- 81:81 # If you enabled the dashboard mod above
restart: unless-stopped
crowdsec:
container_name: crowdsec
image: crowdsecurity/crowdsec:latest
depends_on:
- swag
networks:
docker_vlan:
ipv4_address: X.X.X.X
environment:
- COLLECTIONS=crowdsecurity/nginx crowdsecurity/base-http-scenarios crowdsecurity/appsec-virtual-patching crowdsecurity/whitelist-good-actors
- CUSTOM_HOSTNAME=crowdsec
- PID=1000
- GID=1000
- BOUNCER_KEY_SWAG=${CROWDSEC_API_KEY} | Or API key
volumes:
- /yourpath/DOCKERS/swag/log/nginx:/var/log/swag:ro
- /yourpath/DOCKERS/crowdsec/data:/var/lib/crowdsec/data:rw
- /yourpath/DOCKERS/crowdsec/config:/etc/crowdsec:rw
- /var/log:/var/log/host:ro
security_opt:
- no-new-privileges=true
restart: unless-stopped

Part 2: SWAG Security Hardening - HSTS, Headers, and TLS 1.3
๐ฏ The Goal: HSTS & The A+ Rating
A Word of Caution Before You Touch HSTS
HSTS (HTTP Strict Transport Security) tells browsers "never talk to me over plain HTTP again, HTTPS only, period." That's exactly what you want - right up until your certificate fails to renew for some reason, at which point HSTS will lock you (and everyone else) out of your own domain until it's fixed.
The Reward: It stops "Protocol Downgrade Attacks" dead in their tracks.

Step 1: Enable HSTS
Open /swag/config/nginx/ssl.conf. LinuxServer.io ships this commented out by default specifically to protect people from locking themselves out. Find the HSTS line and uncomment it:
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;max-age=63072000 is two years in seconds. Qualys SSL Labs only requires about six months (roughly 15.7 million seconds) for an A+, so this is comfortably over the bar.
Step 2: Harden the Response Headers
Still in ssl.conf, make sure these are active:
# Stop other sites from embedding yours (clickjacking protection)
add_header X-Frame-Options "SAMEORIGIN" always;
# Stop the browser from guessing MIME types
add_header X-Content-Type-Options "nosniff" always;
# Browser's built-in XSS filter
add_header X-XSS-Protection "1; mode=block" always;
# Referrer privacy
add_header Referrer-Policy "strict-origin-when-cross-origin" always;None of these are exotic - they're the standard baseline any public-facing site should have, and SWAG makes them a one-line uncomment away instead of something you'd have to write from scratch. It seems over time what is enabled by default here has changed so you may have to uncomment or not!
Step 3: Drop Legacy TLS Versions
Still in the same file, find ssl_protocols and restrict it:
ssl_protocols TLSv1.2 TLSv1.3;TLS 1.0 and 1.1 are old enough that keeping them enabled mainly just widens your attack surface for negligible compatibility benefit. TLS 1.2 is still considered secure by Qualys, so this is the safe default - if you know every device hitting your services supports 1.3, you can go further and drop 1.2 too, but that's a "know your audience" call, not a default recommendation. I believe all up to date browsers support TLS 1.3, but even still, this site supports TLS1.2 as well.
Step 4: Restart and Verify
docker compose up -d --force-recreate swag && docker logs -f swagThis re-creates the container and shows you a live running log of the startup so you can ensure everything fires back up ok. CTRL-C to cancel out.
Then run your domain through Qualys SSL Labs. With HSTS, the hardened headers, and legacy TLS disabled, you should land at A or A+.
Part 3: Arming Fail2Ban (The Bot Traps)
By default, Fail2Ban might only watch for SSH attempts. For a web server, attacks come via HTTP - password guessing, bots scanning for wp-login.php, and so on. SWAG ships pre-written jails for Nginx, but they're often disabled.
- Open your jail config:
/swag/fail2ban/jail.local. - Enable the core 5:
[nginx-http-auth]
enabled = true
filter = nginx-http-auth
port = http,https
logpath = /config/log/nginx/error.log
[nginx-badbots]
enabled = true
filter = nginx-badbots
port = http,https
logpath = /config/log/nginx/access.log
[nginx-botsearch]
enabled = true
filter = nginx-botsearch
port = http,https
logpath = /config/log/nginx/access.log
[nginx-deny]
enabled = true
filter = nginx-deny
port = http,https
logpath = /config/log/nginx/error.log
[nginx-unauthorized]
enabled = true
filter = nginx-unauthorized
port = http,https
logpath = /config/log/nginx/error.logFull jail.local reference:
[DEFAULT]
ignoreip = 10.0.0.0/8
192.168.0.0/16
172.16.0.0/12
banaction = iptables-allports
bantime = 1800
findtime = 600
maxretry = 3
[ssh]
enabled = false
[nginx-http-auth]
enabled = true
filter = nginx-http-auth
port = http,https
logpath = /config/log/nginx/error.log
[nginx-badbots]
enabled = true
port = http,https
filter = nginx-badbots
bantime = 12h
logpath = /config/log/nginx/access.log
maxretry = 2
[nginx-botsearch]
enabled = true
port = http,https
filter = nginx-botsearch
logpath = /config/log/nginx/access.log
[nginx-deny]
enabled = true
port = http,https
filter = nginx-deny
logpath = /config/log/nginx/error.log
[nginx-unauthorized]
enabled = true
port = http,https
filter = nginx-unauthorized
logpath = /config/log/nginx/access.log
maxretry = 6
[nginx-limit-req]
enabled = true
port = http,https
filter = nginx-limit-req
logpath = /config/log/nginx/error.log
bantime = 7200- Restart SWAG:
docker restart swag
๐ง Deep Dive: What Are These Traps Actually Catching?
nginx-http-authโ brute-force protection for HTTP Basic Auth popupsnginx-badbotsโ blocks known malicious scraper user agentsnginx-botsearchโ the most active jail, catching scans for/phpmyadmin,/wp-login.php,/setup.cgi, and similarnginx-unauthorizedโ bans IPs repeatedly triggering 401s
The "Perma-Ban" (Recidive Jail)
Standard bans are temporary โ smart bots wait it out. Recidive stops that cycle:
[recidive]
enabled = true
logpath = /config/log/fail2ban/fail2ban.log
banaction = iptables-allports
bantime = 1w
findtime = 1d
maxretry = 3Restart: docker compose up -d --force-recreate swag
Verification & The "Ban-Hammer"
docker exec -it swag fail2ban-client statusExpected:
Status
|- Number of jail: 6
`- Jail list: nginx-badbots, nginx-botsearch, nginx-deny, nginx-http-auth, nginx-limit-req, nginx-unauthorized"Number of jail: 0" means a jail.local syntax error. Check specific catches with:
docker exec -it swag fail2ban-client status nginx-botsearchFail2Ban isn't set-and-forget - review logs regularly, watch for false positives, and remember it's one layer among several, not a silver bullet.
Monitoring and Fine-Tuning
A robust Fail2ban configuration isn't a set-and-forget solution. Continuous monitoring and fine-tuning are essential. Regularly review your Fail2ban logs to identify false positives and adjust your filters accordingly. Pay attention to the number of bans triggered by each jail and investigate any unusual activity. Consider implementing a centralized logging system to aggregate Fail2ban logs for easier analysis.
Commands for Monitoring:
docker exec -it swag /bin/bashfail2ban-client status: Shows the status of all jails.fail2ban-client status <jail_name>: Shows the status of a specific jail.fail2ban-client set <jail_name> <parameter> <value>: Changes a jail's parameters dynamically (live, on the fly).
Important Notes and Cautions
- Testing is Crucial: Always test your configuration changes in a non-production (if possible/available) environment before deploying them to production. Incorrectly configured filters can lead to legitimate users being blocked.
- Regular Updates: Keep your Fail2ban software and filters up to date to protect against new attack vectors.
- Understand Your Logs: Familiarize yourself with your web server and system logs to write effective filters.
- Fail2ban is a Defence Layer: Fail2ban is a valuable security tool, but itโs not a silver bullet. It should be part of a comprehensive security strategy along with CrowdSec and a reverse proxy, plus a firewall. See how many layers we're talking about here and we haven't even gotten into IDS/IPS nor WAF yet!
Part 4: Mapping Defence in Depth to the OSI Model & Your Homelab (Sidebar!)
Infrastructure & Network Segmentation (layering)
Let's reinforce the core concept: layered security. Itโs not about relying on a single solution. Itโs about creating a defence-in-depth strategy. Remember the OSI model from my networking series? Here's how it ties into security with multiple layers:
<pre class="mermaid">
flowchart TB
subgraph L7["๐ฅ๏ธ L7 Application"]
direction TB
L7a["๐ก๏ธ WAF - C/I"]
L7b["๐ป Secure coding - I"]
L7c["๐ Auth & MFA - C/I"]
L7d["๐ก๏ธ EDR - C/I"]
end
subgraph L6["๐จ L6 Presentation"]
direction TB
L6a["๐ TLS - C/I"]
L6b["๐ Encoding & sanitization - I"]
L6c["๐ Masking & tokenization - C/I"]
end
subgraph L5["๐ค L5 Session"]
direction TB
L5a["โณ Timeouts - C"]
L5b["๐ Tokens - C/I"]
L5c["๐ก๏ธ VPN / ZTNA - C"]
end
subgraph L4["๐ฆ L4 Transport"]
direction TB
L4a["๐ TLS / SSH - C/I"]
L4b["๐ช Port filtering ACLs - C"]
L4c["โก Rate limiting / DoS - A"]
end
subgraph L3["๐ฐ๏ธ L3 Network"]
direction TB
L3a["๐ฅ Firewall - C/A"]
L3b["๐ต๏ธ IDS/IPS - C/I"]
L3c["๐งฑ Segmentation VLANs - C"]
L3d["๐ IPsec tunnels - C/I"]
end
subgraph L2["๐ L2 Data Link"]
direction TB
L2a["๐ 802.1X NAC - C"]
L2b["๐ท๏ธ MAC filtering - C"]
L2c["๐ ARP/DHCP checks - I"]
L2d["๐ถ WiFi WPA3 - C"]
end
subgraph L1["โก L1 Physical"]
direction TB
L1a["๐ Locks - C"]
L1b["๐ฅ CCTV - C/I"]
L1c["โก Power/HVAC/Fire - A"]
L1d["๐๏ธ Media sanitization - C/I"]
end
L7 --> L6 --> L5 --> L4 --> L3 --> L2 --> L1
classDef conf fill:#00ffff,stroke:#00ffff,stroke-width:2px,color:#111,font-weight:bold;
classDef integ fill:#ff00ff,stroke:#ff33ff,stroke-width:2px,color:#111,font-weight:bold;
classDef avail fill:#ff8c00,stroke:#ffaa33,stroke-width:2px,color:#111,font-weight:bold;
class L7a,L7c,L7d,L6a,L6c,L5a,L5b,L5c,L4a,L4b,L3a,L3b,L3c,L3d,L2a,L2b,L2d,L1a,L1b,L1d conf;
class L7a,L7b,L7c,L7d,L6a,L6b,L6c,L5b,L4a,L3b,L3d,L2c,L1b,L1d integ;
class L4c,L3a,L1c avail;
</pre>Confidentiality (C)
Integrity (I)
Availability (A)
Obviously in a home network you likely won't have all of this, but it's to illustrate that you should kinda be aiming for it. Just having "a firewall" isn't enough. That's like, the moat around your castle only!
Network Segmentation (VLANs)
If you followed my networking series and setup your VLANs properly, you can now have something like this! Head over to the OPNsense setup guide, if you don't have an effective firewall or want one that SLAPS.
๐ก๏ธ Optional: Contributing to the Global Shield (AbuseIPDB)
If you want to give back, report repeat offenders to AbuseIPDB:
- Create an account at AbuseIPDB.
- Generate an API key.
- Add to the bottom of
jail.local's[DEFAULT]section:
action = %(action_)s
%(action_abuseipdb)s[abuseipdb_apikey="YOUR_API_KEY_HERE", abuseipdb_category="5,14,15,18,19,21"]Reload Fail2Ban: docker exec -it swag /bin/bash, then fail2ban-client reload.
Troubleshooting Pro-Tip: if CrowdSec says "bouncer connected" but isn't banning anything, check docker logs crowdsec. "File not found" errors almost always mean acquis.yaml's path doesn't match the container's internal path (/var/log/swag), not your host path.
Frequently Asked Questions
Will enabling HSTS lock me out of local network access? No โ HSTS is domain-specific. Local IP access (192.168.x.x) is unaffected.
Why run both Fail2Ban and CrowdSec? Isn't that redundant? Fail2Ban is your fast, local reactive agent. CrowdSec is your proactive watchman using community threat intelligence. Together: speed and global awareness.
fail2ban-client status shows 0 jails โ what happened? Almost always a jail.local syntax error.
Is TLS 1.2 still safe in 2026? Yes, with strong ciphers โ kept enabled alongside 1.3 mainly for older smart TVs and IoT devices.
Can I get an A+ rating without a reverse proxy? Very difficult โ individual apps generally aren't built to manage their own TLS and headers the way SWAG does.
What if I get banned by my own Fail2Ban or CrowdSec? Switch networks (phone LTE) to reach your terminal. Fail2Ban: fail2ban-client set <jail-name> unbanip <your-ip>. CrowdSec: cscli decisions delete --ip <your-ip>.
What's Next
Layers 1 through 3 of the Digital Castle are up: the moat, the traps, and the watchman. Next: Authelia - Centralized MFA for Self-Hosted Services, the Gatekeeper that demands a second factor even from traffic that makes it past everything above.
Member discussion