Suricata IDS/IPS
-
Suricata IDS/IPS
- Turning packets into detections
- Installation
- Rule Management with suricata-update
- Configuration
- IDS Mode (Passive)
- IPS Mode (Inline)
- Acceleration Frameworks
- Rules: Anatomy and Syntax
- Rule Examples
- Lua Scripting for Detection
- Rule Validation and Testing
- Performance Tuning
- Logging and EVE JSON
- Remote Suricata via GRE Tunnel
- Key takeaways
- References
Turning packets into detections
Suricata is a high-performance, open-source network threat detection engine maintained by the Open Information Security Foundation (OISF). It can operate as a passive IDS (intrusion detection system), an inline IPS (intrusion prevention system), or a network security monitoring (NSM) sensor. The current stable release is 8.0.x (8.0.5 as of May 2026); the 7.0.x series reaches end-of-life in July 2026.
ℹ️ A firewall decides whether a packet may pass; an IDS/IPS decides whether a packet is malicious. Suricata is the detection layer that complements the packet filtering on the Defensive Measures page — in IPS mode it can also drop, blurring the line between the two.
Suricata’s rule language is a strict superset of Snort’s — Snort rules run unchanged on Suricata. The additions Suricata brings are: application-layer protocol awareness (HTTP, DNS, TLS, SMB, SSH parsed natively), multi-threaded packet processing, and Lua scripting for detection logic that signature syntax alone cannot express.
The malware course Suricata page covers offline PCAP analysis and malware signature development in depth. This page covers Suricata as a deployed network sensor — configuration, IPS operation, production rule management, and advanced rule writing.
Installation
Debian/Ubuntu
The OISF provides an official PPA that tracks current stable releases:
sudo apt install software-properties-common
sudo add-apt-repository ppa:oisf/suricata-stable
sudo apt update
sudo apt install suricata
The distribution apt packages often lag behind; the PPA is preferred for anything beyond basic testing.
Verify the installed version and compiled-in capabilities:
suricata --build-info
# Suricata version 8.0.5 RELEASE
# ...
# AF_PACKET support: yes
# AF_XDP support: yes
# DPDK support: yes (if compiled in)
# Lua support: yes
FreeBSD
pkg install suricata
FreeBSD’s ports tree ships the current stable release and includes AF_PACKET equivalents via libpcap.
Confirming the default configuration file
suricata --dump-config | head -20 # show all resolved config values
sudo suricata -T -c /etc/suricata/suricata.yaml # test config, exit
The -T flag validates suricata.yaml and all referenced rule files, then exits without capturing. Run it after every config change.
Rule Management with suricata-update
suricata-update is the official rule update tool, installed alongside Suricata. It fetches rule sets, merges them, and writes a single combined file that Suricata loads.
First-time setup
# Fetch rule source index
sudo suricata-update update-sources
# List available free sources
suricata-update list-sources
# Enable a source (Emerging Threats Open is the default)
sudo suricata-update enable-source et/open
# Enable Proofpoint ET Pro (requires registration/subscription)
# sudo suricata-update enable-source et/pro --param secret-code=YOUR_CODE
# Run the update: download rules, merge, write to /var/lib/suricata/rules/suricata.rules
sudo suricata-update
# Check what sources are currently enabled
suricata-update list-enabled-sources
After updating, send SIGUSR2 to reload rules without restarting:
sudo kill -USR2 $(pidof suricata)
Tuning: enabling, disabling, and converting rules
Four files in /etc/suricata/ control rule tuning:
| File | Purpose |
|---|---|
/etc/suricata/enable.conf |
Enable rules disabled in source (by SID or group) |
/etc/suricata/disable.conf |
Suppress rules that fire too many false positives |
/etc/suricata/drop.conf |
Convert matching alert rules to drop for IPS mode |
/etc/suricata/modify.conf |
Rewrite rule fields (e.g., change a threshold value) |
# /etc/suricata/disable.conf — disable a noisy rule by SID
2019401
# /etc/suricata/disable.conf — disable all rules in a group
group:emerging-policy
# /etc/suricata/drop.conf — convert ET malware alerts to drops in IPS mode
group:emerging-malware
# /etc/suricata/modify.conf — tighten a threshold
2019401 "threshold:type both, track by_src, count 3, seconds 60" \
"threshold:type both, track by_src, count 1, seconds 10"
After editing, re-run sudo suricata-update to regenerate the merged rules file.
Automatic updates via cron
# /etc/cron.d/suricata-update
0 3 * * * root /usr/bin/suricata-update && kill -USR2 $(pidof suricata)
Configuration
Suricata’s configuration lives in /etc/suricata/suricata.yaml. The file is long (~1500 lines) but well-commented. The sections that matter most for initial deployment are covered here.
Network variables
vars:
address-groups:
HOME_NET: "[192.168.0.0/16,10.0.0.0/8,172.16.0.0/12]"
EXTERNAL_NET: "!$HOME_NET"
HTTP_SERVERS: "$HOME_NET"
SMTP_SERVERS: "$HOME_NET"
DNS_SERVERS: "$HOME_NET"
port-groups:
HTTP_PORTS: "80"
HTTPS_PORTS: "443"
SHELLCODE_PORTS: "!80"
ORACLE_PORTS: 1521
SSH_PORTS: 22
Set HOME_NET to the actual address space of the network Suricata monitors. Rules use these variables; a mismatch between HOME_NET and actual topology is the most common cause of missed alerts.
Output configuration
outputs:
- fast:
enabled: yes
filename: fast.log
append: yes
- eve-log:
enabled: yes
filetype: regular
filename: eve.json
types:
- alert:
payload: yes # base64-encoded payload in alert events
payload-buffer-size: 4kb
metadata: yes
- http:
extended: yes # log full HTTP transaction fields
- dns:
query: yes
answer: yes
- tls:
extended: yes # log JA3/JA3S, cert details
- flow: {}
- fileinfo: {}
- smtp: {}
eve.json is the structured JSON log — it is the primary output for feeding SIEMs (Elastic, Splunk, Wazuh). fast.log is human-readable and useful for watching alerts in real time with tail -f.
Rule files
default-rule-path: /var/lib/suricata/rules
rule-files:
- suricata.rules # merged output of suricata-update
- /etc/suricata/rules/local.rules # your custom rules
Keep custom rules in a separate file so suricata-update does not overwrite them.
IDS Mode (Passive)
IDS mode reads packets from a capture interface without touching traffic. This is the simplest deployment.
AF_PACKET (recommended for Linux)
# suricata.yaml
af-packet:
- interface: eth0
cluster-id: 99
cluster-type: cluster_flow # distribute by 5-tuple; same flow always hits same thread
defrag: yes
mmap-locked: yes
tpacket-v3: yes # requires Linux ≥ 3.2; better performance than v2
ring-size: 2048
block-size: 131072 # 128 KB — default since 8.0 (was 32 KB in 7.x)
threads: auto # one worker thread per available CPU core
Start Suricata:
sudo suricata -c /etc/suricata/suricata.yaml -i eth0
# or via systemd:
sudo systemctl start suricata
The systemd service uses the interface specified in /etc/default/suricata:
# /etc/default/suricata
IFACE=eth0
libpcap (portable, lower performance)
sudo suricata -c /etc/suricata/suricata.yaml --pcap=eth0
Use libpcap for BSD, VMs, or monitoring a SPAN/mirror port from a managed switch where you cannot control the driver.
IDS mode gotchas
Checksum offloading: NICs in IDS mode commonly report bad TCP/UDP checksums because checksum computation is offloaded to hardware and the captured packets have not yet been checksummed. Suricata will drop these by default. Fix it in config:
# suricata.yaml — disable checksum verification per interface
af-packet:
- interface: eth0
checksum-checks: no
Or pass -k none on the command line for testing.
Asymmetric routing: If Suricata only sees one direction of traffic (half the TCP session), stream reassembly fails and application-layer rules will not fire. In IDS-only deployments on a routed network, place the sensor where it sees both directions, or accept that stream-based rules will have reduced coverage.
Promiscuous mode: Suricata puts the interface into promiscuous mode automatically; no manual ip link set eth0 promisc on is required.
IPS Mode (Inline)
In IPS mode Suricata can drop packets matching drop rules. There are two Linux mechanisms: NFQ (Netfilter Queue) for single-NIC deployments, and AF_PACKET inline for two-NIC bridge deployments.
NFQ — single-NIC IPS
NFQ redirects packets from the kernel’s netfilter subsystem into Suricata’s user-space process, which then verdicts each packet (accept or drop). This is the only practical IPS approach when you have a single NIC.
How it works: iptables/nftables sends matching packets to a numbered queue; Suricata binds to that queue number, inspects the packet, and issues an NF_ACCEPT or NF_DROP verdict.
Step 1 — redirect traffic into a queue:
# All inbound traffic
sudo iptables -I INPUT -j NFQUEUE --queue-num 0
# All outbound traffic
sudo iptables -I OUTPUT -j NFQUEUE --queue-num 0
# Traffic forwarded by this host (router/firewall)
sudo iptables -I FORWARD -j NFQUEUE --queue-num 0
For nftables (preferred on modern systems):
sudo nft add table inet suricata
sudo nft add chain inet suricata input '{ type filter hook input priority 0; policy accept; }'
sudo nft add chain inet suricata output '{ type filter hook output priority 0; policy accept; }'
sudo nft add rule inet suricata input queue num 0
sudo nft add rule inet suricata output queue num 0
Step 2 — configure Suricata for NFQ:
# suricata.yaml
nfq:
mode: accept # default verdict for un-inspected packets
fail-open: yes # if Suricata falls behind, pass packets rather than drop them
batchcount: 20 # process up to 20 packets per syscall; higher = lower overhead
Step 3 — start Suricata in NFQ mode:
sudo suricata -c /etc/suricata/suricata.yaml -q 0
# ^--- queue number matches iptables rule
Step 4 — verify IPS is dropping:
# Add a drop rule for ICMP pings to yourself (test only)
echo 'drop icmp any any -> $HOME_NET any (msg:"TEST DROP ICMP"; sid:9999999; rev:1;)' \
> /etc/suricata/rules/test.rules
sudo suricata-update # rebuild merged rules
sudo kill -USR2 $(pidof suricata)
ping -c 3 127.0.0.1 # pings should be dropped
# Remove the test rule after verifying
NFQ gotchas:
- The queue can fill up if Suricata falls behind. With
fail-open: yes, packets pass uninspected; withfail-open: no, packets are dropped when the queue is full — correct for security, catastrophic for availability. - NFQ adds latency: every packet crosses the kernel/userspace boundary twice. For high-throughput links, use AF_PACKET inline instead.
- Do not use NFQ mode on a Suricata instance that also has
droprules without first testing withalertrules. An incorrect rule can black-hole legitimate traffic. - Connection tracking (
conntrack) must be enabled if you use stateful iptables rules alongside NFQ. Conflicts between conntrack and NFQ are a common source of unexpected drops.
AF_PACKET inline — two-NIC bridge IPS
AF_PACKET inline mode creates a software bridge between two interfaces in kernel space. Suricata copies packets between them and can drop before forwarding. No iptables rules are needed; Suricata owns the forwarding path.
# suricata.yaml — inline IPS between eth1 (external) and eth2 (internal)
af-packet:
- interface: eth1
cluster-id: 98
cluster-type: cluster_flow
copy-mode: ips # inline: copy to partner interface; drop if rule says drop
copy-iface: eth2
checksum-checks: no
threads: 4
- interface: eth2
cluster-id: 97
cluster-type: cluster_flow
copy-mode: ips
copy-iface: eth1
checksum-checks: no
threads: 4
# Bring up both interfaces with no IP address (pure bridge)
sudo ip link set eth1 up
sudo ip link set eth2 up
sudo suricata -c /etc/suricata/suricata.yaml \
--af-packet=eth1 --af-packet=eth2
AF_PACKET inline is the recommended production IPS for Linux. It is faster than NFQ (no syscall crossing per packet) and avoids the conntrack complications.
AF_PACKET inline gotchas:
- Both interfaces must be on the same hardware segment — you cannot bridge a VLAN trunk to a routed interface this way.
- If Suricata stops (crash, OOM kill), traffic does not flow unless you have a bypass or failopen NIC that puts the link into bypass mode. Plan for this in production.
-
cluster_flowdistributes by 5-tuple, so a given TCP session always lands on the same thread. With asymmetric hashing on some NICs, you may see imbalanced thread load;cluster_qm(distribute by hardware queue map) can help.
Acceleration Frameworks
For high-throughput links (10 Gbps+) where kernel packet processing becomes the bottleneck, Suricata supports two kernel-bypass frameworks.
AF_XDP
AF_XDP (eXpress Data Path) uses eBPF programs attached to the NIC driver to redirect packets to a memory-mapped ring buffer in Suricata’s address space, bypassing most of the kernel network stack.
Requirements: Linux ≥ 5.4 with XDP support in the NIC driver. Most Intel and Mellanox NICs support it; check with ethtool -i eth0 | grep driver.
# suricata.yaml
af-xdp:
- interface: eth0
threads: 4
force-xdp-mode: drv # drv = kernel driver XDP (fastest); skb = fallback
mem-alignment: page-size
enable-busy-poll: yes # SO_PREFER_BUSY_POLL; reduces latency on Linux ≥ 5.11
busy-poll-time: 20
busy-poll-budget: 64
sudo suricata -c /etc/suricata/suricata.yaml --af-xdp=eth0
AF_XDP can deliver 2–4x the throughput of AF_PACKET on supported hardware, with lower CPU utilization.
DPDK
DPDK (Data Plane Development Kit) bypasses the kernel entirely using a Poll Mode Driver (PMD) that runs in userspace. It requires the NIC to be unbound from its kernel driver and bound to the DPDK vfio-pci driver.
# Bind NIC to DPDK driver (this removes the interface from the OS)
sudo dpdk-devbind.py --bind=vfio-pci 0000:01:00.0 # PCI address from lspci
# suricata.yaml
dpdk:
eal-params:
proc-type: primary
interfaces:
- interface: 0000:01:00.0 # PCI address, not interface name
threads: 8
promisc: yes
multicast: yes
checksum-checks: no
checksum-checks-offload: yes
mtu: 1500
mempool-size: 65535
mempool-cache-size: 512
rx-descriptors: 1024
tx-descriptors: 1024
DPDK is appropriate for ≥ 10 Gbps inline deployments. The operational overhead is significant: DPDK cores busy-poll continuously (high idle CPU) and the NIC is invisible to the OS while bound to DPDK. Reserve DPDK for dedicated sensor appliances, not general-purpose servers.
Rules: Anatomy and Syntax
A Suricata rule has two parts: a header defining the action, protocol, and network tuple; and an options block in parentheses containing detection keywords and metadata.
<action> <proto> <src_ip> <src_port> <direction> <dst_ip> <dst_port> (options)
Actions
| Action | Behavior |
|---|---|
alert |
Generate an alert; do not affect packet flow |
drop |
Drop the packet (IPS mode only); also generate an alert |
pass |
Stop processing this packet against remaining rules |
reject |
Drop packet and send TCP RST / ICMP unreachable to sender |
In IDS mode, drop and reject are logged as alerts but do not affect traffic. Always test new rules with alert before converting to drop.
Protocol
Suricata parses these application-layer protocols automatically when named in the rule header:
tcp udp icmp ip
http http2 dns tls smtp ssh ftp smb nfs rdp dnp3 enip modbus
Using http instead of tcp means Suricata only evaluates the rule against fully-parsed HTTP transactions — faster and more accurate than byte-scanning raw TCP.
Address and port notation
192.168.1.0/24 # CIDR
[192.168.1.0/24,10.0.0.0/8] # group
!192.168.1.100 # negation
any # wildcard
$HOME_NET # variable from suricata.yaml
Ports follow the same conventions: 80, !80, [80,443,8080], 1024:65535 (range), any.
Direction
-> source to destination
<> bidirectional (match in either direction)
Detection keywords
Content matching:
content:"string" # literal bytes; hex as |xx xx|
nocase # case-insensitive
rawbytes # match before any normalization
distance:5 # skip 5 bytes after previous content match
offset:10 # start search at byte 10 from buffer start
depth:20 # stop searching after byte 20 from buffer start
within:30 # next content must appear within 30 bytes of previous match
startswith # content must start at byte 0 of the buffer (Suricata-only)
endswith # content must end at last byte of buffer (Suricata-only)
fast_pattern # hint: use this content for multi-pattern prefilter
Regular expressions:
pcre:"/pattern/flags"
# flags: i=case-insensitive, m=multiline, s=dot matches newline
# A = match anywhere (like content offset:0)
# R = relative to previous content match (like distance:0)
PCRE is powerful but slow — prefilter with content whenever possible:
content:"X-Custom:"; http.header; # fast prefilter
pcre:"/X-Custom:\s+[A-Z]{8}/H"; # expensive PCRE only runs on matching packets
Byte operations:
byte_test:4,>,0x10000000,0,relative # test 4 bytes at current position
byte_jump:4,0,relative # jump forward by the value at current position
isdataat:100 # assert ≥ 100 bytes remain in buffer
isdataat:!100 # assert < 100 bytes remain
Flow keywords:
flow:established # TCP session is fully established
flow:to_server # client → server direction
flow:from_server # server → client direction
flow:to_client # same as from_server
flow:stateless # match even without a full session
Threshold and rate control:
# Fire once per source IP per 60 seconds, regardless of how many packets match
threshold:type limit, track by_src, count 1, seconds 60
# Fire only after seeing 10 matches from same source in 60 seconds
threshold:type threshold, track by_src, count 10, seconds 60
# Fire once after 10 matches, then reset counter
threshold:type both, track by_src, count 10, seconds 60
Suppress (in threshold.conf, not in rule file):
# suppress:gen_id <gid>, sig_id <sid>[, track <by_src|by_dst|by_either>, ip <addr>]
suppress:gen_id 1, sig_id 2019401, track by_src, ip 10.0.0.0/8
Suppression removes alerts without disabling the rule — use it to silence known-good sources while keeping detection active elsewhere.
Sticky buffers (Suricata-specific)
Sticky buffers name the protocol field first, then all following content/pcre keywords apply to that field. This is cleaner than the old content-modifier style and is the preferred form:
# Old style (content modifier — reads backwards):
content:"GET"; http_method;
# New style (sticky buffer — reads left to right):
http.method; content:"GET";
HTTP sticky buffers:
http.method # GET, POST, PUT…
http.uri # /path?query (normalized; use http.uri.raw for un-normalized)
http.uri.raw
http.request_line # full first line: "GET /path HTTP/1.1"
http.header # all request headers as a single buffer
http.header_names # header names only, one per line
http.user_agent # User-Agent value only
http.host # Host value only
http.cookie # Cookie header value
http.request_body # POST/PUT body
http.response_line # "HTTP/1.1 200 OK"
http.stat_code # "200"
http.stat_msg # "OK"
http.response_body # response body (up to response-body-limit bytes)
http.server # Server response header value
http.location # Location response header value
DNS sticky buffers:
dns.query # queried name (request)
dns.answer.name # name in answer RR
dns.answer.data # RDATA of answer RR (A record → IP as string)
TLS sticky buffers:
tls.sni # Server Name Indication from ClientHello
tls.cert_subject # Subject field from server certificate
tls.cert_issuer # Issuer field
tls.cert_serial # Serial number
tls.ja3_hash # JA3 fingerprint of client TLS negotiation
tls.ja3s_hash # JA3S fingerprint of server TLS negotiation
SSH:
ssh.banner # full banner line
ssh.hassh # HASSH fingerprint of client SSH handshake
ssh.hassh.server # HASSH of server
Metadata keywords
msg:"Descriptive alert message"
sid:2000001 # unique rule ID; use 9000000+ for local rules
rev:1 # revision; increment on each change
gid:1 # group ID; leave at default 1
classtype:trojan-activity # maps to a priority in classification.config
priority:1 # 1=high, 2=medium, 3=low (overrides classtype priority)
reference:url,example.com/advisory
reference:cve,2024-12345
metadata:affected_product HTTP_Server, attack_target Server, signature_severity Major
Rule Examples
Simple rules
Detect an unencrypted Telnet session:
alert tcp $EXTERNAL_NET any -> $HOME_NET 23 \
(msg:"POLICY Unencrypted Telnet Access"; \
flow:established,to_server; \
classtype:policy-violation; \
sid:9000001; rev:1;)
Detect an HTTP response containing a Windows executable:
alert http $EXTERNAL_NET any -> $HOME_NET any \
(msg:"MALWARE PE Executable in HTTP Response"; \
http.response_body; content:"|4d 5a|"; startswith; \
flow:established,from_server; \
classtype:trojan-activity; \
sid:9000002; rev:2;)
Detect DNS query for a known C2 domain:
alert dns $HOME_NET any -> any 53 \
(msg:"MALWARE Known C2 Domain Query"; \
dns.query; content:"evil-c2.example.com"; nocase; \
classtype:trojan-activity; \
sid:9000003; rev:1;)
Detect SSH on a non-standard port (possible tunneling):
alert tcp $HOME_NET any -> $EXTERNAL_NET !22 \
(msg:"POLICY SSH on Non-Standard Port"; \
app-layer-protocol:ssh; \
flow:to_server; \
classtype:policy-violation; \
sid:9000004; rev:1;)
Complex rules
Detect HTTP C2 beacon by combining method, missing headers, and repetition:
alert http $HOME_NET any -> $EXTERNAL_NET $HTTP_PORTS \
(msg:"MALWARE Possible HTTP C2 Beacon - Missing Browser Headers"; \
http.method; content:"GET"; \
http.header; content:!"Accept-Language:"; nocase; \
http.header; content:!"Accept-Encoding:"; nocase; \
http.header; content:!"Referer:"; nocase; \
http.user_agent; content:!"Mozilla"; nocase; \
flow:established,to_server; \
threshold:type both, track by_src, count 5, seconds 300; \
classtype:trojan-activity; \
sid:9000010; rev:3;)
Multiple content negations on the same sticky buffer are AND-ed together: the rule fires when all three headers are absent. The threshold requires the pattern to repeat five times in five minutes — reducing false positives from one-off requests.
Detect DNS tunneling by subdomain entropy (long base64-like label):
alert dns $HOME_NET any -> any 53 \
(msg:"EXFIL Possible DNS Tunnel - Long Encoded Subdomain"; \
dns.query; \
pcre:"/^[a-zA-Z0-9+\/=]{32,}\.[a-zA-Z0-9\-]+\.[a-zA-Z]{2,}$/"; \
threshold:type both, track by_src, count 3, seconds 60; \
classtype:policy-violation; \
sid:9000011; rev:2;)
The PCRE matches queries where the first label is 32+ characters of base64-alphabet characters, a strong indicator of DNS exfiltration.
Detect TLS to an unusual destination with a self-signed certificate:
alert tls $HOME_NET any -> $EXTERNAL_NET any \
(msg:"SUSPICIOUS TLS Self-Signed Cert to Non-Whitelisted Host"; \
tls.cert_issuer; content:"O=Internet Widgits"; nocase; \
tls.cert_subject; \
pcre:"/CN=[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}/"; \
flow:established,to_server; \
classtype:bad-unknown; \
sid:9000012; rev:1;)
Combines a common self-signed cert issuer string with a CN that looks like an IP address rather than a hostname.
Multi-rule approach: separate rules for same malware family:
Writing two independent rules for the same C2 protocol means one remains effective if the attacker patches one indicator:
# Rule 1: User-Agent indicator (high confidence, easy to change)
alert http $HOME_NET any -> $EXTERNAL_NET $HTTP_PORTS \
(msg:"MALWARE Trojan.Generic UA Fingerprint"; \
http.user_agent; content:"WinHttpRequest"; \
flow:established,to_server; \
classtype:trojan-activity; sid:9000020; rev:1;)
# Rule 2: URI structure (harder to change — both ends must agree)
alert http $HOME_NET any -> $EXTERNAL_NET $HTTP_PORTS \
(msg:"MALWARE Trojan.Generic URI Pattern"; \
http.uri; pcre:"/\/update\/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}\//"; \
flow:established,to_server; \
classtype:trojan-activity; sid:9000021; rev:1;)
# Rule 3: Beaconing interval (behavioral; survives any payload change)
alert http $HOME_NET any -> $EXTERNAL_NET $HTTP_PORTS \
(msg:"MALWARE Trojan.Generic Beacon Frequency"; \
http.uri; content:"/update/"; \
flow:established,to_server; \
threshold:type threshold, track by_src, count 10, seconds 600; \
classtype:trojan-activity; sid:9000022; rev:1;)
Drop rule for a known exploit (IPS mode):
drop http $EXTERNAL_NET any -> $HTTP_SERVERS $HTTP_PORTS \
(msg:"EXPLOIT Apache mod_cgi CVE-2024-XXXX RCE Attempt"; \
flow:established,to_server; \
http.uri; content:"/cgi-bin/"; \
http.header; content:"Content-Type: application/x-www-form-urlencoded"; \
http.request_body; \
pcre:"/[;&|`$(){}].*\/bin\/(sh|bash)/"; \
classtype:web-application-attack; \
sid:9000030; rev:1;)
Note: always test drop rules extensively with alert first, and add suppression for known-benign sources before converting.
Lua Scripting for Detection
When the keyword-based rule language cannot express the detection logic you need — entropy calculation, stateful counters across multiple flows, binary protocol parsing — Suricata can call a Lua script per packet or per flow event.
For Lua language fundamentals and broader security tool usage, see the Lua in Network Security page. This section covers only the Suricata detection API.
Enabling Lua rules
As of Suricata 8.0, Lua detection is enabled by default but runs in a sandbox. Verify in suricata.yaml:
security:
lua:
allow-rules: yes # default in 8.x; was no in 7.x
Required functions
Every Lua detection script must define exactly two functions:
function init(args)
-- Called once when the rule is loaded.
-- Return a table declaring which buffers this script needs.
-- Most scripts can return an empty table.
return {}
end
function match(args)
-- Called once per packet/transaction that passes the rule's header match.
-- Return 1 to fire the alert; return 0 to not fire.
return 0
end
Buffers that require explicit initialization:
function init(args)
return { packet = true } -- access raw packet bytes
-- OR:
return { payload = true } -- access TCP stream reassembled payload
-- OR:
return { http = true } -- access HTTP transaction fields
-- OR:
return { dns = true } -- access DNS query/answer fields
end
Available Suricata Lua functions
Packet and flow:
p = SCPacketPayload() -- raw packet payload as string
ts = SCPacketTimestamp() -- {sec, usec}
proto = SCFlowProto() -- protocol number (6=TCP, 17=UDP)
toserver = SCFlowGetAppLayerProto() -- app layer protocol string
HTTP (requires http = true in init):
method = HttpGetRequestMethod()
uri = HttpGetRequestUriRaw()
ua = HttpGetRequestHeader("User-Agent")
body = HttpGetRequestBody()
status = HttpGetResponseStatus()
rbody = HttpGetResponseBody()
header = HttpGetResponseHeader("Content-Type")
DNS (requires dns = true in init):
query_name = DnsGetQueryName() -- queried domain
query_type = DnsGetQueryType() -- numeric type (1=A, 28=AAAA, 16=TXT…)
answer_name = DnsGetAnswerName()
answer_ip = DnsGetAnswerIPString()
TLS:
sni = TlsGetSNI()
issuer = TlsGetCertIssuer()
subject = TlsGetCertSubject()
ja3 = TlsGetJA3()
Logging:
SCLogInfo("message") -- write to Suricata log at INFO level
SCLogDebug("message") -- DEBUG level
SCLogWarning("message") -- WARNING level
Lua rule in a .rules file
Reference the Lua script from a rule using the lua keyword:
alert http $HOME_NET any -> $EXTERNAL_NET any \
(msg:"MALWARE High-Entropy URI Parameter Value"; \
http.uri; content:"?"; \
lua:high_entropy_param.lua; \
classtype:trojan-activity; \
sid:9000100; rev:1;)
The content:"?" pre-filter runs first; the Lua script is only called for URIs that contain a query string.
Lua example: Shannon entropy of URI parameter value
Many C2 protocols encode data in URI parameters using Base64 or hex. These encodings have higher Shannon entropy than human-readable strings.
-- high_entropy_param.lua
-- Fire if any URI parameter value has Shannon entropy > 4.5
function init(args)
return {} -- no special buffer needed; we use HttpGetRequestUriRaw()
end
local function entropy(s)
if #s == 0 then return 0 end
local counts = {}
for i = 1, #s do
local c = s:sub(i, i)
counts[c] = (counts[c] or 0) + 1
end
local h = 0
for _, count in pairs(counts) do
local p = count / #s
h = h - p * math.log(p) / math.log(2)
end
return h
end
function match(args)
local uri = HttpGetRequestUriRaw()
if not uri then return 0 end
-- find the query string
local qs = uri:match("%?(.+)$")
if not qs then return 0 end
-- split into key=value pairs
for pair in qs:gmatch("[^&]+") do
local val = pair:match("=[^=]+$")
if val and #val > 8 then
val = val:sub(2) -- strip leading '='
if entropy(val) > 4.5 then
SCLogInfo("High entropy URI param value: " .. val)
return 1
end
end
end
return 0
end
Lua example: detect HTTP parameter count anomaly
Some exploit payloads spray large numbers of identical parameters. This script fires when a request has more than 50 &-separated parameters:
-- too_many_params.lua
function init(args)
return {}
end
function match(args)
local uri = HttpGetRequestUriRaw()
if not uri then return 0 end
local qs = uri:match("%?(.+)$")
if not qs then return 0 end
local count = 1
for _ in qs:gmatch("&") do
count = count + 1
end
if count > 50 then
SCLogInfo("Excessive URI params: " .. tostring(count))
return 1
end
return 0
end
Lua example: DNS rebinding detection
DNS rebinding attacks use short TTLs and alternating A record answers to bypass Same-Origin Policy. This script flags DNS responses where the TTL is unusually short (under 30 seconds) for a domain that has been queried multiple times:
-- short_ttl_dns.lua
-- Requires Suricata 7+ with DNS answer buffer support.
function init(args)
return { dns = true }
end
function match(args)
-- DnsGetAnswerTTL() is available in Suricata 7+
local ttl = DnsGetAnswerTTL()
if not ttl then return 0 end
if ttl < 30 then
local name = DnsGetAnswerName() or "(unknown)"
SCLogInfo("Short TTL DNS answer: " .. name .. " TTL=" .. tostring(ttl))
return 1
end
return 0
end
Rule:
alert dns any any -> $HOME_NET any \
(msg:"SUSPICIOUS DNS Short TTL Response - Possible DNS Rebinding"; \
lua:short_ttl_dns.lua; \
classtype:bad-unknown; \
sid:9000110; rev:1;)
Rule Validation and Testing
# Test config and rules without starting capture
sudo suricata -T -c /etc/suricata/suricata.yaml
# Exits 0 if clean; prints rule errors if not
# Test rules against a PCAP
sudo suricata -r capture.pcap -c /etc/suricata/suricata.yaml \
-l /tmp/suricata-out/ -k none
# Watch alerts in real time
tail -f /var/log/suricata/fast.log
# Query EVE JSON
jq 'select(.event_type=="alert") | {ts:.timestamp, sig:.alert.signature, src:.src_ip, dst:.dest_ip}' \
/var/log/suricata/eve.json
SID allocation
| Range | Owner |
|---|---|
| 1 – 999,999 | Snort community rules |
| 1,000,000 – 1,999,999 | Emerging Threats Open |
| 2,000,000 – 2,999,999 | Emerging Threats Pro |
| 3,000,000 – 3,999,999 | Snort subscriber rules |
| 9,000,000+ | Local / custom rules |
Always use the 9,000,000+ range for local rules to avoid conflicts when importing community rule sets.
Performance Tuning
# suricata.yaml — threading
threading:
set-cpu-affinity: yes
cpu-affinity:
- management-cpu-set:
cpu: [0] # dedicate core 0 to management threads
- receive-cpu-set:
cpu: [1] # core 1 for receive threads
- worker-cpu-set:
cpu: ["2-7"] # cores 2-7 for detection workers
mode: "exclusive"
prio:
default: "high"
# Stream reassembly limits
stream:
memcap: 256mb
checksum-validation: yes
inline: no # set to yes for IPS NFQ mode
reassembly:
memcap: 256mb
depth: 1mb # how much of a stream to reassemble
toserver_chunk_size: 2560
toclient_chunk_size: 2560
# Host table — tracks per-host state
host:
hash-size: 4096
prealloc: 1000
memcap: 32mb
# Flow table — tracks per-5-tuple flow state
flow:
memcap: 128mb
hash-size: 65536
prealloc: 10000
emergency-recovery: 30
For high-traffic deployments, increase memcap values until you see no “memcap” lines in stats.log. The stat to watch is flow.memcap and stream.memcap in /var/log/suricata/stats.log.
Logging and EVE JSON
Suricata writes all events to a structured JSON log called EVE (Extensible Event Format). Every line in eve.json is a self-contained JSON object describing one event — an alert, an HTTP transaction, a DNS query, a TLS session, a completed flow, and so on. This makes EVE the primary output for SIEM integration and post-incident analysis.
Configuring outputs
# suricata.yaml
outputs:
- fast:
enabled: yes
filename: fast.log # one-line human-readable alert summary
append: yes
- eve-log:
enabled: yes
filetype: regular
filename: eve.json
append: yes
# Write each event type to a separate file instead:
# filetype: multi
# filename: eve-%s.json # %s is replaced by event type
types:
- alert:
payload: yes # base64-encoded raw payload bytes
payload-buffer-size: 4kb
payload-printable: yes # printable ASCII representation
packet: yes # base64-encoded raw packet
metadata: yes
http-body: yes # include HTTP body in alert
http-body-printable: yes
tagged-packets: yes
- http:
extended: yes # full request + response fields
- dns:
# version: 2 is default in Suricata 7+; structured answers
version: 2
query: yes
answer: yes
- tls:
extended: yes # cert fields, JA3/JA3S, session ID
- flow: {} # one event per completed flow
- netflow: {} # IPFIX-compatible flow records
- smtp:
extended: yes
- ssh: {}
- stats:
enabled: yes
totals: yes
threads: no # per-thread stats; verbose; disable unless debugging
deltas: yes # show per-interval deltas rather than cumulative
- fileinfo: {} # one event per extracted file
Enabling all event types and setting extended: yes significantly increases disk I/O. For production deployments, disable event types you are not consuming (for example, flow events generate enormous volume on busy links).
EVE JSON structure
Every EVE event has these common fields:
{
"timestamp": "2026-05-12T14:32:01.123456+0000",
"flow_id": 123456789,
"in_iface": "eth0",
"event_type": "alert",
"src_ip": "10.0.0.42",
"src_port": 52341,
"dest_ip": "93.184.216.34",
"dest_port": 443,
"proto": "TCP",
"app_proto": "tls",
...event-specific fields...
}
flow_id is the same across all event types belonging to a single TCP/UDP session, so you can correlate an alert event with the HTTP transaction and flow record that triggered it:
jq 'select(.flow_id == 123456789)' /var/log/suricata/eve.json
Alert events
{
"event_type": "alert",
"src_ip": "10.0.0.42",
"dest_ip": "93.184.216.34",
"dest_port": 80,
"proto": "TCP",
"alert": {
"action": "allowed", // or "blocked" in IPS drop mode
"gid": 1,
"signature_id": 9000010,
"rev": 3,
"signature": "MALWARE Possible HTTP C2 Beacon",
"category": "A Network Trojan was detected",
"severity": 1
},
"http": {
"hostname": "badsite.example.com",
"url": "/update/check",
"http_user_agent": "WinHttpRequest",
"http_method": "GET",
"protocol": "HTTP/1.1",
"status": 200,
"length": 412
},
"payload_printable": "GET /update/check HTTP/1.1\r\nHost: badsite.example.com\r\n..."
}
HTTP events
{
"event_type": "http",
"src_ip": "10.0.0.42",
"dest_ip": "93.184.216.34",
"dest_port": 80,
"http": {
"hostname": "example.com",
"http_port": 80,
"url": "/api/v1/data?id=42",
"http_user_agent": "Mozilla/5.0 ...",
"http_refer": "https://example.com/page",
"http_method": "POST",
"protocol": "HTTP/1.1",
"status": 200,
"length": 1024,
"http_content_type": "application/json",
"request_headers": [
{"name": "Accept", "value": "*/*"},
{"name": "Content-Type", "value": "application/json"}
],
"response_headers": [
{"name": "Content-Type", "value": "application/json"},
{"name": "Server", "value": "nginx/1.24.0"}
]
}
}
DNS events
With version: 2, DNS queries and answers are separate events:
// Query event
{
"event_type": "dns",
"dns": {
"type": "query",
"id": 12345,
"rrname": "malicious-domain.example.com",
"rrtype": "A",
"tx_id": 0
}
}
// Answer event
{
"event_type": "dns",
"dns": {
"type": "answer",
"id": 12345,
"rcode": "NOERROR",
"rrname": "malicious-domain.example.com",
"rrtype": "A",
"ttl": 60,
"rdata": "203.0.113.42"
}
}
TLS events
{
"event_type": "tls",
"tls": {
"sni": "badsite.example.com",
"version": "TLSv1.3",
"subject": "CN=badsite.example.com, O=Evil Corp",
"issuerdn": "CN=badsite.example.com, O=Evil Corp", // self-signed: issuer == subject
"serial": "00:de:ad:be:ef",
"fingerprint": "aa:bb:cc:...",
"notbefore": "2026-01-01T00:00:00",
"notafter": "2027-01-01T00:00:00",
"ja3": {
"hash": "e7d705a3286e19ea42f587b344ee6865",
"string": "771,4866-4865-4867,0-23-65281-10-11-..."
},
"ja3s": {
"hash": "ec74a5c51106f0419184d0dd08fb05bc",
"string": "771,4866,0-23-65281"
}
}
}
Flow events
Flow events are emitted when a TCP session closes or a UDP flow times out:
{
"event_type": "flow",
"src_ip": "10.0.0.42",
"dest_ip": "93.184.216.34",
"dest_port": 443,
"proto": "TCP",
"app_proto": "tls",
"flow": {
"pkts_toserver": 18,
"pkts_toclient": 22,
"bytes_toserver": 2048,
"bytes_toclient": 15360,
"start": "2026-05-12T14:30:00.000000+0000",
"end": "2026-05-12T14:32:01.000000+0000",
"age": 121,
"state": "closed",
"reason": "tcp-fin",
"alerted": false
}
}
Fileinfo events and file extraction
Suricata can extract files transferred over HTTP, SMTP, FTP, NFS, and SMB and write them to disk. Enable in suricata.yaml:
outputs:
- file-store:
version: 2
enabled: yes
dir: /var/log/suricata/filestore
force-magic: yes # run libmagic on every extracted file
force-hash: [md5, sha256]
- eve-log:
types:
- fileinfo:
force-magic: yes
force-hash: [md5, sha256]
Each extracted file gets a fileinfo EVE event:
{
"event_type": "fileinfo",
"src_ip": "93.184.216.34",
"dest_ip": "10.0.0.42",
"http": {
"hostname": "example.com",
"url": "/download/update.exe"
},
"fileinfo": {
"filename": "/download/update.exe",
"magic": "PE32 executable (GUI) Intel 80386",
"state": "CLOSED",
"md5": "d41d8cd98f00b204e9800998ecf8427e",
"sha256": "e3b0c44298fc1c149afbf4c8996fb924...",
"size": 204800,
"tx_id": 0,
"stored": true,
"file_id": 1
}
}
Files on disk are stored by SHA256 hash under /var/log/suricata/filestore/:
# List extracted files
ls /var/log/suricata/filestore/
# Find PE executables
jq 'select(.event_type=="fileinfo" and (.fileinfo.magic // "" | test("PE32|ELF")))' \
/var/log/suricata/eve.json
# Look up a specific file by SHA256
jq --arg h "e3b0c44298fc1c149afbf4c8996fb924" \
'select(.event_type=="fileinfo" and .fileinfo.sha256 == $h)' \
/var/log/suricata/eve.json
Querying EVE JSON with jq
# All alert signatures fired in the last run, sorted by frequency
jq -r 'select(.event_type=="alert") | .alert.signature' eve.json \
| sort | uniq -c | sort -rn | head -20
# Source IPs that triggered the most alerts
jq -r 'select(.event_type=="alert") | .src_ip' eve.json \
| sort | uniq -c | sort -rn | head -10
# All HTTP User-Agents observed
jq -r 'select(.event_type=="http") | .http.http_user_agent // "none"' eve.json \
| sort -u
# DNS queries that resolved to RFC1918 addresses (possible DNS rebinding)
jq 'select(.event_type=="dns" and .dns.type=="answer" and
(.dns.rdata // "" | test("^(10\\.|172\\.(1[6-9]|2[0-9]|3[01])\\.|192\\.168\\.)")))' \
eve.json
# TLS connections with self-signed certificates (issuer == subject)
jq 'select(.event_type=="tls" and .tls.subject == .tls.issuerdn)' eve.json
# All destination IPs contacted (unique), from flow events
jq -r 'select(.event_type=="flow") | .dest_ip' eve.json | sort -u
# Flows by bytes sent to server, descending (find large uploads)
jq 'select(.event_type=="flow") | {src:.src_ip, dst:.dest_ip, dport:.dest_port,
bytes:.flow.bytes_toserver}' eve.json \
| jq -s 'sort_by(-.bytes) | .[:20][]'
# Correlate an alert with its HTTP transaction by flow_id
FLOW=$(jq -r 'select(.event_type=="alert" and .alert.signature_id==9000010)
| .flow_id' eve.json | head -1)
jq --argjson fid "$FLOW" 'select(.flow_id == $fid)' eve.json
Integrating EVE with external tools
Elastic Stack (Filebeat → Elasticsearch → Kibana):
# /etc/filebeat/filebeat.yml
filebeat.inputs:
- type: log
paths:
- /var/log/suricata/eve.json
json.keys_under_root: true
json.add_error_key: true
output.elasticsearch:
hosts: ["https://elastic.internal:9200"]
username: "suricata_writer"
password: "${ES_PASSWORD}"
Use the Elastic Security integration — it ships pre-built Suricata dashboards and detection rules.
Wazuh:
Wazuh ships a built-in Suricata decoder and rule set. Configure the Wazuh agent to read eve.json:
<!-- /var/ossec/etc/ossec.conf on the Wazuh agent -->
<localfile>
<log_format>json</log_format>
<location>/var/log/suricata/eve.json</location>
</localfile>
Wazuh maps EVE alert events to its own rule IDs (86XXX range) and generates MITRE ATT&CK annotations automatically.
Splunk:
Use the Splunk Add-on for Suricata and monitor eve.json via Universal Forwarder. The add-on normalises EVE fields to the Common Information Model (CIM).
stats.log
Suricata writes a statistics log every 8 seconds (configurable) to /var/log/suricata/stats.log. Watch it for capacity and performance signals:
Date: 5/12/2026 -- 14:40:00 (uptime: 0d, 00h 10m 00s)
Counter | TM Name | Value
-----------------+-------------------------------+--------
capture.kernel_packets | Total | 2345678
capture.kernel_drops | Total | 0 <- drops = NIC overwhelmed
flow.memcap_delta | Total | 0 <- nonzero = need more memcap
stream.memcap_delta | Total | 0
detect.alert | Total | 142
decoder.pkts | Total | 2345678
decoder.bytes | Total | 1234567890
tcp.sessions | Total | 15234
http.memuse | Total | 8388608
Key counters to monitor in production:
| Counter | Problem if nonzero |
|---|---|
capture.kernel_drops |
NIC ring buffer overflowing; need more threads or faster NIC |
flow.memcap |
Flow table exhausted; increase flow.memcap
|
stream.memcap |
Stream reassembly memory exhausted; increase stream.memcap
|
detect.engines.reloads |
Rule reloads (normal after kill -USR2) |
tcp.reassembly_gap |
Packet loss in capture path |
Remote Suricata via GRE Tunnel
In some deployments it is impractical to run Suricata on the same host that carries the traffic — the monitored machine may be resource-constrained, hardened against additional software, or you may want a single Suricata instance to receive traffic from multiple sources. A GRE tunnel lets you mirror packets to a remote Suricata host while keeping them in their original form. Suricata has a native GRE decoder and will decapsulate GRE frames automatically — but the approach below is simpler: terminate the tunnel at the kernel level so that Suricata reads the already-decapsulated inner packets off the gre1 interface directly.
The traffic on the monitored host is not affected — tc mirred copies packets into the tunnel, it does not redirect them.
Architecture
┌─────────────────────┐ GRE tunnel ┌─────────────────────┐
│ Monitored host │ ──────────────────────────► │ Suricata host │
│ │ │ │
│ eth0 (traffic) │ original packets, │ gre1 (decapsulated)│
│ gre1 (tunnel out) │ encapsulated in GRE │ suricata -i gre1 │
└─────────────────────┘ └─────────────────────┘
GRE encapsulates each mirrored packet in an outer IP header. The kernel tunnel endpoint on the Suricata host strips that outer header when the packet arrives on the gre1 interface, so Suricata reads the original source/destination IP and ports off gre1 without any additional configuration.
Setup: Suricata host
Configure the receiving GRE tunnel endpoint first so it is ready when the monitored host starts sending:
# Replace with actual IPs
SURICATA_IP=10.10.10.30
MONITORED_IP=10.10.10.20
ip tunnel add gre1 mode gre remote "$MONITORED_IP" local "$SURICATA_IP" ttl 64
ip link set gre1 up
Run Suricata against the tunnel interface:
suricata -i gre1
Or set gre1 as the interface in /etc/suricata/suricata.yaml:
af-packet:
- interface: gre1
cluster-id: 99
cluster-type: cluster_flow
defrag: yes
Setup: monitored host
Create the outbound tunnel endpoint and use tc to mirror traffic:
SURICATA_IP=10.10.10.30
MONITORED_IP=10.10.10.20
IFACE=eth0 # interface carrying the traffic to monitor
ip tunnel add gre1 mode gre remote "$SURICATA_IP" local "$MONITORED_IP" ttl 64
ip link set gre1 up
# Mirror ingress (inbound packets on $IFACE)
tc qdisc add dev "$IFACE" ingress
tc filter add dev "$IFACE" ingress matchall \
action mirred egress mirror dev gre1
# Mirror egress (outbound packets on $IFACE)
tc qdisc add dev "$IFACE" handle 1: root prio
tc filter add dev "$IFACE" parent 1: matchall \
action mirred egress mirror dev gre1
Verify the tunnel is up and traffic is flowing:
# On the monitored host — should show GRE output counters rising
ip -s tunnel show gre1
# On the Suricata host — should show packets arriving on gre1
tcpdump -i gre1 -c 10
Persisting across reboots
The ip tunnel and tc commands above are not persistent. The cleanest approach is a systemd-networkd configuration or a startup script. A minimal approach using a systemd oneshot service on the monitored host:
# /etc/systemd/system/gre-mirror.service
[Unit]
Description=GRE traffic mirror to Suricata
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/local/sbin/gre-mirror-start.sh
ExecStop=/usr/local/sbin/gre-mirror-stop.sh
[Install]
WantedBy=multi-user.target
# /usr/local/sbin/gre-mirror-start.sh
#!/usr/bin/env bash
SURICATA_IP=10.10.10.30
MONITORED_IP=10.10.10.20
IFACE=eth0
ip tunnel add gre1 mode gre remote "$SURICATA_IP" local "$MONITORED_IP" ttl 64
ip link set gre1 up
tc qdisc add dev "$IFACE" ingress
tc filter add dev "$IFACE" ingress matchall action mirred egress mirror dev gre1
tc qdisc add dev "$IFACE" handle 1: root prio
tc filter add dev "$IFACE" parent 1: matchall action mirred egress mirror dev gre1
# /usr/local/sbin/gre-mirror-stop.sh
#!/usr/bin/env bash
IFACE=eth0
tc qdisc del dev "$IFACE" ingress 2>/dev/null
tc qdisc del dev "$IFACE" root 2>/dev/null
ip tunnel del gre1 2>/dev/null
chmod +x /usr/local/sbin/gre-mirror-start.sh /usr/local/sbin/gre-mirror-stop.sh
systemctl enable --now gre-mirror
Monitoring from the Proxmox bridge
If you want a single mirror point that captures all inter-VM traffic without touching individual VMs, run the tc rules on the Proxmox host against the bridge interface instead:
# On the Proxmox host
IFACE=internal # the bridge carrying VM traffic
SURICATA_IP=10.10.10.30
PROXMOX_IP=<proxmox-host-ip>
ip tunnel add gre1 mode gre remote "$SURICATA_IP" local "$PROXMOX_IP" ttl 64
ip link set gre1 up
tc qdisc add dev "$IFACE" ingress
tc filter add dev "$IFACE" ingress matchall action mirred egress mirror dev gre1
tc qdisc add dev "$IFACE" handle 1: root prio
tc filter add dev "$IFACE" parent 1: matchall action mirred egress mirror dev gre1
This captures traffic between all VMs on the internal bridge — including traffic between Kali and the Ubuntu server — without any agent on either VM.
Caveats
- GRE adds 24 bytes of overhead per packet. On links near MTU, mirrored packets will be dropped if the outer packet exceeds the path MTU. Set the tunnel MTU to
interface_MTU - 24(ip link set gre1 mtu 1476for a 1500-byte path) or enable fragmentation on the outer tunnel. - Both endpoints must be reachable at the IP layer before the tunnel can carry traffic. In the lab, both IPs are on the
internalbridge so this is always satisfied. -
tc mirred mirrorcopies at the traffic-control layer, before iptables/nftables. Packets that are dropped by firewall rules are still mirrored.
Key takeaways
- Suricata is one engine, three roles: passive IDS, inline IPS, and NSM sensor. Its rule language is a strict superset of Snort’s, with native application-layer parsing (HTTP/DNS/TLS/SMB/SSH), multithreading, and Lua.
-
Manage rules with
suricata-update: enable sources (ET Open), tune withenable.conf/disable.conf/drop.conf/modify.conf, keep custom rules in a separatelocal.rules, and reload live withkill -USR2. -
Deployment mode is a real decision: AF_PACKET for passive IDS; NFQ (single-NIC) or AF_PACKET-inline (two-NIC bridge) for IPS; AF_XDP/DPDK for 10 Gbps+. Always test
droprules asalertfirst and watchfail-open. -
Write rules header-first, then options: prefer app-layer protocols and sticky buffers (
http.uri,tls.sni,dns.query) over rawtcpbyte-scanning; prefilter expensivepcrewith cheapcontent; allocate local SIDs from 9,000,000+. - Reach for Lua only when keywords can’t express the logic (entropy, cross-flow state, custom parsing) — it runs per-packet behind a content prefilter and is sandboxed by default in 8.x.
-
EVE JSON is the product: every event carries a shared
flow_idfor correlation, and it’s the feed for Elastic, Wazuh, and Splunk. Watchstats.log(kernel_drops,*.memcap) to know when the sensor is overwhelmed.
References
- Suricata documentation (8.0.x). https://docs.suricata.io/en/suricata-8.0.5/
- Suricata rule language reference. https://docs.suricata.io/en/suricata-8.0.5/rules/
- suricata-update documentation. https://suricata-update.readthedocs.io/
- Emerging Threats Open ruleset. https://rules.emergingthreats.net/
- IPS / inline setup for Linux. https://docs.suricata.io/en/suricata-8.0.5/setting-up-ipsinline-for-linux.html
- AF_XDP capture configuration. https://docs.suricata.io/en/suricata-8.0.0/capture-hardware/af-xdp.html
- DPDK capture configuration. https://docs.suricata.io/en/suricata-8.0.0/capture-hardware/dpdk.html
- Lua detection scripting. https://docs.suricata.io/en/suricata-8.0.3/rules/lua-detection.html
Related course pages: Defensive Measures · Capturing Packets with tcpdump · Introduction to Recon
🛠️ Maintenance note: Suricata pins move fast — 8.0.x is current (8.0.5, May 2026) and 7.0.x reaches EOL in July 2026, so re-check the version banner, the
--build-infocapabilities, and the versioned docs URLs each term. Default behaviors also shift between majors (Luaallow-rules, DNSversion: 2, AF_PACKETblock-size). Note: the in-page links to the malware-course Suricata page (../malware/suricata) andlua_securityresolve in the netsec build; verify they exist before relying on them from other courses that include this page.