Suricata IDS lab: write rules, catch a scan, then block it
0 / 10 steps
The goal
Set up Suricata, a free intrusion detection system, on a tiny two-machine network inside your own Linux box. You will write three rules of your own, set off each one on purpose (a web request with a secret word in it, a port scan, a ping), read the alerts in both of Suricata’s log formats, quiet a noisy rule without switching it off for everyone, and finally flip Suricata from “watch and report” to “watch and block”.
Everything stays inside your own machine. The two “machines” are network namespaces in your own Ubuntu VM or WSL, on the private range 10.77.0.0/24 that exists only inside that box. The port scan in step 7 is aimed at your own lab server and nothing else. Never scan a network you don’t own or have written permission to test. Step 10 removes everything.
Why this lab. Firewalls decide what is allowed in. An intrusion detection system watches what actually happens on the wire and shouts when something looks wrong. Security+ expects you to know what IDS and IPS mean, where they sit, and why alerts need tuning, and the fastest way to understand all three is to write a rule and watch it fire.
An IDS is a smoke alarm. It doesn’t put out fires. It also goes off when you make toast. Most of this job is learning the difference. — Nyx
Words you’ll meet
| Term | Meaning |
|---|---|
| IDS | Intrusion detection system. Watches a copy of the traffic and raises an alert. It cannot stop anything. |
| IPS | Intrusion prevention system. Sits in the path of the traffic (“inline”), so it can drop a bad packet before it arrives. |
| NIDS / HIDS | Network-based (watches a network link, like this lab) or host-based (runs on one computer and watches that computer). |
| Rule (signature) | A pattern to look for, such as “a web request containing this word”. Suricata rules use the same style as Snort, an older IDS. |
| False positive | An alert about something harmless. Too many and people stop reading alerts. |
| False negative | Something bad that raised no alert at all. The dangerous one, because nobody knows. |
| Suppression | Telling the IDS to stay quiet about one rule for one known source, instead of deleting the rule. |
Before you start. Use the Ubuntu VM from Lab 00 or Ubuntu 24.04 in WSL2 on Windows. On a Mac, use the Lab 00 VM; Suricata needs Linux networking for this lab. Open a terminal and get a root shell with sudo -i (the prompt changes to #). About 150 MB of RAM is plenty.
Steps
01Install Suricata and the tools
Suricata is the IDS. nmap makes the port scan, jq reads JSON logs, curl makes web requests.
# apt update # apt install -y suricata nmap jq curl # systemctl disable --now suricata # suricata -V
... This is Suricata version 7.0.3 RELEASE
Why switch the service off? Ubuntu starts Suricata in the background as soon as it is installed, watching your real network card. In this lab you run your own copy by hand, so you can see every setting. If systemctl says the system was not booted with systemd (older WSL setups), skip that line.
02Build a tiny network
Two pretend machines joined by one virtual cable called sg0. The client gets two addresses, 10.77.0.10 (a normal user) and 10.77.0.20 (a monitoring box, used in step 8). The server is 10.77.0.80.
# ip netns add client; ip netns add server # ip link add sg0 netns client type veth peer name sg0 netns server # ip -n client addr add 10.77.0.10/24 dev sg0; ip -n client addr add 10.77.0.20/24 dev sg0 # ip -n server addr add 10.77.0.80/24 dev sg0 # ip -n client link set lo up; ip -n client link set sg0 up # ip -n server link set lo up; ip -n server link set sg0 up # ip netns exec client ping -c 1 -W 1 10.77.0.80
The ping should report 1 received, 0% packet loss. A network namespace is a private copy of Linux networking: its own interfaces, addresses and firewall, so it behaves like a separate computer.
03Write three rules
Make a lab folder and write a rule file. Copy the whole block; the three long lines are the rules.
# mkdir -p /tmp/surilab/log && cd /tmp/surilab # cat > local.rules <<'EOF' alert http any any -> $HOME_NET any (msg:"SGLAB canary word in a web request"; flow:established,to_server; http.uri; content:"sg-canary-7731"; nocase; classtype:policy-violation; sid:1000001; rev:1;) alert tcp any any -> $HOME_NET any (msg:"SGLAB possible port scan (many SYNs)"; flags:S,12; flow:stateless; threshold:type both, track by_src, count 20, seconds 60; classtype:attempted-recon; sid:1000002; rev:1;) alert icmp any any -> $HOME_NET any (msg:"SGLAB ping to the home network"; itype:8; classtype:misc-activity; sid:1000003; rev:1;) EOF # : > threshold.config # wc -l local.rules
3 local.rules
Reading a rule. Every rule is action (alert), protocol (http, tcp, icmp), from → to, then options in brackets. $HOME_NET means “our network”. Rule 1 looks inside the web address for the made-up word sg-canary-7731. Rule 2 counts connection attempts (TCP packets with only the SYN flag, the “knock” that starts every connection) and alerts once when one sender knocks 20 times within 60 seconds. Rule 3 alerts on every ping. sid is the rule’s ID number; 1,000,000 and up is the range kept for your own rules. The empty threshold.config is for step 8.
04Start a web server and Suricata
The server gets a small web server so there is something to visit. Then Suricata starts inside the server namespace, listening on its side of the cable.
# ip netns exec server python3 -m http.server 80 --bind 10.77.0.80 >/dev/null 2>&1 & # ip netns exec server suricata -i sg0 -k none -S /tmp/surilab/local.rules -l /tmp/surilab/log --set vars.address-groups.HOME_NET="[10.77.0.0/24]" --set threshold-file=/tmp/surilab/threshold.config --pidfile /tmp/surilab/suricata.pid -D # sleep 5; grep -E 'rules successfully loaded|Engine started' log/suricata.log | cut -d' ' -f6-
i: suricata: This is Suricata version 7.0.3 RELEASE running in SYSTEM mode Info: detect: 1 rule files processed. 3 rules successfully loaded, 0 rules failed, 0 Notice: threads: Threads created -> W: 16 FM: 1 FR: 1 Engine started.
The switches. -i sg0 listens on that interface. -S loads only your rule file. -l is where logs go. -k none stops Suricata ignoring packets whose checksums look wrong, which is normal on virtual cables. --set overrides a setting from /etc/suricata/suricata.yaml without editing it. -D runs it in the background. W: 16 is the number of worker threads; yours will match your CPU.
05Trip the canary rule
Visit the server twice from the client: once with a normal search word, once with the canary word. Then read fast.log, Suricata’s one-line-per-alert log.
# ip netns exec client curl -s -o /dev/null -w '%{http_code}\n' 'http://10.77.0.80/?q=hello' # ip netns exec client curl -s -o /dev/null -w '%{http_code}\n' 'http://10.77.0.80/?q=sg-canary-7731' # sleep 2; cat log/fast.log
200
200
11/04/2026-21:01:58.551427 [**] [1:1000001:1] SGLAB canary word in a web request [**] [Classification: Potential Corporate Privacy Violation] [Priority: 1] {TCP} 10.77.0.10:54856 -> 10.77.0.80:80
Reading it. Both requests worked (200 means OK): an IDS only watches, it doesn’t block. Only the second request raised an alert. [1:1000001:1] is generator : rule ID : revision. Then the message, the category, and who talked to whom. This is how a real canary works: plant a word that should never appear in normal traffic, such as a fake customer record, and alert if anyone ever asks for it.
06Read the same alert in eve.json
eve.json is Suricata’s detailed log: one JSON record per event. JSON (JavaScript Object Notation) is labeled data that tools like a SIEM (security information and event management system) read easily. jq picks out the fields you care about.
# jq -c 'select(.event_type=="alert") | {src: .src_ip, dst: .dest_ip, port: .dest_port, sid: .alert.signature_id, url: .http.url}' log/eve.json # jq -r '.event_type' log/eve.json | sort | uniq -c
{"src":"10.77.0.10","dst":"10.77.0.80","port":80,"sid":1000001,"url":"/?q=sg-canary-7731"}
1 alert
2 fileinfo
2 http
Why two logs? fast.log is for a human glancing at a screen. eve.json is for machines, and it holds far more than alerts: Suricata also wrote an http record for both web requests (the harmless one too) and a fileinfo record for each page it saw sent back. That extra record of normal activity is gold during an investigation. In a real setup you ship eve.json to a SIEM such as Wazuh or Splunk.
07Scan your own server
A port scan knocks on many doors to find which ones are open. Attackers do it early (it is the “reconnaissance” stage of the kill chain). Scan the first 200 ports of your own lab server.
# ip netns exec client nmap -sS -T4 -p 1-200 10.77.0.80 # sleep 2; grep 'port scan' log/fast.log
Nmap scan report for 10.77.0.80
Host is up (0.000026s latency).
Not shown: 199 closed tcp ports (reset)
PORT STATE SERVICE
80/tcp open http
...
11/04/2026-21:02:13.762809 [**] [1:1000002:1] SGLAB possible port scan (many SYNs) [**] [Classification: Attempted Information Leak] [Priority: 2] {TCP} 10.77.0.10:36975 -> 10.77.0.80:194
Reading it. nmap found the one open door, port 80, where your web server is. Suricata saw 200 knocks from one sender and raised one alert, not 200, because the rule’s threshold: type both means “alert once when the count is reached, then stay quiet for the rest of the 60 seconds”. The port in the alert (194 here) is just whichever knock was number 20; nmap tries ports in random order, so yours will differ.
08Tame a noisy rule
Pretend 10.77.0.20 is your monitoring server. It pings every machine all day to check they are alive, and rule 3 alerts on every single ping. Count the alerts per sender, suppress rule 3 for the monitor only, reload the rules, and count again.
# ip netns exec client ping -c 5 -i 0.2 -q -I 10.77.0.20 10.77.0.80 # ip netns exec client ping -c 2 -i 0.2 -q -I 10.77.0.10 10.77.0.80 # sleep 2; grep 'ping to the home' log/fast.log | awk '{print $(NF-2)}' | sort | uniq -c
...
2 10.77.0.10:8
5 10.77.0.20:8
Seven alerts, five of them from the monitor (the :8 is the ping type, not a port). Now suppress, reload, clear the log and repeat:
# echo 'suppress gen_id 1, sig_id 1000003, track by_src, ip 10.77.0.20' > threshold.config # kill -USR2 $(cat suricata.pid); sleep 3; grep 'rule reload complete' log/suricata.log | cut -d' ' -f6- # : > log/fast.log # ip netns exec client ping -c 5 -i 0.2 -q -I 10.77.0.20 10.77.0.80 # ip netns exec client ping -c 2 -i 0.2 -q -I 10.77.0.10 10.77.0.80 # sleep 2; grep 'ping to the home' log/fast.log | awk '{print $(NF-2)}' | sort | uniq -c
Notice: detect: rule reload complete
...
2 10.77.0.10:8
Why suppress instead of delete? The monitor’s pings are harmless and expected: real alerts about nothing worth acting on, the everyday kind of noise people lump in with false positives. Deleting rule 3 would also hide pings from a stranger sweeping your network, which would make that sweep a false negative. Suppressing it for one known source keeps the rule working for everyone else, as the two alerts from 10.77.0.10 show. kill -USR2 tells a running Suricata to reload its rules and threshold file without stopping. Tuning like this is a daily job for a SOC (security operations center) analyst: an IDS that cries wolf all day gets ignored.
09Turn the IDS into an IPS
So far Suricata has only watched a copy of the traffic. To block, it has to stand in the path. Stop it, make a copy of the rules where the canary rule says drop instead of alert, tell the server’s firewall (nftables) to hand all web traffic to Suricata through a queue, and start Suricata reading that queue.
# kill $(cat suricata.pid); sleep 3 # sed 's/^alert http/drop http/' local.rules > ips.rules # ip netns exec server nft add table inet sglab # ip netns exec server nft add chain inet sglab in '{ type filter hook input priority 0; }' # ip netns exec server nft add chain inet sglab out '{ type filter hook output priority 0; }' # ip netns exec server nft add rule inet sglab in tcp dport 80 queue num 0 # ip netns exec server nft add rule inet sglab out tcp sport 80 queue num 0 # ip netns exec server suricata -q 0 -k none -S /tmp/surilab/ips.rules -l /tmp/surilab/log --set vars.address-groups.HOME_NET="[10.77.0.0/24]" --pidfile /tmp/surilab/suricata.pid -D # sleep 5; grep -c 'binding this thread 0 to queue' log/suricata.log
i: suricata: This is Suricata version 7.0.3 RELEASE running in SYSTEM mode 1
Now the same two visits as step 5. The -m 5 gives up after five seconds.
# ip netns exec client curl -s -m 5 -o /dev/null -w '%{http_code}\n' 'http://10.77.0.80/?q=hello' # ip netns exec client curl -s -m 5 -o /dev/null -w '%{http_code}\n' 'http://10.77.0.80/?q=sg-canary-7731'; echo "exit $?" # sleep 2; grep Drop log/fast.log
200
000
exit 28
11/04/2026-21:02:33.209609 [Drop] [**] [1:1000001:1] SGLAB canary word in a web request [**] [Classification: Potential Corporate Privacy Violation] [Priority: 1] {TCP} 10.77.0.10:57678 -> 10.77.0.80:80
Reading it. The normal visit still works (200). The canary visit never gets an answer: 000 means no reply and curl’s exit 28 means “timed out”, because Suricata threw that packet away. The log line now starts with [Drop]. Both directions go through the queue because Suricata needs to see the whole conversation to understand a web request. The trade-off: an IPS is inline, so if it crashes or a rule is wrong, real traffic stops too. That is why new rules usually run in IDS (alert) mode first, and why many IPS devices are set to “fail open” (let traffic through if the IPS dies) or “fail closed” (block everything), a choice Security+ likes to ask about.
10Clean up
Stop Suricata and the web server, delete both namespaces (this also deletes the nftables queue rules inside the server), and remove the lab folder.
# kill $(cat /tmp/surilab/suricata.pid) # pkill -f 'http.server 80 --bind 10.77.0.80' # ip netns del client; ip netns del server # cd / && rm -rf /tmp/surilab # exit
Suricata stays installed but switched off. To remove it too: sudo apt remove suricata.
What you learned
- IDS vs IPS — an IDS watches a copy and alerts; an IPS sits inline and can drop, which also means it can break things
- Rules — action, protocol, from → to, then options; sid 1000000 and up for your own
- fast.log and eve.json — one line for people, full JSON records for a SIEM
- Thresholds — one alert for a 200-port scan instead of 200 alerts
- Tuning — suppress a known, harmless source instead of deleting a rule and creating a blind spot
- False positive vs false negative — noise wastes time; silence about a real attack is worse
Tried it? Tell us
Nova built this lab from Steven’s ideas, and we ran every step once before publishing. If a step broke, behaved differently for you, or you know a better way, tell us. Ideas for the next lab are welcome too — no promises on timing.
NextKeycloak single sign-on: one login, two apps→
Go deeper
- Wazuh lab — a free SIEM that can collect Suricata’s eve.json
- NAT and firewall lab — the nftables basics behind step 9
- CompTIA Security+ roadmap — this lab covers Domain 4, objectives 4.1 (IDS/IPS) and 4.4 (alerting and monitoring)
- Suricata rules documentation — every rule keyword, and suricata-update for the free ET Open rule set
Original walkthrough. Every command was run end to end, exactly as shown, on Ubuntu 24.04 (WSL2, kernel 6.6.87.2, Suricata 7.0.3, nmap 7.94) on 4 Nov 2026; outputs are from that run, trimmed where marked with .... Your timestamps, ports and thread counts will differ. StashGrid isn’t affiliated with CompTIA or the OISF, which makes Suricata. If something doesn’t match what your machine does, tell me and I will fix it.