Skip to main content

Securing Your Homelab with Traefik and CrowdSec

·1216 words·6 mins·
Photo by Joakim Honkasalo on Unsplash

Exposing self-hosted services to the internet is a balancing act between convenience and risk. Traefik makes routing easy, but it does not detect hostile traffic on its own. CrowdSec fills that gap by parsing logs, detecting attack patterns, and sharing threat intelligence across its user community.

This post shows the stack I run in my homelab: Traefik v3.3 behind Cloudflare, with CrowdSec acting as both a WAF and an IP-reputation firewall.

What is CrowdSec?
#

CrowdSec is an open-source intrusion prevention system that analyzes logs, detects attack patterns, and turns those detections into decisions such as bans or alerts. Its main strength is the combination of local detection and shared threat intelligence: when one deployment sees malicious behavior, that signal helps improve protection for the wider community. In practice, that makes it a strong fit for exposed homelab services, reverse proxies, SSH access, and other small infrastructure that still needs real security controls.

CrowdSec has a modular architecture with a central engine, a REST API (LAPI), and multiple bouncers that enforce decisions at different layers. For this Traefik deployment, I use the CrowdSec bouncer plugin for HTTP-layer blocking and the crowdsec-firewall-bouncer-iptables for network-layer blocking on the host.

Why this setup?
#

The goal is to block bad traffic as early as possible while keeping the stack simple to operate.

  • Traefik handles routing, TLS, headers, and rate limiting.
  • CrowdSec reads Traefik and host logs, then turns detections into decisions.
  • The Traefik bouncer blocks malicious HTTP requests.
  • The firewall bouncer drops banned IPs at the network layer.

Deployment model
#

Two enforcement points work together:

LayerComponentWhat it blocks
HTTPcrowdsec-bouncer-traefik-pluginBanned IPs and malicious requests through AppSec
Networkcrowdsec-firewall-bouncer-iptablesBanned IPs before they reach the Docker network

The important design choice is the shared log volume: Traefik writes access logs into a named Docker volume, and CrowdSec reads them back without a host bind mount.

Compose stack
#

Before the first deployment, generate CROWDSEC_FIREWALL_BOUNCER_API_KEY and CROWDSEC_TRAEFIK_BOUNCER_API_KEY and store them in your secret manager (for example Ansible Vault or a .env file that is never committed). These keys are long-lived shared secrets used by each bouncer to authenticate to CrowdSec LAPI, so create one key per bouncer and rotate them with your regular credential lifecycle.

# Option 1: generate strong random keys yourself (recommended before first compose up)
openssl rand -hex 32  # use as CROWDSEC_FIREWALL_BOUNCER_API_KEY
openssl rand -hex 32  # use as CROWDSEC_TRAEFIK_BOUNCER_API_KEY

# Option 2: let CrowdSec generate and print keys (after crowdsec container is running)
docker exec crowdsec cscli bouncers add firewall-bouncer -o raw
docker exec crowdsec cscli bouncers add traefik-bouncer -o raw

If you use the BOUNCER_KEY_* environment variables shown above, CrowdSec pre-registers those exact values on startup, which is ideal for immutable and repeatable deployments.

The full stack lives in one compose.yaml file:

services:
  crowdsec:
    container_name: crowdsec
    image: crowdsecurity/crowdsec:latest-debian
    restart: unless-stopped
    environment:
      - BOUNCER_KEY_firewall-bouncer=${CROWDSEC_FIREWALL_BOUNCER_API_KEY}
      - BOUNCER_KEY_traefik-bouncer=${CROWDSEC_TRAEFIK_BOUNCER_API_KEY}
      - COLLECTIONS=crowdsecurity/appsec-virtual-patching crowdsecurity/appsec-generic-rules crowdsecurity/appsec-crs crowdsecurity/traefik crowdsecurity/http-cve crowdsecurity/base-http-scenarios crowdsecurity/sshd crowdsecurity/linux
    ports:
      - 7422:7422  # AppSec (WAF)
      - 8080:8080  # LAPI (bouncers)
    volumes:
      - ./crowdsec:/etc/crowdsec
      - /var/log/auth.log:/var/log/auth.log:ro
      - /var/log/journal:/var/log/host:ro
      - /var/log/syslog:/var/log/syslog:ro
      - crowdsec-db:/var/lib/crowdsec/data
      - traefik_logs:/var/log/traefik:ro
    security_opt:
      - no-new-privileges:true

  traefik:
    container_name: traefik
    image: traefik:v3.3
    restart: unless-stopped
    ports:
      - 80:80
      - 443:443
    environment:
      - CF_DNS_API_TOKEN=${CF_DNS_API_TOKEN}
      - CROWDSEC_TRAEFIK_BOUNCER_API_KEY=${CROWDSEC_TRAEFIK_BOUNCER_API_KEY}
    depends_on:
      - crowdsec
    volumes:
      - ./traefik:/etc/traefik
      - traefik_logs:/var/log/traefik

volumes:
  crowdsec-db:
  traefik_logs:

Traefik configuration
#

Static configuration
#

Traefik keeps HTTP-to-HTTPS redirection, dashboard exposure, certificate management, and the CrowdSec plugin in one place.

global:
  checkNewVersion: false
  sendAnonymousUsage: false

log:
  level: INFO
  filePath: /var/log/traefik/traefik.log

accessLog:
  filePath: /var/log/traefik/access.log
  bufferingSize: 0

api:
  dashboard: false
  insecure: false

entryPoints:
  web:
    address: :80
    http:
      redirections:
        entryPoint:
          to: websecure
          scheme: https
  websecure:
    address: :443

providers:
  file:
    directory: /etc/traefik/dynamic
    watch: true

experimental:
  plugins:
    bouncer:
      moduleName: github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
      version: v1.4.1

certificatesResolvers:
  production:
    acme:
      email: your-email@example.com
      storage: /etc/traefik/certs/acme.json
      caServer: https://acme-v02.api.letsencrypt.org/directory
      dnsChallenge:
        provider: cloudflare
        resolvers:
          - 1.1.1.1:53
          - 8.8.8.8:53

Dynamic middleware
#

The dynamic configuration defines the CrowdSec bouncer middleware, security headers, rate limiting, and IP allowlist. The wan-exposed chain applies all protections to WAN traffic, while the default chain allows internal traffic without interference:

http:
  middlewares:
    crowdsec-bouncer:
      plugin:
        bouncer:
          enabled: true
          updateIntervalSeconds: 10
          crowdsecMode: stream
          crowdsecAppsecEnabled: true
          crowdsecAppsecHost: crowdsec:7422
          crowdsecAppsecFailureBlock: true
          crowdsecAppsecUnreachableBlock: true
          crowdsecLapiHost: crowdsec:8080
          crowdsecLapiKey: '{{ env "CROWDSEC_TRAEFIK_BOUNCER_API_KEY" }}'

    default-headers:
      headers:
        frameDeny: true
        browserXssFilter: true
        contentTypeNosniff: true
        forceSTSHeader: true
        stsSeconds: 15552000
        stsIncludeSubdomains: true
        stsPreload: true
        customFrameOptionsValue: SAMEORIGIN

    ratelimit:
      rateLimit:
        average: 100
        burst: 200
        period: 1s

    default-allowlist:
      ipAllowList:
        sourceRange:
          - 127.0.0.1/32   # localhost
          - 172.16.0.0/12  # internal Docker network
          - 192.168.0.0/16 # internal network

    wan-exposed:
      chain:
        middlewares:
          - crowdsec-bouncer
          - default-headers
          - ratelimit

Route all WAN traffic through the wan-exposed chain, while keeping internal services on the default chain with just the allowlist. That way, only traffic that reaches the WAN entry point is subject to CrowdSec detection, security headers, and rate limiting, while internal traffic is unaffected.

CrowdSec configuration
#

Log acquisition
#

CrowdSec reads both Traefik access logs and host logs for a comprehensive view of traffic and system events. The Traefik logs are shared via a Docker volume, while host logs are bind-mounted read-only:

filenames:
  - /var/log/traefik/access.log
labels:
  type: traefik
---
filenames:
  - /var/log/auth.log
  - /var/log/syslog
labels:
  type: syslog
---
source: journalctl
journalctl_filter:
  - "--directory=/var/log/host/"
labels:
  type: syslog
---
appsec_config: crowdsecurity/appsec-default
labels:
  type: appsec
listen_addr: 0.0.0.0:7422
source: appsec

Remediation profiles
#

The default remediation profiles are simple IP bans for both individual IPs and CIDR ranges. You can customize the duration and scope of these bans by editing the profiles in /etc/crowdsec/profiles/ or creating new ones:

name: default_ip_remediation
filters:
  - Alert.Remediation == true && Alert.GetScope() == "Ip"
decisions:
  - type: ban
    duration: 4h
on_success: break
---
name: default_range_remediation
filters:
  - Alert.Remediation == true && Alert.GetScope() == "Range"
decisions:
  - type: ban
    duration: 4h
on_success: break

Firewall bouncer
#

The Traefik plugin blocks HTTP requests, but I still install the kernel-level bouncer on the host.

sudo apt update
sudo apt install -y crowdsec-firewall-bouncer-iptables rsyslog

Enable rsyslog so host logs are available to CrowdSec:

sudo systemctl enable --now rsyslog

Then configure the firewall bouncer to use your local LAPI endpoint and API key in /etc/crowdsec/bouncers/crowdsec-firewall-bouncer-iptables.yaml by updating api_url and api_key:

...
api_url: http://localhost:8080/
api_key: <CROWDSEC_FIREWALL_BOUNCER_API_KEY>
...

Then restart the bouncer service:

sudo systemctl restart crowdsec-firewall-bouncer-iptables

That gives me two independent enforcement points: one in Traefik and one in iptables.

Hub collections
#

The COLLECTIONS variable auto-installs the parsers and scenarios I want on startup.

CollectionWhat it detects
crowdsecurity/traefikTraefik access log parsers and HTTP attack scenarios
crowdsecurity/http-cveExploitation attempts for published HTTP CVEs
crowdsecurity/base-http-scenariosGeneric web scanners and bots
crowdsecurity/appsec-crsOWASP Core Rule Set for AppSec
crowdsecurity/appsec-virtual-patchingVirtual patches for known vulnerabilities
crowdsecurity/appsec-generic-rulesSQLi, XSS, and path traversal rules
crowdsecurity/sshdSSH brute-force and invalid-user attempts
crowdsecurity/linuxGeneral Linux parsers for sudo, PAM, and related events

Validation checklist
#

After deployment, I usually check the stack in this order:

docker exec crowdsec cscli decisions list
docker exec crowdsec cscli bouncers list
docker exec crowdsec cscli hub update && docker exec crowdsec cscli hub upgrade
docker exec crowdsec cscli alerts list --limit 10

The main things I want to confirm are:

  • Traefik is issuing certificates and serving only HTTPS.
  • CrowdSec can read Traefik logs and host logs.
  • The bouncers can talk to the LAPI.
  • Bans show up in both the Traefik plugin and the firewall bouncer.

References
#