Initial version of fail2ban-webui

Web interface to view, ban and unban fail2ban IPs on several hosts via
SSH, generalised from the private f2b-web tool.

- Web UI (Flask/Waitress): hosts.yaml, local/OIDC/proxy authentication,
  English/German UI, origin map, optional Loki/Grafana/GeoIP, CLI helpers
- Agent f2b-webctl with install.sh/uninstall.sh (dedicated user + sudo,
  from= restriction, protected networks, manual jail)
- Hardened Docker image, examples, documentation, smoke test, testbed

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
schiwagoaandClaude Opus 5.5 committed 2026-10-25 15:38:17 +01:00
commit 2e3f79236c
44 files changed
+4280

No files matched your search

+150
View File
@@ -0,0 +1,150 @@
# Operations
All commands are run in the directory with `docker-compose.yaml` (e.g. `/opt/fail2ban-webui`).
## Status and logs
```bash
docker compose ps # Up … (healthy)
docker compose logs --tail 100 fail2ban-webui
docker compose logs -f fail2ban-webui | grep -E 'AUDIT|LOGIN|ERROR|WARNING'
docker compose run --rm fail2ban-webui check # every agent reachable?
```
Log lines of interest:
| Line | Meaning |
|---|---|
| `fail2ban-webui 1.0.0 – hosts: … – auth: … – loki: … – geoip: …` | start summary |
| `AUDIT {…}` | ban/unban with user, client IP, hosts and result |
| `LOGIN_OK` / `LOGIN_FAILED` / `LOGIN_LOCKED` / `LOGOUT` | sign-in events |
| `OIDC_FAILED` / `OIDC_DENIED` | SSO errors / account not allowed |
The audit log is also kept in `data/audit.log` (one JSON object per line):
```bash
tail -n 20 data/audit.log | python3 -m json.tool --json-lines
```
On the hosts every agent action is in syslog:
```bash
journalctl -t f2b-webctl --since today
```
## Update
```bash
cp -a .env docker-compose.yaml config /backups/fail2ban-webui_$(date +%F)/ # or your backup location
docker compose pull
docker compose up -d
docker compose ps
docker compose run --rm fail2ban-webui check
```
Read the [changelog](../CHANGELOG.md) first – if it mentions a new agent version, update
the agents as well (re-run `install.sh` with the same options, see [agent.md](agent.md#update)).
The `1` tag only moves within the 1.x series; breaking changes get a new major version.
Rebuild/pull regularly even without new features: the image is rebuilt with current
Debian security updates.
## Backup
| What | Why |
|---|---|
| `.env` | settings, password hash, OIDC secret |
| `config/hosts.yaml` | host list |
| `data/ssh/` | private key and pinned host keys – without them every agent needs the new key |
| `data/audit.log` | audit trail |
| `data/secret_key` | only keeps sessions valid; losing it signs everybody out |
`data/geoip/` is downloaded again automatically.
```bash
tar -cpzf /backups/fail2ban-webui_$(date +%F_%H-%M).tar.gz .env docker-compose.yaml config data
sha256sum /backups/fail2ban-webui_*.tar.gz | tail -n1
```
The archive contains the private SSH key – store it accordingly.
## Moving to another host
1. `docker compose stop` on the old host, create the archive as above.
2. Copy it, unpack with `tar -xpzf` (keeps owner 10050 and permissions).
3. On **every agent** add the new address to `from=` – easiest by re-running
`install.sh … --from <new address>`.
4. `docker compose up -d`, then `check`.
## Rotating the SSH key
```bash
docker compose run --rm fail2ban-webui keygen --force
# on every host:
bash install.sh --key-file new.pub --from … (same options as before)
```
The installer replaces only entries with the same key; remove the old one with
`uninstall.sh` first or delete its line from `authorized_keys` (it contains
`command="…f2b-webctl"`).
## Protecting the login with fail2ban
The UI locks a client after 5 failed logins itself. To ban persistent attackers on the
firewall, let fail2ban on the **proxy host** read the container log.
`/etc/fail2ban/filter.d/fail2ban-webui.conf`
```ini
[Definition]
# Docker json-file log: {"log":"… WARNING webui: LOGIN_FAILED user=x ip=203.0.113.7\n",…}
# user names never contain spaces (they are sanitised), so only the real " ip=" matches
failregex = LOGIN_FAILED user=\S* ip=<HOST>
LOGIN_LOCKED ip=<HOST>
ignoreregex =
```
`/etc/fail2ban/jail.d/fail2ban-webui.local`
```ini
[fail2ban-webui]
enabled = true
filter = fail2ban-webui
backend = polling
logpath = /var/lib/docker/containers/*/*-json.log
maxretry = 5
findtime = 1h
bantime = 1d
# the proxy runs in Docker? Then ban in the DOCKER-USER chain:
# banaction = iptables-allports[chain=DOCKER-USER]
```
```bash
fail2ban-client reload
fail2ban-regex /var/lib/docker/containers/*/*-json.log /etc/fail2ban/filter.d/fail2ban-webui.conf
```
This only works when the container sees the real client address (`TRUSTED_PROXY_HOPS`
set correctly). User names are logged with spaces and control characters replaced, so a
crafted user name cannot inject a second log line.
## Troubleshooting
| Symptom | Cause / fix |
|---|---|
| Container exits: `… is not writable for UID 10050` | `chown -R 10050:10050 data` |
| Container exits: `AUTH_MODE=local needs AUTH_PASSWORD_HASH` | run `hash-password`, paste the line with single quotes |
| Container exits: `No hosts configured` | `config/hosts.yaml` missing or empty; on Windows/Docker Desktop: the drive cannot be bind-mounted |
| Login always fails after pasting the hash | single quotes missing in `.env` – Compose ate the `$` parts |
| Everybody locked out together / audit shows the proxy IP | `TRUSTED_PROXY_HOPS` not set |
| `Host key verification failed` | host not in `known_hosts` or its key changed → `keyscan`, compare fingerprints |
| `Permission denied (publickey)` | key not installed for that user, wrong `user:` in `hosts.yaml`, wrong `from=` (the address the host sees!), or sshd does not read that `authorized_keys` file |
| `sudo: a password is required` | sudo rule missing → re-run `install.sh` |
| `The agent does not run as root` | forced command without `sudo -n` for a non-root user → re-run `install.sh` |
| `Timeout talking to …` | firewall/port, or the host is slow – raise `SSH_TIMEOUT` |
| `The jail manual-block does not exist on this host` | installed with `--no-jail` or with another `--manual-jail`; check `MANUAL_JAIL` in `/etc/f2b-webctl.conf` |
| Bans have no start/end time | fail2ban older than 0.11 (no `--with-time`) |
| Banned IP still reaches a Docker service | banaction blocks only `INPUT` – see [Docker hosts](agent.md#docker-hosts) |
| OIDC: `redirect_uri` mismatch | provider must allow exactly `<PUBLIC_URL>/auth/callback` |
| OIDC: `mismatching_state` | session cookie lost between redirect and callback: `PUBLIC_URL` host differs from the one in the browser, or `https` without `Secure`/proxy headers |
| OIDC: "account … is not allowed" | user not in `OIDC_ALLOWED_GROUPS`; check that the provider sends the `groups` claim |