Introducing caddy-fail2ban

I run quite a few services in my homelab and they have been fronted by nginx basically since the beginning. Recently, I wanted to modernise things a bit and found myself migrating to caddy which meant replacing all the use cases I had developed over the year.

By and large, the migration was super smooth, but one issue I ran into was how to use fail2ban with caddy. After checking with the community it became clear that caddy doesn't have an equivalent to the nginx approach which loads a map dynamically from disk. As suggested in that post, I built my own plug-in caddy-fail2ban.

Motivation

Normally, you wouldn't use HTTP-level blocking, instead relying on iptables to block connections at the IP level. However, if you find yourself behind a reverse proxy which you can't put fail2ban on, then you have a problem, because the TCP/IP connection is coming from that reverse proxy. Here's how that looks:

HAProxy -->   Caddy     --> application
(public)    (private)

So the first proxy here is HAProxy (or really any proxy), but I don't want to deploy fail2ban on it. Instead I leverage the proxy protocol which communicates the remote IP address.

This means we need to allow the connection from HAProxy and then decide based on the IP address it provides.

Design

The design is dead simple: Taking inspiration from the nginx rule, a new fail2ban action writes to a file on disk, simply adding a line per IP. The plug-in reads that file and watches it for updates. As the file changes, it reloads it and maintains a list of banned IPs in memory. The plug-in is a simple matcher so it can be used to match requests. This is what it looks like in your Caddyfile:

@banned {
    fail2ban ./banned-ips
}
handle @banned {
    abort
}

By providing this matcher, the plug-in will monitor the file ./banned-ips and match any request that is from an IP listed in that file. You can then take action like sending back an abort.

Instructions on how to use the plug-in are at https://github.com/Javex/caddy-fail2ban. Please try it out and open an issue if you run into problems. It's very new and I'm sure there's many use cases I've missed.