Suricata IDS/IPS Inline: Tuning Without Killing Throughput

Most Suricata deployments fail the same way: someone enables every ruleset, points it at a busy link, and within a day the alerts are an unread firehose and the box is dropping packets it cannot inspect fast enough. Suricata is excellent, but out of the box it is tuned for nothing in particular. Making it useful is about constraining it — the right mode, the right rules, and threading that matches your hardware.

IDS vs IPS: Decide First

The single biggest decision, because it changes the deployment and the blast radius of a mistake:

  • IDS (passive) — Suricata sees a copy of traffic (SPAN port, TAP, or AF_PACKET in tap mode). It alerts and logs but cannot block. A bad rule is noise.
  • IPS (inline) — traffic flows through Suricata, which can drop packets. A bad rule is an outage.

Start in IDS. Run it passive for weeks, tune the rules, and only go inline once you trust the ruleset — because inline, a false positive on a drop rule blocks legitimate traffic, and now your security tool is the incident.

Passive IDS with AF_PACKET

For IDS, AF_PACKET in tap mode reads from a mirror/SPAN interface with no kernel overhead from a capture library:

suricata.yaml
af-packet:
- interface: eth1 # SPAN/mirror port
threads: 4
cluster-id: 99
cluster-type: cluster_flow
defrag: yes
use-mmap: yes
tpacket-v3: yes

cluster_flow hashes packets to threads by flow, so both directions of a connection land on the same worker — essential, because Suricata reassembles streams and needs to see the whole flow on one thread.

Terminal window
suricata -c /etc/suricata/suricata.yaml -i eth1

Inline IPS: AF_PACKET Bridge or NFQUEUE

Two ways to put Suricata in the path:

AF_PACKET inline bridges two interfaces — traffic in eth1 out eth2 and back, with Suricata between. Add a copy-iface peer:

af-packet:
- interface: eth1
threads: 4
cluster-id: 98
cluster-type: cluster_flow
copy-mode: ips
copy-iface: eth2
- interface: eth2
threads: 4
cluster-id: 97
cluster-type: cluster_flow
copy-mode: ips
copy-iface: eth1

NFQUEUE hooks Suricata into netfilter — you choose exactly which traffic to inspect with an iptables/nftables rule, which is powerful on a Linux router:

Terminal window
# Send forwarded traffic to queue 0
nft add rule inet filter forward queue num 0
Terminal window
suricata -c /etc/suricata/suricata.yaml -q 0

NFQUEUE is the more flexible option on a host already doing routing/firewalling — you inspect only what matters (e.g., only inbound to the DMZ) instead of everything, which is half the performance battle.

Rulesets: Less Is More

The instinct to enable every ruleset is exactly wrong. Thousands of rules you do not need cost CPU on every packet and bury the alerts that matter. Manage rules with suricata-update:

Terminal window
# List available sources
suricata-update list-sources
# Enable a curated source (free ET Open)
suricata-update enable-source et/open
# Update
suricata-update
# Reload rules without restarting (no traffic interruption)
suricatasc -c reload-rules

Then prune. Disable rule categories irrelevant to your environment, and use threshold/suppression for the noisy-but-not-actionable:

/etc/suricata/threshold.config
# Suppress a rule entirely for a known-good scanner
suppress gen_id 1, sig_id 2013028
# Rate-limit a chatty rule: alert once per 60s per source
threshold gen_id 1, sig_id 2019401, type limit, track by_src, count 1, seconds 60

A tuned 8,000-rule set that alerts on real things beats a 40,000-rule set you have learned to ignore. Suricata’s value is the alert you actually read.

Alert, Then Selectively Drop

In IPS mode, a rule’s action matters. ET rules ship as alert; switch only the ones you trust to drop. Rather than editing rules, override actions by SID:

Terminal window
# suricata-update with a modify list to flip specific SIDs to drop
# /etc/suricata/enable.conf + modify.conf
# modify.conf: turn a trusted signature into a drop
2013028 "alert (.+)" "drop \1"

The safe path inline: everything stays alert first, you watch the eve.json logs for what would have dropped, and you promote a signature to drop only after confirming it never fires on legitimate traffic. Going straight to drop on the full ruleset is how Suricata blocks your own backups or a SaaS integration and gets blamed for an outage it was technically right about.

Performance: Match Threads to Reality

Suricata is multi-threaded, but you have to feed it correctly:

  • Workers runmode (default with AF_PACKET clustering) — each thread does capture-to-detection for its flows. Set threads to your physical cores, not hyperthreads, and pin with cluster_flow.
  • Watch the drops. The metric that matters is capture.kernel_drops — packets that arrived but Suricata could not inspect in time. Non-zero means it is overwhelmed and not seeing all traffic, which silently defeats the point.
Terminal window
# Live stats — kernel_drops must stay at/near zero
suricatasc -c dump-counters | grep -E 'kernel_packets|kernel_drops'
# Or read stats.log / eve.json stats events
tail -f /var/log/suricata/stats.log | grep -E 'capture.kernel'

If drops climb: reduce rules, add worker threads, enable hardware offload appropriately, or split the traffic across more capture interfaces. A Suricata dropping 5% of packets is a Suricata missing 5% of attacks while looking perfectly healthy in the dashboard.

The eve.json Pipeline

Suricata’s real output is eve.json — structured events (alerts, flows, DNS, TLS, HTTP, file extraction) you ship to a SIEM or Elastic. The alert is the headline; the flow/DNS/TLS records are the context that makes an alert investigable:

outputs:
- eve-log:
enabled: yes
filetype: regular
filename: eve.json
types:
- alert
- dns
- tls
- http
- flow

Ship eve.json, build dashboards on it, and alert on the signatures you tuned. An IDS whose output nobody pipes anywhere is a CPU-burning logfile.

Tuning the Engine, Not Just the Rules

Dropping packets is not always a rule-count problem. Suricata’s defaults assume a modest link, and three knobs decide whether it keeps up before you ever touch the ruleset.

Stream reassembly memory. On a busy link Suricata tracks far more concurrent flows than the defaults allow, and when the flow or stream tables fill it starts bypassing packets — which looks like a detection gap, not an error:

suricata.yaml
flow:
memcap: 1gb
hash-size: 262144
prealloc: 100000
stream:
memcap: 4gb
reassembly:
memcap: 8gb
depth: 1mb # bytes per flow reassembled; raise for large file transfers

MPM matcher. The multi-pattern matcher is the hot path for every packet. On modern x86 with the rule volume of a real ruleset, hyperscan is dramatically faster than the ac default:

mpm-algo: hyperscan

Watch the memory-pressure counters alongside kernel_drops — these reveal a different failure than raw CPU saturation:

Terminal window
suricatasc -c dump-counters | \
grep -E 'flow.memcap_pressure|flow.emerg_mode_entered|tcp.reassembly_memuse|tcp.ssn_memcap_drop'
# Rising flow.memcap_pressure / non-zero emerg_mode_entered / ssn_memcap_drop = raise the memcaps, not the thread count

A box at 40% CPU still dropping packets is almost always memcap starvation, not a threading shortfall. Adding worker threads there wastes cores; raising the memcaps fixes it.

When a Drop Rule Causes an Outage: The Drill

Before trusting inline, rehearse the failure you most fear — a drop rule blocking legitimate traffic — so you know exactly how to find and reverse it under pressure. Run inline with a single deliberately broad drop, generate traffic that trips it, then trace and unblock.

Terminal window
# Find what dropped, from the structured log — verdict + signature
grep '"action":"blocked"' /var/log/suricata/eve.json | \
jq '{sig: .alert.signature, sid: .alert.signature_id, src: .src_ip, dest: .dest_ip}'

That signature_id is the lever. The fastest reversal that does not require a restart is to suppress the SID and hot-reload:

Terminal window
# /etc/suricata/threshold.config — stop the offending SID firing at all
suppress gen_id 1, sig_id 2013028
Terminal window
suricatasc -c reload-rules # applies without dropping the inline path

The slower-but-cleaner fix is to flip the signature back from drop to alert in modify.conf and re-run suricata-update, but in an active outage suppress + reload-rules stops the bleeding in seconds with no traffic interruption. The lesson of the drill: every inline drop needs a known, rehearsed off-switch, and reload-rules — not a restart — is it, because a restart on a bridge or NFQUEUE path drops live traffic while it comes back.

The Honest Summary

Suricata becomes useful when you stop treating it as “turn on all the rules and watch the screen.” Run it passive first, enable a curated ruleset and prune hard, watch kernel_drops like a hawk, and promote rules to drop one trusted signature at a time. Do that and it is a sharp instrument. Skip it and you get the common outcome: a box at 100% CPU, dropping packets it cannot inspect, generating alerts nobody reads — secure on paper, blind in practice.