How to use fail2ban to ban IPs in Caddy instead of iptables?

1. The problem I’m having:

I am in the process of switching my Gitea installation from Nginx to Caddy. As a security measure, I have set up fail2ban to block repeat login attempts. The source of IPs is the application log, the blocking/banning happens inside Nginx at the moment. Normally, this would be at the iptables level and wouldn’t concern the reverse proxy.

However, this particular deployment is actually fronted by two proxies:

Internet -> HAProxy -> Caddy -> Gitea

This creates a situation where banning an IP address will have no effect: Since the TCP connection is between HAProxy & Caddy, banning the public IP on the host where Caddy runs does nothing.

Instead, what I currently do within Nginx is to use the map command to dynamically read a ban file and reject requests at the HTTP level based on banned IPs.

Is this possible with Caddy? The map directive does not seem to support dynamic loading of content like this. I couldn’t find anything else that came close.

2. Error messages and/or full log output:

There are no errors or relevant logs.

3. Caddy version:

v2.7.6 h1:w0NymbG2m9PcvKWsrXO6EEkY9Ru4FJK8uQbYcev1p3A=

4. How I installed and ran Caddy:

a. System environment:

Arch Linx, x64. Running inside a docker container.

b. Command:

docker compose up -d

c. Service/unit/compose file:

      context: .
    restart: unless-stopped
      - "80:80"
      - "443:443"
      - ./Caddyfile:/etc/caddy/Caddyfile
      - ./sites:/etc/caddy/sites
      - ./hosts:/etc/caddy/hosts
      - data:/data
      - config:/config
      - /usr/share/webapps:/srv
      - env
      - reverse-proxy


    name: reverse-proxy

d. My complete Caddy config:

        acme_dns cloudflare {env.CF_API_TOKEN}
} {
        reverse_proxy gitea:3000

5. Links to relevant resources:

Relevant Nginx config:

map  $remote_addr $blck_lst_ses { include; }
server {
  listen 443 ssl http2;


  # With SSL via Let's Encrypt
  ssl_certificate /etc/letsencrypt/live/; # managed by Certbot
  ssl_certificate_key /etc/letsencrypt/live/; # managed by Certbot

  proxy_set_header Host $host;
  proxy_set_header X-Real-IP $remote_addr;
  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  proxy_set_header X-Forwarded-Proto $scheme;
  if ($blck_lst_ses != "") {
      rewrite ^.*$ /banned last;

  location /banned {
        default_type text/html;
        return 403 '<h1>Banned</h1>';


enabled = true
filter = gitea
logpath = /var/lib/gitea/gitea/log/gitea.log
maxretry = 10
findtime = 3600
bantime = 900
action = nginx-block-map
ignoreip =

You just need to trigger a config reload whenever the list is updated.

Or, you could write your own plugin that parses a file and reloads the file contents periodically without reloading the config. Writing plugins for Caddy is pretty trivial. Extending Caddy — Caddy Documentation

You could implement it as a request matcher, then pair it with the abort directive to close the connection if it matches.

If you have another proxy in front of Caddy, remember, you should configure trusted_proxies in Caddy so that it trusts connections from the proxy in front of it, so it can grab the real client IP from X-Forwarded-For. Or since you’re using HAProxy, you could configure PROXY protocol (configure listener wrappers in Caddy)

1 Like

Thanks for the reply! Triggering a reload when the list has changed is a great idea. I’ll look into that.

I was considering writing a plugin, it seemed simple enough so if my dynamic reload idea doesn’t pan out I might try that.

As for HAProxy, I’m in fact already using the PROXY protocol with my current Nginx deployment, but found from the docs that should be simple enough with Caddy.

I’ve gone ahead and created GitHub - Javex/caddy-fail2ban: Fail2ban module for caddy which is a dead simple implementation of the suggestion. It’s the most basic prototype so I wouldn’t expect performance, but it’s a starting point for further improvements.

1 Like