# fail2ban on OpenWrt

> Run fail2ban on an OpenWrt router: install it, log to a file, use nftables, add dropbear and LuCI jails, and see when banIP's log monitor fits better.
>
> Source: https://fail2ban.dev/guides/openwrt/ · Updated 2026-10-26 · Checked against fail2ban 1.1.1
> fail2ban.dev is an independent guide, not the official fail2ban project.

Yes, fail2ban runs on OpenWrt: install the `fail2ban` package with `opkg install fail2ban` (or `apk add fail2ban` on releases that use apk). It pulls in Python 3, so it fits x86 and larger ARM routers better than small flash devices. Three changes make it work. Write the system log to a file, because fail2ban can't read OpenWrt's in-memory log. Set `banaction = nftables`, because the package's defaults expect iptables. Then enable the stock `dropbear` jail, plus a small custom filter for LuCI. On small routers, **banIP**'s built-in log monitor does the same job without Python.

## First: do you need it at all?

OpenWrt's default firewall rejects connections from the WAN to the router itself, so dropbear (SSH) and LuCI are only reachable from your LAN. If you haven't opened them to the internet, nobody outside can brute-force them, and fail2ban has nothing to do. The better way to manage a router remotely is a VPN such as WireGuard, with SSH and LuCI left closed on WAN. fail2ban is worth it when you have opened port 22 or LuCI to the internet, or don't trust every device on your LAN.

## Install the fail2ban package

```bash
opkg install fail2ban
# OpenWrt releases that use apk:
# apk update && apk add fail2ban
```

- A procd init script, `/etc/init.d/fail2ban`. It is enabled at install and restarts the daemon when files in `/etc/fail2ban` change.
- A firewall include that restarts fail2ban whenever the firewall reloads, so its rules come back.
- A ban database in `/var/lib/fail2ban/`. On OpenWrt `/var` is in RAM, which spares the flash, but bans don't survive a reboot.

## Step 1: write the log to a file

OpenWrt's `logd` keeps the system log in a ring buffer that you read with `logread`. There is no `/var/log/auth.log`, and fail2ban can only follow files or the systemd journal. Ask `logd` to also write a file:

```bash
uci commit system
/etc/init.d/log restart
```

```log
Sun Oct 25 18:36:12 2026 authpriv.warn dropbear[2101]: Bad password attempt for 'root' from 203.0.113.42:51234
Sun Oct 25 18:36:14 2026 authpriv.warn dropbear[2101]: Login attempt for nonexistent user from 198.51.100.7:40122
Sun Oct 25 18:40:05 2026 daemon.err uhttpd[1873]: luci: failed login on /admin for admin from 203.0.113.42
```

fail2ban has no LuCI filter. LuCI logs `luci: failed login on <path> for <user> from <IP>` through syslog. Depending on the release, the tag is `user.info :` or `daemon.err uhttpd[…]:`. This filter accepts both:

```ini
[Definition]
failregex = ^\s*\w+\.\w+(?: [\w.-]+(?:\[\d+\])?)?\s*: luci: failed login on \S* for <F-USER>\S+</F-USER> from <HOST>\s*$
ignoreregex =
```

## Step 3: the jails

```ini
[DEFAULT]
# OpenWrt 22.03+ uses nftables (firewall4); the package's jail.conf expects iptables
banaction          = nftables
banaction_allports = nftables[type=allports]
backend            = auto
# never ban yourself: LAN and loopback
ignoreip           = 127.0.0.1/8 ::1 192.168.1.0/24
bantime            = 1h
findtime           = 10m
maxretry           = 5

[dropbear]
enabled = true
port    = 22
logpath = /var/log/messages

[luci]
enabled  = true
filter   = luci
port     = 80,443
logpath  = /var/log/messages
```

```bash
fail2ban-client status dropbear
fail2ban-client status luci
nft list table inet f2b-table
```

## banIP: the lightweight alternative

**banIP** is OpenWrt's own blocking package, written in shell with nftables sets, so it needs no Python. Besides blocklist feeds, it has a background log monitor that reads `logread` and bans addresses that cause failed logins, which is the same job fail2ban does here. Its default search terms are `Exit before auth from` (dropbear) and `luci: failed login`. More patterns for sshd, Asterisk and others are listed in its README.

```bash
uci set banip.global.ban_enabled='1'
uci commit banip
/etc/init.d/banip start
/etc/init.d/banip status
```

| Feature | fail2ban | banIP log monitor |
|---|---|---|
| Runtime | Python 3 daemon | Shell + nftables, no Python |
| Flash needed | Several MB | Small |
| Matching | Full regex filters with counting windows (`maxretry`/`findtime`) | Search terms, count per blocking cycle (`ban_logcount`) |
| Repeat offenders | [recidive jail](/config/recidive/), incremental bans | Blocklist entries with expiry, optional subnet blocking |
| Also blocks by feeds and countries | No | Yes |
| Best for | x86 or larger routers, custom services | Most OpenWrt devices |

Older scripts such as **bearDropper** also parse dropbear logs, but check that the version you find supports your OpenWrt release before using it. banIP is maintained in the official package feed.

## FAQ

### Is fail2ban available for OpenWrt?

Yes. `fail2ban` is in the official OpenWrt package feed: `opkg install fail2ban`, or `apk add fail2ban` on releases that use apk. It brings Python 3 and nftables with it. Before it can ban anything, set `log_file` in the system config so there is a log to read, and set `banaction = nftables`.

### Does fail2ban work with dropbear?

Yes. fail2ban ships a `dropbear` filter that matches `Bad password attempt for '<user>' from <IP>`, `Login attempt for nonexistent user from <IP>` and the `Max auth tries reached` exit. On OpenWrt, enable the `[dropbear]` jail with `logpath = /var/log/messages` after configuring `logd` to write that file.

### Should I use banIP or fail2ban on OpenWrt?

On most routers, banIP: it is native, needs no Python, and its log monitor already covers dropbear and LuCI. Choose fail2ban when you have flash and RAM to spare and want regex filters, `maxretry`/`findtime` counting, the recidive jail or filters for other services running on the router.

### How do I see failed LuCI logins on OpenWrt?

Run `logread -e luci` to show LuCI's log lines, or `logread -f` to watch them live. Failed attempts appear as `luci: failed login on / for root from <IP>`, and successful ones as `accepted login`.
