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>
6.1 KiB
Configuration
The web UI is configured with environment variables (.env, loaded via env_file:) and
the host list config/hosts.yaml. A complete, commented template is
examples/.env.example. Changes need
docker compose up -d (which recreates the container).
Values containing $ (password hashes, some secrets) must be wrapped in single quotes
in .env, otherwise Docker Compose expands them.
Hosts – config/hosts.yaml
hosts:
- name: web01 # shown in the UI: letters, digits, . _ -
address: web01.example.org # DNS name or IP, reachable from the container
port: 22 # optional, default 22
user: f2bweb # optional, default SSH_USER
After adding a host run keyscan again, then restart:
docker compose run --rm -it fail2ban-webui keyscan
docker compose run --rm fail2ban-webui check
docker compose up -d --force-recreate
F2B_HOSTS=name=address,name2=address2 (port 22, user SSH_USER) is still accepted for
compatibility; entries from both are combined.
General
| Variable | Default | Meaning |
|---|---|---|
PUBLIC_URL |
– | external URL, e.g. https://fail2ban.example.org. Required for OIDC; with https:// session cookies get the Secure flag |
DEFAULT_LANG |
en |
language until a user picks one in the dropdown (en, de, …) |
TRUSTED_PROXY_HOPS |
0 |
number of reverse proxies whose X-Forwarded-For/-Proto headers are trusted. Behind one proxy: 1. Wrong values make the audit log and the login lockout use the wrong client IP |
SESSION_HOURS |
12 |
session lifetime |
SESSION_COOKIE_SECURE |
from PUBLIC_URL |
force the Secure cookie flag on/off |
SECRET_KEY |
generated | key that signs session cookies; by default created once in data/secret_key |
TZ |
Europe/Vienna (compose) |
time zone of the log and audit timestamps |
CONFIG_FILE |
/config/hosts.yaml |
path of the host list inside the container |
DATA_DIR |
/data |
data directory inside the container |
Authentication
See authentication.md for the complete setup.
| Variable | Default | Meaning |
|---|---|---|
AUTH_MODE |
local |
local, oidc, local,oidc or proxy |
AUTH_USER |
admin |
local user name |
AUTH_PASSWORD_HASH |
– | scrypt hash from hash-password (single quotes!) |
AUTH_PASSWORD_HASH_FILE |
– | alternative: file containing the hash (e.g. a Docker secret) |
LOGIN_MAX_ATTEMPTS |
5 |
failed logins per client IP before the lockout |
LOGIN_LOCK_MINUTES |
15 |
duration of the lockout window |
OIDC_ISSUER |
– | issuer URL; the discovery document is read from <issuer>/.well-known/openid-configuration |
OIDC_DISCOVERY_URL |
– | full discovery URL instead of OIDC_ISSUER |
OIDC_CLIENT_ID / OIDC_CLIENT_SECRET |
– | client credentials |
OIDC_SCOPES |
openid email profile |
requested scopes |
OIDC_USERNAME_CLAIM |
preferred_username |
claim shown as user name (falls back to email, then sub) |
OIDC_GROUPS_CLAIM |
groups |
claim with the group list |
OIDC_ALLOWED_GROUPS |
– | only members of these groups may sign in (comma separated) |
OIDC_ALLOWED_USERS |
– | only these user names may sign in |
OIDC_BUTTON_LABEL |
SSO |
text on the login button ("Sign in with …") |
AUTH_PROXY_ALLOWED_IPS |
– | proxy mode: only accept requests from these addresses/networks |
AUTH_PROXY_HEADER |
– | proxy mode: header with the user name, e.g. X-authentik-username |
AUTH_PROXY_LOGOUT_URL |
– | proxy mode: target of the "Sign out" button |
Agents (SSH)
| Variable | Default | Meaning |
|---|---|---|
SSH_USER |
f2bweb |
default user for hosts without user: |
SSH_KEY |
/data/ssh/id_ed25519 |
private key |
KNOWN_HOSTS |
/data/ssh/known_hosts |
pinned host keys (written by keyscan) |
SSH_TIMEOUT |
40 |
seconds per agent call |
CACHE_SECONDS |
15 |
how long the ban list is cached between page refreshes |
MANUAL_JAIL |
manual-block |
jail name shown in the UI hint – the agent decides the real jail (/etc/f2b-webctl.conf) |
AUDIT_LOG |
/data/audit.log |
audit log (JSON lines) |
The SSH client ignores the system ssh_config, uses only the configured key, never
asks questions and refuses unknown host keys.
GeoIP
| Variable | Default | Meaning |
|---|---|---|
GEOIP_AUTO_DOWNLOAD |
0 |
1: download the free DB-IP Lite city database (CC BY 4.0) and refresh it monthly |
GEOIP_DB |
/data/geoip/dbip-city-lite.mmdb |
path of the database – any MaxMind-format city database works (e.g. GeoLite2 City) |
With a city database the UI also shows the origin map (country-only databases have no coordinates). The map is bundled with the app (Natural Earth, public domain) – no external tile server is contacted. Without a database the "Origin" column stays empty and the map is hidden.
Loki and Grafana
With Loki the UI gets a History button per IP showing the matching log lines of the last days; with Grafana every ban gets a link to Explore.
| Variable | Default | Meaning |
|---|---|---|
LOKI_URL |
– | e.g. http://loki:3100; empty = history disabled |
LOKI_SELECTOR |
{job=~".+"} |
LogQL stream selector for the search, e.g. {job=~"fail2ban|sshd|nginx"} |
LOKI_DAYS |
7 |
how far back to search |
LOKI_LIMIT |
50 |
maximum lines |
LOKI_USER / LOKI_PASSWORD |
– | basic auth |
LOKI_TENANT |
– | X-Scope-OrgID for multi-tenant Loki |
GRAFANA_URL |
– | e.g. https://grafana.example.org |
GRAFANA_DASHBOARD |
– | path of a dashboard for the header link, e.g. /d/abc123/security |
GRAFANA_LOKI_UID |
loki |
UID of the Loki data source in Grafana (for Explore links) |
Example – exclude the UI's own access log from the search:
LOKI_SELECTOR={job=~"fail2ban|ufw|nginx|sshd", domain!="fail2ban.example.org"}
Agent settings
Protected addresses and the manual jail are configured on each host in
/etc/f2b-webctl.conf – see agent.md.