InfraGuard

Auto-block Pipeline

The background defense flow where collectors and the scheduler auto-list attacker IPs meeting configurable triggers and enforce them via kernel ipset.

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

Layout

Auto-block has no dedicated page. Its control and observation points are:

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_ts while the JA3 trigger uses event_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=50 catches nothing. To actually fire, lower it to about 15–20. Run --dry first 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.

  1. Internal / self-network exclusion — skip if SecurityScope::isInternal($ip) (skipped_internal). This decision unions registered instance IPs (/24 prefix + exact IP) with the Operations Policy internal_networks whitelist. The query stage also pre-excludes excludePrefixes() via NOT LIKE.
  2. Whitelist exoneration — skip if infr_whitelist has an active, non-expired /32 or /24 entry (skipped_wl).
  3. Innocence veto — applies only to JA3-only candidates with no hard signal (severity trigger). Within the reputation_cap budget, it checks AbuseIPDB reputation; if the IP is spotless (abuse score 0 AND reports 0 AND not Tor) it is auto-whitelisted for innocent_days (infr_whitelist(source=auto)) instead of blocked (innocent). Severity candidates skip this gate and are blocked outright.
  4. server_index resolution — global→[0], per_server→resolve the target dest_ip via ServerIndexResolver::byIp (fallback [0]).
  5. Write — upsert into infr_block via InfrBlock::updateOrCreate([server_index, cidr], …) with source='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:

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:

  1. Load from blacklist_info (/24) + infr_block (/32) where server_index IN (0, <sv>) AND is_active/is_use=1.
  2. Balanced dedup — skip any /32 whose parent /24 already covers it.
  3. Force the kernel ipset to hash:net (recreate if a different type), load into a temp set, then atomic swap (no downtime).
  4. Ensure iptables -I INPUT -m set --match-set blacklist src -j DROP exists (idempotent).
  5. 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_black needs root plus the kernel ipset/iptables tools, and the blacklist ipset 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.

Blocks aren't reaching the actual firewall.

An innocent IP got blocked.

It's per_server but records land as global (server_index=0).

The scheduler runs on multiple servers at once.

Related

The security tab of Operations Policy, where auto-block lives
The security tab of Operations Policy, where auto-block lives
The blocklist, where auto-blocks land
The blocklist, where auto-blocks land