v0.8.1 · updated · GitHub

guard

Tail a log source and auto-block IPs that exceed thresholds. Normal runs modify the firewall — every run can add iptables/nftables/DOCKER-USER rules. Use --dry-run to execute detection without firewall or state-file changes.

Requires root
Normal guard runs need sudo; --dry-run does not. Test with --never-block YOUR_IP or run blocklist refresh first.

Synopsis

caddy-analyze guard [source] [flags]
# source: file, docker://name, k8s://pod -n ns, journalctl://unit, or via caddy-analyzer.json
# see Log Sources for syntax — this page covers guard flags only

Flags

FlagShortDefaultDescription
--limit-l100Max HTTP requests per IP in window before block
--window-w1mSliding window (per-second buckets)
--duration-d10mBan duration (0 = permanent)
--auth-limit10401/403 before block
--notfound-limit50404 before block
--detect-confidence8Min confidence 1-10 for pattern-based blocks (0 disables only this block path; it is not a dry run)
--firewall-backendautoauto|iptables|docker|nftables|hybrid
--dry-runfalseReport would-be blocks and emit would_block audit events without changing firewall or state
--iptables-timeout10sPer-invocation iptables timeout
--country-blockISO codes to block immediately (e.g. CN,RU)
--no-blocklistfalseDisable 8 blocklist feeds
--blocklist-refresh6hRefresh interval (min 1h, 0 off)
--cache-dir~/.cache/…/blocklistsBlocklist cache dir
--geoip-dbGeoIP mmdb path (auto-discovery)
--no-auto-downloadfalseDisable GeoIP auto-download
--never-blockCIDRs/IPs never block (protect your IP!)
--never-block-fileFile with CIDRs (one per line, # comment)
--protect-selftrueDiscover and protect server interface/public egress IPs; --protect-self=false disables these checks
--audit-log/var/log/…-audit.jsonlJSON lines audit (0600)
--audit-syslogForward audit events to a syslog UDP address
--webhook-url / --audit-webhookAsync webhook notification; provider: generic, slack, discord, pagerduty
--pagerduty-routing-keyPagerDuty routing key (required for PagerDuty, never logged)
--audit-timeout / --audit-retries5s / 2Per-delivery timeout and bounded retries
--audit-rate-limit0Minimum notification interval per IP
--state-file/var/lib/…/blocked.jsonPersist blocks across restarts
--subnet-limit0/24 or /64 distributed scan threshold
--rps-anomaly0EWMA RPS spike factor (audit alert only; requires --audit-log)
--cred-stuffing-limit0Distinct IPs failing same path (audit alert only; requires --audit-log)
--ua-rotation10UA rotation threshold
--trust-forwardedfalseUse last public XFF hop

Automatic self-ban protection

Since v0.8.0, --protect-self=true protects the server's interface and public egress addresses by default. This includes requests from processes sharing the same home router/NAT. Discovery runs at startup, every 30 seconds and again before each new ban, so a WAN address change is checked before blocking. A successful refresh replaces old addresses; it does not append entries to the whitelist file.

Protection takes precedence over request/auth/404 thresholds, pattern detection, blocklist feeds, country rules and subnet bans containing a server address. Existing tracked rules covering a protected address are removed from the firewall and saved state. Failed removals remain tracked for retry. Dry-run reports would_unblock without changing either.

Public discovery uses direct, family-specific HTTPS requests to ipify, with icanhazip as fallback. HTTP proxy settings are ignored. Each request has a 4-second timeout and discovery has a 10-second overall limit. A failure in any required family, including IPv6 configured without Internet connectivity, fails the check. Before-ban checks are synchronous and add latency.

If discovery fails at startup, guard refuses to start; runtime failures retain known protected addresses and suspend new bans until discovery recovers. Both errors instruct you to restart with --protect-self=false. See Troubleshooting → server IP protection.

On a remote VPS, the administrator's home/mobile IP is usually different from the server's IP: add it separately with --never-block or whitelist. Shared-NAT sources receive the same public-IP exemption; a different device's IPv6 address is not automatically protected. Run guard on the same network/egress path as the processes being protected. This feature does not change Fail2Ban or prevent explicit manual block commands.

Minimal example

modifies firewall — server addresses are protected automatically. For remote administration, also allowlist the client IP.

# Replace this with the public IP of your administrator workstation, not the VPS
ADMIN_IP=203.0.113.10
sudo caddy-analyze guard --never-block "$ADMIN_IP" --limit 50 --window 1m /var/log/caddy/access.log

Fixed-IP or restricted-network server

modifies firewall — disable server-IP discovery and keep explicit trusted addresses. This skips both interface and public-IP checks, not the explicit whitelist.

sudo caddy-analyze guard /var/log/caddy/access.log \
  --protect-self=false --never-block 203.0.113.10

Advanced example

modifies firewall — hybrid backend + country + persistent state. To safely preview the same logic, add --dry-run; the terminal shows would-be blocks and the audit sink receives would_block events.

# hybrid = INPUT + DOCKER-USER; blocks survive restart; full audit
sudo caddy-analyze guard docker://caddy \
  --firewall-backend hybrid \
  --country-block CN,RU,IR \
  --never-block 10.0.0.0/8,192.168.1.50 \
  --never-block-file /etc/caddy-analyzer/whitelist.txt \
  --audit-log /var/log/caddy-analyzer-audit.jsonl \
  --state-file /var/lib/caddy-analyzer/blocked.json \
  --blocklist-refresh 6h \
  --detect-confidence 8
# verify what was blocked:
sudo caddy-analyze unban --list
cat /var/log/caddy-analyzer-audit.jsonl | tail
Remote administrator access
Automatic protection covers the server, not every administrator connection. Obtain your workstation's public IP on that workstation and pass it with --never-block. Use --dry-run to preview decisions; it still performs self-IP discovery unless disabled. Whitelist-file changes require restarting guard.