Traefik v3 ingress and Security Stack - Docker Compose
On this page 13 sections
- Traefik with CrowdSec and Authentik Security Architecture
- Overview
- Architecture Overview
- Prerequisites
- Traefik Configuration
- Dynamic Configuration Files
- CrowdSec Deployment
- Application Configuration
- Testing and Verification
- Security Hardening Recommendations
- Troubleshooting
- Maintenance and Monitoring
- References
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:
- CrowdSec (Edge): Real-time attack detection and IP-based remediation
- Authentik: SSO and application-level authentication
- 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
traefiknetwork - 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 fromX-Forwarded-Forheaders.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
-
Deploy CrowdSec:
docker compose up -d crowdsec crowdsec-bouncer -
Generate Bouncer API Key:
docker exec crowdsec cscli bouncers add traefik-bouncerCopy the output key and update the
CROWDSEC_BOUNCER_API_KEYenvironment variable. -
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
traefikNetwork: Allows Traefik to route incoming requests. Only the app container is exposed to this network.bookstack-internalNetwork: Isolated backend communication. The database is completely unreachable from the Internet or Traefik container.
Testing and Verification
Verify Real IP Transparency
-
Check Whoami Output:
# Access your whoami service curl https://whoami.fullmetaled.comVerify that
X-Forwarded-Forheader contains your actual client IP, not Cloudflare's edge node IP. -
Verify Bouncer is Receiving Real IP:
docker logs -f crowdsec-bouncer | grep ipConfirm the log entries show your actual IP, not a Cloudflare IP.
Test CrowdSec Banning
-
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" -
Attempt Access:
From the banned IP, try accessing a protected service. You should receive an HTTP 403 Forbidden. -
Verify in Bouncer Logs:
docker logs -f crowdsec-bouncerYou should see
"status":403for the banned IP. -
Remove Ban:
docker exec crowdsec cscli decisions delete --ip 14.202.153.198
Test Authentik Authentication
- Access a service configured with the
secure-access@filemiddleware. - If not already authenticated, you should be redirected to the Authentik login page.
- 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:
- Verify LAPI URL:
docker logs crowdsec-bouncer | grep "error" - Ensure bouncer is on the correct network:
docker network inspect crowdsec-internal - 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:
- Verify
whoamioutput shows your actual IP inX-Forwarded-Forheader. - Update Traefik static config to include all Cloudflare IP ranges.
- Update bouncer
TRUSTED_PROXIESenvironment variable. - 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:
- Verify collections:
docker exec crowdsec cscli collections list - Verify log acquisition:
docker exec crowdsec cscli config show --full | grep -A 5 "acquisitions" - 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)