This project can perform security-sensitive operations (bans, configuration changes). Deploy it as you would deploy every other administrative interface.
- Do not expose the UI directly to the Internet.
- Prefer one of:
- VPN-only access
- Reverse proxy with strict allowlists
- OIDC enabled (in addition to network controls)
If you must publish it, put it behind TLS and an authentication layer, and restrict source IPs.
See docs/reverse-proxy.md for hardened proxy examples and WebSocket forwarding requirements.
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.
The WebSocket endpoint (/api/ws) is protected by:
- Origin validation: The upgrade handshake verifies that the
Originheader matches the request'sHostheader (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.
The callback endpoints (/api/ban, /api/unban) are protected by CALLBACK_SECRET (X-Callback-Secret header). If no secret is specified, Fail2Ban UI generates one on first start. Additional hardening:
- Use a long, random secret and rotate it on suspected leakage
- Restrict network access so only known Fail2Ban hosts can reach callback endpoints
Rotate the secret if you suspect leakage.
For SSH-managed hosts:
- Use a dedicated service account (not a human user).
- Require key-based auth.
- Restrict sudo to the minimum set of commands required to operate Fail2Ban (at minimum
fail2ban-client *andsystemctl restart fail2ban). - Use filesystem ACLs for
/etc/fail2banrather than broad permissions to allow full modification capabilities for the specific user.
Fail2Ban UI uses StrictHostKeyChecking=accept-new for all SSH-managed hosts. This means:
- First connection: the remote host's key is accepted automatically and saved to the known-hosts file. No manual approval is required.
- Subsequent connections: the saved key is verified. If the key has changed (e.g. host was re-imaged, or a MITM is in path), the connection is rejected and an error is logged.
The known-hosts file location is resolved in this order:
SSH_KNOWN_HOSTSenvironment variable (if set)~/.ssh/known_hosts(home directory of the user running Fail2Ban UI)
To pre-trust a host before first use, add its key manually:
ssh-keyscan -H <host> >> ~/.ssh/known_hostsTo re-trust a host after a key change (e.g. you rebuilt it intentionally):
ssh-keygen -R <host> -f ~/.ssh/known_hosts
# then let Fail2Ban UI reconnect — the new key will be accepted and savedWhen 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.
The MikroTik integration connects over SSH. On first use it performs Trust On First Use (TOFU):
- The remote host's SSH key fingerprint (SHA-256) is accepted automatically and saved to the
HostFingerprintfield in Settings. - All subsequent connections verify the live fingerprint against the saved value. A mismatch aborts the connection and logs an error — this prevents silent man-in-the-middle attacks against the router management channel.
To reset after a deliberate key change (e.g. you reset the router):
Go to Settings → Advanced Actions → Mikrotik and clear the HostFingerprint field, then save. The next connection will re-learn and save the new key.
Local connector deployments typically require access to:
/var/run/fail2ban/fail2ban.sock/etc/fail2ban/- selected log paths (read-only, mounted to same place inside the container, where they are on the host.)
Avoid running with more privileges than necessary. If you run in a container, use the repository deployment guide and, where needed, the optional container SELinux modules.
Do not disable SELinux as a shortcut. Fix labeling, booleans, and policy issues instead.
The UI installs an action that runs curl from the Fail2Ban service context to reach /api/ban and /api/unban. With SELinux enforcing, you may see denials such as curl / fail2ban_t / name_connect / tcp_socket / http_port_t (for example when the callback URL uses HTTPS on port 443).
On RHEL-family systems, setroubleshoot typically recommends enabling the nis_enabled boolean, which allows this class of outbound connection:
sudo setsebool -P nis_enabled 1Prefer that over ad-hoc audit2allow modules unless your organization requires a different control.
If the UI runs in Podman/Docker with a local connector, extra rules can be needed so container_t can use the Fail2Ban socket and read the right logs (not the same problem as the callback boolean above). Sources and build steps are in deployment/container/SELinux/.
Fail2Ban UI supports three alert providers: Email (SMTP), Webhook, and Elasticsearch. Each has specific security considerations.
- Use TLS (
Use TLSenabled) for all SMTP connections. - Avoid disabling TLS verification (
Skip TLS Verification) in production. If you must, ensure the network path to the SMTP server is trusted. - Use application-specific passwords or OAuth tokens where supported (e.g. Gmail, Office365) instead of primary account passwords.
- Use HTTPS endpoints whenever possible.
- If the webhook endpoint requires authentication, use custom headers (e.g.
Authorization: Bearer <token>) rather than embedding credentials in the URL. - Avoid disabling TLS verification for production endpoints. The
Skip TLS Verificationoption exists for development/self-signed environments only.
- Use API key authentication over basic auth when possible. API keys can be scoped to specific indices and rotated independently.
- Restrict the API key to write-only access on the
fail2ban-events-*index pattern. Avoid cluster-wide or admin-level keys. - Consider using Elasticsearch's built-in role-based access control to limit what the Fail2Ban UI service account can do.
Fail2Ban UI enforces per-source-IP rate limits on endpoints that are either externally reachable or sensitive:
| Endpoint | Limit | Rationale |
|---|---|---|
POST /api/ban, POST /api/unban |
60 requests / minute | Public callback endpoints — limits abuse from compromised or misconfigured hosts |
GET /auth/login, GET /auth/callback |
10 requests / minute | Limits OIDC login enumeration and token-endpoint hammering |
Requests that exceed the limit receive HTTP 429 Too Many Requests. The limit resets automatically (token bucket — capacity refills at 1 token/second for callbacks, and ~1 token/6s for auth endpoints).
All authenticated, mutating API requests (any method other than GET, HEAD, or OPTIONS on a non-public route) must include the header:
X-Requested-With: XMLHttpRequest
The built-in JavaScript API helper (serverHeaders()) sets this header automatically on every request from the browser UI. Third-party API clients and scripts that call authenticated endpoints must add the header manually.
The following paths are exempt (they use their own auth mechanism):
/api/ban,/api/unban— protected byX-Callback-Secret/api/healthcheck/callback— protected byX-Callback-Secret/auth/*— OIDC flow endpoints
A missing header returns HTTP 403 Forbidden.
When the ENCRYPTION_KEY environment variable is set, all credentials stored in the database are encrypted with AES-256-GCM before being written and decrypted on read. The following fields are covered:
- Callback secret
- SMTP password
- Advanced Actions config (MikroTik password, pfSense API token, OPNsense API key/secret, Vision One API token)
- Webhook, Elasticsearch, and Threat Intelligence API keys/passwords
- Per-server agent secrets
Encrypted values are stored with an enc: prefix so they can be distinguished from plaintext during migration.
Setup:
# Generate a 32-byte random key as a 64-character hex string:
openssl rand -hex 32
# → e.g. a3f1...c9d2
# Set it in your environment file:
ENCRYPTION_KEY=a3f1...c9d2Migration from plaintext: existing plaintext rows are read correctly on startup. On the next settings save, all credentials are re-written as encrypted. No manual data migration is required.
Key rotation: update ENCRYPTION_KEY and then resave settings in the UI — this re-encrypts all credentials under the new key.
If
ENCRYPTION_KEYis removed after credentials have been encrypted, startup will log decrypt warnings and the rawenc:...ciphertext will be used as the value — effectively breaking those integrations. Always keep a backup of the key alongside your database backup.
Both CI workflows (build.yml on pushes to main, release.yml on v* tags) scan the container image with Trend Micro Artifact Scanner (TMAS) before anything is pushed to a registry. A failed scan stops the workflow, so no image or release is published.
Build amd64 image locally (fail2ban-ui:scan)
→ TMAS scans it for vulnerabilities, malware, and secrets
→ results are checked against the severity threshold
→ pass: multi-arch build (amd64 + arm64) is pushed (and the GitHub release is created)
→ fail: workflow stops, nothing is published
The scan logic lives in a shared composite action, .github/actions/tmas-scan/action.yml, used by both workflows. The multi-arch build that follows reuses the build cache, so the extra scan build adds little time.
The build fails when any of the following are found:
- Vulnerabilities at or above the configured severity (default:
critical) - Any malware
- Any unmitigated secrets (API keys, private keys, tokens baked into the image)
If the TMAS_API_KEY secret is not set, the scan is skipped with a warning and the build continues. This keeps forks without a Vision One tenant building, but means a missing secret silently disables the gate. Check for the TMAS_API_KEY secret not set warning on the run page.
1. Create a Vision One API key
In Vision One, go to Administration → API Keys and add a key (e.g. fail2ban-ui-ci). Assign a custom role with only the Artifact Scanner Run artifact scan permission. This is separate from the Suspicious Objects key used by the Vision One integration. Keep the two keys separate so each has the minimum permissions.
2. Add the repository secret and variables
In the GitHub repository, go to Settings → Secrets and variables → Actions:
| Type | Name | Required | Default | Description |
|---|---|---|---|---|
| Secret | TMAS_API_KEY |
Yes | — | The Vision One API key from step 1 |
| Variable | TMAS_REGION |
No | ap-southeast-2 |
Region of your Vision One tenant (see below) |
| Variable | TMAS_FAIL_ON |
No | critical |
Lowest severity that fails the build: critical, high, medium, low, or none |
TMAS_FAIL_ON=none still runs the scan and uploads the report, but only malware and secrets fail the build. Use it to temporarily unblock builds while fixing vulnerabilities.
3. Pick the region
TMAS uses AWS-style region codes that match your Vision One tenant's data residency:
| Vision One region | TMAS_REGION |
|---|---|
| United States | us-east-1 |
| Australia | ap-southeast-2 |
| Canada | ca-central-1 |
| Germany (EU) | eu-central-1 |
| India | ap-south-1 |
| Japan | ap-northeast-1 |
| Singapore | ap-southeast-1 |
| United Arab Emirates | me-central-1 |
| United Kingdom | eu-west-2 |
Each workflow run shows:
- Job summary: a table of critical / high / medium / low vulnerability counts plus malware and secret findings.
- Annotations: an error per failed check (e.g.
15 vulnerabilities at or above 'critical' severity). - Artifact
tmas-report-<run id>: the full JSON report, with every finding, package, installed version, and fixed version. It is uploaded even when the scan fails.
Most findings fall into one of three groups:
| Source | Example package | Fix |
|---|---|---|
| Go standard library | stdlib go1.x.y |
Bump the golang: builder image in the Dockerfile to a supported Go release |
| Go modules | golang.org/x/net, github.com/jackc/pgx/v5 |
go get <module>@latest && go mod tidy; check locally with govulncheck ./... |
| Alpine packages | python3, busybox, nghttp2-libs |
Bump the alpine: base image; apk upgrade in the Dockerfile picks up patch releases on each build |
Alpine findings with a fixed version of Unknown have no upstream fix yet. Check the Alpine security tracker and remove the affected package if nothing uses it.
Known open finding: CVE-2025-60876 (busybox wget header injection) has no fix in any Alpine release. It is mitigated in the image by removing the busybox wget applet and the ssl_client package, but TMAS still reports it against the busybox package version. It is medium severity and does not fail the default threshold.
- Only the amd64 image is scanned. The arm64 image is built from the same Dockerfile and dependencies but is not scanned separately.
- The scan runs on each build. A vulnerability published after a release will not be caught until the next build or release.
- Back up
/config(DB + settings) regularly. - Treat the database as sensitive operational data.
- Keep the host and container runtime patched.
- Review Fail2Ban actions deployed to managed hosts as part of change control.