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>
107 lines
4.0 KiB
Markdown
107 lines
4.0 KiB
Markdown
# Development
|
||
|
||
## Layout
|
||
|
||
```
|
||
app/webui/ Flask application (package "webui")
|
||
app.py routes, security headers, audit log
|
||
auth.py local / OIDC / proxy authentication, login lockout
|
||
backend.py SSH to the agents, GeoIP, Loki
|
||
cli.py serve, hash-password, keygen, keyscan, check
|
||
config.py settings from the environment, hosts.yaml
|
||
i18n.py, locales/ translations (one JSON file per language)
|
||
templates/, static/
|
||
agent/ f2b-webctl, install.sh, uninstall.sh
|
||
docker/Dockerfile production image
|
||
examples/ docker-compose.yaml, .env.example, hosts.yaml.example
|
||
dev/testbed/ web UI + two fail2ban hosts in Docker
|
||
tests/smoke_test.py web UI tests without Docker/SSH
|
||
```
|
||
|
||
## Smoke test (no Docker needed)
|
||
|
||
```bash
|
||
python3 -m venv .venv
|
||
.venv/bin/pip install -r requirements.txt
|
||
.venv/bin/python tests/smoke_test.py
|
||
```
|
||
|
||
The agents are simulated. Covered: login, lockout, CSRF/Origin checks, translations,
|
||
ban/unban validation, audit log, proxy mode, config validation, SSH command line, log
|
||
injection.
|
||
|
||
## Testbed (Docker)
|
||
|
||
`dev/testbed` starts the web UI and two Debian hosts with sshd and fail2ban (dummy ban
|
||
actions – nothing is blocked). The agents are installed with `agent/install.sh` at
|
||
container start:
|
||
|
||
| Host | Agent setup |
|
||
|---|---|
|
||
| `agent1` | user `f2bweb` + sudo, `--from` the testbed network, extra protected IP |
|
||
| `agent2` | user `root`, key in `authorized_keys2`, manual jail named `blocklist` |
|
||
|
||
```bash
|
||
cd dev/testbed
|
||
printf 'DEFAULT_LANG=en\nAUTH_MODE=local\nAUTH_USER=admin\nCACHE_SECONDS=5\n' > .env
|
||
docker compose build
|
||
docker compose run --rm -it webui hash-password >> .env
|
||
docker compose run --rm webui keygen
|
||
docker compose run --rm -T --entrypoint sh webui -c 'cat > /data/hosts.yaml' < hosts.yaml
|
||
docker compose up -d agent1 agent2
|
||
docker compose run --rm webui keyscan --yes
|
||
docker compose run --rm webui check
|
||
docker compose up -d webui # http://127.0.0.1:18080
|
||
docker compose down -v # remove everything
|
||
```
|
||
|
||
`hosts.yaml` is written into the data volume because Docker Desktop cannot bind-mount
|
||
network drives. After recreating an agent its host key changes – run `keyscan` again.
|
||
|
||
On Windows with Git Bash, prefix `docker compose exec … /path` with `MSYS_NO_PATHCONV=1`,
|
||
otherwise the path is rewritten to a Windows path.
|
||
|
||
## Adding a language
|
||
|
||
1. Copy `app/webui/locales/en.json` to `app/webui/locales/<code>.json` (e.g. `fr.json`).
|
||
2. Set `"_name"` to the language's own name (`"Français"`) – it appears in the dropdown.
|
||
3. Translate the values; keep `{placeholders}` unchanged. Missing keys fall back to English.
|
||
4. Run the smoke test and check the UI.
|
||
|
||
## Building the image
|
||
|
||
```bash
|
||
docker build -f docker/Dockerfile --build-arg VERSION=1.0.0 -t fail2ban-webui:1.0.0 .
|
||
# multi-arch, as in CI
|
||
docker buildx build -f docker/Dockerfile --platform linux/amd64,linux/arm64 \
|
||
--build-arg VERSION=1.0.0 -t git.retronerd.at/schiwagoa/fail2ban-webui:1.0.0 --push .
|
||
```
|
||
|
||
Scan before publishing:
|
||
|
||
```bash
|
||
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock aquasec/trivy:0.58.1 \
|
||
image --scanners vuln --ignore-unfixed --severity HIGH,CRITICAL fail2ban-webui:1.0.0
|
||
```
|
||
|
||
## Agent protocol
|
||
|
||
The web UI runs `ssh … <user>@<host> "<command>"`; the agent reads the command from
|
||
`SSH_ORIGINAL_COMMAND` and prints exactly one JSON line:
|
||
|
||
```json
|
||
{"ok": true, "host": "web01", "agent_version": "1.0.0", "manual_jail": "manual-block",
|
||
"jails": {"sshd": {"bans": [{"ip": "203.0.113.7", "start": "2026-10-25 10:00:00",
|
||
"bantime": 600, "end": "2026-10-25 10:10:00"}], "total": 42}}}
|
||
```
|
||
|
||
Errors: `{"ok": false, "error_key": "agent_protected", "error_args": {"ip": "…"}, "error": "English text"}`.
|
||
The web UI translates `error_key` with `locales/*.json` (keys starting with `agent_`);
|
||
unknown keys show the English `error`.
|
||
|
||
## Versioning
|
||
|
||
- Version numbers follow SemVer and are set at build time (`VERSION` build arg →
|
||
`APP_VERSION`); the agent has its own `VERSION` in `agent/f2b-webctl`.
|
||
- Every change goes into `CHANGELOG.md` under *Unreleased*.
|