Skip to content

Portscan Detection

Antonios Voulvoulis edited this page Apr 14, 2026 · 1 revision

Portscan Detection

Type: Module Layer: L1 — Traffic pressure (L3/L4) Since: v1.80.x Config: conf.d/portscan/main.conf (.local override) Daemon dependency: NO (kernel logging is self-contained; userspace analysis is optional)


Purpose

See also: Glossary | Health Model | Architecture | Known Limitations

Portscan detection identifies hosts probing multiple closed ports in a time window. It operates via a kernel-side logging chain and a userspace log parser that aggregates events and issues bans. The kernel chain logs suspicious connection attempts; the classic detector (or Suricata) analyzes the logs and triggers bans via the manual blacklist.


How It Works

Portscan detection has two stages:

Stage 1: Kernel logging (always active when chain exists)

The portscan_detection chain logs new TCP SYN and UDP connections to non-excluded ports. These log entries use the NFTBAN_PORTSCAN: prefix and are written to the kernel journal or kern.log.

portscan_detection chain:
  TCP SYN to any port → LOG "NFTBAN_PORTSCAN:SYN" (rate-limited 10/sec burst 50)
  UDP to non-{53,123,443} → LOG "NFTBAN_PORTSCAN:UDP" (rate-limited 10/sec burst 50)

The chain logs only — it does not drop traffic. Detection and enforcement happen in stage 2.

Stage 2: Userspace analysis (requires classic detector or Suricata)

The classic detector runs periodically (via maintenance timer) and:

  1. Reads kernel log entries matching NFTBAN_PORTSCAN: prefix
  2. Aggregates per source IP: ports contacted, time window, pattern type
  3. Classifies scan patterns: vertical (many ports on one host), horizontal (one port across hosts), strobe (rapid burst), block (sequential range)
  4. If thresholds are exceeded → issues a ban via blacklist_manual_ipv4/ipv6

Bans from portscan detection land in the shared manual blacklist set and are enforced at pipeline phase 3 (Ban Enforcement) by the input_blacklist_manual_drop counter.

Detection modes

Mode Source Availability
classic Kernel log parsing (NFTBAN_PORTSCAN: prefix) Always available
suricata Suricata EVE JSON alerts (portscan signatures) Requires Suricata installed
hybrid Both classic and Suricata together Maximum coverage
auto Suricata if available, otherwise classic Default

Kernel Objects

Chain (1 per family, required when ENABLED)

Chain Rules Purpose
portscan_detection 2 (TCP SYN log + UDP log) Logs connection attempts for userspace analysis

The chain must have > 0 rules. A chain with 0 rules is DEGRADED (B80-3).

No dedicated counters

This is the critical evidence limitation of the portscan module.

Unlike DDoS (which has input_ct_ssh_drop, input_syn_rate_exceeded, etc.), portscan has no dedicated named counter in the nftables schema. The chain only logs — it does not drop or count.

Consequences:

  • The validator cannot prove portscan enforcement from kernel evidence alone
  • Enforcement evidence requires parsing kernel logs (fragile, subject to rotation, rate limiting, and journal size limits)
  • The validator reports effective state as IDLE due to lack of observable evidence — this represents "no measurable activity," not confirmed inactivity

Bans issued by the portscan detector are counted by input_blacklist_manual_drop, but this counter is shared with operator manual bans and LoginMon bans. It cannot attribute drops to portscan specifically.


Configuration

Key File Default Meaning
PORTSCAN_ENABLED conf.d/portscan/main.conf "false" Master enable/disable
PORTSCAN_MODE conf.d/portscan/main.conf "auto" Detection mode

Classic detector thresholds

Key Default Meaning
PORTSCAN_CLASSIC_MIN_PORTS 5 Minimum unique ports to begin tracking
PORTSCAN_CLASSIC_TIME_WINDOW 60s Detection aggregation window
PORTSCAN_CLASSIC_VERTICAL_PORTS 10 Ports on one host to trigger vertical scan detection
PORTSCAN_CLASSIC_VERTICAL_WINDOW 60s Time window for vertical scan
PORTSCAN_CLASSIC_HORIZONTAL_TARGETS 5 Hosts probed on one port to trigger horizontal detection
PORTSCAN_CLASSIC_HORIZONTAL_WINDOW 30s Time window for horizontal scan

Ban durations

Scan type Duration
Vertical (many ports, one host) 1800s (30 min)
Horizontal (one port, many hosts) 3600s (1 hour)
Block (sequential port range) 7200s (2 hours)
Strobe (rapid burst) 600s (10 min)

Progressive banning: repeat offenders get 2x duration, up to 86400s (24h).


State Model (v1.81)

This module follows the 4-axis model:

  • Config: ENABLED / DISABLED
  • Structural: PRESENT / MISSING (chain exists + jump rule in input chain)
  • Runtime: PASS (no daemon dependency for kernel logging; userspace analysis is not part of runtime axis evaluation)
  • Effective: IDLE (always, due to lack of observable evidence from kernel). The chain may be actively logging, but without a dedicated counter the validator has no kernel evidence to prove it.

Truth table

Config Structural Runtime Effective System Contribution
DISABLED skip
DISABLED PRESENT (residual) PASS valid residual (chain logs but no analysis)
ENABLED PRESENT (chain exists) PASS IDLE (no evidence) Module contributes no effective evidence; system state determined by other modules
ENABLED PRESENT (chain exists, rules = 0) PASS DEGRADED (empty chain, B80-3)
ENABLED MISSING PASS DEGRADED

Why this module contributes no effective evidence

When portscan is ENABLED and structurally PRESENT, the validator cannot prove enforcement for this module using kernel evidence. The chain is logging and the classic detector may be banning, but without a dedicated counter there is no kernel proof. This module's effective axis is based on STRUCTURAL evidence only. The overall system state is determined by other modules — if DDoS is ENFORCING, the system is PROTECTED regardless of portscan's evidence gap.


CLI Commands

nftban portscan status       # Show module state and recent activity
nftban portscan enable       # Enable portscan detection
nftban portscan disable      # Disable portscan detection
nftban portscan check        # Run manual detection now
nftban portscan history      # View detected port scans (last 24h)
nftban portscan sync         # Sync logs from journalctl

Verification (MANDATORY)

# Check if chain exists (structural)
nft list chain ip nftban portscan_detection
# Expected: chain with 2 log rules (TCP SYN + UDP)
# Empty chain = DEGRADED

# Check jump rule exists in input chain
nft list chain ip nftban input | grep portscan
# Expected: "jump portscan_detection"

# Check kernel logs for portscan events
journalctl -k --since "1 hour ago" | grep "NFTBAN_PORTSCAN" | head -5
# Shows logged connection attempts (if any)
# Zero entries = no suspicious traffic logged (NEUTRAL, not failure)

# Check module state via validator
nftban-validate --json | jq '.modules.portscan'
# Expected: {"config":"enabled","structural":"present","effective":"idle"}
# effective is always "idle" — no counter evidence available

# Check if classic detector found anything
nftban portscan history
# Shows detected scans and bans issued (last 24h)

Failure Modes

DEGRADED: ENABLED but chain missing

Symptom: Validator reports structural: "missing" for portscan. Cause: Module enabled but chain not loaded. Fix: nftban portscan enable or nftban firewall rebuild

DEGRADED: Chain exists but has 0 rules (B80-3)

Symptom: Finding VAL-CHAIN-004 for portscan_detection. Cause: Failed enable or partial rebuild. Fix: nftban portscan enable

Detection running but 0 bans

Symptom: Kernel logs show NFTBAN_PORTSCAN: entries but nftban portscan history shows no detections. Possible causes:

  • Thresholds not reached (scanners hit fewer ports than configured minimum)
  • Log source misconfiguration (classic detector reading wrong file — see v1.81.1 portscan log-path collision fix)
  • Log rotation or journal vacuuming removed evidence before analysis

Not a cause: Zero detections does NOT mean the module is broken. It may mean no scanner exceeded the configured thresholds.

Residual chain after disable

When portscan is DISABLED but the chain persists from a prior enable, the chain continues logging to kernel journal. No analysis or banning occurs because the classic detector does not run when disabled. This is a valid residual state — informational, not DEGRADED.


Limitations

  • No dedicated kernel counter. This is the primary evidence gap. The validator cannot prove portscan enforcement from kernel alone. A future schema version may add a portscan_drop counter.
  • Log-based evidence is fragile. Kernel log entries can be lost to printk rate limiting, journal vacuuming (SystemMaxUse), or logrotate. If log evidence is unavailable, the detector finds nothing — not because there are no scans, but because the evidence was lost.
  • Shared enforcement counter. Bans issued by the portscan detector land in blacklist_manual_ipv4/ipv6 and are counted by input_blacklist_manual_drop. This counter is shared with operator manual bans (nftban ban) and LoginMon bans. Per-source attribution requires daemon journal evidence (ban log CLASS field).
  • Rate-limited logging. The kernel chain logs at 10/second burst 50. Under a fast portscan (e.g., masscan at 10K packets/sec), most packets are NOT logged. The classic detector sees a sample, not the full scan. This is by design — logging every packet would overwhelm the journal.
  • Classic detector is shell-based. The log parsing and aggregation runs in bash. On high-volume hosts (300K+ log lines), performance is bounded by the v1.82 Step 5 fix (tail -5000 input cap). Suricata mode provides better accuracy for high-volume environments.

Clone this wiki locally