Contents

Suricata on the Edge Router

Putting a real IDS on the WAN link, and being honest about what encryption left it able to see

Contents

The firewall on your edge router makes decisions using about six facts: source address, destination address, source port, destination port, protocol, and connection state. That is a remarkably small amount of information on which to base the security of everything behind it, and it is why a firewall will cheerfully forward a Cobalt Strike beacon that happens to be going to port 443. Port 443 is allowed outbound. The firewall’s job is finished.

An intrusion detection system exists to look at the seventh fact and everything after it — the actual bytes. Suricata reassembles TCP streams, identifies the application protocol regardless of port number, and matches the reconstructed conversation against thousands of signatures describing things that are known to be bad. It will tell you that the traffic to port 443 from your IoT VLAN is not TLS at all, which is the kind of statement a firewall structurally cannot make.

I have run Suricata on the edge of my own network for years, and it has been simultaneously one of the most educational things in the rack and one of the most consistently disappointing. Both of those are worth explaining, because the gap between the marketing and the reality of network IDS in 2025 is wide and rarely stated plainly.

IDS or IPS: decide before you install

Advertisement

Suricata runs in two fundamentally different modes and the choice determines your entire failure profile.

IDS mode taps a copy of the traffic. Suricata sits off to one side, receives packets via a mirror port or af-packet in listen mode, and generates alerts. If Suricata crashes, hangs, or falls behind, your internet keeps working and you get no alerts. The failure is silent and safe.

IPS mode puts Suricata inline. Packets pass through it via NFQUEUE or af-packet inline mode, and a drop rule actually drops. If Suricata crashes or falls behind, your internet stops. The failure is loud and total.

There is a well-worn homelab pattern of enabling IPS mode on day one because blocking sounds better than watching. Then a rule update ships a signature with a bad match, the household’s video calls start dropping, and you spend a Sunday discovering that “block malicious traffic” and “block traffic” are separated by one poorly-written regex.

Start in IDS mode. Run it for a month. Read every alert. When you have a tuned ruleset and you know what normal looks like on your link, promote a small, specific subset of rules to drop and leave the rest alerting. That is the sequence that works, and I say it as someone who did it in the other order and regretted it.

Getting it running

If you already run OPNsense on a dedicated firewall box, Suricata is a bundled plugin and the GUI handles interface selection, rule downloads and the IDS/IPS toggle. That is the low-friction path and it is fine. Everything below applies equally — the GUI is writing the same YAML.

On a plain Linux router, the interesting part of /etc/suricata/suricata.yaml is the network definitions, because getting them wrong makes every rule meaningless:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
vars:
  address-groups:
    HOME_NET: "[192.168.1.0/24,192.168.10.0/24,192.168.20.0/24]"
    EXTERNAL_NET: "!$HOME_NET"
    HTTP_SERVERS: "$HOME_NET"
    DNS_SERVERS: "[192.168.1.53]"

  port-groups:
    HTTP_PORTS: "[80,8080,8000]"
    SHELLCODE_PORTS: "!80"

af-packet:
  - interface: eth0          # the WAN-facing NIC
    cluster-id: 99
    cluster-type: cluster_flow
    defrag: yes
    use-mmap: yes
    tpacket-v3: yes

outputs:
  - eve-log:
      enabled: yes
      filetype: regular
      filename: eve.json
      types:
        - alert:
            payload: yes
            payload-printable: yes
            metadata: yes
        - http:
            extended: yes
        - dns:
            query: yes
        - tls:
            extended: yes
        - flow

Nearly every signature is written as “EXTERNAL_NET talking to HOME_NET” or the reverse. Leave HOME_NET at the default any and both halves of that expression are true simultaneously, so a third of the ruleset matches nothing and another third matches everything. If you have carved your network into VLANs, every internal subnet goes in that list.

Rules come from suricata-update, which pulls the free Emerging Threats Open ruleset by default:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
$ suricata-update list-sources
Name: et/open
  Vendor: Proofpoint
  Summary: Emerging Threats Open Ruleset
Name: oisf/trafficid
  Summary: Suricata Traffic ID ruleset
Name: abuse.ch/sslbl-blacklist
  Summary: Abuse.ch SSL Blacklist

$ suricata-update enable-source et/open
$ suricata-update enable-source abuse.ch/sslbl-blacklist
$ suricata-update
21/7/2025 -- 09:14:22 - <Info> -- Loading /etc/suricata/enable.conf
21/7/2025 -- 09:14:31 - <Info> -- Loaded 48211 rules.
21/7/2025 -- 09:14:33 - <Info> -- Disabled 621 rules.
21/7/2025 -- 09:14:38 - <Info> -- Enabled 47590 rules.
21/7/2025 -- 09:14:41 - <Info> -- Writing rules to /var/lib/suricata/rules/suricata.rules

$ suricata -T -c /etc/suricata/suricata.yaml
Notice: suricata: Configuration provided was successfully loaded. Exiting.

That -T is a config test and you should wire it into whatever restarts the service, because a rule file with one syntax error takes the whole engine down.

The month of tuning

Advertisement

Forty-seven thousand rules on a home link produces a lot of alerts, and most of them are wrong about you specifically. The et/open ruleset is written for a generic corporate network and includes signatures for policy violations that are perfectly normal in a house — BitTorrent, gaming protocols, consumer VPN clients, and about forty rules that fire on any Windows machine doing telemetry.

The mechanical part is easy. disable.conf takes rule SIDs and regexes:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# /etc/suricata/disable.conf
# ET INFO rules fire constantly on any residential link. Low signal.
re:ET INFO

# 2013028: BitTorrent DHT ping. Intentional, it is a torrent box.
2013028

# 2100498: GPL ATTACK_RESPONSE id check returned root — fires on my own
# Ansible runs against the fleet. Verified 2025-07-18.
2100498

The intellectual part is harder, and it is this: you have to actually read the alerts before you disable them. A rule that fires ten times a day is either telling you something real ten times a day, or it is noise. Distinguishing those requires opening the alert, reading the payload, and deciding. There is no shortcut and no rule of thumb, and this is where most homelab Suricata deployments die — the alert volume exceeds anyone’s patience in week two and the whole thing gets ignored or uninstalled.

jq against eve.json is how you triage. Get the shape of the noise first:

1
2
3
4
5
6
7
8
$ jq -r 'select(.event_type=="alert") | .alert.signature' /var/log/suricata/eve.json \
    | sort | uniq -c | sort -rn | head
  4412 ET INFO Observed DNS Query to .cloud TLD
  1889 ET INFO Session Traffic Similar to SSL/TLS
   903 SURICATA STREAM excessive retransmissions
   210 ET P2P BitTorrent DHT ping request
    47 ET SCAN Suspicious inbound to MSSQL port 1433
     3 ET MALWARE Observed DNS Query to Known Sinkhole Domain

Everything above the fold is noise. That last line is the one you would have missed, and finding it in a haystack of 7,400 is the entire tuning problem in one screen.

Piping eve.json somewhere queryable pays off fast — my alerts go to the Wazuh manager, which has a Suricata decoder built in and lets a network alert correlate against host events from the same machine. Correlating a SCAN alert on the wire with a failed-login burst in auth.log is worth more than either alone.

Writing a rule of your own

The stock ruleset knows about the internet’s generic bad actors and knows nothing about your house. The rules that have earned their place on my box are the handful I wrote, and Suricata’s rule language is legible enough that this takes an afternoon rather than a project.

A rule is an action, a protocol, an address/port pair with a direction, and a body of matching options:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# /etc/suricata/rules/local.rules

# The thermostat should only ever talk to its vendor's API over TLS.
# Anything else from that address is worth a look.
alert tcp 192.168.20.31 any -> $EXTERNAL_NET !443 (
    msg:"LOCAL IoT thermostat contacted non-TLS external port";
    flow:to_server,established;
    threshold:type limit, track by_src, count 1, seconds 3600;
    classtype:policy-violation; sid:1000001; rev:1;)

# Plaintext DNS leaving the network from anything except the resolver.
# Everything is meant to use the local sinkhole; this catches hardcoded 8.8.8.8.
alert dns $HOME_NET any -> $EXTERNAL_NET 53 (
    msg:"LOCAL DNS query bypassing internal resolver";
    flow:to_server;
    threshold:type limit, track by_src, count 1, seconds 600;
    classtype:policy-violation; sid:1000002; rev:2;)

Custom SIDs live in the 1000000+ range, which is reserved for exactly this so an upstream update never collides with you. rev increments when you edit a rule. The threshold keyword is the difference between a useful rule and a self-inflicted flood: without it, the second rule fires on every single query and buries everything else.

Both of those rules describe my policy rather than known malware, and that turns out to be where a home IDS earns most of its keep. Nobody publishes a signature for “the smart plug in the kitchen started talking to a host in a country it has no business in”, because nobody else has my kitchen. The generic ruleset finds generic attackers; a dozen lines of local policy find the device that changed behaviour after a firmware update, which is the more likely event by a wide margin.

The encryption problem, said out loud

Here is what nobody puts on the feature list. Somewhere north of 95% of the traffic crossing your WAN link is TLS-encrypted, and Suricata cannot see inside any of it.

For an encrypted flow, Suricata gets: the addresses and ports, the SNI hostname from the ClientHello, the certificate details, the JA3/JA4 fingerprint of the client, and the size and timing of the packets. That is a real amount of information and thousands of rules are written against it — SNI matching against malicious-domain lists, JA3 hashes for known malware families, certificate issuer checks. It is also a fraction of what the ruleset assumes it has. The rules that check HTTP URIs, POST bodies, shellcode patterns and file magic are, for the overwhelming majority of your traffic, dead weight. They load, they consume memory, and they will never match anything.

The industry answer is TLS interception: terminate TLS on the router, inspect, re-encrypt. In a home network that means installing a root CA on every device, which is a wonderful idea until you meet certificate pinning, mobile apps that refuse to work, a partner’s phone you have no business modifying, and the fact that you have now built a single box that holds plaintext for every banking session in the house. I do not do it and I would advise against it.

So the honest scope of Suricata on a home edge in 2025: it sees your DNS (unless clients use DoH, in which case it does not), it sees SNI and certificate metadata, it sees the shape of flows, and it fully inspects the small unencrypted remainder. That catches scans, malware calling known-bad domains, unencrypted IoT devices behaving badly, and protocol anomalies. That is genuinely useful. It is much narrower than “deep packet inspection” implies.

Sizing the box

Advertisement

Suricata’s appetite scales with three things, and knowing which one is biting you saves a pointless hardware purchase.

Rules times traffic is the CPU cost. Every packet is evaluated against the loaded ruleset, and the multi-pattern matcher is good but it is not free. Forty-seven thousand rules at a gigabit needs real cores — four modern ones handle it, one does not. Cutting the ruleset in half is often cheaper and more effective than buying a bigger machine, and it is the first thing to try when you see kernel drops.

Flow tracking is the memory cost. Suricata holds state for every conversation, and the defaults assume a busier network than a house. A few hundred megabytes covers a home link comfortably. If you have raised flow.memcap because a guide told you to, put it back.

Stream reassembly depth is the sneaky one. Suricata buffers the start of each TCP stream so signatures can match across packet boundaries, and the default depth of 1 MB per stream multiplied by thousands of concurrent flows is where surprise memory consumption comes from. It rarely needs raising.

The practical shape: a four-core mini-PC with 8 GB runs Suricata on a gigabit residential link with the full ET Open ruleset and room left over. A two-core fanless appliance will route that same link perfectly and then drop packets the moment you enable inspection, which produces alerts with holes in them — the worst outcome available, because it looks like it is working.

Watch capture.kernel_drops for a week before you trust anything the tool tells you.

Troubleshooting

“Suricata is dropping packets.” Check stats.log or suricatasc -c dump-counters for capture.kernel_drops. Non-zero means Suricata is behind and your alerts have holes. Raise af-packet ring-size, add threads to match your core count, or accept that a single-core mini-PC will not inspect a gigabit link. Rule count matters here too — 47,000 rules take real CPU per packet.

“No alerts at all.” In that order: confirm the interface is right and actually sees traffic (tcpdump -i eth0 -c 5); confirm HOME_NET is correct; confirm rules loaded (suricata -T prints the count); then generate a known alert with curl http://testmynids.org/uid/index.html, which the ET ruleset fires on by design. If the test alert appears and nothing else does, your tuning is too aggressive.

“Offloading hides the packets.” Modern NICs do segmentation and checksum offload, so Suricata sees 64 KB super-frames that never existed on the wire, and stream reassembly gets confused. Turn it off on the monitored interface: ethtool -K eth0 gro off lro off tso off gso off. Make it persistent, because it resets on reboot and the resulting weirdness is baffling if you have forgotten.

“IPS mode broke the internet.” iptables -F on the NFQUEUE rules restores service while you work out which signature did it. Before enabling IPS mode, know how to disable it from the console of a box whose network you have just broken.

Verdict

Suricata on the edge is worth running if you will read the alerts. That is the whole test, and it is a test about you rather than about the software. The tool is excellent — fast, mature, well-documented, genuinely multithreaded, and free. The work it demands is a month of tuning followed by a habit of looking, and a tool nobody looks at has negative value because it creates a feeling of safety that is not backed by anything.

If you have an OPNsense box with cores to spare, enable it in IDS mode with et/open, tune it for a month, and see whether you still open the alerts in September. If you do, promote a handful of high-confidence rules to drop and enjoy a genuinely better-instrumented network. If the alerts have become wallpaper, uninstall it without guilt and put the effort into DNS filtering and network segmentation, which deliver more security per hour of your attention than any IDS will.

And keep the expectation calibrated. A network IDS in 2025 is a smoke detector on a link where most of the rooms are sealed. It sees smoke in the corridor. Host-based tooling sees inside the rooms, and if you can only afford one, run that instead — CrowdSec on the edge reads decrypted application logs and catches things Suricata never will, for a tenth of the setup effort.

Advertisement
Advertisement
Smarc
Written by Smarc

Founder and editor of vo.rs. A lifelong tinkerer who self-hosts far more than is sensible, hardens Linux boxes for fun, and prods the latest AI tools to see what they can really do. The how-to guides here are the notes Smarc wishes had existed the first time round.