1 Introduction
A network intrusion detection system (NIDS) watches a copy of the traffic crossing your network and raises an alert when a packet or a conversation matches a signature. Placed inline, the same engine becomes an IPS that can drop the traffic instead. Two open-source engines dominate the field and share almost the same rule language:
Snort
Created by Martin Roesch in 1998, now maintained by Cisco Talos. Its rule syntax became the de facto standard. Snort 3, a complete rewrite, is the current version.
Suricata
Developed since 2009 by the non-profit OISF. Multi-threaded, with rich protocol parsers (HTTP, DNS, TLS, SMB…) and detailed JSON logs (EVE). It reads most Snort rules.
Network rules complement the other detection layers: YARA inspects files, Sigma inspects logs, Suricata and Snort inspect the wire. They are where network IOCs (an IP address, a domain, a URL pattern, a TLS fingerprint) are turned into alerts, and they see every machine on the segment, including printers, cameras and servers where you cannot install an agent.
1 line
a rule is a single line: a header and options in parentheses
sid + rev
unique ID and revision of each rule, mandatory
1,000,000+
the SID range reserved for your local rules
2 Rule anatomy
A rule has two parts. The header says which traffic is concerned:
alert http $HOME_NET any -> $EXTERNAL_NET any ( options... )
| | | | | | |
action proto src IP src dir dst IP dst port
port
| Field | Values |
|---|---|
| Action | alert (log it), pass (stop inspecting), and in IPS mode drop (silently block) or reject (block and send a reset). By default pass wins over drop, which wins over alert. |
| Protocol | tcp, udp, icmp, ip, or an application protocol (http, dns, tls, smb, ssh…) recognised whatever the port. |
| Addresses | An IP, a CIDR, a list [10.0.0.0/8,!10.1.1.1], any, or a variable such as $HOME_NET / $EXTERNAL_NET defined in suricata.yaml. |
| Ports | A number, a range 1024:, a list [80,443], any or a variable ($HTTP_PORTS). |
| Direction | -> from source to destination, <> both ways. |
The options, between parentheses, say what to look for and how to report it. Each option ends with a semicolon. Here is a complete rule, then the meaning of each keyword:
local.rules
alert http $HOME_NET any -> $EXTERNAL_NET any (msg:"MLAB DEMO Suspicious User-Agent mlab-demo-bot"; flow:established,to_server; http.user_agent; content:"mlab-demo-bot"; nocase; classtype:trojan-activity; sid:1000002; rev:1;)
Reporting options
| msg:"..."; | The alert text. Conventionally starts with a source prefix (ET MALWARE, MLAB DEMO). |
| sid:1000002; | Signature ID, unique across all loaded rules. Use 1,000,000 to 1,999,999 for local rules; Emerging Threats uses the 2,000,000 range. |
| rev:1; | Revision, incremented each time you edit the rule. |
| classtype:trojan-activity; | Category from classification.config; it sets the default priority (1 = highest). |
| reference:url,...; | Link to documentation, a CVE (reference:cve,2021-44228;) or a report. |
| metadata:key value, ...; | Free key/value pairs copied into EVE alerts: creation date, ATT&CK technique, confidence… |
Detection options
| flow:established,to_server; | Only packets of an established TCP session going to the server (also to_client). Cheap and essential: put it in every TCP rule. |
| content:"abc"; | Exact bytes to find. Binary bytes go between pipes: content:"|4D 5A|";. Prefix with ! to require absence. |
| nocase; startswith; endswith; | Modify the preceding content. |
| offset / depth / distance / within | Position constraints: where to start, how far to look, and relative to the previous match. |
| http.uri; dns.query; tls.sni;… | Sticky buffers: every following content applies to that parsed field (URI, user agent, DNS name, TLS server name, downloaded file…) instead of the raw packet. |
| pcre:"/regex/flags"; | Perl regular expression, applied to the current buffer. Always pair it with a content. |
| fast_pattern; | Tells the engine which content to use for its fast pre-filter. By default it picks the longest one. |
| threshold:type limit, track by_src, count 1, seconds 3600; | Alert rate limiting: here at most one alert per source per hour. Types: limit, threshold, both. |
| detection_filter:track by_src, count 20, seconds 60; | Only alert once the rule has matched more than 20 times in 60 seconds from the same source (then on every further match). |
3 Worked examples
All the rules below load in Suricata 8 and use fictional indicators: evil-update.example uses the reserved .example TLD, and 203.0.113.0/24 is a documentation range.
Step 1: header plus the bare minimum
ICMP echo requests (ping, itype:8) reaching your network. Useless in production, perfect to check that your setup produces alerts.
alert icmp any any -> $HOME_NET any (msg:"MLAB DEMO ICMP echo request to home net"; itype:8; classtype:misc-activity; sid:1000001; rev:1;)
Step 2: turning network IOCs into rules
A threat report lists a C2 domain. Detect it in DNS queries and in the TLS Server Name Indication, which stays visible even for HTTPS. endswith also matches subdomains such as cdn.evil-update.example.
alert dns $HOME_NET any -> any any (msg:"MLAB DEMO DNS query for evil-update.example"; dns.query; content:"evil-update.example"; nocase; endswith; classtype:bad-unknown; sid:1000003; rev:1;)
alert tls $HOME_NET any -> $EXTERNAL_NET any (msg:"MLAB DEMO TLS SNI for evil-update.example"; flow:established,to_server; tls.sni; content:"evil-update.example"; nocase; endswith; classtype:bad-unknown; sid:1000005; rev:1;)
Step 3: a realistic C2 beacon rule
Our fictional malware sends POST /gate.php?id= followed by 16 hexadecimal characters, without a Referer header (real browsers almost always send one when following links). Long rules can be split over several lines with a trailing \:
alert http $HOME_NET any -> $EXTERNAL_NET any (msg:"MLAB DEMO Beacon to fake gate.php panel"; \
flow:established,to_server; \
http.method; content:"POST"; \
http.uri; content:"/gate.php?id="; startswith; fast_pattern; \
pcre:"/^\/gate\.php\?id=[0-9a-f]{16}$/"; \
http.header_names; content:!"Referer"; \
threshold:type limit, track by_src, count 1, seconds 3600; \
reference:url,academy.mlab.sh/page/suricata-snort-rules; \
classtype:trojan-activity; \
metadata:mitre_tactic_id TA0011, mitre_technique_id T1071.001, created_at 2026_10_21; \
sid:1000004; rev:2;)
Cheap filters first
alert http and flow limit the rule to outgoing HTTP requests; http.method restricts it to POST.
A strong fast pattern
/gate.php?id= at the start of the URI is the most distinctive string: it is the one the pre-filter should look for.
pcre for the exact shape
Only runs when the content matched, and checks the ID is exactly 16 hex characters.
Negative check and rate limit
http.header_names; content:!"Referer"; requires the header to be absent; threshold avoids 1,440 alerts a day for a beacon that calls home every minute.
Step 4: counting events (brute force)
A single SYN to port 22 is normal; more than 20 from the same external source in a minute looks like SSH brute force or a scan. flags:S,12; matches a pure SYN while ignoring the two ECN bits.
alert tcp $EXTERNAL_NET any -> $HOME_NET 22 (msg:"MLAB DEMO Possible SSH brute force"; flow:to_server; flags:S,12; detection_filter:track by_src, count 20, seconds 60; classtype:attempted-admin; sid:1000007; rev:1;)
Step 5: inspecting downloaded files
The file.data buffer contains the body of HTTP responses (decompressed and de-chunked). This rule fires when a Windows executable (MZ header) is downloaded over plain HTTP, a classic policy alert. Suricata can also extract such files and compute their hashes, ready for a sandbox.
alert http any any -> any any (msg:"MLAB DEMO file download with EXE magic"; flow:established,to_client; file.data; content:"MZ"; startswith; classtype:policy-violation; sid:1000008; rev:1;)
4 Testing & tools
Validate, then replay a capture
Never push a rule to a sensor without these two steps. The output below is real: the demo capture contains the beacon request from step 3.
# 1. Syntax check: load the config + your rules, then exit
$ suricata -T -c /etc/suricata/suricata.yaml -S local.rules -v
...
Info: detect: 1 rule files processed. 8 rules successfully loaded, 0 rules failed, 0 rules skipped
Notice: suricata: Configuration provided was successfully loaded. Exiting.
# 2. Replay a capture through the rules (-k none: ignore bad checksums in pcaps)
$ suricata -r demo.pcap -S local.rules -k none -l ./out
$ cat ./out/fast.log
10/21/2026-12:02:41.519555 [**] [1:1000002:1] MLAB DEMO Suspicious User-Agent mlab-demo-bot [**] [Classification: A Network Trojan was detected] [Priority: 1] {TCP} 192.168.1.50:49152 -> 203.0.113.10:80
10/21/2026-12:02:41.519555 [**] [1:1000004:2] MLAB DEMO Beacon to fake gate.php panel [**] [Classification: A Network Trojan was detected] [Priority: 1] {TCP} 192.168.1.50:49152 -> 203.0.113.10:80
10/21/2026-12:02:41.519555 [**] [1:1000006:1] MLAB DEMO legacy-style URI match [**] [Classification: A Network Trojan was detected] [Priority: 1] {TCP} 192.168.1.50:49152 -> 203.0.113.10:80
# 3. The same alerts, with full context, in JSON
$ jq 'select(.event_type=="alert") | .alert.signature' ./out/eve.json
A single syntax error (here, a missing ; after the content) makes Suricata skip the rule. In test mode it tells you exactly which line:
E: detect: error parsing signature "alert http $HOME_NET any -> $EXTERNAL_NET any (msg:"MLAB DEMO broken"; http.uri; content:"/gate.php" sid:1000010; rev:1;)" from file local.rules at line 1
To build test captures, record your own traffic with tcpdump -w test.pcap while reproducing the behaviour in a lab, or use public collections such as malware-traffic-analysis.net (handle these pcaps as live malware).
Managing rule sets with suricata-update
# Download and merge Emerging Threats Open (the default source)
$ sudo suricata-update
# List and enable extra free sources
$ sudo suricata-update list-sources
$ sudo suricata-update enable-source tgreen/hunting
$ sudo suricata-update
# Disable a noisy rule by SID: add it to /etc/suricata/disable.conf, then re-run
$ echo "1000001" | sudo tee -a /etc/suricata/disable.conf
Snort and Suricata: same family, different dialects
In the classic syntax, inherited from Snort 2, a buffer is a modifier written after the content. Suricata still accepts it (this rule loads and fired in the test above):
alert tcp $HOME_NET any -> $EXTERNAL_NET $HTTP_PORTS (msg:"MLAB DEMO legacy-style URI match"; flow:established,to_server; content:"/gate.php"; http_uri; classtype:trojan-activity; sid:1000006; rev:1;)
Suricata now prefers dotted sticky buffers placed before the content (http.uri; content:"...";). Snort 3 also uses sticky buffers, but with its own names (http_uri;) and content modifiers separated by commas inside the same option:
alert http $HOME_NET any -> $EXTERNAL_NET any ( msg:"MLAB DEMO gate.php request"; flow:established,to_server; http_uri; content:"/gate.php?id=",nocase; classtype:trojan-activity; sid:1000009; rev:1; )
Test Snort 3 rules with snort -c /usr/local/etc/snort/snort.lua -R local.rules -T. Many app-layer keywords (dns.query, tls.sni, file.data with dots) are Suricata-only: always validate a rule on the engine that will run it.
Check an alert's destination on mlab.sh
An alert fired on an IP or domain you do not know? Extract the indicators from the alert and check their reputation before escalating.
Extract IOCs on mlab.sh5 Common pitfalls
A wrong $HOME_NET
If HOME_NET in suricata.yaml does not match your real address ranges, every $HOME_NET -> $EXTERNAL_NET rule is silently blind. It is the first thing to check on a new sensor.
Expecting to see inside HTTPS
Without TLS interception, http.uri rules only see plain HTTP. For encrypted traffic, work with what stays visible: SNI, certificates (hidden in TLS 1.3), JA3/JA4 fingerprints if enabled, DNS queries, and flow timing.
Content that is far too common
content:"GET"; or content:"admin"; alone matches a large share of all traffic. Rules need distinctive content, anchored to the right buffer.
Duplicate SIDs and forgotten rev
Two rules with the same SID: one is rejected at load time. Editing a rule without bumping rev: your SIEM can no longer tell the old alerts from the new.
IP-only rules from stale feeds
Attacker IPs are often shared hosting or cloud addresses that change hands within days (see IP reputation). Give IOC-based rules an expiry date in metadata and review them.
Testing a pcap without -k none
Captures taken on the sending host often have invalid checksums (offloaded to the network card). Suricata then ignores the packets and your rule seems not to work.
Performance tips
A sensor inspects every packet in real time: a slow rule means dropped packets, and dropped packets mean missed attacks. The engine first runs a fast multi-pattern search on one content per rule, and only evaluates the rest of the rule on candidates.
Always have at least one distinctive content
A rule with only pcre or only flags must be evaluated against every packet of its protocol.
Use the narrowest protocol and buffer
alert http + http.uri inspects a few bytes per request; alert tcp any any -> any any + raw content inspects everything.
Choose the fast pattern deliberately
If the longest content is a common string (a User-Agent prefix, Mozilla/5.0), set fast_pattern on the rarer one.
Measure
A Suricata build with rule profiling enabled writes the cost of each rule (rule_perf.log); watch the capture.kernel_drops counter in stats.log.
6 Going further
Community rule sets
Emerging Threats Open
Tens of thousands of free rules maintained by Proofpoint and the community, for Suricata and Snort. The default source of suricata-update, and the best place to learn by reading.
Snort rules (Cisco Talos)
Community rules (free), Registered rules (free account, published with a delay) and Subscriber rules (paid, real-time).
abuse.ch
SSLBL and ThreatFox publish Suricata rule sets generated from their threat intel feeds (malicious certificates, C2 indicators).
Hunting rule sets
Sources such as tgreen/hunting in suricata-update are deliberately broad: great for threat hunting dashboards, too noisy for paging an analyst.
Connecting the dots
Suricata's EVE JSON logs (alerts, but also every DNS query, HTTP request and TLS handshake) are excellent input for a SIEM, where Sigma rules can correlate them with endpoint events. Tag your rules with MITRE ATT&CK techniques in metadata, feed them with indicators received over STIX/TAXII, and use the C2 traffic observed in a sandbox to write behaviour-based rules that outlive any single suspicious domain.
Related Modules
Sigma rules for log detection
Write vendor-neutral Sigma detection rules in YAML and convert them to Splunk, Elastic or Microsoft Sentinel KQL with sigma-cli.
Reading an IP reputation
Assessing the danger of an IP address using reputation databases and threat intelligence.
What is DNS?
The Internet's phone book: how DNS translates domain names into IP addresses.
Source: mlab Academy — Cybersecurity Awareness Platform
URL: https://academy.mlab.sh//page/suricata-snort-rules
Module: Network detection with Suricata & Snort — Detection Engineering
Disclaimer: This content is for awareness purposes only.