The Complete Self-Hosting Security Stack: Fail2Ban + CrowdSec + Authelia
Running self-hosted services on the open internet without a security stack is like leaving your front door unlocked in a busy neighborhood. Individual tools help, but real protection comes from layering defenses so each one covers the gaps of the others.
This guide builds a complete security stack using three open-source tools:
- Fail2Ban — reactive log-based banning for brute-force attacks
- CrowdSec — community-powered threat intelligence and behavioral detection
- Authelia — authentication portal with single sign-on and multi-factor authentication
Together, they give you intrusion prevention, shared threat intelligence, and access control. All running in Docker, all free.
Why Layer Security?
No single tool catches everything. Here’s what each layer handles:
| Threat | Fail2Ban | CrowdSec | Authelia |
|---|---|---|---|
| SSH brute force | ✅ Bans after N failures | ✅ Bans + shares IP globally | — |
| Known malicious IPs | — | ✅ Community blocklists | — |
| Web app credential stuffing | ✅ Log-based detection | ✅ Behavioral scenarios | ✅ Rate limiting + MFA |
| Unauthorized service access | — | — | ✅ Auth portal + policies |
| Automated scanners/bots | Partial | ✅ HTTP scenario detection | ✅ Blocks unauthenticated access |
| Stolen credentials | — | — | ✅ MFA stops reuse |
Fail2Ban is your fast, proven reactive layer. CrowdSec adds proactive community intelligence. Authelia ensures that even if an attacker gets past the network layer, they still need valid credentials and a second factor.
Prerequisites
Before starting, you’ll need:
- A Linux server (Ubuntu 22.04/24.04 or Debian 12 recommended)
- Docker and Docker Compose installed
- A reverse proxy (Nginx Proxy Manager, Traefik, or Caddy)
- A domain name with DNS pointing to your server
- Basic familiarity with YAML and the command line
This guide assumes you’re running services behind a reverse proxy with HTTPS already configured.
Architecture Overview
Here’s how the stack fits together:
Traffic hits your reverse proxy first. CrowdSec’s bouncer drops known-bad IPs before they touch anything. Authelia challenges unknown users with login + MFA. Fail2Ban watches logs from all layers and bans repeat offenders at the firewall level.
Step 1: Set Up Fail2Ban
Fail2Ban is the foundation. It watches logs and bans IPs that trigger too many failures.
Install Fail2Ban
sudo apt update && sudo apt install -y fail2ban
Configure the SSH Jail
Create a local override file (never edit the main config directly):
sudo nano /etc/fail2ban/jail.local
[DEFAULT]
bantime = 1h
findtime = 10m
maxretry = 3
banaction = nftables-multiport
banaction_allports = nftables-allports
[sshd]
enabled = true
port = ssh
logpath = /var/log/auth.log
maxretry = 3
bantime = 4h
Add a Jail for Your Reverse Proxy
If you’re running Nginx Proxy Manager or plain Nginx:
sudo nano /etc/fail2ban/jail.local
Append:
[nginx-http-auth]
enabled = true
logpath = /var/log/nginx/error.log
maxretry = 5
bantime = 1h
[nginx-botsearch]
enabled = true
logpath = /var/log/nginx/access.log
maxretry = 2
bantime = 24h
Create an Authelia Fail2Ban Filter
This catches failed login attempts at the Authelia layer:
sudo nano /etc/fail2ban/filter.d/authelia.conf
[Definition]
failregex = ^.*Unsuccessful 1FA authentication attempt by user .*remote_ip="?<HOST>"?.*$
^.*Unsuccessful 2FA authentication attempt by user .*remote_ip="?<HOST>"?.*$
ignoreregex =
Add the jail:
[authelia]
enabled = true
logpath = /path/to/authelia/authelia.log
port = http,https
filter = authelia
maxretry = 3
bantime = 4h
findtime = 5m
Start and Verify
sudo systemctl enable --now fail2ban
sudo fail2ban-client status
sudo fail2ban-client status sshd
You should see the jails listed as active with zero currently banned IPs (hopefully).
Step 2: Add CrowdSec for Threat Intelligence
CrowdSec goes beyond Fail2Ban by sharing threat data with a global community. When someone else’s server gets attacked by an IP, your server learns about it too.
Deploy CrowdSec with Docker Compose
Create a directory for your security stack:
mkdir -p ~/security-stack && cd ~/security-stack
# docker-compose.yml
services:
crowdsec:
image: crowdsecurity/crowdsec:latest
container_name: crowdsec
restart: unless-stopped
environment:
- COLLECTIONS=crowdsecurity/nginx crowdsecurity/http-cve crowdsecurity/linux
- GID=1000
volumes:
- ./crowdsec/config:/etc/crowdsec
- ./crowdsec/data:/var/lib/crowdsec/data
- /var/log:/var/log:ro
# Mount your reverse proxy logs:
- /path/to/nginx/logs:/var/log/nginx:ro
networks:
- security
crowdsec-bouncer:
image: fbonalair/traefik-crowdsec-bouncer:latest
container_name: crowdsec-bouncer
restart: unless-stopped
environment:
- CROWDSEC_BOUNCER_API_KEY=${CROWDSEC_API_KEY}
- CROWDSEC_AGENT_HOST=crowdsec:8080
depends_on:
- crowdsec
networks:
- security
networks:
security:
external: true
Note: The bouncer image depends on your reverse proxy. Use
crowdsecurity/nginx-bouncerfor Nginx, or the Traefik bouncer for Traefik. Check the CrowdSec Hub for your setup.
Register the Bouncer
docker exec crowdsec cscli bouncers add my-bouncer
Copy the generated API key into your .env file:
echo "CROWDSEC_API_KEY=your-generated-key-here" >> .env
Subscribe to Community Blocklists
CrowdSec’s free tier includes community-curated blocklists. Enroll your instance:
docker exec crowdsec cscli console enroll your-enrollment-key
Get your enrollment key from app.crowdsec.net after creating a free account.
Install Relevant Parsers and Scenarios
docker exec crowdsec cscli collections install crowdsecurity/nginx
docker exec crowdsec cscli collections install crowdsecurity/http-cve
docker exec crowdsec cscli collections install crowdsecurity/linux
docker exec crowdsec cscli collections install crowdsecurity/sshd
Verify It’s Working
# Check decisions (current bans)
docker exec crowdsec cscli decisions list
# Check alerts
docker exec crowdsec cscli alerts list
# Check metrics
docker exec crowdsec cscli metrics
Within a few hours, you’ll start seeing community-sourced bans supplementing your local detections.
Step 3: Deploy Authelia for Authentication
Authelia adds the access control layer. Every request to a protected service passes through Authelia first — if you’re not authenticated, you get a login page. If you are, the request passes through transparently.
Create the Authelia Configuration
mkdir -p ~/security-stack/authelia && cd ~/security-stack/authelia
Create configuration.yml:
# configuration.yml
server:
address: 'tcp://0.0.0.0:9091'
log:
level: info
file_path: /config/authelia.log
totp:
issuer: yourdomain.com
authentication_backend:
file:
path: /config/users_database.yml
password:
algorithm: argon2id
iterations: 3
memory: 65536
parallelism: 4
key_length: 32
salt_length: 16
access_control:
default_policy: deny
rules:
# Public services (no auth required)
- domain: public.yourdomain.com
policy: bypass
# Services requiring login only
- domain: jellyfin.yourdomain.com
policy: one_factor
# Sensitive services requiring MFA
- domain:
- portainer.yourdomain.com
- proxmox.yourdomain.com
- gitea.yourdomain.com
policy: two_factor
session:
name: authelia_session
secret: generate-a-long-random-secret-here
expiration: 12h
inactivity: 45m
domain: yourdomain.com
storage:
local:
path: /config/db.sqlite3
notifier:
filesystem:
filename: /config/notification.txt
# For production, use SMTP:
# smtp:
# host: smtp.gmail.com
# port: 587
# username: [email protected]
# password: app-password-here
# sender: [email protected]
Create the Users Database
nano users_database.yml
users:
yourusername:
displayname: "Your Name"
password: "$argon2id$v=19$m=65536,t=3,p=4$generated-hash"
email: [email protected]
groups:
- admins
Generate a password hash:
docker run --rm authelia/authelia:latest \
authelia crypto hash generate argon2 \
--password 'your-secure-password'
Add Authelia to Docker Compose
Append to your docker-compose.yml:
authelia:
image: authelia/authelia:latest
container_name: authelia
restart: unless-stopped
volumes:
- ./authelia:/config
environment:
- TZ=America/New_York
networks:
- security
labels:
# If using Traefik:
- "traefik.enable=true"
- "traefik.http.routers.authelia.rule=Host(`auth.yourdomain.com`)"
- "traefik.http.routers.authelia.tls.certresolver=letsencrypt"
- "traefik.http.services.authelia.loadbalancer.server.port=9091"
Configure Your Reverse Proxy for Forward Auth
For Nginx, add this to each protected server block:
location /authelia {
internal;
proxy_pass http://authelia:9091/api/verify;
proxy_set_header X-Original-URL $scheme://$http_host$request_uri;
proxy_set_header X-Forwarded-For $remote_addr;
}
location / {
auth_request /authelia;
auth_request_set $user $upstream_http_remote_user;
proxy_pass http://your-service:port;
}
For Traefik, add the forwardAuth middleware:
http:
middlewares:
authelia:
forwardAuth:
address: "http://authelia:9091/api/verify?rd=https://auth.yourdomain.com"
trustForwardHeader: true
authResponseHeaders:
- Remote-User
- Remote-Groups
Start Everything
cd ~/security-stack
docker compose up -d
Visit auth.yourdomain.com — you should see the Authelia login portal. Register your TOTP device and test that protected services redirect you to log in.
Connecting the Layers
The real power comes from integration. Here’s how to make all three tools work together.
Feed Authelia Logs to Fail2Ban and CrowdSec
Mount the Authelia log file so both tools can read it:
# In your CrowdSec service volumes:
volumes:
- ./authelia/authelia.log:/var/log/authelia.log:ro
Install the Authelia parser for CrowdSec:
docker exec crowdsec cscli parsers install LePresworWorker/authelia-logs
docker exec crowdsec cscli scenarios install LePresworWorker/authelia-bf
Now both Fail2Ban and CrowdSec independently watch Authelia for brute-force attempts. Fail2Ban provides fast local banning. CrowdSec shares the attacker’s IP with the community.
Whitelist Your Local Network
Prevent locking yourself out. In Fail2Ban:
[DEFAULT]
ignoreip = 127.0.0.1/8 ::1 192.168.0.0/16 10.0.0.0/8
In CrowdSec, create a whitelist:
docker exec crowdsec cscli parsers install crowdsecurity/whitelists
Edit the whitelist config to include your LAN ranges.
In Authelia, use the bypass policy for local network access if desired:
access_control:
rules:
- domain: "*.yourdomain.com"
networks:
- 192.168.0.0/16
policy: bypass
Testing the Stack
Test Fail2Ban
From a different IP (or using a VPN), deliberately fail SSH login 3+ times:
ssh wronguser@your-server-ip
# Enter wrong password 3 times
Check the ban:
sudo fail2ban-client status sshd
# Should show the IP in "Banned IP list"
Test CrowdSec
Check if community blocklists are active:
docker exec crowdsec cscli decisions list --origin CAPI
You should see IPs pre-banned from community intelligence.
Test Authelia
- Visit a protected service — you should be redirected to the Authelia login page
- Enter wrong credentials 3 times — Fail2Ban should ban your IP
- Log in correctly, then access another protected service — SSO should pass you through without re-authenticating
Monitoring and Maintenance
CrowdSec Dashboard
CrowdSec provides a free web dashboard at app.crowdsec.net showing:
- Active decisions and alerts
- Community blocklist coverage
- Attack trends and geographic data
Useful Commands
# Fail2Ban: unban an IP
sudo fail2ban-client set sshd unbanip 1.2.3.4
# CrowdSec: manually ban an IP
docker exec crowdsec cscli decisions add --ip 1.2.3.4 --reason "manual ban"
# CrowdSec: remove a ban
docker exec crowdsec cscli decisions delete --ip 1.2.3.4
# Check all active bans across tools
sudo fail2ban-client status | grep "Jail list"
docker exec crowdsec cscli decisions list
Log Rotation
Make sure your logs don’t fill the disk. Add logrotate rules for Authelia:
sudo nano /etc/logrotate.d/authelia
Troubleshooting
Locked yourself out? SSH in from a whitelisted IP and run:
sudo fail2ban-client unban --all
docker exec crowdsec cscli decisions delete --all
CrowdSec not detecting attacks? Check that log paths are correct and parsers match your log format:
docker exec crowdsec cscli parsers list
docker exec crowdsec tail -f /var/log/crowdsec.log
Authelia not redirecting? Verify your reverse proxy’s forwardAuth configuration. The most common issue is incorrect X-Forwarded-* headers. Check Authelia logs:
docker logs authelia
Services slow after adding Authelia? Each request triggers a verification call. Make sure Authelia is on the same Docker network as your reverse proxy to minimize latency.
What This Stack Doesn’t Cover
This is a strong foundation, but consider adding:
- Cloudflare WAF or Tunnel — blocks attacks before they reach your server
- WireGuard VPN — for services that should never be publicly exposed
- Unattended upgrades — keep your OS patched automatically
- OSSEC or Wazuh — host-based intrusion detection for deeper monitoring
Conclusion
Security is never a single tool — it’s layers. Fail2Ban gives you fast, proven log-based banning. CrowdSec adds community intelligence so you benefit from attacks blocked worldwide. Authelia ensures that even if an attacker slips past the network defenses, they still face a login wall with MFA.
Deploy all three and you’ve got a security stack that handles the vast majority of threats facing self-hosted infrastructure. The setup takes an afternoon, but the protection runs 24/7.
Start with Fail2Ban if you’re brand new. Add CrowdSec when you’re comfortable. Layer Authelia on top when you’re ready for proper access control. Each tool is valuable on its own — together, they’re a fortress.