Suricata Yelled, But It Didn’t Explain
You know the feeling. Your IDS fires an alert. “SSH_OUTBOUND_ANOMALY_DETECTED” or whatever. Great. Now what? Did someone actually get in? Was it a false positive? What exactly happened? Suricata and Snort are alarmists — they see something that matches a rule and scream. Useful, sure. But they leave you holding the bag.
Zeek is different. Zeek doesn’t care about matching signatures. It’s a programmable network analysis framework that watches traffic and spits out events — structured, timestamped, queryable records of what actually occurred on your network. DNS lookups, HTTP requests, file transfers, TLS handshakes, SMB sessions. Zeek documents everything, then gets out of your way.
Think of it like the difference between a security guard shouting “SOMEONE BROKE IN!” versus a forensics team documenting exactly who, when, how, and what they touched. You’re building a home lab where you can actually investigate what happened, not just react to alarms.
What Zeek Actually Is (And Isn’t)
Here’s the bit that confuses people: Zeek is not an IDS, even though it looks like one. It doesn’t have a signature database. It doesn’t say “this traffic is malicious.” What it does is turn network traffic into structured logs.
You point Zeek at a packet stream (a SPAN mirror port, a PF_RING tap, or even a pcap file), and it assembles that chaos into log entries. Connections. DNS queries. HTTP requests. File transfers. TLS certificates. SMB commands. Each log is tab-separated, machine-readable, and timestamped to the microsecond.
Want to know every DNS query your Plex server made last Tuesday? Query dns.log.
Want a timeline of all outbound HTTPS connections from your lab subnet in the last 24 hours? conn.log + grep + sort.
Want to extract the actual binary files that crossed your network? files.log tells you which ones and where Zeek cached them.
That’s the magic: Zeek is a translator. It speaks packet. Your logs speak English (or JSON, or whatever format you want).
Zeek vs. Suricata vs. Snort
| Aspect | Zeek | Suricata | Snort |
|---|---|---|---|
| Purpose | Network analysis + scripting | Signature-based IDS/IPS | Signature-based IDS/IPS |
| Output | Structured logs (conn, dns, http, ssl, files, x509, smb, …) | Alerts + eve.json | Alerts + unified2 |
| Querying | grep, zeek-cut, Elastic, ClickHouse | grep, ELK, Suricata Eve output | grep, Splunk |
| Customization | Zeek script language (Bro DSL) | Lua plugins | C plugins |
| CPU overhead | Higher (protocol analysis is deep) | Moderate | Moderate |
| Learning curve | Steep (custom language) | Moderate | Moderate |
| Best for | Forensics, hunting, baselining | Intrusion detection | Intrusion detection |
Real talk: Suricata will yell faster. Zeek will tell you the full story. In a home lab, you want the story. Speed doesn’t matter at 1 Gbps on a home connection.
The Zeek Logs You’ll Actually Use
When Zeek runs, it writes a bunch of logs to a directory (usually logs/). Here are the ones worth knowing:
- conn.log — every TCP/UDP connection: source, dest, ports, protocol, duration, bytes. This is your bread and butter.
- dns.log — all DNS queries: who asked, what domain, answer, response code. Gold for finding C2 or exfil attempts.
- http.log — HTTP requests and responses: method, URI, status, user-agent, referrer. Catches web-based malware comms.
- ssl.log — TLS handshakes: server cert, issuer, validity period, cipher suite. Finds self-signed certs or mismatches.
- files.log — metadata about files seen in traffic: MD5, SHA256, MIME type, filename. Zeek can extract the actual binaries too.
- x509.log — certificate details extracted from SSL/TLS and SMB. Useful for cert pinning validation.
- smb.log — SMB commands and responses: user, computer, action, tree. Catches lateral movement.
- ntlm.log — NTLM authentication events (newer Zeek). User, host, success/failure.
- weird.log — anomalies Zeek couldn’t classify. Often the most interesting logs.
Setting Up Zeek in a Home Lab
Docker Compose (the Easy Path)
Docker is your friend here. The zeek/zeek image has everything baked in.
version: '3.8'
services: zeek: image: zeek/zeek:latest container_name: zeek cap_add: - NET_ADMIN volumes: - ./logs:/opt/zeek/logs - ./share/zeek/site:/opt/zeek/share/zeek/site environment: HOME: /opt/zeek networks: - zeek_net # If you're using a SPAN mirror or PF_RING tap: command: zeek -i eth0 -C
networks: zeek_net: driver: bridgeKey flags:
-i eth0— listen on interface eth0 (adjust to your tap/mirror)-C— read from live traffic (disable checksumming validation)-F— read from stdin (for piping pcaps or live capture)
The container writes logs to ./logs/, which persists on your host.
For Live Capture: SPAN or PF_RING
Your home lab switch probably has port mirroring (SPAN on Cisco, monitoring on Ubiquiti, traffic mirroring on Mikrotik). Point the mirror to a dedicated NIC on your Zeek box, then run Zeek on that interface. Zero impact on production traffic.
If your switch is dumb and has no mirroring, PF_RING is an option — it’s a kernel module that lets you tap traffic at line rate. But honestly, for a home lab, a mirror port is simpler.
Actually Using Zeek Logs
zeek-cut: The Best Friend Tool
zeek-cut is a tiny utility that extracts columns from Zeek logs. Way better than awk.
# All HTTPS connections from your lab subnet in the last dayzeek-cut id.orig_h id.resp_h id.resp_p proto < logs/conn.log | grep 443
# Every DNS lookup for suspicious domains (exfiltration attempt detection)zeek-cut ts query answer < logs/dns.log | grep -i "\.ru\|\.xyz" | tail -20
# All HTTP requests to IPs outside your local rangezeek-cut ts id.orig_h host uri < logs/http.log | grep -v "192.168\|10\."Sample conn.log Entry
Here’s what a real line looks like (fields tab-separated):
1576944000.000000 CXk3dJ4pP13Qs84dMj 192.168.1.100 54321 8.8.8.8 53 udp - - - - 0 53 1 53 0 S0 - - 0 Df - - - -That’s: timestamp, flow ID, src IP, src port, dst IP, dst port, protocol, state, and a bunch of flags. Parse it with zeek-cut or load it into Elastic.
Writing Custom Zeek Scripts
Here’s where Zeek gets powerful. You can write scripts to detect anomalies, extract events, or trigger actions.
A simple example: detect unusually long DNS names (often used for exfiltration over DNS):
# Detect DNS queries with suspiciously long domain names# Exfil-over-DNS often uses long subdomains to hide data
event dns_request(c: connection, msg: dns_msg, query: string, qtype: count, qclass: count) { # Check if the domain name (without the root .) exceeds 100 characters if (|query| > 100) { print fmt("LONG_DNS: %s -> %s for %s (length: %d)", c$id$orig_h, c$id$resp_h, query, |query|);
# You could also trigger an alert here # NOTICE([t=c$start_time, uid=c$uid, $msg="Suspicious long DNS name"]); }}Drop this in your Zeek site directory (share/zeek/site/), restart Zeek, and it’ll start logging long DNS names.
The Zeek language looks like C, but it’s actually event-driven: you write handlers for things like dns_request, http_request, file_over_http, etc. Each event fires as Zeek parses the traffic.
Integrating with Elasticsearch / OpenSearch
Raw logs are searchable, but indexing them in Elastic makes them useful. Set up an ELK stack (or OpenSearch if you want the open-source route), point Zeek’s logs at a Filebeat instance, and suddenly you have a searchable interface.
Alternatively, use Corelight (the commercial Zeek fork) which bakes in better Elastic integration. Or SecurityOnion, which is a full security monitoring distro built on Zeek + Suricata + Elastic.
For a home lab, though, plain Zeek + Elastic is overkill. zeek-cut + grep + tail will get you 90% of the way there.
Real Home Lab Use Cases
Catching a Smart TV Phone Home
Your new Roku TV starts making connections to random IPs. Query conn.log:
zeek-cut ts id.orig_h id.resp_h id.resp_p < logs/conn.log | grep "192.168.1.50"See outbound connections to Akamai? To ad networks? That’s your data. Block the IP at the firewall if it’s sketchy. Zeek let you investigate instead of guessing.
Detecting Torrent Leaks
A roommate runs a torrent client. Zeek catches the DHT and peer traffic:
zeek-cut id.orig_h id.resp_p proto < logs/conn.log | grep "6881\|6889"Boom. Bittorrent ports. Or you run a script that detects unusual connection patterns (lots of short connections, many peers, high port numbers) — hallmarks of P2P.
Lateral Movement Detection
If a device on your lab subnet suddenly starts scanning other devices (port scans, SMB probes, SSH attempts), Zeek’s conn.log shows it:
zeek-cut id.orig_h id.resp_h id.resp_p state < logs/conn.log | sort | uniq -c | sort -rn | head -20Count connections per source host. If a device goes from zero connections to hundreds, it’s probably doing something.
File Extraction and Hashing
Zeek can extract files from network traffic and hash them. Check files.log:
# All EXEs seen on your networkzeek-cut md5 sha256 filename mime_type < logs/files.log | grep -i "exe\|dll"Query VirusTotal with the hash. If it’s malware, you now know which device sent it and when.
JA3/JA4 Fingerprinting
Newer Zeek logs TLS client fingerprints (JA3/JA4). These are cryptographic hashes of the TLS handshake itself — unique per client software. Catch tools like:
- Metasploit (distinctive JA3 hash)
- Curl/wget (different from browsers)
- Custom C2 frameworks (rarely match known tools)
Query ssl.log:
zeek-cut ts id.orig_h ja3 < logs/ssl.log | grep "YOUR_KNOWN_BAD_JA3_HERE"Performance Notes
Zeek is hungry. A 1 Gbps saturated connection will chew ~2 cores and 8 GB RAM. For a home lab doing 10-100 Mbps average, one core and 2 GB RAM is fine.
If you’re on a Raspberry Pi: don’t. Zeek needs real hardware.
If you’re on a beefy server: Zeek scales well. Throw threads at it (-t flag), pin it to cores, run PF_RING for kernel bypass.
One More Thing: Beaconing Detection
Malware and C2 frameworks “phone home” on a regular schedule (every hour, every 12 hours, etc.). Zeek can detect this with a simple script:
# Simplified beaconing detection: same destination, regular intervals
global beacon_candidates: table[addr, addr, port] of vector of time;
event connection_established(c: connection) { local key = [c$id$orig_h, c$id$resp_h, c$id$resp_p];
if (key !in beacon_candidates) beacon_candidates[key] = vector();
beacon_candidates[key] += c$start_time;
# If we've seen 5+ connections to the same dest:port, check intervals if (|beacon_candidates[key]| >= 5) { local intervals: vector of interval = vector(); for (i in [0 : |beacon_candidates[key]| - 1]) intervals += (beacon_candidates[key][i+1] - beacon_candidates[key][i]);
# Calculate stddev; if too low, it's probably beaconing # (This is pseudocode — real implementation needs stats) print fmt("BEACON_SUSPECT: %s -> %s:%d (intervals: %s)", key[0], key[1], key[2], intervals); }}Real beaconing detection is more nuanced (you need proper stats, allow for jitter), but you get the idea.
Wrapping Up: Your Network Forensics Foundation
Zeek won’t prevent breaches. It won’t stop malware. But it’ll document everything that happens on your network in excruciating detail, and let you ask questions later.
“Did anyone exfiltrate data last week?” Query dns.log and http.log.
“What’s that device doing at 3 AM?” Check conn.log.
“Is this file known malware?” Hash it from files.log, check VirusTotal.
That’s forensics. That’s investigation. That’s what separates “I got hacked and have no idea what happened” from “I got hacked, here’s exactly what they did, and here’s the evidence.”
Set up a SPAN port on your lab switch, spin up Zeek in Docker, point it at the mirror, and let it document. In six months, you’ll have a baseline of your network. And when something does go sideways, you’ll have the receipts.
Your 2 AM self — the one who’s trying to figure out why there’s a weird connection in the logs — will thank you.