diff --git a/README.md b/README.md index 1fe248e..f13f37c 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ **Enterprise-Grade Intrusion Detection System Management Platform** [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) -[![Go Version](https://img.shields.io/badge/Go-1.24+-00ADD8?logo=go)](https://golang.org/) +[![Go Version](https://img.shields.io/badge/Go-1.25+-00ADD8?logo=go)](https://golang.org/) [![Platform](https://img.shields.io/badge/Platform-Linux-lightgrey)](https://www.linux.org/) *Swissmade open-source solution for centralized Fail2Ban management across distributed infrastructure* @@ -23,13 +23,15 @@ The project is maintained by Swissmakers GmbH and released under GPL-3.0. Fail2Ban UI does not replace Fail2Ban. It connects to existing Fail2Ban instances and adds: -- A Dashboard for active jails and recent ban/unban activity +- A Dashboard for active jails and recent ban/unban activity with real-time WebSocket updates - Server Manager for adding new fail2ban servers to Fail2ban-UI -- Central search and unban across jails and servers +- Central search and unban / ban across jails and servers - Remote editing / creating, of jail/filter configuration (depending on connector) - Filter debug integration and live log-pattern testing -- Advanced ban actions for recurring offenders e.g. automatically ban on pfSense and Mikrotik, when threshold is reached. -- Optional email alerts with GeoIP/Whois enrichment for selected "alert countries" only. +- Ban Insights with an interactive 3D threat globe showing blocks per country +- Advanced ban actions for recurring offenders e.g. automatically ban on pfSense, Mikrotik, or OPNsense when threshold is reached +- Data management possibility for permanent block logs and stored ban events +- Optional email alerts with GeoIP/Whois enrichment for selected "alert countries" only - Optional OIDC login (Keycloak, Authentik, Pocket-ID) - Least-privilege, SELinux-aware container deployment (policies provided) - .. and much more to come. @@ -170,6 +172,8 @@ Global Fail2Ban settings including default bantime, findtime, maxretry, banactio * Do not expose the UI directly to the public Internet. Put it behind a reverse proxy, VPN, firewall rules, and/or OIDC. * SSH connector should use a dedicated service account with minimal sudo permissions and ACLs. +* All IP addresses are validated (strict IPv4/IPv6/CIDR parsing) before being passed to any integration or command, preventing command injection. +* WebSocket connections are protected by origin validation (same-origin only) and require authentication when OIDC is enabled. See [`docs/security.md`](https://github.com/swissmakers/fail2ban-ui/blob/main/docs/security.md) for details. diff --git a/docs/api.md b/docs/api.md index 80b5575..ac85b03 100644 --- a/docs/api.md +++ b/docs/api.md @@ -4,8 +4,12 @@ This is a short index for operators. The UI primarily uses these endpoints. Path ## Authentication -- When OIDC is enabled, most `/api/*` endpoints require an authenticated session. -- Callback endpoints are authenticated using `X-Callback-Secret`. +- When OIDC is enabled, all `/api/*` endpoints (including WebSocket) require an authenticated session, except the callback endpoints. +- Callback endpoints (`/api/ban`, `/api/unban`) are authenticated using `X-Callback-Secret`. + +## Input validation + +All endpoints that accept IP addresses validate them server-side using Go's `net.ParseIP` / `net.ParseCIDR`. Requests with invalid IPs receive a `400 Bad Request` response. This applies to ban/unban callbacks, manual ban/unban from the dashboard, and the advanced actions test endpoint. ## Common headers @@ -14,49 +18,79 @@ This is a short index for operators. The UI primarily uses these endpoints. Path ## Endpoints -Server management -- `GET /api/servers` -- `POST /api/servers` -- `DELETE /api/servers/:id` -- `POST /api/servers/:id/test` +### Server management +- `GET /api/servers` -> List configured servers +- `POST /api/servers` -> Create or update a server +- `DELETE /api/servers/:id` -> Delete a server +- `POST /api/servers/:id/default` -> Set server as default +- `POST /api/servers/:id/test` -> Test server connectivity +- `GET /api/ssh/keys` -> List available SSH keys -Jails and configuration -- `GET /api/summary` -- `GET /api/jails/manage` -- `POST /api/jails/manage` -- `GET /api/jails/:jail/config` -- `POST /api/jails/:jail/config` -- `POST /api/jails/:jail/unban/:ip` -- `POST /api/jails/:jail/ban/:ip` +### Jails and configuration +- `GET /api/summary` -> Dashboard summary (jails, banned IPs per server) +- `GET /api/jails/manage` -> List jails with enabled/disabled status +- `POST /api/jails/manage` -> Update jail enabled/disabled state +- `POST /api/jails` -> Create a new jail +- `DELETE /api/jails/:jail` -> Delete a jail +- `GET /api/jails/:jail/config` -> Get jail/filter configuration +- `POST /api/jails/:jail/config` -> Update jail/filter configuration +- `POST /api/jails/:jail/logpath/test` -> Test log path accessibility +- `POST /api/jails/:jail/unban/:ip` -> Unban an IP from a jail +- `POST /api/jails/:jail/ban/:ip` -> Ban an IP in a jail -Events and analytics -- `GET /api/events/bans` -- `GET /api/events/bans/stats` -- `GET /api/events/bans/insights` +### Events and analytics +- `GET /api/events/bans` -> List ban/unban events (paginated, filterable) +- `DELETE /api/events/bans` -> Delete all stored ban events +- `GET /api/events/bans/stats` -> Ban statistics (counts, timeseries) +- `GET /api/events/bans/insights` -> Ban insights (countries, top IPs, top jails) -Settings -- `GET /api/settings` -- `POST /api/settings` -- `POST /api/settings/test-email` +### Advanced actions +- `GET /api/advanced-actions/blocks` -> List permanent block records +- `DELETE /api/advanced-actions/blocks` -> Delete all permanent block records +- `POST /api/advanced-actions/test` -> Manually test block/unblock on configured integration -Filter debugging -- `GET /api/filters` -- `POST /api/filters/test` +### Settings +- `GET /api/settings` -> Get current application settings +- `POST /api/settings` -> Update application settings +- `POST /api/settings/test-email` -> Send a test email -Service control -- `POST /api/fail2ban/restart` +### Filter management +- `GET /api/filters` -> List available filters +- `GET /api/filters/:filter/content` -> Get filter file content +- `POST /api/filters` -> Create a new filter +- `POST /api/filters/test` -> Test filter regex against log lines +- `DELETE /api/filters/:filter` -> Delete a filter -Callbacks (Fail2Ban actions) -- `POST /api/ban` -- `POST /api/unban` +### Service control +- `POST /api/fail2ban/restart` -> Restart / Reloads the Fail2Ban service + +### Version +- `GET /api/version` -> Get running version and optional update check + +### WebSocket +- `GET /api/ws` -> WebSocket endpoint (upgrade) + +The WebSocket connection streams, real-time events to the frontend: +- `heartbeat` -> periodic health check (~30s) +- `console_log` -> debug console log lines (when debug mode is enabled) +- `ban_event` -> real-time ban event broadcast +- `unban_event` -> real-time unban event broadcast + +The WebSocket enforces same-origin policy via the `Origin` header and requires authentication when OIDC is enabled. + +### Callbacks (Fail2Ban actions) +- `POST /api/ban` -> Receive ban notification from Fail2Ban +- `POST /api/unban` -> Receive unban notification from Fail2Ban Callbacks require: - Header: `X-Callback-Secret: ` - JSON body fields (typical): `serverId`, `ip`, `jail`, `hostname`, `failures`, `logs` -Authentication routes (OIDC) -- `GET /auth/login` -- `GET /auth/callback` -- `GET /auth/logout` -- `GET /auth/status` -- `GET /auth/user` \ No newline at end of file +All IPs in callback payloads are validated before processing. + +### Authentication routes (OIDC) +- `GET /auth/login` -> Initiate OIDC login flow +- `GET /auth/callback` -> OIDC provider callback +- `GET /auth/logout` -> Logout and clear session +- `GET /auth/status` -> Check authentication status +- `GET /auth/user` -> Get current user info \ No newline at end of file diff --git a/docs/architecture.md b/docs/architecture.md index 88733df..222c8ea 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -23,9 +23,11 @@ Fail2Ban UI consists of : ## Components (high level) -- REST API: server management, jail/filter config read/write, ban/unban actions, settings -- WebSocket hub: streams ban/unban events and (optional) debug console logs +- REST API: server management, jail/filter config read/write, ban/unban actions, settings, data management (clear events/blocks) +- WebSocket hub: streams real-time ban/unban events and (optional) debug console logs, protected by origin validation and session auth - Storage: server definitions, settings, ban history, permanent block records +- Integrations: MikroTik (SSH), pfSense (REST API), OPNsense (REST API) with input validation on all parameters +- Ban Insights: country-level analytics with interactive 3D threat globe visualization Additional resources: - Container deployment guide: `deployment/container/README.md` @@ -62,11 +64,11 @@ Additional resources: │ │ • /auth/login | /auth/callback | /auth/logout │ │ │ e │ │ • /auth/status | /auth/user │ │ │ t │ │ • POST /api/ban | POST /api/unban ← Fail2ban callbacks (a valid Callback │ │ │ -│ │ • GET /api/ws (WebSocket) Secret is needed) │ │ │ -│ │ • /static/* | /locales/* │ │----┘ +│ │ • /static/* | /locales/* Secret is needed) │ │----┘ │ └───────────────────────────────────────────────────────────────────────────┘ │ │ ┌───────────────────────────────────────────────────────────────────────────┐ │ -│ │ PROTECTED (when OIDC enabled): GET / | GET and POST to all other /api/* │ │ +│ │ PROTECTED (when OIDC enabled): │ │ +│ │ GET / | all other /api/* | GET /api/ws (WebSocket, same-origin only) │ │ │ └───────────────────────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────────────────────┘ ``` @@ -84,16 +86,19 @@ Additional resources: │ │ • POST /jails/:jail/unban/:ip • POST /jails/:jail/ban/:ip │ │ │ │ • GET /settings • POST /settings │ │ │ │ • GET /events/bans • GET /events/bans/stats | /insights │ │ +│ │ • DELETE /events/bans • DELETE /advanced-actions/blocks │ │ │ │ • GET /version (optional GitHub request if UPDATE_CHECK) │ │ │ │ • GET /servers | POST/DELETE /servers | POST /servers/:id/test │ │ │ │ • GET /filters/* • POST /filters/test | POST/DELETE /filters │ │ │ │ • POST /fail2ban/restart • GET/POST /advanced-actions/* │ │ │ │ • POST /ban (callback) • POST /unban (callback) │ │ +│ │ All IP inputs validated via net.ParseIP / net.ParseCIDR │ │ │ └────────────────────────────────────────────────────────────────────────────┘ │ │ │ │ │ ┌─────────────────────────────────┴──────────────────────────────────────────┐ │ -│ │ WebSocket Hub (GET /api/ws) │ │ +│ │ WebSocket Hub (GET /api/ws — same-origin, auth required with OIDC) │ │ │ │ • register / unregister clients │ │ +│ │ • Origin header validated against Host (rejects cross-site connections) │ │ │ │ • broadcast to all clients: │ │ │ │ - type: "heartbeat" (every ~30s) │ │ │ │ - type: "console_log" (debug console lines) │ │ @@ -110,8 +115,10 @@ Additional resources: │ │ Connector Manager │ │ Integrations + Email │ │ │ │ • Local (fail2ban.sock) │ │ • Mikrotik / pfSense / │ │ │ │ • SSH (exec on remote) │ │ OPNsense (block/unblock)│ │ -│ │ • Agent (HTTP to agent) │ │ • SMTP alert emails │ │ -│ │ • New server init: ensure │ └────────────────────────────┘ │ +│ │ • Agent (HTTP to agent) │ │ • Input validated (IP + │ │ +│ │ • New server init: ensure │ │ identifiers sanitized) │ │ +│ │ │ │ • SMTP alert emails │ │ +│ │ │ └────────────────────────────┘ │ │ │ action.d (ui-custom- │ │ │ │ action.conf) │ │ │ └────────────────────────────┘ │ diff --git a/docs/security.md b/docs/security.md index 2b43ce7..edd0378 100644 --- a/docs/security.md +++ b/docs/security.md @@ -12,6 +12,24 @@ This project can perform security-sensitive operations (bans, configuration chan If you must publish it, put it behind TLS and an authentication layer, and restrict source IPs. +## Input validation + +All user-supplied IP addresses are validated using Go's `net.ParseIP` and `net.ParseCIDR` before they are passed to any integration, command, or database query. This applies to: + +- Ban/Unban callbacks (`/api/ban`, `/api/unban`) +- Manual ban/unban actions from the dashboard +- Advanced action test endpoint (`/api/advanced-actions/test`) +- All integration connectors (MikroTik, pfSense, OPNsense) + +Integration-specific identifiers (address list names, alias names) are validated against a strict alphanumeric pattern (`[a-zA-Z0-9._-]`) to prevent injection in both SSH commands and API payloads. + +## WebSocket security + +The WebSocket endpoint (`/api/ws`) is protected by: + +- **Origin validation**: The upgrade handshake verifies that the `Origin` header matches the request's `Host` header (same-origin policy). Cross-origin WebSocket connections are rejected. This prevents cross-site WebSocket hijacking attacks. +- **Authentication**: When OIDC is enabled, the WebSocket endpoint requires a valid session. + ## Callback endpoint protection The fail2ban callback endpoints (`/api/ban`, `/api/unban`) are only reachable with a correct `CALLBACK_SECRET`. This secret must be atleast 20 characters long. If not specified a secure secret, will be automatically genereated on first start. It can be further protected by: @@ -30,6 +48,14 @@ For SSH-managed hosts: - Restrict sudo to the minimum set of commands required to operate Fail2Ban (typically `fail2ban-client` and optionally `systemctl restart fail2ban`). - Use filesystem ACLs for `/etc/fail2ban` rather than broad permissions to allow full modification capabilities for the specific user. +## Integration connector hardening + +When using external firewall integrations (MikroTik, pfSense, OPNsense): + +- Use a dedicated service account on the firewall device with the minimum permissions needed (address-list management only on MikroTik; alias management only on pfSense/OPNsense). +- For pfSense/OPNsense: use a dedicated API token with limited scope. +- Restrict network access so the Fail2ban-UI host is the only source allowed to reach the firewall management interface. + ## Least privilege and file access Local connector deployments typically require access to: diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 88876a0..f1a5595 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -65,6 +65,195 @@ getfacl /etc/fail2ban sudo podman exec -it fail2ban-ui ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o BatchMode=yes -i /config/.ssh/id_rsa -p 2222 testuser@127.0.0.1 ``` +## Ban/unban notifications not showing up in the UI + +This is one of the most common issues. The UI receives ban/unban events from Fail2Ban via HTTP callbacks. If nothing appears in the dashboard or "Recent stored events", the callback chain is broken somewhere. Follow these steps systematically. + +### Step 1: Verify the action file exists and is correct + +Fail2ban-UI creates a custom action file at `/etc/fail2ban/action.d/ui-custom-action.conf` on each managed host. This file contains `curl` commands that notify the UI when bans/unbans happen. + +```bash +# Check if the action file exists: +cat /etc/fail2ban/action.d/ui-custom-action.conf + +# You should see actionban and actionunban sections with curl commands pointing +# to your Fail2ban-UI callback URL (e.g. http://10.88.0.1:8080/api/ban) +``` + +If the file does not exist or looks wrong, go to Settings → Manage Servers in the UI, select the server, and click "Test connection". The UI will re-deploy the action file automatically for local connectors. + +### Step 2: Verify jail.local references the action + +Fail2ban-UI writes a `jail.local` that uses the custom action. Check that it is in place: + +```bash +cat /etc/fail2ban/jail.local | head -30 + +# Look for the lines like: +# action = %(action_mwlg)s +# and a definition of action_mwlg that references ui-custom-action +``` + +If your `jail.local` was created manually or by another tool, the `ui-custom-action` might not be referenced. The easiest fix: let the UI manage `jail.local` by removing your manual version and restarting from the UI. + +### Step 3: Check network connectivity from Fail2Ban host to the UI + +The `curl` command in the action file must be able to reach the UI's callback URL. Test this from the Fail2Ban host (or from inside the container if Fail2Ban runs in one): + +```bash +# Replace with your actual Fail2ban-UI address: +curl -s -o /dev/null -w "%{http_code}" http://10.88.0.1:8080/api/version + +# Expected: 200 +# If you get connection refused, timeout, or another error, +# fix network/firewall rules first. +``` + +Common issues: +- Container using bridge networking but callback URL points to `127.0.0.1` (use the host IP or `--network=host`) +- Firewall on the UI host blocks the port + +### Step 4: Verify the callback secret + +Every callback must include the header `X-Callback-Secret`. The value must match what the UI expects. You can find the current secret in Settings → General Settings → Callback Secret (or check the container environment). + +```bash +# Check what secret the action file uses: +grep "X-Callback-Secret" /etc/fail2ban/action.d/ui-custom-action.conf + +# Compare with the UI's expected secret (from the settings page or env var) +``` + +If they do not match, re-deploy the action file via "Test connection" from the UI, or manually update the secret in the action file and restart Fail2Ban. + +### Step 5: Simulate a ban notification with curl + +This is the most direct way to test the full callback chain. Run this from any host that can reach the UI: + +```bash +FAIL2BAN_UI_HOST="your_fail2ban_host" +SECRET="your_secret" + +curl -v -X POST http://$FAIL2BAN_UI_HOST:8080/api/ban \ + -H "Content-Type: application/json" \ + -H "X-Callback-Secret: $SECRET" \ + -d '{ + "serverId": "local", + "ip": "203.0.113.42", + "jail": "sshd", + "hostname": "testhost", + "failures": "5", + "logs": "Jun 15 12:00:00 testhost sshd: Failed password for root from 203.0.113.42" + }' +``` + +Expected response: +```json +{"message":"Ban notification processed successfully"} +``` + +If it works, you should immediately see: +- A new entry in "Recent stored events" on the dashboard +- A real-time WebSocket update (the entry appears without refreshing) + +Common error responses: +- `401 Unauthorized` with `"Callback secret not configured"` → Secret not set in UI settings +- `401 Unauthorized` with `"Invalid callback secret"` → Secret mismatch +- `400 Bad Request` with `"invalid IP"` → The IP address in the payload is malformed +- `400 Bad Request` with `"Invalid request"` → JSON parsing failed (check `ip` and `jail` fields are present) + +To simulate an unban: + +```bash +curl -v -X POST http://$FAIL2BAN_UI_HOST:8080/api/unban \ + -H "Content-Type: application/json" \ + -H "X-Callback-Secret: $SECRET" \ + -d '{ + "serverId": "local", + "ip": "203.0.113.42", + "jail": "sshd", + "hostname": "testhost" + }' +``` + +### Step 6: Check what Fail2Ban is actually sending + +If the curl test above works but real bans still don't show up, Fail2Ban itself might not be executing the action correctly. Check: + +```bash +# Trigger a real ban (use a test jail or ban a test IP): +fail2ban-client set sshd banip 198.51.100.1 + +# Watch the Fail2Ban log for errors: +tail -f /var/log/fail2ban.log + +# Look for lines like: +# ERROR ... Action ... failed +# WARNING ... Command ... failed +``` + +You can also manually run the exact `curl` command from the action file to see what happens. Extract it from the action file and run it in your shell (replace the Fail2Ban variables like ``, ``, etc. with real values): + +```bash +# Extract and run the actionban command manually: +grep -A5 "actionban" /etc/fail2ban/action.d/ui-custom-action.conf + +# Then execute the curl command with real values substituted. +# This reveals whether jq is missing, curl has TLS issues, etc. +``` + +Common issues at this stage: +- **`jq` not installed**: The action file uses `jq` to build JSON. Install it: `dnf install jq` or `apt install jq` +- **TLS certificate issues**: If the callback URL uses HTTPS with a self-signed cert, the action file needs the `-k` flag (Fail2ban-UI adds this automatically when the callback URL starts with `https://`) +- **Fail2Ban not restarted**: After the action file is deployed, Fail2Ban must be restarted to pick up changes: `systemctl restart fail2ban` + +### Step 7: Check the Fail2ban-UI logs + +The UI logs every incoming callback with details. Check the container or service logs: + +```bash +# Container: +podman logs -f fail2ban-ui + +# systemd: +journalctl -u fail2ban-ui -f + +# Look for lines like: +# ✅ Parsed ban request - IP: ..., Jail: ... +# ⚠️ Invalid callback secret ... +# ❌ JSON parsing error ... +``` + +If you enabled debug mode in the UI settings, you will also see the raw JSON body of every incoming callback. + +### Step 8: Verify the `serverId` resolves + +The callback payload includes a `serverId`. The UI uses this to match the event to a configured server. If neither matches any known server, the UI will reject the callback. + +Check that the `serverId` in the action file matches the server ID shown in Settings → Manage Servers. You can see the configured server IDs via: + +```bash +curl -s http://$FAIL2BAN_UI_HOST:8080/api/servers \ + -H "X-F2B-Server: default" | jq '.servers[] | {id, name, hostname}' +``` + +### Quick reference: end-to-end callback flow + +``` +Fail2Ban detects intrusion + → triggers actionban in ui-custom-action.conf + → curl POST /api/ban with JSON payload + X-Callback-Secret header + → Fail2ban-UI validates secret + → Fail2ban-UI validates IP format + → Fail2ban-UI resolves server (by serverId) + → Stores event in SQLite (ban_events table) + → Broadcasts via WebSocket to all connected browsers + → Optional: sends email alert, evaluates advanced actions +``` + +If any step fails, the chain stops and the event will not appear in the UI. + ## Bans fail due to firewall backend (nftables / firewalld) Symptoms often mention `iptables (nf_tables)` or action startup errors. @@ -92,6 +281,34 @@ podman logs fail2ban-ui # Also enable debug logging over env or over the webUI ``` +## WebSocket not connecting + +If the real-time dashboard updates (ban/unban events appearing without page refresh) are not working: + +Check: + +* Browser console for WebSocket errors (F12 → Console tab) +* The WebSocket status indicator in the UI footer +* If using a reverse proxy, ensure it supports WebSocket upgrades + +Common issues: + +* **Reverse proxy not forwarding WebSocket**: Nginx requires explicit WebSocket upgrade configuration: + ```nginx + location /api/ws { + proxy_pass http://127.0.0.1:8080; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + } + ``` + +* **Origin mismatch**: The WebSocket endpoint validates that the `Origin` header matches the `Host` header. If your reverse proxy rewrites the `Host` header but not the `Origin`, the connection will be rejected. Ensure both headers are consistent. + +* **OIDC session expired**: When OIDC is enabled, the WebSocket requires a valid session. If the session expires, the WebSocket connection will fail with a 302 redirect or 401 error. Re-login to the UI to fix this. + ## Database issues Check: