fail2ban-ui-agent is a small HTTP service that runs on a host where Fail2ban is installed. It exposes a JSON REST API secured by a shared secret so Fail2ban-UI can drive the same operations as local or SSH connectors (jails, filters, ban/unban, reload, logpath checks, and callbacks via a poller). This document summarizes behavior, the API surface, runtime configuration, the integrated container image built on LinuxServer.io’s prebuilt fail2ban image.
Audience: Operators and integrators using the Fail2ban-UI agent connector; developers extending or packaging the agent.
NOTE: Treat this component as development-oriented until you harden secrets, TLS, and network exposure for your environment.
- Introduction
- Architecture and Fail2ban-UI integration
- HTTP API
- Environment variables
- Build and run (host binary)
- Command-line interface
- Integrated container image (LinuxServer Fail2ban)
- Packaging
- License
- Additional resources
Remote control plane for Fail2ban on a single host, consumed by Fail2ban-UI’s Agent-Connector.
The agent does not replace Fail2ban; it requires a working fail2ban daemon and appropriate permissions to manage jails and configuration files. Every fail2ban-client call runs with -c <AGENT_FAIL2BAN_CONFIG_DIR>, so the agent always talks to the daemon that owns that configuration tree.
Overview
- Fail2ban-UI connects to the agent using the server URL and agent secret pre-configured per Fail2ban server.
- All management traffic uses the v1 API with header
X-F2B-Token(see Section 3). - Fail2ban-UI polls
GET /v1/healthto show the server health and to detect a stale callback configuration. - Configuration files are replaced atomically. The previous content of a changed file is kept as a private
<file>.f2bui.bak, so a failed write never leaves a truncated jail or filter behind.
Fail2ban-UI records bans and unbans that happen on the host. For agent-managed hosts, the agent reports them:
- Fail2ban-UI pushes its callback URL, callback secret and server ID with
PUT /v1/callback/config. The agent stores them in${AGENT_FAIL2BAN_CONFIG_DIR}/fail2ban-ui-agent.id(mode0600). - A poller compares the banned IPs of every jail each
AGENT_CALLBACK_POLL_INTERVALand POSTs each change to Fail2ban-UI’s/api/banor/api/unban, authenticated withX-Callback-Secret. - Each change gets a stable
X-Callback-Event-ID. When Fail2ban-UI is unreachable, the agent keeps up to 1000 events for one hour and retries them in order with exponential backoff (up to 5 minutes). Fail2ban-UI discards duplicates by event ID, so a retry never records a ban twice. - The agent compares only jails that exist in both snapshots. A reload that briefly removes a jail does not flood Fail2ban-UI with unbans.
- When a server is removed from Fail2ban-UI, it calls
DELETE /v1/callback/config?serverId=<id>. The agent deletes the store only if it still belongs to that server.
Precedence: the store pushed by Fail2ban-UI always wins. The AGENT_CALLBACK_* variables are only a fallback for hosts that Fail2ban-UI has not configured yet, or after the store was deleted.
IMPORTANT: The poller-based callback path is the only supported model for agent connectors; please do not try to copy ui-custom-action scripts on agent-managed hosts.
The agent pings Fail2ban every AGENT_HEALTH_INTERVAL so that protection resumes without an operator when the daemon hangs or dies:
- After
AGENT_HEALTH_MAX_RETRIESfailed pings in a row, the agent reloads Fail2ban (AGENT_HEALTH_AUTO_RELOAD). - If Fail2ban still does not answer, later attempts restart it through
systemctl,serviceorrc-service(AGENT_HEALTH_AUTO_RESTART). Without a service manager, the agent falls back to a reload. - The wait between attempts doubles each time (up to 30 minutes). After 5 attempts the agent stops remediating until a ping succeeds again, so a broken configuration cannot cause a restart loop.
- When systemd reports
fail2banasinactive, an administrator stopped it on purpose. The agent then does not restart it. - During a known service operation or unresolved interrupted command, probes and remediation are deferred. The authenticated health endpoint reports
busy: true; this does not mean the agent transport is offline.
Base URL is http(s)://<host>:<AGENT_PORT> (default port 9700 unless overridden).
| Scope | Requirement |
|---|---|
/v1/* |
Header X-F2B-Token: <AGENT_SECRET> must match the agent’s configured secret (constant-time compare on the server). |
/healthz, /readyz |
No token. The responses contain no details. |
Responses are JSON. Errors include an "error" string and, where the caller can act on it, a machine-readable "code" (see Section 3.4). Request bodies are limited to 5 MiB.
| Method | Path | Purpose |
|---|---|---|
GET |
/healthz |
Liveness: always 200 {"status":"ok"} while the agent process serves requests |
GET |
/readyz |
Readiness: 200 {"ready":true} or 503 {"ready":false}. Ready means the last check is recent, Fail2ban answered pong, the config directory is writable, and fail2ban-client and fail2ban-regex are installed |
All of the following require X-F2B-Token.
Health
| Method | Path | Purpose |
|---|---|---|
GET |
/v1/health |
Readiness checks, agent and Fail2ban versions, running jails, supervisor state and callback status. The callback block carries a fingerprint (HMAC-SHA256 of server ID, callback URL and callback secret, keyed with the agent secret) instead of the secret, so Fail2ban-UI can detect a stale configuration |
Callback configuration
| Method | Path | Purpose |
|---|---|---|
PUT |
/v1/callback/config |
Body: serverId, callbackUrl, callbackSecret, optional callbackHostname. The URL must be http(s)://host[:port][/path] without credentials, query or fragment |
DELETE |
/v1/callback/config?serverId=<id> |
Delete the store if it belongs to <id>. Returns {"ok":true,"cleared":true}, or cleared:false with reason server_mismatch or not_configured |
Fail2ban service actions
New clients submit durable operations and poll their status. The request returns promptly, disconnecting the browser, HTTP caller, or UI process does not cancel the worker.
| Method | Path | Purpose |
|---|---|---|
GET |
/v1/operations/capabilities |
Protocol version, supported operation kinds, and configuration snapshot support |
POST |
/v1/operations |
Body: id, kind (reload, restart, validate, ban, or unban), plus jail and ip for ban/unban. Returns 202 and the operation record. Reusing an ID for the same command and target returns the original record; changing either returns 409 |
GET |
/v1/operations/{id} |
Read the durable result without querying the Fail2ban socket |
POST |
/v1/operations/{id}/reconcile |
Body: kind. Return an existing operation, or reserve a missing ID without dispatching a command. This prevents a delayed original request from executing after the UI starts recovery |
POST |
/v1/config/snapshots |
Body: id. Store an immutable rollback snapshot before editing configuration |
POST |
/v1/config/snapshots/{id}/restore |
Restore the snapshot, including removing newly created configuration files |
DELETE |
/v1/config/snapshots/{id} |
Remove a snapshot after the caller has confirmed completion or rollback |
Operation states are queued, running, succeeded, failed, and unknown. Mutation workers have a 30-minute budget. A timeout or process interruption leaves an unknown result: the daemon may have received the command. Recovery only probes it, never repeats the command. quiescent: true confirms that the daemon answers again, not that the requested configuration was applied. The UI must verify the intended runtime state before completing its transaction or retrying. Conflicting mutations return 409 operation_busy while a command is active or the daemon has not yet responded after interruption.
Records live in ${AGENT_FAIL2BAN_CONFIG_DIR}/.fail2ban-ui-operations, so the configuration volume must persist across container replacement. Operation records are retained for idempotency. Rollback snapshots live in .fail2ban-ui-snapshots and are removed explicitly after a confirmed outcome. Snapshots cover jail.conf, jail.local, the agent callback registration, and regular .conf/.local files in jail.d, filter.d, and action.d. They preserve content and mode, skip unchanged files, and restore ownership when permitted; insufficient chown privileges do not prevent configuration recovery. Snapshot creation rejects symlinks and files larger than 10 MiB or a total larger than 32 MiB.
The legacy synchronous endpoints remain available and use the same durable worker:
| Method | Path | Purpose |
|---|---|---|
POST |
/v1/actions/reload |
Reload Fail2ban; returns {"ok":true,"output":...} |
POST |
/v1/actions/restart |
Restart Fail2ban; returns {"ok":true,"mode":"restart"}, or "mode":"reload" when no service manager could restart it |
POST |
/v1/actions/validate |
Test the configuration with fail2ban-client -t: 200 {"ok":true,"output":...}, or 422 with code config_invalid and the test output |
Legacy callers may supply Idempotency-Key. Responses include operationId. If the HTTP wait expires before the command finishes, the endpoint returns 503 operation_pending; callers must query the operation before retrying. An HTTP timeout never cancels the underlying worker.
Jails
| Method | Path | Purpose |
|---|---|---|
GET |
/v1/jails |
List jails from fail2ban-client (runtime-oriented) |
GET |
/v1/jails/all |
Broader jail listing for "Manage jails" UI |
GET |
/v1/jails/{jail} |
Banned IPs / counts for a jail |
POST |
/v1/jails/{jail}/ban |
Ban an IP |
POST |
/v1/jails/{jail}/unban |
Unban an IP |
GET |
/v1/jails/{jail}/config |
Read jail config (with .local / .conf fallback semantics) |
PUT |
/v1/jails/{jail}/config |
Write jail config |
POST |
/v1/jails |
Create jail; adds the [jail] section header when the content lacks it |
DELETE |
/v1/jails/{jail} |
Delete jail.d/{jail}.local and jail.d/{jail}.conf |
POST |
/v1/jails/update-enabled |
Map of jail name -> enabled flag |
POST |
/v1/jails/test-logpath |
Test log path pattern |
POST |
/v1/jails/test-logpath-with-resolution |
Resolve %(var)s style log paths then test |
GET |
/v1/jails/check-integrity |
jail.local presence / managed / legacy UI-action markers |
POST |
/v1/jails/ensure-structure |
Write the managed jail.local from the optional JSON content. Returns {"ok":true,"skipped":false}; a jail.local that neither the agent nor Fail2ban-UI wrote is never touched (skipped:true, reason:"unmanaged") |
Jail and filter names use letters, digits, _ and - and must not start with -. The jail names DEFAULT, INCLUDES, all and check-integrity are reserved (case-insensitive).
Filters
| Method | Path | Purpose |
|---|---|---|
GET |
/v1/filters |
List filter names |
GET |
/v1/filters/{name} |
Read filter config |
PUT |
/v1/filters/{name} |
Write filter .local |
POST |
/v1/filters |
Create filter |
DELETE |
/v1/filters/{name} |
Delete filter.d/{name}.local and filter.d/{name}.conf |
POST |
/v1/filters/test |
Run fail2ban-regex; returns {"output","filterPath","exitCode"} for every completed run, 500 only when it could not run. Files named in the filter’s [INCLUDES] are copied from filter.d (with their .local files, up to 3 levels) so unsaved filter content is tested exactly as Fail2ban would load it |
| Code | Status | Meaning |
|---|---|---|
auth_invalid_token |
401 | Missing or wrong X-F2B-Token |
invalid_name |
400 | Jail or filter name rejected |
invalid_ip |
400 | Not an IP address or CIDR |
not_found |
404 | Jail or filter file does not exist |
callback_invalid |
400 | Callback configuration or serverId rejected |
logpath_invalid |
400 | Logpath is not absolute, contains .. or unsupported characters |
logpath_unresolved |
422 | A %(var)s in the logpath is not defined in the configuration |
logpath_inaccessible |
422 | The agent may not read the logpath directory |
config_invalid |
422 | fail2ban-client -t rejected the configuration |
The agent refuses to start when a variable has an invalid value (port, duration, boolean, path) and lists every problem at once.
| Variable | Default | Description |
|---|---|---|
AGENT_BIND_ADDRESS |
0.0.0.0 |
Listen address (IP) |
AGENT_PORT |
9700 |
Listen port |
AGENT_SECRET |
(empty) | Required. At least 16 characters and not a placeholder such as change-me. Generate one with openssl rand -hex 32 |
AGENT_TLS_CERT_FILE / AGENT_TLS_KEY_FILE |
(empty) | Set both to serve HTTPS on AGENT_PORT |
AGENT_FAIL2BAN_CONFIG_DIR |
/etc/fail2ban |
Fail2ban configuration root, passed to every fail2ban-client call as -c |
AGENT_LOG_ROOT |
/var/log |
Where /var/log paths are found during logpath tests (for example a container mount) |
AGENT_HEALTH_INTERVAL |
30s |
Supervisor check interval (minimum 5s) |
AGENT_HEALTH_AUTO_RELOAD |
true |
Reload Fail2ban after repeated failed pings |
AGENT_HEALTH_AUTO_RESTART |
true |
Restart Fail2ban when a reload is not enough |
AGENT_HEALTH_MAX_RETRIES |
3 |
Failed pings in a row before remediation starts |
Booleans accept true/false, yes/no, on/off and 1/0.
NOTE: AGENT_FAIL2BAN_RUN_DIR was removed. The agent ignores it and logs a warning; fail2ban-client finds its socket through AGENT_FAIL2BAN_CONFIG_DIR.
Callback poller
| Variable | Default | Description |
|---|---|---|
AGENT_CALLBACK_URL |
— | Fallback callback URL, used only while no store from Fail2ban-UI exists |
AGENT_CALLBACK_SECRET |
— | Fallback callback secret |
AGENT_CALLBACK_SERVER_ID |
— | Fallback server ID |
AGENT_CALLBACK_HOSTNAME |
host name | Hostname reported with each event; also used with UI-pushed callbacks when the server entry has no hostname |
AGENT_CALLBACK_POLL_INTERVAL |
4s |
Poll interval: 0 disables the poller, otherwise at least 1s |
Set AGENT_CALLBACK_URL, AGENT_CALLBACK_SECRET and AGENT_CALLBACK_SERVER_ID together; the agent ignores an incomplete set and logs a warning. The store at ${AGENT_FAIL2BAN_CONFIG_DIR}/fail2ban-ui-agent.id wins over these variables.
Procedure
-
Build a static Linux binary:
cd /path/to/fail2ban-ui-agent CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o fail2ban-ui-agent ./cmd/agent -
Run with a random secret:
sudo AGENT_SECRET="$(openssl rand -hex 32)" ./fail2ban-ui-agentEnter the same secret in the agent server settings of Fail2ban-UI.
Global help:
./fail2ban-ui-agent --helpSubcommands:
| Subcommand | Purpose |
|---|---|
health-check |
GET /healthz of the local agent |
health-check --ready |
GET /readyz; exits 0 only when the agent can manage Fail2ban |
health-check --detail |
GET /v1/health with X-F2B-Token (from --secret or AGENT_SECRET) |
test connection |
Check agent → Fail2ban-UI: GET <base>/auth/status; with a callback secret, also GET <base>/api/healthcheck/callback |
health-check flags: --url <url> (default: 127.0.0.1 for a wildcard AGENT_BIND_ADDRESS, AGENT_PORT, https when AGENT_TLS_CERT_FILE is set; certificate verification is skipped only for that derived loopback URL), --secret <token>, --json.
test connection flags: --callback-url <url>, --callback-secret <token>, --json. Without --callback-url, it tests the callback the agent itself would use: the store pushed by Fail2ban-UI, then AGENT_CALLBACK_*. The output names the source (flags, store or env).
Exit codes: 0 success, 1 check failed, 2 usage error (including an unknown command or flag).
Examples:
./fail2ban-ui-agent health-check
./fail2ban-ui-agent health-check --ready
./fail2ban-ui-agent health-check --detail --json
./fail2ban-ui-agent test connection
./fail2ban-ui-agent test connection --callback-url https://ui.example.com --callback-secret your-callback-secretThe root Dockerfile in this directory produces an image that:
- Builds this agent as a static binary for each target platform (
linux/amd64,linux/arm64). - Uses the prebuilt image
lscr.io/linuxserver/fail2ban:latestas the runtime base (LinuxServer.io Fail2ban container: Fail2ban, s6-overlay, and their layout under/config, etc.). - Installs the binary to
/usr/local/bin/fail2ban-ui-agent. - Adds s6 custom-init and custom-services files from
docker/linuxserver/so the agent starts with Fail2ban without the need of bind-mounting the binary or those scripts from the host.
Pre-built multi-arch image available here:
podman pull swissmakers/fail2ban-ui-agent:latestProcedure
-
From this directory:
podman build -t localhost/fail2ban-ui-agent:latest .or:
docker build -t localhost/fail2ban-ui-agent:latest .
-
Publish
AGENT_PORT(default 9700 in the imageENV) or use host networking in Compose as your environment requires. -
Set
AGENT_SECRETin the container environment; align the same value in Fail2ban-UI’s agent server settings.container-compose.ymlreads it from the shell and refuses to start without it:AGENT_SECRET="$(openssl rand -hex 32)" podman compose -f container-compose.yml up -d -
Mount a persistent
/configtree compatible with LinuxServer Fail2ban (see their documentation for layout and permissions).
For systemd unit files and an RPM spec skeleton (needs to be finished), see here:
packaging/README.mdpackaging/systemd/fail2ban-ui-agent.servicepackaging/rpm/fail2ban-ui-agent.spec
The agent version is defined in internal/version/version.go; CI tags images with it.
- fail2ban-ui-agent (sources and the binary you build from them) is licensed under the GNU Affero General Public License v3.0 (AGPL-3.0-only). Full text:
LICENSEin this directory; summary: GNU AGPLv3. - Fail2ban-UI (main application) is licensed under the same AGPLv3 where stated in that repository.
- Fail2ban-UI agent connector implementation:
internal/fail2ban/connector_agent.go(Fail2ban-UI project). - LinuxServer.io Fail2ban image: linuxserver/docker-fail2ban (upstream documentation and license information).