Review queue

Featuresby Aaron Reimannupdated Oct 4, 2026#bans#review-queue#security#fail2ban#pressable

Suspicious IPs from Gatekeeper / Wordfence / nginx land in the review queue at /bans/queue. You scan, click Approve or Dismiss, and approved IPs go to fail2ban via SSH within ~60 seconds (or auto-ban immediately if the server has auto_ban_llar enabled). Auto-approve-repeats can short-circuit the human step for IPs sighted 2+ times across the fleet.

fail2ban SSH execution is SpinupWP-only. Host-level kernel blocking (fail2ban-client over SSH via iptables/nftables) requires server access that Pressable does not grant. However, with Gatekeeper in Companion 1.39.0+, Pressable sites now stream their active login lockouts directly into the Review Queue over HMAC REST for fleet-wide visibility and repeat-offender tracking, while enforcing HTTP 429 backoff blocks directly in WordPress.

The bans page

/bans has three tabs:

  • Queue (/bans/queue) — pending entries awaiting decision.
  • Active (/bans/active) — Clockwork Control’s still-open ban records (unbanned_at null). Linux fail2ban itself drops the iptables block after 24 hours (bantime = 86400); these rows stay for audit until retention archives them.
  • History (/bans/history) — the 50 most recent approve / dismiss / unban / expire events.

The old URLs /review and /blocked-ips 301-redirect to the new tabs. Mutation endpoints (/review/{entry}/approve, /blocked-ips/{ip}/unban) keep their old paths because forms in dashboard partials still post to them.

Per-row actions

On a queue row:

  • Approve — send to fail2ban (the IP enters queued_for_ban).
  • Dismiss — drop, don’t ban. Audit row in action_logs.
  • Bulk Approve / Bulk Dismiss at the top of the page when you want to clear a batch.

On an active row:

  • Unban — fail2ban-client unban over SSH, mark unbanned_at locally.

The Active tab’s search box filters instantly as you type, no need to hit Enter — same client-side pattern as Features → Sites (fleet view); see that page for how it works and its “current page only” limitation.

Auto-approve repeats

The toggle at the top of /bans/queue enables auto-approval for IPs seen 2+ times across the fleet. clockwork:auto-approve-repeats runs every minute and promotes any qualifying IP. Single-site-twice and cross-server-once both qualify.

When the toggle is off, the command becomes a no-op without removing the schedule entry. So you can flip it on for a noisy week and back off without restarting anything.

Ban retention & bulk clearing

Linux fail2ban jails automatically lift kernel iptables blocks after 24 hours (bantime = 86400). Clockwork Control keeps the blocked_ips row with unbanned_at null until retention archives it. Repeat-offender auto-approve keys off pending review_queue entries, not these active rows.

  • Configurable retention policy (bans.retention_months, default 12 months / 1 year). Adjust from /bans via the “Policy” card or /settings → Fleet Policies → Firewall & Ban Retention (that card links to /bans/active).
  • Nightly automated cleanup (clockwork:prune-expired-bans scheduled at 04:33 daily) sets unbanned_at and decided_by = retention on rows older than the threshold. The original llm_verdict is left intact. Action log type is ban_expired with actor auto.
  • Bulk Clear tool (/bans/active → “Bulk Clear”): prune on demand by cutoff (1, 3, 6, 12 months, or all active bans). Same archive shape; action log actor is manual. Cutoff values are allow-listed — a negative months value is rejected, not treated as “clear everything.”

How an entry gets here

Three sources feed the queue:

  • Gatekeeper Companion REST pull every 15 min (clockwork:pull-llar-lockouts). Queries GET /wp-json/clockwork/v1/lockouts directly over HMAC-signed HTTPS. If REST fails on a site with MySQL creds, it falls back to reading the native <prefix>clockwork_lockouts table directly. LLAR’s own tables/options are no longer scraped (removed in 1.10.1). Gated by IngestScheduleGate.
  • Wordfence direct-DB pull every 15 min (clockwork:pull-wordfence-blocks). Same gate.
  • nginx tailing every 5 min (clockwork:tail-nginx-logs). Continuous.

Pre-filter at ingest

Before anything reaches the queue, App\Services\Fail2ban\IgnoreIpMatcher filters CF edges, fleet IPs, and loopback. These get dropped on the floor and counted as filtered_protected in the run summary. This is what stops us from banning Cloudflare on a CF-proxied site whose nginx logs still record CF edges as REMOTE_ADDR.

The matcher uses the same protected list that ignoreip uses on the server-side fail2ban jail, so the in-app filter and on-server whitelist can never drift.

What “approved” actually does

  1. UI writes the row to queued_for_ban with decided_by='manual' (or 'auto-repeat' for the auto path).
  2. clockwork:process-pending-bans runs every minute, picks up the row, asks IgnoreIpMatcher again (defense in depth), and SSH-issues sudo -n fail2ban-client set clockwork banip <ip>.
  3. Local BlockedIp row gets the ban metadata + a banned_at timestamp.

If the matcher refuses at execution time (e.g. the ignoreip list grew between approve and execute), no ban is issued and the queue entry returns to pending.

Chat notification on ban

Every ban — manual (BlockedIpsController::ban), single review-queue approval (ReviewQueueController::banSingle), the bulk-approve processor (clockwork:process-pending-bans), and both auto-ban paths (clockwork:pull-llar-lockouts / clockwork:pull-wordfence-blocks) — fires ChatNotifier::ipBlocked($blocked) right after the BlockedIp row is created. The message shows the IP, server, source, LLM verdict (if any), site (if scoped), and decided_by (auto for the automated paths, the reviewer’s name for a manual approval). Toggle it per-channel like any other event — ip_blocked in ChatNotifier::EVENTS, on by default and intentionally excluded from the is_inactive quiet-site gate (an active ban is treated as an incident, not a routine nag).

Server-side pre-requisites

A server can’t ban anything until “Provision fail2ban” has been clicked on its detail page once. Without provisioning, queued bans for that server’s sites stay in the queue forever. See Integrations → SSH + fail2ban.

When the queue stops moving

Two common causes:

  • The scheduler isn’t running. Approved entries sit in queued_for_ban forever. See Runbooks → Scheduler stuck.
  • Ingest is paused. Check /settings/ingest — the per-source toggles control whether new entries arrive.

If you ever ban the wrong IP and need to undo it everywhere, see Runbooks → Bad IP ban recovery.

Clockwork Web Dev
Brought to you by Clockwork Web Dev

A real WordPress development agency building battle-tested operational tooling to run client fleets with zero bloat and total control.

Visit Clockwork Web Dev→