The Complete Self-Hosting Security Stack: Fail2Ban + CrowdSec + Authelia
Running self-hosted services is great — until someone else discovers them. The moment you expose a port to the internet, bots start probing. Brute-force SSH attempts, credential stuffing on web apps, vulnerability scanners — it never stops.
No single tool solves this. You need layers. This guide walks through building a complete security stack using three open-source tools that complement each other perfectly:
- Fail2Ban — local log-based intrusion prevention (reactive, per-service)
- CrowdSec — community-powered threat detection (proactive, shared intelligence)
- Authelia — authentication gateway with SSO and 2FA (access control)
Together, they form a defense-in-depth strategy: Fail2Ban and CrowdSec handle the perimeter, Authelia handles identity. Let’s set it up.
How the Layers Work Together
Think of it as three concentric rings:
- Outer ring — CrowdSec: Blocks known-bad IPs before they even reach your services, using community threat lists updated in real time
- Middle ring — Fail2Ban: Watches your service logs for suspicious patterns and bans IPs showing attack behavior
- Inner ring — Authelia: Requires authentication (including 2FA) before anyone touches your apps
An attacker has to get past shared community intelligence, survive local behavioral analysis, and have valid credentials with a TOTP code. That’s a hard target.
Prerequisites
- Linux server (Ubuntu/Debian recommended)
- Docker and Docker Compose installed
- A reverse proxy (Caddy, Nginx, or Traefik) — we’ll use Caddy in examples
- A domain name with DNS pointed at your server
- Basic familiarity with YAML and the command line
Layer 1: Fail2Ban — Local Brute-Force Protection
Fail2Ban monitors log files for failed authentication attempts and temporarily bans offending IPs using your firewall.
Installation
sudo apt update && sudo apt install -y fail2ban
Basic Configuration
Create a local config (never edit the defaults directly):
sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
Edit /etc/fail2ban/jail.local with sane defaults:
[DEFAULT]
bantime = 1h
findtime = 10m
maxretry = 5
banaction = iptables-multiport
# Email notifications (optional)
# destemail = [email protected]
# action = %(action_mwl)s
[sshd]
enabled = true
port = ssh
logpath = %(sshd_log)s
maxretry = 3
bantime = 24h
Adding Jails for Docker Services
If you run services in Docker, logs often go to container stdout. Create custom jails by pointing at the right log paths:
# /etc/fail2ban/jail.local
[vaultwarden]
enabled = true
port = http,https
filter = vaultwarden
logpath = /path/to/vaultwarden/vaultwarden.log
maxretry = 5
bantime = 4h
[nextcloud]
enabled = true
port = http,https
filter = nextcloud
logpath = /path/to/nextcloud/data/nextcloud.log
maxretry = 5
bantime = 4h
Create matching filter files in /etc/fail2ban/filter.d/:
# /etc/fail2ban/filter.d/vaultwarden.conf
[Definition]
failregex = ^.*?Username or password is incorrect\. Try again\. IP: <ADDR>\..*$
ignoreregex =
Restart and verify:
sudo systemctl restart fail2ban
sudo fail2ban-client status
sudo fail2ban-client status sshd
Deep dive: See our complete Fail2Ban guide for more jails, regex patterns, and advanced configuration.
Layer 2: CrowdSec — Community Threat Intelligence
Fail2Ban only knows about attacks on your server. CrowdSec aggregates attack data from thousands of servers worldwide and shares blocklists. Think of it as a community immune system.
Docker Compose Setup
services:
crowdsec:
image: crowdsecurity/crowdsec:latest
container_name: crowdsec
restart: unless-stopped
environment:
COLLECTIONS: >-
crowdsecurity/linux
crowdsecurity/sshd
crowdsecurity/nginx
crowdsecurity/http-cve
crowdsecurity/whitelist-good-actors
GID: "${GID-1000}"
volumes:
- ./crowdsec/acquis.yaml:/etc/crowdsec/acquis.yaml
- crowdsec-db:/var/lib/crowdsec/data/
- crowdsec-config:/etc/crowdsec/
- /var/log:/var/log/host: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
volumes:
crowdsec-db:
crowdsec-config:
networks:
security:
external: true
Log Acquisition Config
Create crowdsec/acquis.yaml to tell CrowdSec which logs to watch:
# System logs
filenames:
- /var/log/host/auth.log
- /var/log/host/syslog
labels:
type: syslog
---
# Caddy/Nginx access logs
filenames:
- /var/log/host/caddy/access.log
labels:
type: nginx
Firewall Bouncer (for iptables)
For direct iptables blocking (works alongside or instead of the reverse proxy bouncer):
# Inside the CrowdSec container
docker exec crowdsec cscli bouncers add firewall-bouncer
# Install on the host
curl -s https://install.crowdsec.net | sudo bash
sudo apt install crowdsec-firewall-bouncer-iptables
Edit /etc/crowdsec/bouncers/crowdsec-firewall-bouncer.yaml with the API key from the cscli bouncers add command.
Verify It’s Working
docker exec crowdsec cscli metrics
docker exec crowdsec cscli alerts list
docker exec crowdsec cscli decisions list
Deep dive: See our CrowdSec setup guide for collections, custom scenarios, and console dashboard configuration.
Layer 3: Authelia — Authentication Gateway
Fail2Ban and CrowdSec protect the perimeter. Authelia protects access. It sits in front of your services and requires authentication before anyone can reach them.
Docker Compose Setup
services:
authelia:
image: authelia/authelia:latest
container_name: authelia
restart: unless-stopped
volumes:
- ./authelia/configuration.yml:/config/configuration.yml:ro
- ./authelia/users_database.yml:/config/users_database.yml
- authelia-data:/data
environment:
TZ: America/New_York
expose:
- 9091
networks:
- proxy
- security
volumes:
authelia-data:
networks:
proxy:
external: true
security:
external: true
Core Configuration
Create authelia/configuration.yml:
server:
address: 'tcp://:9091'
log:
level: info
totp:
issuer: yourdomain.com
authentication_backend:
file:
path: /config/users_database.yml
password:
algorithm: argon2id
iterations: 3
memory: 65536
parallelism: 4
salt_length: 16
key_length: 32
access_control:
default_policy: deny
rules:
# Public services (no auth required)
- domain: public.yourdomain.com
policy: bypass
# Services requiring 2FA
- domain: "*.yourdomain.com"
policy: two_factor
session:
name: authelia_session
secret: your-session-secret-change-me
expiration: 12h
inactivity: 45m
domain: yourdomain.com
storage:
local:
path: /data/db.sqlite3
notifier:
# Use SMTP in production
filesystem:
filename: /data/notification.txt
User Database
Create authelia/users_database.yml:
users:
admin:
displayname: "Admin User"
# Generate with: docker run --rm authelia/authelia:latest \
# authelia crypto hash generate argon2 --password 'yourpassword'
password: "$argon2id$v=19$m=65536,t=3,p=4$..."
email: [email protected]
groups:
- admins
Reverse Proxy Integration (Caddy)
Add to your Caddyfile:
After deploying, navigate to auth.yourdomain.com to register your TOTP device.
Deep dive: See our Authentik vs Authelia comparison for help choosing between the two and advanced SSO configurations.
Wiring the Stack Together
Here’s where the magic happens — making all three layers aware of each other.
Feed Authelia Logs to Fail2Ban
Create /etc/fail2ban/filter.d/authelia.conf:
[Definition]
failregex = ^.*Unsuccessful 1FA authentication attempt by user .*from <ADDR>.*$
^.*Unsuccessful 2FA authentication attempt by user .*from <ADDR>.*$
ignoreregex = ^.*level=info.*$
Add the jail:
# /etc/fail2ban/jail.local
[authelia]
enabled = true
port = http,https
filter = authelia
logpath = /path/to/authelia/authelia.log
maxretry = 3
bantime = 4h
Feed Authelia Logs to CrowdSec
Add to crowdsec/acquis.yaml:
---
filenames:
- /var/log/authelia/authelia.log
labels:
type: authelia
Install the Authelia collection:
docker exec crowdsec cscli collections install LePresworWorker/authelia
docker exec crowdsec cscli hub update
Whitelist Your Own IPs
Don’t lock yourself out. In CrowdSec:
docker exec crowdsec cscli parsers install \
crowdsecurity/whitelists
Edit the whitelist to include your home IP and VPN range.
In Fail2Ban (/etc/fail2ban/jail.local):
[DEFAULT]
ignoreip = 127.0.0.1/8 ::1 192.168.0.0/16 10.0.0.0/8
The Complete Docker Compose
Here’s a unified compose file bringing it all together:
services:
authelia:
image: authelia/authelia:latest
container_name: authelia
restart: unless-stopped
volumes:
- ./authelia/configuration.yml:/config/configuration.yml:ro
- ./authelia/users_database.yml:/config/users_database.yml
- authelia-data:/data
expose:
- 9091
networks:
- proxy
- security
crowdsec:
image: crowdsecurity/crowdsec:latest
container_name: crowdsec
restart: unless-stopped
environment:
COLLECTIONS: >-
crowdsecurity/linux
crowdsecurity/sshd
crowdsecurity/nginx
crowdsecurity/http-cve
crowdsecurity/whitelist-good-actors
volumes:
- ./crowdsec/acquis.yaml:/etc/crowdsec/acquis.yaml
- crowdsec-db:/var/lib/crowdsec/data/
- crowdsec-config:/etc/crowdsec/
- /var/log:/var/log/host:ro
networks:
- security
volumes:
authelia-data:
crowdsec-db:
crowdsec-config:
networks:
proxy:
external: true
security:
external: true
Fail2Ban runs on the host (not in Docker) for direct iptables access. CrowdSec and Authelia run in containers for easier management.
Verification Checklist
After deploying everything, verify each layer:
# Fail2Ban: check active jails
sudo fail2ban-client status
# CrowdSec: check it's parsing logs and has decisions
docker exec crowdsec cscli metrics
docker exec crowdsec cscli alerts list --limit 5
# Authelia: visit auth.yourdomain.com and register TOTP
# Then try accessing a protected service — you should get redirected
# Test the chain: make 5 bad login attempts at Authelia
# -> Fail2Ban should ban the IP
# -> CrowdSec should create an alert
# -> Both should be visible in their respective dashboards
Troubleshooting
Locked yourself out
# Fail2Ban: unban your IP
sudo fail2ban-client set authelia unbanip YOUR_IP
# CrowdSec: remove a decision
docker exec crowdsec cscli decisions delete --ip YOUR_IP
Fail2Ban not catching Authelia failures
Check the log format matches the regex. Test with:
sudo fail2ban-regex /path/to/authelia.log /etc/fail2ban/filter.d/authelia.conf
CrowdSec not processing logs
Verify acquisition paths are correct and the container can read them:
docker exec crowdsec cscli metrics show acquisition
If metrics show 0 lines read, check volume mounts and file permissions.
Authelia redirect loops
Usually a reverse proxy misconfiguration. Ensure:
- The
forward_authdirective points to the correct Authelia address - The Authelia
session.domainmatches your actual domain - HTTPS is working correctly (Authelia requires secure cookies)
High memory usage from CrowdSec
The local API database grows over time. Prune old decisions:
docker exec crowdsec cscli alerts flush --max-items 1000 --max-age 72h
What About Cloudflare?
If you’re behind Cloudflare, you get an extra layer for free (DDoS protection, WAF rules). But Cloudflare can’t protect non-HTTP services, and it doesn’t help with services you access directly. This stack works with or without Cloudflare — they’re complementary.
Maintenance Schedule
| Task | Frequency | Command |
|---|---|---|
| Check Fail2Ban status | Weekly | sudo fail2ban-client status |
| Review CrowdSec alerts | Weekly | docker exec crowdsec cscli alerts list |
| Update CrowdSec hub | Monthly | docker exec crowdsec cscli hub update |
| Review Authelia access logs | Monthly | Check Authelia log for unusual patterns |
| Update all containers | Monthly | docker compose pull && docker compose up -d |
| Test backup/restore | Quarterly | Restore Authelia DB + user file on test system |
Conclusion
Security isn’t a product — it’s layers. Fail2Ban gives you fast, local reaction to attacks. CrowdSec adds community intelligence so you block threats before they hit. Authelia ensures that even if an attacker gets past both, they still can’t access your services without credentials and a 2FA code.
This stack is free, open-source, and runs comfortably on modest hardware. A Raspberry Pi 4 can handle all three without breaking a sweat.
Start with Fail2Ban (it takes 10 minutes), add CrowdSec (another 15 minutes), then layer Authelia on top when you’re ready for SSO. Each piece makes the others stronger.
Your server is on the internet. Make it a hard target.