# fail2ban with Traefik

> Use fail2ban with Traefik: enable the access log, the traefik-auth filter and its modes, a 404 jail, Docker Compose, DOCKER-USER bans, real IPs and CrowdSec.
>
> Source: https://fail2ban.dev/guides/traefik/ · Updated 2026-10-25 · Checked against fail2ban 1.1.1
> fail2ban.dev is an independent guide, not the official fail2ban project.

To use fail2ban with Traefik, enable Traefik's access log in Common Log Format (`--accesslog=true`, `--accesslog.filepath=/var/log/traefik/access.log`), mount that directory on the host, and enable fail2ban's stock `traefik-auth` jail, which bans clients that fail HTTP basic auth (status 401). If Traefik runs in Docker, add `banaction = iptables-allports` and `chain = DOCKER-USER`, because Docker-forwarded traffic never passes the `INPUT` chain.

```ini
[traefik-auth]
enabled   = true
backend   = auto
logpath   = /var/log/traefik/access.log
banaction = iptables-allports
chain     = DOCKER-USER
maxretry  = 5
bantime   = 1h
```
sudo fail2ban-client reload
sudo fail2ban-client status traefik-auth
```

fail2ban reads files (or the journal); Traefik's access log is off by default and goes to stdout when enabled without a path. A Compose setup for Traefik v3 that writes it to a host directory:

```yaml
services:
  traefik:
    image: traefik:v3.5
    command:
      - --providers.docker=true
      - --providers.docker.exposedbydefault=false
      - --entrypoints.web.address=:80
      - --entrypoints.websecure.address=:443
      - --accesslog=true
      - --accesslog.filepath=/var/log/traefik/access.log
      - --accesslog.format=common
      - --accesslog.filters.statuscodes=400-499
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - /var/log/traefik:/var/log/traefik

  whoami:
    image: traefik/whoami
    labels:
      - traefik.enable=true
      - traefik.http.routers.whoami.rule=Host(`whoami.example.com`)
      - traefik.http.routers.whoami.entrypoints=websecure
      - traefik.http.routers.whoami.tls=true
      - traefik.http.routers.whoami.middlewares=auth
      - traefik.http.middlewares.auth.basicauth.users=admin:$$apr1$$H6uskkkW$$IgXLP6ewTrSuBkTrqE8wj/
```
- `filters.statuscodes=400-499` keeps only client errors, which is all fail2ban needs and keeps the file small. Leave it out if you also want the log for other purposes.
- Don't set `accesslog.bufferingsize`: buffered lines reach the file late, and fail2ban reacts late.
- `$$` escapes `$` in Compose labels; generate your own hash with `htpasswd -nb admin 'secret'`.

Rotate the file on the host with logrotate; Traefik reopens its log file when it receives `USR1`. Pin the Traefik image tag you actually run; the example uses a v3 tag.

## Step 2: the traefik-auth filter and its modes

A failed basic-auth request in Traefik's CLF looks like this (client, user, time, request, status, size, referrer, user agent, request count, router, backend URL, duration):

```log
```

| Mode | Counts 401s… | Use when |
|---|---|---|
| `normal` (default) | with a username (`- admin [`) | You want to catch password guessing only |
| `ddos` | without a username (`- - [`) | You want to catch clients hammering protected URLs without credentials |
| `aggressive` | both | Most setups behind basic auth or forward auth |

```ini
[traefik-auth]
enabled = true
backend = auto
mode    = aggressive
logpath = /var/log/traefik/access.log
```

## Step 3: ban 404 scanners behind Traefik

Traefik's CLF starts like nginx's combined format (client, `-`, user, `[time]`, `"request"`, status), so the generic access-log filter from the [nginx guide](/guides/nginx/) works unchanged:

```ini
[Definition]
failregex = ^<HOST> \S+ \S+ \[\] "[A-Z]+ [^"]*" (?:<codes>)\s
ignoreregex = ^<HOST> \S+ \S+ \[\] "(?:GET|HEAD) /(?:favicon\.ico|robots\.txt|apple-touch-icon\S*\.png)\s
datepattern = ^[^\[]*\[({DATE})
              {^LN-BEG}

[Init]
codes = 401|403|404
```
[traefik-404]
enabled   = true
backend   = auto
filter    = nginx-4xx[codes="404"]
logpath   = /var/log/traefik/access.log
banaction = iptables-allports
chain     = DOCKER-USER
maxretry  = 20
findtime  = 10m
bantime   = 1h
```

## Step 4: make the ban apply to Traefik in Docker

Traefik's ports 80 and 443 are published by Docker, which DNATs them to the container. Packets then traverse `FORWARD`, not `INPUT`, so the default fail2ban ban (iptables `INPUT` or the nftables `input` hook) never sees them. `chain = DOCKER-USER` with `iptables-allports` puts the ban where Docker looks first. Full background, the nftables variant and the startup-order fix: [fail2ban with Docker containers](/guides/docker/).

If Traefik runs with `network_mode: host` or as a binary on the host, traffic does pass `INPUT` — then drop the `banaction`/`chain` lines and use the default action.

## Real client IP: forwardedHeaders and Cloudflare

Traefik writes the client address into the access log's first field. When the request carries an `X-Forwarded-For` header that Traefik kept, the log shows that header's value instead of the TCP peer address. Traefik keeps the header only from addresses listed in the entrypoint's `forwardedHeaders.trustedIPs` and strips it from everyone else, so a client can't fake its logged IP:

```yaml
```

## Test it

```bash
sudo fail2ban-regex /var/log/traefik/access.log 'traefik-auth[mode=aggressive]'

# trigger: wrong password five times from another machine
curl -u admin:wrong https://whoami.example.com/

sudo fail2ban-client status traefik-auth
sudo iptables -L DOCKER-USER -n --line-numbers
```

## fail2ban vs CrowdSec for Traefik

CrowdSec is the other common choice with Traefik. It parses the same access log, but blocks through a Traefik bouncer plugin (a middleware) or a firewall bouncer, and shares attack signals with its community blocklist. fail2ban is a single package with no account and no plugin, bans in the host firewall, and is easy to reason about. If you want ready-made scenarios and community blocklists, CrowdSec fits; if you want a small, local tool you fully control, fail2ban does the job. A full comparison is in [fail2ban vs CrowdSec](/alternatives/crowdsec/).

## The traefik-auth filter reference

{{component:filter-ref:traefik-auth}}

## FAQ

### Does fail2ban work with Traefik?

Yes. Enable Traefik's access log in Common Log Format, write it to a file on the host, and enable the stock `traefik-auth` jail with `logpath` pointing to it. When Traefik runs in Docker, also set `banaction = iptables-allports` and `chain = DOCKER-USER` so bans affect traffic forwarded to the container.

### Where does Traefik write its access log?

Nowhere until you enable it. `--accesslog=true` alone writes to stdout; add `--accesslog.filepath=/var/log/traefik/access.log` and mount `/var/log/traefik` from the host so fail2ban can read the file. The stock jail expects exactly `/var/log/traefik/access.log`.

### What is the difference between traefik-auth modes?

`normal` counts 401 responses that include a username (wrong password), `ddos` counts 401 responses without a username (no credentials sent), and `aggressive` counts both. Set it with `mode = aggressive` in the jail. Because browsers send one credential-less request first, keep `maxretry` at five or higher in ddos or aggressive mode.

### Can fail2ban read Traefik JSON logs?

Not with the stock filter, which expects Common Log Format. Switch to `--accesslog.format=common`, or write a custom filter that matches the JSON fields `ClientHost` and `DownstreamStatus`, test it with `fail2ban-regex`, and set a matching `datepattern`.
