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:

  1. Outer ring — CrowdSec: Blocks known-bad IPs before they even reach your services, using community threat lists updated in real time
  2. Middle ring — Fail2Ban: Watches your service logs for suspicious patterns and bans IPs showing attack behavior
  3. 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:

a}#g}urtPahrf.roaf}ryetnoeovearvuec.werrtyaucrdseorrosoedudipem_r_y_apsda/_pireouahrnormtpeo.xvahiaxcyii/dyocnaaemae.uurgucttsr{teohhahxmezRfeal/ealm{ifmnipaooaal:rt::e9we390a-009rU091ds01-e{arutRhemote-GroupsRemote-NameRemote-Email

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_auth directive points to the correct Authelia address
  • The Authelia session.domain matches 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

TaskFrequencyCommand
Check Fail2Ban statusWeeklysudo fail2ban-client status
Review CrowdSec alertsWeeklydocker exec crowdsec cscli alerts list
Update CrowdSec hubMonthlydocker exec crowdsec cscli hub update
Review Authelia access logsMonthlyCheck Authelia log for unusual patterns
Update all containersMonthlydocker compose pull && docker compose up -d
Test backup/restoreQuarterlyRestore 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.