-
Notifications
You must be signed in to change notification settings - Fork 0
Portscan Detection
Type: Module Layer: L1 — Traffic pressure (L3/L4) Since: v1.80.x Config:
conf.d/portscan/main.conf(.localoverride) Daemon dependency: NO (kernel logging is self-contained; userspace analysis is optional)
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.
Portscan detection has two stages:
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.
The classic detector runs periodically (via maintenance timer) and:
- Reads kernel log entries matching
NFTBAN_PORTSCAN:prefix - Aggregates per source IP: ports contacted, time window, pattern type
- Classifies scan patterns: vertical (many ports on one host), horizontal (one port across hosts), strobe (rapid burst), block (sequential range)
- 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.
| 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 |
| 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).
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.
| 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 |
| 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 |
| 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).
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.
| 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 |
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.
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# 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)Symptom: Validator reports structural: "missing" for portscan.
Cause: Module enabled but chain not loaded.
Fix: nftban portscan enable or nftban firewall rebuild
Symptom: Finding VAL-CHAIN-004 for portscan_detection.
Cause: Failed enable or partial rebuild.
Fix: nftban portscan enable
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.
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.
-
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_dropcounter. -
Log-based evidence is fragile. Kernel log entries can be lost to
printkrate 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/ipv6and are counted byinput_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.
NFTBan Wiki
Getting Started
Architecture
Modules
- BotGuard (HTTP L7)
- DDoS Protection (L3/L4)
- Portscan Detection
- Login Monitoring
- Blacklist & Threat Intelligence
- Suricata IDS Integration
- DNS Tunnel Suspicion
Operator Reference
- CLI Commands Reference
- Configuration Reference
- Systemd Units & Timers
- Optimization & Tuning
- Security Operations Guide
- GeoIP Database Guide
- FHS Compliance
- Troubleshooting: Smoke & Selftest
Verification & Trust
- Glossary & Vocabulary
- Known Limitations
- Metrics & Evidence Model
- Binary Verification (SLSA)
- Security Architecture
Reference
Legal