Overview — what it does
Auto-block is InfraGuard's background pipeline that detects, vets, and blocks attacker IPs with no human in the loop. It is not a page — it is a flow across collectors, the scheduler, and the kernel ipset. This article documents that whole structure.
The pipeline has two independent blocking paths that converge on a single enforcement point (ipset).
| Path | Runs as | Writes to | Unit | Source |
|---|---|---|---|---|
| ① Collector auto-block | Node collectors on each host (srct_tx · kts_tx · eve_ingest) |
blacklist_info |
/24 (C-class) | Suricata L7 · Kitsune ML |
| ② Scheduler auto-block | InfraGuard app (infr:auto-block → AutoBlockService) |
infr_block |
/32 (individual) or /24 | suricata_alert_ct · suricata_tls_ct |
| ③ Enforcement (shared) | infr_sync_black.mjs (host cron) |
kernel ipset blacklist |
hash:net | union of ①+② |
Both paths run a whitelist + innocence check immediately before blocking. Only confirmed abuse survives; likely-false-positive candidates are exonerated. Path ② (scheduler) is OFF by default and must be enabled by an admin.
When to use
- Turn automated defense on when botnets, scanners, and brute-force probes arrive continuously and manual blocking cannot keep up.
- When Detections ▸ JA3 shows fingerprint-rotating botnets, tune the threshold so they are caught automatically.
- When a source repeatedly fires a given severity (red/orange) IDS alert in Threats and you want it blocked automatically.
- When reviewing the block reasons accumulating in Blocklist & Reputation and exonerating over-blocks via Whitelist.
Layout
Auto-block has no dedicated page. Its control and observation points are:
- Configuration — Operations Policy ▸ Security settings tab ▸ Auto-block card (master switch, mode, scope, triggers, innocence).
- Result observation — Blocklist & Reputation (collector /24 + scheduler /32 blocks, reasons, release).
- Exoneration — Whitelist (manual / auto-innocent IPs & CIDRs, with expiry).
- Trigger sources — Threats (IDS severity), Detections (JA3 rotation).
How to use
1. Path ② scheduler auto-block — enable and tune
The default is OFF (autoblock_on=0). Flip the master switch in Operations Policy ▸ Security settings, then adjust the policy.
Mode
| Value | Meaning |
|---|---|
global (default) |
A compromise seen on one server is applied fleet-wide (server_index=0). |
per_server |
Applied only to the attacked server — the target dest_ip is resolved to a server_index via infr_server_index. |
Scope
| Value | Scheduler writes | Description |
|---|---|---|
balanced (recommended, default) |
individual /32 | IDS programs use /24; InfraGuard adds individual /32 — sync_black skips any /32 already covered by a /24. |
broad |
/24 | Blocks the whole C-class — wide coverage but risks innocent neighbor IPs. |
granular |
individual /32 | One IP at a time — precise but slow against rotation. |
Triggers (OR — a candidate needs only one)
① IDS ruleset severity suricata_alert_ct
WHERE alert_ts >= NOW() - INTERVAL {window_min} MINUTE
AND severity <= {sev_max} -- crit=1, warn=2 (Suricata: 1=critical … 3=low)
GROUP BY src_ip HAVING COUNT(*) >= {sev_count}
② JA3 rotation botnet suricata_tls_ct
WHERE event_ts >= NOW() - INTERVAL {ja3_window_min} MINUTE
AND ja3_hash IS NOT NULL
GROUP BY src_ip HAVING COUNT(DISTINCT ja3_hash) >= {ja3_rotation}
The severity trigger's timestamp column is
alert_tswhile the JA3 trigger usesevent_ts(they differ by table). When tuning thresholds, keep in mind which column each window filters on.
Policy defaults (OpsPolicy::autoBlock())
| Setting key | Default | Clamp | Meaning |
|---|---|---|---|
autoblock_on |
0 |
— | Master switch (1=on). Default OFF |
autoblock_mode |
global |
global|per_server | Blocking mode |
autoblock_scope |
balanced |
broad|granular|balanced | Blocking scope |
autoblock_interval_min |
10 |
≥1 | Effective sweep interval (min) |
autoblock_window_min |
10 |
≥1 | Severity evaluation window (min) |
autoblock_sev_on |
1 |
— | Trigger ① enabled |
autoblock_severity |
crit |
crit|warn | crit=sev≤1, warn=sev≤2 |
autoblock_sev_count |
1 |
≥1 | Min alerts in window |
autoblock_ja3_on |
1 |
— | Trigger ② enabled |
autoblock_ja3_rotation |
50 |
≥2 | distinct JA3 fingerprints ≥ N |
autoblock_ja3_window_min |
360 |
≥1 | JA3 evaluation window (min · 6h) |
autoblock_innocent_on |
1 |
— | Innocence veto enabled |
autoblock_innocent_days |
30 |
≥1 | Auto-whitelist duration (days) |
autoblock_reputation_cap |
25 |
0–200 | Reputation lookups per sweep |
JA3 rotation tuning (measured): real-world JA3 rotation offenders typically peak at about 32 distinct JA3 per /32 session (datacenter/cloud bots, GREASE-free = stable and blockable). So the default
ja3_rotation=50catches nothing. To actually fire, lower it to about 15–20. Run--dryfirst to see the candidate count.
2. Candidate vetting order (AutoBlockService::run)
Each collected candidate IP must pass these gates in order before it is actually blocked.
- Internal / self-network exclusion — skip if
SecurityScope::isInternal($ip)(skipped_internal). This decision unions registered instance IPs (/24 prefix + exact IP) with the Operations Policyinternal_networkswhitelist. The query stage also pre-excludesexcludePrefixes()viaNOT LIKE. - Whitelist exoneration — skip if
infr_whitelisthas an active, non-expired /32 or /24 entry (skipped_wl). - Innocence veto — applies only to JA3-only candidates with no hard signal (severity trigger). Within the
reputation_capbudget, it checks AbuseIPDB reputation; if the IP is spotless (abuse score 0 AND reports 0 AND not Tor) it is auto-whitelisted forinnocent_days(infr_whitelist(source=auto)) instead of blocked (innocent). Severity candidates skip this gate and are blocked outright. - server_index resolution —
global→[0],per_server→resolve the targetdest_ipviaServerIndexResolver::byIp(fallback[0]). - Write — upsert into
infr_blockviaInfrBlock::updateOrCreate([server_index, cidr], …)withsource='schedule',is_active=true,reason='[InfraGuard Auto] …'.
collect candidates (OR) → internal exclude → whitelist → (JA3-only) innocence veto → server_index → write infr_block
isSpotless()returns false (i.e. block cautiously) if the reputation lookup fails, is unconfigured, or is unresolved. Innocence exoneration is a conservative gate that fires only when an IP is clearly clean.
3. Scheduler & command
infr:auto-block is a one-shot command that evaluates once and exits — not a daemon. The scheduler ticks every minute, but the effective interval is autoblock_interval_min.
# Check the gate (immediate no-op if policy is OFF), then evaluate + write once
php artisan infr:auto-block
# Count candidates only, no writes (for tuning/validation)
php artisan infr:auto-block --dry
# Force one run, bypassing the policy gate (on + interval)
php artisan infr:auto-block --force
# Suppress output when nothing was blocked (for scheduling)
php artisan infr:auto-block --quiet-if-ok
Schedule registration (routes/console.php):
Schedule::command('infr:auto-block --quiet-if-ok')
->everyMinute()
->withoutOverlapping(2)
->onOneServer()
->when($is_leader); // leader gating — only one node across a fleet
Gate logic:
autoblock_on=0→ the command immediately reports "skipped" (default OFF, safe).- Interval not yet elapsed since the last run → skip.
--forceand--drybypass the gate.
Example output:
infr:auto-block — candidates 12 (sev 3 · ja3 9) · blocked 7 · wl-skip 1 · internal-skip 2 · innocent 2
4. Path ① collector auto-block (Node)
Node collectors on each host consume Suricata events and block directly into blacklist_info at /24. This path is independent of the scheduler (path ②) and always runs.
| Collector | Layer | Detects |
|---|---|---|
infr_eve_ingest.cjs |
Ingest | Suricata eve.json → loads suricata_http_ct · suricata_tls_ct · suricata_flow_ct · suricata_alert_ct |
infr_srct_tx.cjs |
Signature/heuristic | Aggregates Suricata L7 → 5-category scanner/bot detection → blacklist_info /24 |
infr_kts_tx.cjs |
Kitsune ML | conntrack + autoencoder anomaly detection (anomaly_score) → blacklist_info /24 |
srct_tx's five threat categories:
(A) [HTTP] URL scanner .env / .git / wp-login and other scanner-signature URLs
(B) [HTTP] bot User-Agent curl / python-requests / sqlmap and other tool signatures
(C) [HTTP] brute probing many 4xx responses in a short window (path brute-force)
(D) [TLS] JA3 anomaly many JA3, missing SNI and other botnet fingerprints
(E) [FLOW] TCP flag anomaly many short scan sessions (port scan)
srct_tx also runs whitelist filtering + auto-innocent exoneration just before blocking: only E-only (short-scan-alone) candidates get an AbuseIPDB reputation check, and if they are clearly clean (0 reports · abuse score 0 · not Tor) they are registered as a 30-day exemption (infr_whitelist(source=auto)) and the block is skipped. If any hard signal (A/A2/B/C/D) is present it blocks outright. Its constants and thresholds are managed in Rules & Thresholds.
5. Path ③ enforcement — sync_black → ipset
infr_sync_black.mjs runs periodically on each host and syncs both lists into the kernel ipset blacklist.
# Run per host with its server_index (requires root)
node infr_sync_black.mjs sv=4
# crontab (every 5 min, flock to prevent overlap)
*/5 * * * * /usr/bin/flock -n /var/run/sync_black.lock \
/usr/bin/node /root/server_monitor/infr_sync_black.mjs sv=4 \
>> /var/log/sync_black.log 2>&1
Behavior:
- Load from
blacklist_info(/24) +infr_block(/32) whereserver_index IN (0, <sv>) AND is_active/is_use=1. - Balanced dedup — skip any /32 whose parent /24 already covers it.
- Force the kernel ipset to
hash:net(recreate if a different type), load into a temp set, then atomic swap (no downtime). - Ensure
iptables -I INPUT -m set --match-set blacklist src -j DROPexists (idempotent). - Safety net — private (RFC1918), loopback, and self-network prefixes (
HOME_NET_PREFIX) are never loaded into ipset even if they slip into the DB.
sync_blackneeds root plus the kernelipset/iptablestools, and theblacklistipset must already exist on the target host:ipset create blacklist hash:net family inet hashsize 8192 maxelem 65536 iptables -I INPUT -m set --match-set blacklist src -j DROP
Data sources
All source and record tables live on the legacy_glory connection (the DB shared by collectors and enforcement scripts).
| Table | Connection | Role | Unit |
|---|---|---|---|
suricata_alert_ct |
legacy_glory | Severity trigger source (cols alert_ts · severity · src_ip · dest_ip · signature) |
— |
suricata_tls_ct |
legacy_glory | JA3 rotation trigger source (cols event_ts · ja3_hash · src_ip) |
— |
blacklist_info |
legacy_glory | Collector auto-block list (IDS programs · srct/kts) | /24 |
infr_block |
legacy_glory | InfraGuard scheduler-owned block list (migration 000751) | /32·/24 |
infr_whitelist |
legacy_glory | Block exemptions (manual · auto-innocent · seed) | /32·/24 |
infr_server_index |
default (MAIN) | Sparse, non-sequential server_index ↔ alias/instance lookup (000750) | — |
infr_block schema (summary)
| Column | Type | Meaning |
|---|---|---|
server_index |
int | 0=global · else=server resolved via infr_server_index |
cidr |
varchar(43) | individual IP (/32) or normalized /24 · UNIQUE(server_index,cidr) |
ip |
varchar(45) | original IP (display) |
reason |
varchar(500) | trigger evidence ([InfraGuard Auto] sev1 x3 …) |
source |
varchar(16) | auto · manual · schedule |
is_active |
bool | whether sync_black loads it |
expires_at |
datetime | NULL=permanent |
last_seen_at |
datetime | last re-detection (purge decision) |
Access
Auto-block has no user-facing execution UI. The relevant permission gates are (levels: demo=1 · staff=5 · admin=10):
| Target | Required access |
|---|---|
| Auto-block policy config (Operations Policy ▸ Security settings) | Admin (Administration, level ≥5 / admin) |
| Viewing block results (Blocklist) | Security read (staff, level ≥2) |
| Manual block/release · Whitelist writes | staff (level ≥2) + audit log |
Scheduler execution (infr:auto-block) |
System (not a logged-in user) — leader-gated |
The credentials the scheduler uses are never hardcoded — they are externalized to
infr_collector/.env.sync_black's DB credentials and self-network prefixes are likewise injected via env or the Inventory ▸ Tools mashup substitution (no hardcoding).
Troubleshooting & FAQ
I turned it on but nothing is being blocked.
- Check the candidate count with
--dry:php artisan infr:auto-block --dry. - The JA3 threshold is likely too high (default 50). Real offenders sit around ~32, so lower it to 15–20.
- Severity trigger:
autoblock_severity=critonly catches sev≤1 (red). To include orange, setwarn(sev≤2). - If candidates are all
internal-skip/wl-skip, review the Operations Policy internal whitelist or Whitelist.
Blocks aren't reaching the actual firewall.
- An entry in
infr_blockis not enforced untilsync_blackruns. Check the target host's cron, thesv=<server_index>argument, and that theblacklistipset exists. - Under
balancedscope, a /32 already covered by a parent /24 is intentionally deduped and not loaded separately (this is normal).
An innocent IP got blocked.
- Register it as /32 or /24 in Whitelist; it is exempt from the next sweep on (release the existing
infr_blockrow from Blocklist). - Auto-innocent exoneration applies only to JA3-only candidates. IPs caught by the severity trigger are blocked regardless of reputation, so they require manual exemption.
It's per_server but records land as global (server_index=0).
- If the target
dest_ipcannot be resolved viainfr_server_index, it safely falls back to[0]. Verify the collector/instance IP mapping.
The scheduler runs on multiple servers at once.
->onOneServer()->when($is_leader)leader gating ensures only one node runs it. Check the leader-lock state in the observability dashboard.
Related
- Operations Policy — auto-block master switch, triggers, scope (Security settings tab)
- Blocklist & Reputation — auto-block result list, reasons, release
- Whitelist — exempt IPs/CIDRs, auto-innocent exemption
- Detections ▸ JA3 — JA3 rotation trigger source
- Threats — IDS severity trigger source
- IP Reputation (AbuseIPDB) — reputation lookup for the innocence veto

