# Block brute force attacks with fail2ban

Install fail2ban to automatically ban IPs that repeatedly fail to log in over SSH, and extend it to your Rails app&#39;s login form.

Hatchbox servers only accept SSH keys, so password guessing can never succeed. Bots still try all day long, though. Every attempt costs a little CPU, fills your auth log, and makes real problems harder to spot.

[fail2ban](https://github.com/fail2ban/fail2ban) watches your logs and temporarily bans any IP that fails to log in too many times. Hatchbox doesn&#39;t install it for you, but it works alongside the UFW firewall Hatchbox configures and keeps working after you update the server in Hatchbox. These steps work on Ubuntu 22.04 and 24.04.

## Install fail2ban

Log in to your server as `root` (the `deploy` user has no sudo password) and run:

```
apt update
apt install -y fail2ban
systemctl enable --now fail2ban
```

Ubuntu&#39;s package enables the SSH jail out of the box, so fail2ban is already watching for failed SSH logins. Check it with:

```
fail2ban-client status sshd
```

## Configure the SSH jail

Don&#39;t edit `/etc/fail2ban/jail.conf` because package upgrades overwrite it. Put your settings in `/etc/fail2ban/jail.local` instead:

```
[DEFAULT]
# Never ban Hatchbox, your own IP, or the other servers in your cluster
ignoreip = 127.0.0.1/8 ::1 138.197.51.75 104.131.53.138 2604:a880:800:14::17a:1000 203.0.113.10

# Ban for 1 hour after 5 failures within 10 minutes
bantime  = 1h
findtime = 10m
maxretry = 5

# Ban repeat offenders for longer each time, up to a week
bantime.increment = true
bantime.maxtime   = 1w

[sshd]
enabled = true
backend = systemd
```

A few notes on these settings:

- **ignoreip** lists addresses that are never banned. Keep the [Hatchbox IP addresses](https://hatchbox.io/docs/articles/8-hatchbox-ip) in there so a failed connection can never lock Hatchbox out of your server and break deploys. Replace `203.0.113.10` with your own IP, and in a multi-server cluster add the private IPs of your other servers, since Hatchbox reaches them through your jump server.
- **bantime**, **findtime** and **maxretry** are the ban length, the window, and the number of failures allowed in that window.
- **bantime.increment** doubles the ban each time the same IP comes back, so persistent bots end up banned for days instead of an hour.
- **backend = systemd** reads SSH logins from the systemd journal, which works whether or not your server writes `/var/log/auth.log`.

If your server uses a custom SSH port, add `port = 2222` (with your port) under `[sshd]`.

Restart fail2ban to load the new settings and check the jail:

```
systemctl restart fail2ban
fail2ban-client status sshd
```

After a few hours on a public server, you&#39;ll usually see something like this:

```
Status for the jail: sshd
|- Filter
|  |- Currently failed: 3
|  |- Total failed:     412
|  `- Journal matches:  _SYSTEMD_UNIT=sshd.service + _COMM=sshd
`- Actions
   |- Currently banned: 27
   |- Total banned:     61
   `- Banned IP list:   45.148.10.73 193.32.162.158 ...
```

## Managing bans

```
# Which jails are running
fail2ban-client status

# Unban an address
fail2ban-client set sshd unbanip 203.0.113.10

# Ban an address by hand
fail2ban-client set sshd banip 203.0.113.10

# Watch fail2ban work
tail -f /var/log/fail2ban.log
```

Bans are temporary, so if you lock yourself out, you can wait out the ban or connect from another IP and unban yourself. Then add your IP to `ignoreip`.

To block an IP permanently, use the firewall rules on the server&#39;s page in Hatchbox rather than fail2ban. Those are applied to UFW and reapplied whenever the server is updated.

## Protecting your app&#39;s login form

fail2ban ships with filters for dozens of services in `/etc/fail2ban/filter.d`. On a Hatchbox server, PostgreSQL, Redis and your other services are not reachable from the internet thanks to the firewall, so SSH is the jail that matters most. The other common target is your application&#39;s login form.

Caddy on Hatchbox doesn&#39;t write per-request access logs, so the simplest approach is to log failed logins from Rails and point fail2ban at that file. This example is for native (non-Docker) clusters, where your app&#39;s `log` folder lives at `/home/deploy/APP_NAME/shared/log`.

First, set up a dedicated logger in your app:

```
# config/initializers/auth_logger.rb
AUTH_LOGGER = Logger.new(Rails.root.join(&quot;log/auth.log&quot;))
```

Then log every failed attempt with the visitor&#39;s IP, for example in your sessions controller:

```
AUTH_LOGGER.warn &quot;Failed login from #{request.remote_ip}&quot;
```

On the server, create a filter at `/etc/fail2ban/filter.d/rails-auth.conf`. `&lt;HOST&gt;` is fail2ban&#39;s placeholder for the IP address:

```
[Definition]
failregex = Failed login from &lt;HOST&gt;\s*$
```

Add a jail for it to `/etc/fail2ban/jail.local`. The wildcard covers every app on the server, and only web traffic is blocked for banned IPs:

```
[rails-auth]
enabled  = true
filter   = rails-auth
logpath  = /home/deploy/*/shared/log/auth.log
port     = http,https
maxretry = 10
findtime = 10m
bantime  = 1h
```

Restart fail2ban, then test the filter against your log to make sure it matches:

```
systemctl restart fail2ban
fail2ban-regex /home/deploy/APP_NAME/shared/log/auth.log /etc/fail2ban/filter.d/rails-auth.conf
```

If your app sits behind Cloudflare or another CDN, Rails logs the visitor&#39;s real IP, but connections to your server come from the CDN. A firewall ban on the visitor&#39;s IP has no effect, so block them at the CDN instead. fail2ban includes a `cloudflare-token` action that bans through Cloudflare&#39;s API, or you can use Cloudflare&#39;s rate limiting rules.
