This is the runbook behind my home-network monitoring setup: a cheap tap switch, Security Onion as the sensor, and a router that turns what it sees into blocks.
What this is: how I watch the traffic on my home LAN and act on it. One $28 smart switch mirrors the internet link. Security Onion runs Zeek, Suricata, and a search store. A small script polls the store and pushes what it finds to the router, which enforces it. I use TP-Link for the tap, Security Onion for detection, and a GL.iNet router running OpenWrt for the blocking. Substitute your own equivalents where they differ.
Every IP below is a placeholder (
203.0.113.0/24is the RFC 5737 test-net range). Swap in your own values. Nothing here is an attack recipe; it is a defensive, read-only monitoring stack.
1. Why watch your home network at all
Home routers are the new edge. They sit in front of every TV, phone, laptop, and IoT device you own, and most of them do nothing but forward packets. When something on your LAN tries to phone home to a command-and-control server, or a scanner on the internet probes the services you forwarded, nobody tells you.
A cloud provider sends you an email when a compute instance gets pwned. Your home router sends you nothing. That is the gap this setup closes.
Cost: a ~$28 smart switch, and a spare machine to run the sensor. Time: a weekend. The software is free and open source.
One design decision up front, because it shapes everything: detection and enforcement are separate machines. Security Onion only sees traffic and raises alerts. It cannot block anything. The router is the enforcement point. The automation layer in between is a plain script on a schedule. Keep those three roles apart and the whole thing is easier to reason about.
2. The design gate: visibility, not software
A network sensor is a passive listener. It only sees the traffic that reaches its network interface: its own traffic, broadcasts, and whatever is mirrored or tapped to it. No tap or mirror, and your fancy alerting dashboard just shows Zeek’s logs of the sensor’s own network chatter.
So solve the visibility question before you pick software. Security Onion’s Suricata runs as an intrusion detection system (alerts only) by default. Inline intrusion prevention is a different deployment nobody runs in a home.
And one thing you will be asked constantly: does the mirror slow anything down? No. Port mirroring is an ASIC copy. It never slows the forwarded path. If the mirror port overflows, it drops copies; live traffic is never affected.
The only real sizing rule: the tap switch’s port speed must be at least your WAN speed. At or below 1 Gbps, a gigabit smart switch is lossless. Above that, you need a 2.5 Gbps one, and the pickings are thin.
3. Getting vision: the tap switch
I use a TP-Link TL-SG105E ($28). A Netgear GS105E ($35) does the same job. Both do port mirroring and VLANs, which is all a home tap needs.
The install is almost too easy, and that is the trap. Out of the box these are plain unmanaged switches: plug in power and cables and traffic passes with zero config. There is no “must configure before it works” window. The web interface exists only for the mirror.
The wiring:
| Port | Connected to |
|---|---|
| 1 | ISP modem / ONT |
| 2 | Router WAN |
| 3 | Sensor network card (the mirror/destination port) |
| 4, 5 | spare |
The mirror takes port 1 (the ONT, traffic in both directions) and copies it to port 3 (the sensor). Every device’s internet traffic, wired and Wi-Fi both, crosses that one ONT link, so all of it is captured. East-west traffic between devices on your LAN stays dark. That is acceptable for a home setup.
Two things I learned the hard way:
Bench-configure it first. Out of the box, and after a factory reset, the switch is in DHCP-client mode. Plug it in between the ONT and the router and it can grab a public ISP address instead of its static config, which makes its management IP go silent. The fix is a physical factory reset (hold RESET ~10 seconds until the LEDs blink), which returns it to the static default. Configure the mirror on a bench before you put it inline, then wire it in. Always bench first, never configure a DHCP-client device in the WAN path.
Power loss fails closed. A dead smart switch passes nothing. The tap’s “fails open” only covers config loss, where a wiped mirror reverts to plain switching and traffic still flows. A power loss takes the whole network down, because the switch is inline. Mitigation: power the switch from the same strip as the modem and router, and keep a short bypass patch cable (modem directly to router) so a switch failure is a ~10-second restore, not an outage.
4. The stack: Security Onion
Security Onion is a free network security monitoring distro. It bundles the pieces on one box:
| Component | What it produces | Why it matters |
|---|---|---|
| Zeek | conn, http, dns, ssl, files logs | Ground truth of what happened: who talked to whom, on what port, how many bytes, connection state, the requested hostname |
| Suricata | alerts with rule names, severity, classification | Known-bad signatures from the open Emerging Threats ruleset, roughly 50K rules |
| Elasticsearch + Kibana | the search store and dashboards | Where everything lands and where you query it |
All of it logs to Elasticsearch under data-stream indices. Zeek and Suricata data live in separate indices.
I run Security Onion 3.2 on a dedicated Ubuntu 22.04 box, mostly idle until it is needed. The important warnings for anyone doing the same:
- Use Ubuntu 22.04, not 24.04. Security Onion 3.2 supports only 22.04. It is also what the stock ISO is built on. Check the compatibility matrix before picking a newer Ubuntu.
- Give the sensor two NICs. One management NIC on your LAN, one sensor NIC attached to the mirror port, with no IP on it. The sensor must be silent; it only listens. A mirror feed into the management NIC shows nothing by design.
- The web UI access list is a trap. Set the allowed IP or subnet to your LAN subnet, not the sensor’s own IP. The wizard pre-fills the box’s own IP, which locks everyone else out: the console answers locally but remote HTTPS dies. The signature is local curl returns a redirect, remote returns a timeout, while SSH still works. Use
so-firewall includehostto add your analyst subnet; that writes the durable pillar rather than only the live iptables file.
5. The query surface: the fields that matter
You cannot build this without knowing how Security Onion maps Zeek fields into Elasticsearch JSON. The ones you will use constantly:
source.ip / source.port / source.geo.country_iso_code
source.as.number / source.as.organization.name
destination.ip / destination.port
network.transport (tcp/udp/icmp) / network.bytes
connection.state <- Zeek conn state: S0, SF, RSTO, OTH...
container.id <- which Zeek log: conn, http, dns, ssl, files
http.uri / http.method / http.user_agent
ssl.server_name <- SNI, the domain that was requested
connection.state is the single most useful field. It tells you whether a connection actually happened:
S0= SYN seen, no reply. The firewall dropped it. This is an attempt.SF= normal established and finished. A real connection with data.RSTO/RSTR= reset.OTH= odd or malformed.
This is how you tell scanner noise from a real exchange. It is the foundation of every investigation in this setup.
One gotcha: a field that returns empty means you guessed the path wrong. dns.query is a top-level field here, not nested under a zeek.* namespace. When a query comes back empty, dump one document and read the actual key names instead of assuming.
6. The poller: a script that watches the store
Detection logs are only useful if you look at them. The standard approach is a small script that polls Elasticsearch on a schedule and tells you when something is new.
I run one every 5 minutes. Two patterns make it work:
Stateful windowing and dedupe. The script persists state between runs: the last check time, a set of already-seen event IDs, and a set of seen hosts. Each run queries for events newer than the last check (with a small overlap), reports only the ones not already seen, then saves state. Without this you re-alert on every event forever.
Stdout is the alert channel. The script prints a report to stdout only when there is something new. An empty stdout delivers nothing. This is the cleanest alerting pattern: no false “all clear” messages, no alert fatigue.
What it watches, each as one query:
- New Suricata alerts (high or critical severity flagged)
- New detections from the rules engine
- Geo policy contacts: any flow reaching a country I block at the router. Split into real contacts (data exchanged, a genuine bypass) versus a scan surge (a burst of blocked attempts, meaning the router is doing its job)
- Exploit probes: HTTP requests carrying remote-code-execution payload signatures in the URI
- Scan bursts: several connections from one source to forwarded services
- New DHCP devices, so you notice a new device on the LAN
7. Enforcement: the router does the blocking
Security Onion sees and reports. It blocks nothing. Enforcement lives on the router, which runs OpenWrt, using ipset and iptables.
The block mechanism is one ipset set plus two DROP rules. The set holds all the networks you want to drop; the rules drop new connections that match it.
The policy this enforces is a zero-contact rule against India and Israel, in both directions, with no exemptions. I do not want outbound connections from anything on my LAN reaching those countries, and I do not want inbound connections from those countries reaching my forwarded services. The router drops both.
# rebuild the set: batch load, single pass (~1s for 11k entries)
ipset create geo-block hash:net family inet hashsize 8192 maxelem 131072 -exist
ipset flush geo-block
{ for f in in.zone il.zone cloud.zone c2.zone; do
while read -r net; do case "$net" in *.*/*) echo "add geo-block $net";; esac; done < "$f"
done; } | ipset restore -exist
# two DROP rules, NEW-state only (established traffic still flows)
iptables -I forwarding_rule -m conntrack --ctstate NEW -m set --match-set geo-block src -j DROP -m comment --comment "geo-block inbound IN/IL"
iptables -I forwarding_rule -m conntrack --ctstate NEW -m set --match-set geo-block dst -j DROP -m comment --comment "geo-block outbound IN/IL"
Four zone files feed it:
in.zone,il.zone: the country CIDR lists for India and Israel, pulled fromipdeny.com/ipv4/allocation.htmlcloud.zone: cloud-region prefixes physically hosted in those countries. This closes the “registered elsewhere, hosted there” gap: a company can be registered in one country and host in another, so plain country CIDRs miss it.c2.zone: the auto-blocked command-and-control hosts the poller writes
The design choices that matter:
hash:netlookup is O(1) per packet regardless of set size. Eleven thousand entries or sixty-five thousand cost the same. Size is not a performance problem.- Batch the load. A per-entry
ipset addis a subprocess per line, which took 18 seconds for 11K entries.ipset restoredoes the same in about a second. Always batch. - NEW-state only. Established connections keep flowing, so dropping a NEW connection to a blocked country does not break an in-progress legitimate session.
ipset destroyfails while iptables references the set. Use flush and restore instead, which keeps the set object alive.
8. The auto-block loop
The poller closes the circle. Three detectors feed one shared push function:
- Exploit-payload hosts: IPs extracted from the URI of a remote-code-execution request
- High-severity Suricata targets: external IPs in critical alerts, either an outbound C2 callout or an inbound attack source
- Geo-policy bypass endpoints: the external peer of any real contact with India or Israel. Any flow between my LAN and those two countries is a bypass, because the router should have dropped it. A hit with bytes transferred gets acted on.
All of them pass through a single filter that checks every candidate against a list of non-negotiables. These are the rails, and each one was learned from a near-miss:
- Never block a LAN, private, or reserved range. One bad parse and you brick your own network access.
- Never block your own WAN IP. It shows up as a candidate when the geo logic picks the other side of an inbound flow.
- Never block anycast CDN ranges. Cloudflare, Akamai, and Fastly IPs get geo-tagged as various countries constantly, but they are global infrastructure. Blocking one breaks legitimate sites that are fronted by them. Match on ASN ownership (reliable), not geo-location (a static database guess).
- TTL on auto-blocks. Botnet C2 hosts are ephemeral. A 30-day expiry keeps the list from growing to the ipset cap over years.
- Verify the push. Check the
ipset testreturn code, not its stdout. On some platforms the success message goes to stderr.
9. Making your own rules
A sensor is only as good as its rule set. There are two layers in Security Onion: Suricata signatures for network traffic, and Sigma or YARA rules for log events and files.
Start by tuning what is already there. Alert noise kills visibility. When a rule fires too often, tune it rather than disable it:
- Suppress to silence a specific source or destination
- Threshold to only alert after a rate is hit
- Modify to adjust the rule’s fields
Write a new rule by testing it first. Before you create a Suricata or Sigma rule, run the equivalent query in the hunt/search interface. If the query finds nothing, the rule will never fire. If it finds too much, tune before you create. This one habit saves hours.
The most useful rule I added was one that makes an existing signature more robust. The stock rule for a ThinkPHP remote-code-execution vulnerability was being evaded by an injected path segment that broke its proximity constraint. The evasion-tolerant variant matches on the payload markers themselves without the proximity requirement, plus a generic variant that catches the encoded form. Malware authors tune around rule engines constantly; a rule that matches the payload rather than its exact position is more durable.
Verify a rule change actually loaded. The reload command responds, but check the engine log for zero failed rules before you trust it. A rule that fails to parse silently does nothing.
10. The daily routine
This is a monitoring stack, and monitoring is a habit, not an install. A few minutes a day:
- Check new alerts. Read the summary, then pull the full document, not just the alert title. Alert titles are misleading. A “brute force” alert turned out to be a crypto-miner exploit; a country tag turned out to be a Cloudflare edge. Read the raw fields.
- Classify by connection state and bytes. A dropped attempt is noise. A real exchange with bytes transferred is worth investigating.
- Check the source’s ASN, not just its country. GeoIP on anycast IPs is a guess. The organization name is reliable. This is how a cloud-server edge in India gets correctly recognized as global CDN infrastructure rather than a blocked-country contact.
- Follow the payload. The HTTP URI often contains the actual exploit, and the host it tries to download from is exactly what you want to block.
- Keep an eye on the router health. The sensor can be down and you will not know until you look.
Once a week, check for new devices on the LAN, and make sure the sensor is still capturing.
11. What it costs
| Item | Cost |
|---|---|
| TP-Link TL-SG105E smart switch (tap) | ~$28 |
| USB gigabit adapter (sensor NIC, if the box lacks a second port) | ~$12 |
| Spare machine or VM for the sensor | whatever is lying around |
| Security Onion, Zeek, Suricata, Elasticsearch | $0 (open source) |
| Total | ~$40 plus a box |
The whole stack has zero licensing. Enterprise pricing buys stacking, SNMP, and a CLI that a home setup never touches.
