Tech Expert & Vibe Coder

With 15+ years of experience, I specialize in self-hosting, AI automation, and Vibe Coding – building applications using AI-powered tools like Google Antigravity, Dyad, and Cline. From homelabs to enterprise solutions.

Debugging Traefik v3 middleware chain conflicts when combining authelia SSO with crowdsec IP filtering for exposed services

Why I Needed This

I run several self-hosted services through Traefik v3 on my Proxmox setup. Some of them need to be accessible from outside my network—things like my n8n instance for webhook triggers, a few monitoring endpoints, and a personal wiki I access from different locations.

The problem: I wanted both strong authentication (Authelia for SSO) and automatic protection against brute force attempts (CrowdSec). Sounds straightforward until you try to chain them together in Traefik v3 and things stop working in confusing ways.

What pushed me to actually debug this was a Saturday morning where I couldn’t access my own n8n instance. No error page, no auth prompt—just timeouts. The logs showed both middlewares firing, but somehow the request never made it through.

My Actual Setup

Here’s what I was working with:

  • Traefik v3.0 running in a Docker container on Proxmox
  • Authelia 4.38 for SSO, also containerized
  • CrowdSec bouncer integrated via traefik-crowdsec-bouncer plugin
  • Dynamic configuration through Docker labels
  • Services behind Traefik: n8n, Synology DSM proxy, Uptime Kuma

My initial middleware chain looked like this in the Docker labels:

traefik.http.routers.n8n.middlewares=crowdsec-bouncer@file,authelia@file

The idea was simple: check the IP with CrowdSec first, then authenticate with Authelia. If the IP was banned, reject immediately. If not, require login.

What Broke and How

The first sign of trouble was inconsistent behavior. Sometimes I’d get the Authelia login page. Sometimes I’d get a blank response. Sometimes it worked fine for ten minutes, then stopped.

I spent way too long looking at the wrong things—checking DNS, verifying certificates, restarting containers. The actual problem was in how Traefik v3 handles middleware order and response manipulation.

The Header Conflict

CrowdSec’s bouncer plugin modifies headers as part of its decision process. It adds custom headers to track decisions and sometimes injects redirect responses for captcha challenges.

Authelia also manipulates headers—specifically the X-Forwarded-* headers and authentication state headers. When both middlewares tried to modify headers in sequence, Traefik v3’s stricter header handling caused silent failures.

The logs showed both middlewares executing, but the request context got corrupted somewhere between them. Traefik would then timeout waiting for a clean response.

The Redirect Loop

When I reversed the order (Authelia first, then CrowdSec), I hit a different problem: redirect loops.

Authelia would redirect unauthenticated users to its login portal. CrowdSec would see that redirect, treat it as a new request, and apply its own logic. If the IP had any captcha requirement, it would inject its own redirect. Traefik would then try to honor both redirects, and the whole thing collapsed.

The ForwardAuth Timing Issue

Both Authelia and CrowdSec use ForwardAuth mechanisms. Authelia sends the request to its auth endpoint. CrowdSec’s bouncer does something similar with its decision API.

In Traefik v3, when you chain two ForwardAuth middlewares, the second one doesn’t always receive the modified request from the first. It gets the original request context. This meant CrowdSec was making decisions without seeing Authelia’s authentication state, and vice versa.

What Actually Worked

I tried three different approaches before finding something stable.

Attempt 1: Separate Middleware Chains (Failed)

I created separate routers with different middleware chains based on the service. Public-facing services got CrowdSec only. Internal services got Authelia only.

This worked but defeated the purpose. I wanted both protections on the same services.

Attempt 2: Custom Middleware Order with Priorities (Partial Success)

Traefik v3 lets you set priorities on routers, but not on individual middlewares within a chain. I tried using router priorities to control execution order:

traefik.http.routers.n8n-crowdsec.priority=100
traefik.http.routers.n8n-crowdsec.middlewares=crowdsec-bouncer@file
traefik.http.routers.n8n-auth.priority=50
traefik.http.routers.n8n-auth.middlewares=authelia@file

This created two separate routing decisions, which broke path matching and made the whole setup fragile. Not practical.

Attempt 3: CrowdSec at Network Level (What I Use Now)

The solution that actually works: move CrowdSec’s IP filtering to the network layer instead of the application middleware layer.

I configured CrowdSec’s firewall bouncer directly on the Proxmox host running Traefik. This bouncer integrates with iptables and blocks banned IPs before they even reach Traefik.

My current setup:

  • CrowdSec agent runs on the Proxmox host, monitoring logs from all containers
  • Firewall bouncer blocks IPs at the iptables level
  • Traefik only handles Authelia middleware for authentication
  • No middleware chain conflicts because CrowdSec never touches Traefik’s request handling

Docker labels now look like this:

traefik.http.routers.n8n.middlewares=authelia@file
traefik.http.routers.n8n.rule=Host(`n8n.yourdomain.com`)

Simple, clean, and it actually works reliably.

The Configuration Details

On the Proxmox host, I installed the CrowdSec firewall bouncer:

apt install crowdsec-firewall-bouncer-iptables

The bouncer config (/etc/crowdsec/bouncers/crowdsec-firewall-bouncer.yaml):

mode: iptables
pid_dir: /var/run/
update_frequency: 10s
daemonize: true
log_mode: file
log_dir: /var/log/
log_level: info
api_url: http://localhost:8080
api_key: YOUR_BOUNCER_KEY
disable_ipv6: false
deny_action: DROP
deny_log: false

CrowdSec agent is configured to parse Traefik’s access logs. In /etc/crowdsec/acquis.yaml:

filenames:
  - /var/log/traefik/access.log
labels:
  type: traefik

Traefik’s static config mounts the log directory:

volumes:
  - /var/log/traefik:/var/log/traefik

And enables access logging in traefik.yml:

accessLog:
  filePath: "/var/log/traefik/access.log"
  format: json

What I Learned About Traefik v3 Middleware

Traefik v3 changed how middlewares interact compared to v2. The execution model is stricter about header modifications and response handling.

Key behaviors I observed:

  • ForwardAuth middlewares in a chain don’t see each other’s modifications to the request context
  • Header conflicts between middlewares cause silent failures, not explicit errors
  • Redirect responses from one middleware can prevent subsequent middlewares from executing
  • The order matters more than it did in v2, but ordering alone doesn’t solve conflicts

The documentation doesn’t make this clear. I only figured it out by enabling debug logging and watching the request flow through each middleware.

Debug Logging That Helped

In Traefik’s static config:

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

This showed me exactly where requests were getting stuck. The pattern was always the same: first middleware executes, modifies something, second middleware receives the original request, tries to modify the same thing, Traefik gives up.

Current Limitations

This setup works but has trade-offs:

CrowdSec decisions are slower to update. The firewall bouncer polls the CrowdSec API every 10 seconds. If an IP gets banned, there’s up to a 10-second window where it can still reach Traefik. With the plugin approach, decisions were instant.

No per-service CrowdSec rules. The firewall bouncer blocks at the host level, not per-service. If I want different CrowdSec policies for different services, I can’t do that anymore. Everything uses the same ban lists.

Lost some visibility. When CrowdSec was a Traefik middleware, I could see its decisions in Traefik’s access logs. Now those decisions happen before Traefik sees the request, so they don’t show up in the same logs. I have to check CrowdSec’s logs separately.

Captcha challenges don’t work. CrowdSec can present captcha challenges before blocking an IP. The firewall bouncer can’t do this—it just blocks. I lost that graduated response capability.

What I’d Do Differently

If I were starting fresh, I’d design around this limitation from the beginning instead of trying to make the middleware chain work.

Specifically:

  • Run CrowdSec’s agent and firewall bouncer from day one, not as an afterthought
  • Keep Traefik’s middleware chain simple—one authentication middleware per service, nothing else
  • Accept that network-level and application-level protections serve different purposes and shouldn’t be mixed in the same layer

I also would have read Traefik v3’s migration guide more carefully. It mentions middleware behavior changes but doesn’t emphasize how breaking they can be for complex chains. I assumed my v2 configs would mostly work. They didn’t.

When Middleware Chains Still Make Sense

Not every middleware combination has these problems. I successfully chain other middlewares without issues:

  • Rate limiting + Authelia: works fine
  • Headers modification + Authelia: works fine
  • Compression + anything: works fine

The conflict is specific to combining two ForwardAuth-based middlewares that both modify headers and can inject redirects. If your middlewares don’t do both of those things, you’re probably fine.

Key Takeaways

Middleware chains in Traefik v3 are powerful but fragile when combining complex authentication and security layers.

If you’re trying to use both Authelia and CrowdSec:

  • Don’t chain them as Traefik middlewares—it looks like it should work but fails in subtle ways
  • Put CrowdSec at the network/firewall level instead
  • Keep Traefik’s middleware chain focused on application-level concerns only
  • Enable debug logging before you waste time guessing what’s wrong

The separation between network-level security (CrowdSec via firewall) and application-level authentication (Authelia via middleware) is cleaner anyway. Each tool operates in its proper domain.

I wish I’d understood this before spending a weekend debugging middleware chains.

Leave a Comment

Your email address will not be published. Required fields are marked *