This lab installs Suricata on OPNsense in alert-only (IDS) mode, loads the free Emerging Threats ruleset, and proves the pipeline end-to-end by triggering a recognisable alert from a custom rule. By the end you can read an EVE JSON alert and know whether the firewall is actually inspecting traffic, or just claiming to.
The lab deliberately uses IDS mode, not IPS. Blocking traffic in a lab is a quick way to lock yourself out of OPNsense. The principles are identical; only the action field of the rule changes between alert and drop.
Objective
By the end of this lab, you can:
- Install the os-suricata plugin and confirm Suricata is running.
- Configure Suricata in IDS mode on the LAN interface with the correct HOME_NET.
- Subscribe to a free ruleset and confirm rules loaded.
- Add a custom rule that fires on a payload you can produce on demand.
- Read the EVE JSON alert output and identify the rule by signature id, source, destination, and category.
Requirements
You need:
- An OPNsense instance with a LAN interface carrying real traffic.
- A host on the LAN that can
curlto the gateway or to any service — what it talks to matters less than that it talks. - Outbound HTTPS from OPNsense to pull rulesets.
- Shell access on OPNsense.
jqon a host where you want to parse EVE JSON. OPNsense does not shipjqby default; copy the JSON out and parse it elsewhere if you prefer.
Tasks
Task 1: Install the plugin
System → Firmware → Plugins → os-suricata → Install. The plugin installs Suricata and the GUI management pages. Confirm:
# On OPNsense
suricata --build-info | head -3
service suricata status
suricata --build-info reports the compile options; the output
should include the NFQUEUE/AF_PACKET capture methods. The service
may show as not running — that is fine, the next task starts it.
Task 2: Configure HOME_NET and interfaces
Services → Intrusion Detection → Administration:
| Field | Value |
|---|---|
| Enabled | on |
| Save logs to | /var/log/suricata |
| Interfaces | LAN |
Services → Intrusion Detection → Interfaces → + Add (LAN):
| Field | Value |
|---|---|
| Enabled | on |
| Interface | LAN |
| Description | LAN IDS |
| HOME_NET | 10.10.10.0/24 |
| EXTERNAL_NET | !$HOME_NET |
| Default log level | Notice |
| Inspection | IDS (alert-only) |
| Pattern matcher | Hyperscan (if available) |
The choice IDS (alert-only) is the key setting. IPS mode would install an NFQUEUE hook that drops packets; the GUI warning that follows the IPS-mode toggle is not a politeness, it is a real risk of locking yourself out.
Confirm the configuration was written:
cat /var/suricata/suricata.yaml | grep -A2 'HOME_NET:'
The output must show HOME_NET: "[10.10.10.0/24]". If HOME_NET
is empty or any, every alert loses context — you cannot tell
which alerts are local-to-internal and which are inbound.
Task 3: Download the ruleset
Services → Intrusion Detection → Download Rules:
| Field | Value |
|---|---|
| Ruleset | ET Open (Emerging Threats Open) |
| Enable | on |
Click Download. The GUI pulls the ruleset from
rules.emergingthreats.net and stores them under
/usr/local/etc/suricata/rules/.
Confirm:
ls -l /usr/local/etc/suricata/rules/
ls /usr/local/etc/suricata/rules/ | wc -l
wc -l /usr/local/etc/suricata/rules/emerging-*.rules
ET Open ships roughly 50,000 rules in 30+ categories. The file size and line count are the visible signal that the download worked.
Task 4: Add a custom rule
A custom rule is the diagnostic every IDS lab needs. The rule
below fires on any TCP packet with the literal string
OPNSENSE-LAB-TEST-PAYLOAD anywhere in the payload:
Services → Intrusion Detection → Rules → + Add:
| Field | Value |
|---|---|
| Action | alert |
| Protocol | tcp |
| Source | any |
| Destination | any |
| Source port | any |
| Destination port | any |
| Direction | -> |
| Description | opnsense-lab-canary |
| Payload | content:“OPNSENSE-LAB-TEST-PAYLOAD”; nocase; |
Click Save then Apply. The GUI writes the rule to
/usr/local/etc/suricata/rules/opnsense-lab-custom.rules.
Verify:
cat /usr/local/etc/suricata/rules/opnsense-lab-custom.rules
A Suricata rule reads as one line of options-separated keywords. The custom rule should look like:
alert tcp any any -> any any (msg:"opnsense-lab-canary"; content:"OPNSENSE-LAB-TEST-PAYLOAD"; nocase; sid:1000001; rev:1;)
The signature id (sid) must be unique. The OPNsense auto-sid
generator assigns one starting from 1000001. Two rules with the
same sid silently fail to load — Suricata logs a warning and
drops the second.
Task 5: Start Suricata
Services → Intrusion Detection → Administration → Apply.
Confirm the daemon is up:
service suricata status
ps -axo pid,command | grep suricata
sockstat -l | grep -E 'suricata'
A running Suricata in IDS mode is not listening on any
socket — it is capturing packets via AF_PACKET on the LAN
interface. The proof of life is ps showing the suricata
process running with the LAN interface listed in its arguments:
ps -axo pid,command | grep -E '/suricata'
If the process is running but the interface is missing, Suricata captured on no interface and is silently not seeing traffic. Stop and restart the service after any interface change.
Task 6: Generate the canary traffic
From a LAN host, generate an HTTP request with the canary string in the body. The simplest path is a request to OPNsense itself:
# On the LAN host
curl -X POST -d 'OPNSENSE-LAB-TEST-PAYLOAD' \
http://10.10.10.1:80/anything
This may 404 — that is fine. The point is the request body crossed the LAN interface where Suricata is capturing.
If the LAN host has a web server already, you can also use:
# On the LAN host, hit any local service
curl -A "OPNSENSE-LAB-TEST-PAYLOAD" http://10.10.10.50/
Either path produces a TCP packet on the LAN interface with
the literal OPNSENSE-LAB-TEST-PAYLOAD in the payload, which
the custom rule matches.
Task 7: Read the EVE JSON alert
The EVE JSON log is the structured output every modern IDS
ships. The file is /var/log/suricata/eve.json on OPNsense.
tail -f /var/log/suricata/eve.json | jq 'select(.alert)'
The output is one JSON object per alert:
{
"timestamp": "2026-08-14T12:00:00.000000+0000",
"flow_id": 1234567890,
"in_iface": "em1",
"event_type": "alert",
"src_ip": "10.10.10.100",
"src_port": 51234,
"dest_ip": "10.10.10.1",
"dest_port": 80,
"proto": "TCP",
"alert": {
"action": "allowed",
"gid": 1,
"signature_id": 1000001,
"rev": 1,
"signature": "opnsense-lab-canary",
"category": ""
},
"payload_printable": "OPNSENSE-LAB-TEST-PAYLOAD",
"payload": "T1BTTlNFTlNFLUxBQi1URVNULVBBeUxPQUQ=",
"tags": []
}
The four fields that prove the alert is yours:
signature_id: 1000001— the sid of the rule you wrote.signature: "opnsense-lab-canary"— themsg:string.payload_printableshows the literal canary.action: "allowed"because this is IDS mode — the alert fired but the packet was allowed through.
If the file is empty or the alert is missing, Suricata is not seeing your traffic. Check Services → Intrusion Detection → Administration → Log for the engine’s view.
Task 8: Confirm an ET Open rule fires
The custom rule proves the inspection pipeline is wired. Now prove the ruleset is loaded. Pick a benign, well-known rule from ET Open and verify it is in the running config:
grep -c '^alert' /usr/local/etc/suricata/rules/emerging-*.rules
A non-zero count is the visible signal. ET Open also ships a
test sid — 2100498 is a known informational rule that fires
on any IP packet:
grep 'sid:2100498' /usr/local/etc/suricata/rules/emerging*.rules
If the rule is not in the loaded ruleset, you have not enabled the right ET Open category in the GUI. Re-check Task 3.
Task 9: Validate HOME_NET scoping
A healthy HOME_NET definition keeps noise out of the alert log. Check what Suricata thinks HOME_NET is:
grep 'HOME_NET' /var/suricata/suricata.yaml
suricata --list-app-layer-protos | head
If HOME_NET is [10.10.10.0/24] and an alert fires on traffic
that has nothing to do with 10.10.10.0/24 (e.g. a packet
to/from 10.10.20.0/24, the IPsec lab’s other side), the rule
that fired is one of the rules that intentionally match on
EXTERNAL_NET — DNS amplification, scanner traffic. Those are
expected. Anything that looks like internal-to-internal traffic
on a different subnet is misconfiguration.
Task 10: Disable the canary and stop Suricata
When you have validated the pipeline, disable the canary rule to avoid noise in the next lab:
Services → Intrusion Detection → Rules → opnsense-lab-canary → toggle off → Apply.
Stop Suricata to free the AF_PACKET socket and free the CPU:
service suricata stop
ps -axo pid,command | grep suricata || echo "no suricata running"
Validation
service suricata statusreports running before Task 10./var/log/suricata/eve.jsoncontains an entry withsignature_id: 1000001,signature: "opnsense-lab-canary", andpayload_printablematching the canary string.- The custom rule is visible at
/usr/local/etc/suricata/rules/opnsense-lab-custom.rules. - ET Open rules are loaded:
wc -lon/usr/local/etc/suricata/rules/emerging-*.rulesreturns a non-zero count. - HOME_NET in
suricata.yamlis[10.10.10.0/24].
Expected Result
Suricata running in IDS mode on the LAN interface, subscribed
to ET Open, with a custom canary rule that fires on a payload
the operator can produce at will. The EVE JSON log shows the
alert with the expected signature id, the literal payload, and
an action: "allowed" because IDS mode does not drop.
Troubleshooting
No alert appears.
Check, in order: (1) Suricata process is running with the LAN
interface listed (ps -axo command | grep suricata), (2) HOME_NET
is set, (3) the custom rule is loaded (grep canary /var/log/suricata/suricata.log after a reload), (4) the
canary string actually crossed the LAN interface (capture with
tcpdump -i em1 port 80 on OPNsense), (5) the rule action is
alert and not pass.
Alert fires but signature_id is wrong.
A sid collision with an ET Open rule. Sids above 1,000,000 are
reserved for local rules; if your sid overlaps with an ET rule,
Suricata drops the local one. Change the local sid to something
uniquely high like 10000042.
Engine fails to start with hyperscan error.
The Hyperscan pattern matcher is faster but requires AVX2. On
older CPUs, switch to Aho-Corasick under Services →
Intrusion Detection → Interfaces → Pattern matcher.
Engine starts but the log shows failed to load rule file.
A rule file has a syntax error. The log line tells you which file
and which sid. Common causes: missing semicolon at the end of the
rule, unescaped colon in msg:, or a stale sid reference in a
references: field.
Performance is poor.
Suricata in IDS mode on a 1 Gbps link needs ~2-4 cores. If
packet drops appear in stats.log, reduce the number of enabled
rules or move from IDS mode to AF_PACKET with one worker thread
per core. The OPNsense GUI exposes this under Services →
Intrusion Detection → Interfaces → Advanced.
Cleanup
Stop Suricata and remove the lab’s footprints.
# Stop the daemon
service suricata stop
# Disable the canary rule in the GUI before removing the file:
# Services → Intrusion Detection → Rules → opnsense-lab-canary → Delete
# Remove the canary file
rm -f /usr/local/etc/suricata/rules/opnsense-lab-custom.rules
# Disable ET Open if you do not want it after the lab
# Services → Intrusion Detection → Download Rules → ET Open → disable
# Confirm Suricata is gone
ps -axo pid,command | grep -E '/suricata' || echo "no suricata running"
service suricata status
# Truncate the alert log so the next lab starts clean
: > /var/log/suricata/eve.json
Do not leave the canary rule loaded. It will fire on every HTTP request with the literal string, including legitimate admin panels that mention “test” in their content.
What you learned
- Suricata IDS mode inspects but does not drop. IPS mode drops via NFQUEUE. The action field in the alert changes but the rule format does not.
- HOME_NET is the operator’s choice and changes how alerts are categorised. An empty HOME_NET makes every alert lose context.
- A custom rule with a unique canary string is the smallest diagnostic that proves the inspection pipeline is wired end to end.
- EVE JSON is the structured output. Read
signature_id,signature,payload_printable, andactionfirst; the rest is enrichment.