The blocklist import service targets a dedicated jail called
'blocklist-import' (BLOCKLIST_JAIL constant in blocklist_service.py),
but that jail was never defined in the dev fail2ban configuration.
Every import attempt immediately failed with UnknownJailException.
Add Docker/fail2ban-dev-config/fail2ban/jail.d/blocklist-import.conf:
a manual-ban jail with no log-based detection that accepts banip
commands only, using iptables-allports with a 1-week bantime.
Also track the new file in .gitignore (whitelist) and fix a
pre-existing blank-line-with-whitespace lint error in setup_service.py.
- Implement ban model, service, and router endpoints in backend
- Add ban table component and dashboard integration in frontend
- Update ban-related types and API endpoints
- Add comprehensive tests for ban service and dashboard router
- Update documentation (Features, Tasks, Architecture, Web-Design)
- Clean up old fail2ban configuration files
- Update Makefile with new commands
Two root causes:
1. Docker/compose.debug.yml volume mount ./Docker/logs was already
correct (./logs) — no change needed there.
2. Docker/logs/access.log did not exist on first checkout because
*.log is gitignored. fail2ban fails to start if the file is absent.
Fix: touch Docker/logs/access.log and auth.log in the Makefile 'up'
target so both stub files are always created before the stack starts,
regardless of whether they were previously generated by simulation scripts.
Task 1 — fix Stop/Reload Jail returning 404
Root cause: reload_jail and reload_all sent an empty config stream
(["reload", name, [], []]). In fail2ban's reload protocol the end-of-
reload phase deletes every jail still in reload_state — i.e. every jail
that received no configuration commands. An empty stream means *all*
affected jails are silently removed from the daemon's runtime, causing
everything touching those jails afterwards (including stop) to receive
UnknownJailException → HTTP 404.
Fixes:
- reload_jail: send ["start", name] in the config stream; startJail()
removes the jail from reload_state so the end phase commits instead of
deletes, and un-idles the jail.
- reload_all: fetch current jail list first, build a ["start", name]
entry for every active jail, then send reload --all with that stream.
- stop_jail: made idempotent — if the jail is already gone (not-found
error) the operation silently succeeds (200 OK) rather than returning
404, matching the user expectation that stop = ensure-stopped.
- Router: removed dead JailNotFoundError handler from stop endpoint.
391 tests pass (2 new), ruff clean, mypy clean (pre-existing
config.py error unchanged).
Task 2 — access list simulator
- Docker/simulate_accesses.sh: writes fake HTTP-scan log lines in
custom format (bangui-access: http scan from <IP> ...) to
Docker/logs/access.log so the bangui-access jail detects them.
- fail2ban/filter.d/bangui-access.conf: failregex matching the above.
- fail2ban/jail.d/bangui-access.conf: polling jail on access.log,
same settings as bangui-sim (maxretry=3, bantime=60s).
- .gitignore: whitelist new bangui-access.conf files.
- Docker/fail2ban-dev-config/README.md: added "Testing the Access
List Feature" section with step-by-step instructions and updated
Configuration Reference + Troubleshooting.
The backend container mounted fail2ban-dev-config as an anonymous named
volume, while the fail2ban container used a bind-mount of the same local
directory. The backend's /config was therefore always empty, causing
sqlite3.OperationalError when ban_service attempted to open the path
returned by 'get dbfile' (/config/fail2ban/fail2ban.sqlite3).
Change the backend volume declaration from the named volume reference
to the same bind-mount used by fail2ban:
fail2ban-dev-config:/config:ro → ./fail2ban-dev-config:/config:ro
Also removes the now-unused 'fail2ban-dev-config' named-volume entry.
Affected endpoints (all returned HTTP 500, now return HTTP 200):
GET /api/dashboard/bans
GET /api/dashboard/accesses
GET /api/dashboard/bans/by-country
fetchAccesses was passing the hardcoded absolute path /api/dashboard/accesses
to get(), which prepends BASE_URL (/api), producing /api/api/dashboard/accesses.
Added ENDPOINTS.dashboardAccesses and switched to use it, consistent with every
other function in dashboard.ts.
Vite runs inside the frontend container where 'localhost' resolves to
the container itself, not the backend. Change the /api proxy target
from http://localhost:8000 to http://backend:8000 so the request is
routed to the backend service over the compose network.
- Add SetupGuard component: redirects to /setup if setup not complete,
shown as spinner while loading. All routes except /setup now wrapped.
- SetupPage redirects to /login on mount when setup already done.
- Fix async blocking: offload bcrypt.hashpw and bcrypt.checkpw to
run_in_executor so they never stall the asyncio event loop.
- Hash password with SHA-256 (SubtleCrypto) before transmission; added
src/utils/crypto.ts with sha256Hex(). Backend stores bcrypt(sha256).
- Add Makefile with make up/down/restart/logs/clean targets.
- Add tests: _check_password async, concurrent bcrypt, expired session,
login-without-setup, run_setup event-loop interleaving.
- Update Architekture.md and Features.md to reflect all changes.