# 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/.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 … @ ""`; 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*.