Skip to content
IT related

Traefik v3 ingress and Security Stack - Docker Compose

Updated 4 min read

Traefik with CrowdSec and Authentik Security Architecture

Overview

This document outlines the deployment of a production-grade reverse proxy and security infrastructure using Traefik v3.4, CrowdSec, and Authentik. The architecture provides edge-based DDoS/attack mitigation, identity-based access control, and real IP transparency for proper forensic logging in a medical imaging environment.

Core Security Layers:

  1. CrowdSec (Edge): Real-time attack detection and IP-based remediation
  2. Authentik: SSO and application-level authentication
  3. Traefik: Reverse proxy, TLS termination, and middleware orchestration

Architecture Overview

Internet (Cloudflare)
    ↓
Traefik (Port 80/443)
    ├─→ CrowdSec Bouncer Middleware (ForwardAuth)
    │   └─→ Blocks malicious IPs before auth
    │
    ├─→ Authentik Middleware (ForwardAuth)
    │   └─→ Enforces SSO for protected apps
    │
    └─→ Backend Services (Bookstack, Whoami, etc.)

Prerequisites

  • Docker and Docker Compose
  • Cloudflare SSL certificates (origin certificates)
  • Cloudflare domain with proxying enabled
  • CrowdSec API key (generated via cscli bouncers add)
  • Authentik instance running and accessible on the traefik network
  • Medical imaging workstations with consistent IP ranges (for whitelisting)

Traefik Configuration

Static Configuration (docker-compose.yml)

version: "3.9"

services:
  traefik:
    image: traefik:v3.4
    container_name: traefik
    restart: unless-stopped
    security_opt:
      - no-new-privileges:true

    networks:
      - traefik

    ports:
      - "80:80"
      - "443:443"
      - "8080:8080"

    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./certs:/certs:ro
      - ./dynamic:/dynamic:ro
      - ./logs:/logs

    command:
      # EntryPoints
      - "--entrypoints.web.address=:80"
      - "--entrypoints.web.http.redirections.entrypoint.to=websecure"
      - "--entrypoints.web.http.redirections.entrypoint.scheme=https"
      - "--entrypoints.web.http.redirections.entrypoint.permanent=true"
      - "--entrypoints.websecure.address=:443"
      - "--entrypoints.websecure.http.tls=true"

      # IP Transparency: Trust Cloudflare's forwarded headers
      - "--entrypoints.websecure.forwardedHeaders.trustedIPs=173.245.48.0/20,103.21.244.0/22,103.22.200.0/22,103.31.4.0/22,141.101.64.0/18,108.162.192.0/18,190.93.240.0/20,188.114.96.0/20,197.234.240.0/22,198.41.128.0/17,162.158.0.0/15,104.16.0.0/13,104.24.0.0/14,172.64.0.0/13,131.0.72.0/22"

      # Dynamic Providers (read all YAML files from /dynamic directory)
      - "--providers.file.directory=/dynamic/"
      - "--providers.file.watch=true"

      # Docker Provider
      - "--providers.docker=true"
      - "--providers.docker.exposedbydefault=false"
      - "--providers.docker.network=traefik"

      # API & Dashboard
      - "--api.dashboard=true"
      - "--api.insecure=false"

      # Observability (Required for CrowdSec log parsing)
      - "--log.level=INFO"
      - "--accesslog=true"
      - "--accesslog.format=json"
      - "--accesslog.filepath=/logs/access.log"

    labels:
      # Traefik Dashboard Self-Routing
      - "traefik.enable=true"
      - "traefik.http.routers.dashboard.rule=Host(`traefik.dashboard.fullmetaled.com`)"
      - "traefik.http.routers.dashboard.entrypoints=websecure"
      - "traefik.http.routers.dashboard.service=api@internal"
      - "traefik.http.routers.dashboard.tls=true"

      # Basic Auth for Dashboard
      - "traefik.http.middlewares.dashboard-auth.basicauth.users=admin:$$apr1$$lgiS599s$$dsaecQPIetGM8SQTVd.pN/"
      - "traefik.http.routers.dashboard.middlewares=dashboard-auth@docker"

networks:
  traefik:
    name: traefik

Key Configuration Notes

  • forwardedHeaders.trustedIPs: Cloudflare IP ranges. Critical for extracting the true client IP from X-Forwarded-For headers.
  • file.directory: Watch the entire /dynamic/ directory for YAML files. All files are merged into one virtual configuration.
  • accesslog.filepath: Must point to a shared volume. CrowdSec reads this to detect attacks.

Dynamic Configuration Files

Directory Structure

/dynamic/
├── 01-authentik.yaml      # Authentik middleware definition
├── 02-crowdsec.yaml       # CrowdSec bouncer middleware
├── 03-chains.yaml         # Middleware chains for reuse
└── tls.yaml               # Cloudflare certificate configuration

01-authentik.yaml

http:
  middlewares:
    authentik-auth:
      forwardAuth:
        address: http://authentik:9000/outpost.goauthentik.io/auth/traefik
        trustForwardHeader: true
        authResponseHeaders:
          - X-authentik-username
          - X-authentik-groups
          - X-authentik-entitlements
          - X-authentik-email
          - X-authentik-name
          - X-authentik-uid
          - X-authentik-jwt
          - X-authentik-meta-jwks
          - X-authentik-meta-outpost
          - X-authentik-meta-provider
          - X-authentik-meta-app
          - X-authentik-meta-version

02-crowdsec.yaml

http:
  middlewares:
    crowdsec-bouncer:
      forwardAuth:
        address: http://crowdsec-bouncer:8080/api/v1/forwardAuth
        trustForwardHeader: true

03-chains.yaml

http:
  middlewares:
    # Complete security stack: CrowdSec first (edge protection),
    # then Authentik (identity verification)
    secure-access:
      chain:
        middlewares:
          - crowdsec-bouncer
          - authentik-auth

tls.yaml

tls:
  certificates:
    - certFile: /certs/origin.pem
      keyFile: /certs/privatekey.pem

CrowdSec Deployment

docker-compose.yml (CrowdSec Stack)

version: "3.9"

services:
  crowdsec:
    image: crowdsecurity/crowdsec
    container_name: crowdsec
    restart: always
    environment:
      COLLECTIONS: "crowdsecurity/traefik crowdsecurity/http-cve crowdsecurity/whitelist-good-actors"
      GID: "1000"
    volumes:
      - ./crowdsec-db:/var/lib/crowdsec/data/
      - ./crowdsec-config:/etc/crowdsec/
      - /home/ed/docker/traefik/logs:/remotelogs/traefik:ro
    networks:
      - crowdsec-internal
    restart: unless-stopped

  crowdsec-bouncer:
    image: fbonalair/traefik-crowdsec-bouncer
    container_name: crowdsec-bouncer
    environment:
      # CrowdSec Local API endpoint (internal Docker DNS)
      - CROWDSEC_AGENT_URL=http://crowdsec:8080
      # API key generated via: docker exec crowdsec cscli bouncers add traefik-bouncer
      - CROWDSEC_BOUNCER_API_KEY=YOUR_KEY_HERE
      # Log level for debugging
      - CROWDSEC_BOUNCER_LOG_LEVEL=INFO
      # Trust internal Docker and Cloudflare IPs to pass client headers
      - TRUSTED_PROXIES=172.16.0.0/12,173.245.48.0/20,103.21.244.0/22,103.22.200.0/22,103.31.4.0/22,141.101.64.0/18,108.162.192.0/18,190.93.240.0/20,188.114.96.0/20,197.234.240.0/22,198.41.128.0/17,162.158.0.0/15,104.16.0.0/13,104.24.0.0/14,172.64.0.0/13,131.0.72.0/22
    networks:
      - traefik
      - crowdsec-internal
    restart: unless-stopped

networks:
  traefik:
    external: true
  crowdsec-internal:
    driver: bridge

CrowdSec Acquisition Configuration

Create ./crowdsec-config/acquis.yaml:

filenames:
  - /remotelogs/traefik/*.log
labels:
  type: traefik

This tells CrowdSec to monitor Traefik's access logs and detect malicious patterns.

Initial Setup Steps

  1. Deploy CrowdSec:

    docker compose up -d crowdsec crowdsec-bouncer
    
  2. Generate Bouncer API Key:

    docker exec crowdsec cscli bouncers add traefik-bouncer
    

    Copy the output key and update the CROWDSEC_BOUNCER_API_KEY environment variable.

  3. Restart Bouncer:

    docker compose up -d crowdsec-bouncer
    

Application Configuration

Example: Bookstack with Dual-Network Architecture

version: "3.9"

services:
  bookstack:
    image: lscr.io/linuxserver/bookstack
    container_name: bookstack
    environment:
      - PUID=1000
      - PGID=1000
      - APP_URL=https://docs.fullmetaled.com
      - DB_HOST=bookstack_db
      - DB_PORT=3306
      - DB_USER=bookstack
      - DB_PASS=YOUR_PASSWORD
      - DB_DATABASE=bookstackapp
    volumes:
      - ./bookstack_app_data:/config
    restart: always
    depends_on:
      - bookstack_db
    networks:
      - traefik            # For Traefik to route requests
      - bookstack-internal # For internal DB communication
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.bookstack.rule=Host(`docs.fullmetaled.com`)"
      - "traefik.http.routers.bookstack.entrypoints=websecure"
      - "traefik.http.routers.bookstack.tls=true"
      - "traefik.http.services.bookstack.loadbalancer.server.port=80"
      # Specify which network Traefik should use
      - "traefik.docker.network=traefik"
      # Apply security chain: CrowdSec → Authentik
      - "traefik.http.routers.bookstack.middlewares=secure-access@file"

  bookstack_db:
    image: lscr.io/linuxserver/mariadb
    container_name: bookstack_db
    environment:
      - PUID=1000
      - PGID=1000
      - MYSQL_ROOT_PASSWORD=YOUR_PASSWORD
      - MYSQL_DATABASE=bookstackapp
      - MYSQL_USER=bookstack
      - MYSQL_PASSWORD=YOUR_PASSWORD
    volumes:
      - ./bookstack_db_data:/config
    networks:
      - bookstack-internal # Database is isolated from proxy
    restart: always

networks:
  traefik:
    external: true
  bookstack-internal:
    driver: bridge

Network Architecture Benefits

  • traefik Network: Allows Traefik to route incoming requests. Only the app container is exposed to this network.
  • bookstack-internal Network: Isolated backend communication. The database is completely unreachable from the Internet or Traefik container.

Testing and Verification

Verify Real IP Transparency

  1. Check Whoami Output:

    # Access your whoami service
    curl https://whoami.fullmetaled.com
    

    Verify that X-Forwarded-For header contains your actual client IP, not Cloudflare's edge node IP.

  2. Verify Bouncer is Receiving Real IP:

    docker logs -f crowdsec-bouncer | grep ip
    

    Confirm the log entries show your actual IP, not a Cloudflare IP.

Test CrowdSec Banning

  1. Add Manual Decision (Ban):

    # Replace 14.202.153.198 with your test IP
    docker exec crowdsec cscli decisions add --ip 14.202.153.198 --reason "manual verification"
    
  2. Attempt Access:
    From the banned IP, try accessing a protected service. You should receive an HTTP 403 Forbidden.

  3. Verify in Bouncer Logs:

    docker logs -f crowdsec-bouncer
    

    You should see "status":403 for the banned IP.

  4. Remove Ban:

    docker exec crowdsec cscli decisions delete --ip 14.202.153.198
    

Test Authentik Authentication

  1. Access a service configured with the secure-access@file middleware.
  2. If not already authenticated, you should be redirected to the Authentik login page.
  3. Upon successful login, you should be able to access the service.

Security Hardening Recommendations

1. Whitelist Medical Workstations

If your radiology workstations have static IPs, whitelist them in CrowdSec to prevent accidental bans:

docker exec crowdsec cscli whitelist add --ip 192.168.1.100 --reason "Radiology workstation"

2. Rotate Secrets Regularly

  • CrowdSec API keys
  • Authentik admin credentials
  • Database passwords

Store these in a secrets management system (e.g., HashiCorp Vault, Kubernetes Secrets) rather than in compose files.

3. Monitor Alert Logs

CrowdSec generates alerts for detected threats. Monitor these regularly:

docker exec crowdsec cscli alerts list

4. Keep Collections Updated

CrowdSec's threat intelligence is delivered via "Collections." Update them periodically:

docker exec crowdsec cscli hub update
docker exec crowdsec cscli collections install crowdsecurity/traefik

5. Log Rotation

Ensure Traefik's access logs are rotated to prevent disk exhaustion:

# Example logrotate config
/home/ed/docker/traefik/logs/*.log {
  daily
  rotate 7
  compress
  delaycompress
  notifempty
  create 0640 ed ed
  sharedscripts
}

Troubleshooting

HTTP 500 Error on Protected Services

Cause: Bouncer cannot reach the CrowdSec LAPI.

Fix:

  1. Verify LAPI URL: docker logs crowdsec-bouncer | grep "error"
  2. Ensure bouncer is on the correct network: docker network inspect crowdsec-internal
  3. Regenerate API key: docker exec crowdsec cscli bouncers delete traefik-bouncer && docker exec crowdsec cscli bouncers add traefik-bouncer

Real IP Shows as Cloudflare Edge IP

Cause: trustedIPs in Traefik or TRUSTED_PROXIES in bouncer are incorrect.

Fix:

  1. Verify whoami output shows your actual IP in X-Forwarded-For header.
  2. Update Traefik static config to include all Cloudflare IP ranges.
  3. Update bouncer TRUSTED_PROXIES environment variable.
  4. Restart both services: docker compose up -d

Authentik Redirect Loop

Cause: Authentik middleware applied to the Authentik service itself.

Fix: Only apply the authentik-auth middleware to application containers, not to Authentik itself. Authentik should be accessible without authentication.

CrowdSec Not Detecting Attacks

Cause: Collections not installed or log acquisition not configured.

Fix:

  1. Verify collections: docker exec crowdsec cscli collections list
  2. Verify log acquisition: docker exec crowdsec cscli config show --full | grep -A 5 "acquisitions"
  3. Check that Traefik logs are being written: ls -lh ./logs/

Maintenance and Monitoring

Daily Checks

  • Monitor Traefik dashboard for router status: https://traefik.dashboard.fullmetaled.com
  • Check CrowdSec alerts: docker exec crowdsec cscli alerts list -o json
  • Verify disk space for log files: df -h ./logs/

Weekly Checks

  • Review CrowdSec decisions: docker exec crowdsec cscli decisions list
  • Check for false positives in Authentik logs: docker logs authentik
  • Verify all app containers are healthy: docker ps -a

Monthly Checks

  • Update CrowdSec collections: docker exec crowdsec cscli hub update
  • Rotate API keys and credentials
  • Test disaster recovery (restore from backups)

References


Document Version: 1.0
Last Updated: January 5, 2026
Author: IT Infrastructure
Environment: Medical Imaging Portal (Production)

IT related RSS feed Published with Oblish

Keyboard shortcuts

/
Search
[
Previous page
]
Next page
?
Show this list
Esc
Close