Part 06 — Building a Security Operations Center Homelab (Installing Zeek on TheGateWatcher)
Hello everyone, and welcome back to the Building a Security Operation Center Series. :) Previously, in Part 05 we installed and setup…
Part 06 — Building a Security Operations Center Homelab (Installing Zeek on TheGateWatcher)
Hello everyone, and welcome back to the Building a Security Operation Center Series. :) Previously, in Part 05 we installed and setup Suricata. https://medium.com/@seancnelsonis/part-05-building-a-security-operations-center-homelab-setting-up-suricata-on-thegatewatcher-b955e6b7962a
Today, we are installing Zeek on our perimeter host.
Zeek: The Who, What, Where, When & Why
Who made it?
Zeek was originally created by Vern Paxson at Lawrence Berkeley National Laboratory in the mid-1990s under the name “Bro”, slang for Big Brother, because it watches your network. It was renamed to Zeek in 2018 to shed the surveillance connotation and better reflect its open, community-driven nature. Today it’s maintained by the Zeek Project, a non-profit with contributions from Corelight (the commercial backer), UIUC, and the broader security research community. It’s used by national labs, universities, Fortune 500 SOCs, and ISPs worldwide.
WHAT does it do?
This is the most important thing to get right because it’s fundamentally different from Suricata, and understanding the distinction is what makes our SOC architecture coherent.

Zeek doesn’t fire alerts by default. Instead, it parses and logs every protocol interaction it sees and writes them into organized log files:
- conn.log: every TCP/UDP/ICMP connection (src, dst, port, bytes, duration)
- dns.log: every DNS query and response
- http.log: every HTTP request (method, URI, user-agent, status code)
- ssl.log: every TLS handshake (certificate details, cipher, SNI)
- files.log: every file transferred over the network (with MD5/SHA256 hash)
- ssh.log: every SSH connection attempt
- weird.log: protocol violations and anomalies Zeek can't classify
This is network forensics gold. When an incident happens, Zeek logs tell you exactly what was talking to what, when, and how, even for encrypted traffic (you can’t see inside TLS, but Zeek logs the metadata around it).
WHERE does it sit in our architecture?
Zeek runs alongside Suricata on TheGateWatcher in passive mode it taps the same AF_PACKET socket on eth1 and eth2 without interfering with traffic flow. The two tools are complementary, not redundant: Suricata says "alert — possible C2 beacon," and then you pivot to Zeek's conn.log to see the full connection history for that IP over the past week.
Important limitation we already know: Because my switch (TP-Link TL-SG105) is unmanaged, LAN-to-LAN traffic (e.g., my Xbox talking to my laptop) never passes through TheGateWatcher, so Zeek won’t see it. Same blind spot as Suricata. That’s a future managed switch upgrade, not a blocker for now.
WHEN Do WeUse The Logs?
Zeek logs serve three distinct use cases in our SOC:
1. Real-time ingestion into Wazuh/Elasticsearch (future TheBrainStorage work): Zeek’s TSV log format is well-supported by Filebeat and the Wazuh agent. Once TheBrainStorage is online, we’ll have Zeek’s logs searchable in Kibana alongside Suricata alerts.
2. Incident investigation: An alert fires in Suricata. You pull Zeek’s conn.log and dns.log to build a timeline: When did this host first contact that IP? What domains did it resolve before and after? How much data was transferred? Zeek answers these questions even when you weren't actively watching.
3. Threat hunting: Periodically querying Zeek logs to look for anomalies that didn’t trigger any Suricata rule. Long-duration connections, unusual port usage, DNS lookups to algorithmically generated domains, large data exfiltration, etc. These behavioral patterns are in Zeek’s logs waiting to be found.
WHY add it on top of Suricata?
Signature detection alone has a fundamental ceiling. Suricata can only catch what its rules describe. A zero-day, a novel C2 protocol, or an attacker carefully staying below Suricata’s thresholds will slip through but it will always leave behavioral traces in Zeek’s logs. This is why enterprise SOCs don’t pick one or the other; they run both. The combination gives you:
- Detection coverage (Suricata) + forensic depth (Zeek)
- Alerting (Suricata) + investigation context (Zeek)
- Known-bad signatures (Suricata) + anomaly surface area (Zeek)
How it fits our log architecture
Zeek writes logs to /var/log/zeek/current/ by default. Each log rotates hourly. The same pattern we established with Suricata applies here:
- Zeek logs live on TheGateWatcher’s local disk
- Wazuh agent (future) ships copies to TheBrainStorage in real time
- Original files remain on TheGateWatcher
One practical note: Zeek is significantly more verbose than Suricata. On a home network it’s manageable, but conn.log grows fast because it logs every connection. Logrotate config will be important here too, same story as Part 05, just more urgent.
The nine stages of Part 06
Stage 1: Check what’s available Stage 2: Install build dependencies Stage 3: Pull the Zeek source code Stage 4: Configure the build Stage 5: Compile Stage 6: Install Stage 7:Configure Zeek for your network Stage 8: Auto-start on boot Stage 9: Logrotate
Stage 1: Check What’s Available
Check if Zeek is in the Debian repos:
apt-cache show zeek 2>/dev/null | grep -E "Version|Package" || echo "Not in default repos"
Check your OS and architecture:
cat /etc/os-release | grep -E "^ID|^VERSION_ID|^PRETTY"
uname -m
Check current available disk space:
df -h /
Zeek is not always in the default Debian repos, and when it is, it’s often an older version. The Zeek Project maintains their own package feed with current releases, and for a production-grade SOC tool you generally want that over whatever Debian ships.


Disk space is great. The only wrinkle is Debian 13 (Trixie). This is a newer release, and Zeek’s official package feed currently publishes builds for Debian 12 (Bookworm) but not yet Trixie. We need to confirm whether their ARM64 Bookworm packages install cleanly on Trixie before pointing it at them.
The safest path for our setup is to build Zeek from source. It sounds intimidating but it’s a well-documented process, and we’ve already done harder things in this series (compiling the r8125 driver). The upside: we get the current stable release, full aarch64 support, and it’s exactly what the Zeek Project recommends when distro packages aren’t available.
Stage 2: Install Build Dependencies
Let’s run this block. It’ll take a few minutes:
sudo apt update && sudo apt install -y \
cmake make gcc g++ flex bison libpcap-dev \
libssl-dev python3 python3-dev swig zlib1g-dev \
libmaxminddb-dev libkrb5-dev libcurl4-openssl-dev \
git libsasl2-dev
We want to confirm everything installed without errors before we pull the Zeek source.


Stage 3: Pull The Zeek Source Code
clone the Zeek repo and check out the latest stable release:
cd ~
git clone --recursive https://github.com/zeek/zeek.git
cd zeek
git checkout v7.0.0
git submodule update --init --recursive
The — recursive flag matters here. Zeek has a lot of submodules (broker, spicy, bifcl, etc.) and missing any of them will cause the build to fail silently later. The submodule update at the end makes sure everything is pinned to the right commit for v7.0.0.
This will take a few minutes on the clone. When it finishes run:
git describe --tags


All submodules checked out cleanly. we’re on the current stable release.
Stage 4: Configure the build
This is the step that checks your system for all the dependencies we just installed and prepares the build instructions. Run:
./configure --prefix=/opt/zeek
— prefix=/opt/zeek: installs Zeek into its own clean directory rather than scattering files across /usr/local. Easier to manage, easier to remove if needed.


All the critical checks passed:

Stage 5: Compile
This is the long one.
This will be 20–30 minutes on the Pi 5. I wont close our SSH session, won’t let my laptop sleep and drop the connection.





After 20–30 minutes. Zeek compiled successfully. No errors.
Stage 6: Install
This copies everything from the build directory into /opt/zeek where we told it to go.
sudo make install


Then we add Zeek to our PATH so you can call it without typing the full path every time:
echo 'export PATH=/opt/zeek/bin:$PATH' >> ~/.bashrc
source ~/.bashrc
Test it with:
zeek --version

Quick notes about what we did.
When you install Zeek to /opt/zeek, its binaries (zeek, zeekctl, zkg) live in /opt/zeek/bin/. My shell doesn't know to look there by default. It only searches a predefined list of directories called the PATH. If I typed zeek — version without updating PATH, you'd get command not found.
Line 1: echo ‘export PATH=/opt/zeek/bin:$PATH’ >> ~/.bashrc
This appends one line to my .bashrc file. The script that runs every time I open a new shell session. That line tells my shell: "add /opt/zeek/bin to the front of the directories you search for commands." The :$PATH at the end means "then keep everything that was already in PATH", so nothing gets removed, I’m just prepending Zeek's bin directory to the existing list.
Line 2: source ~/.bashrc
.bashrc only runs automatically when I open a new shell. source tells My current shell to re-read and apply it right now, without needing to log out and back in. Without this, zeek — version would still fail until My next SSH session.
Zeek is installed and now callable from anywhere on the system!
Stage 7: Configure Zeek for your network
Now we point Zeek at our interfaces. The main config file is node.cfg:
sudo nano /opt/zeek/etc/node.cfg
We’ll see a default [zeek] block. Replace the entire contents with this:
[zeek]
type=standalone
host=localhost
interface=eth1
[worker-eth2]
type=worker
host=localhost
interface=eth2
This tells Zeek to monitor both your LAN (eth1) and WAN (eth2) interfaces. Save and exit (Ctrl+X, Y, Enter).

Then open the networks config:
sudo nano /opt/zeek/etc/networks.cfg
Add our LAN subnet so Zeek knows what to treat as “local” traffic:
Add your LAN subnet so Zeek knows what to treat as "local" traffic:

Next step, start Zeek:
sudo /opt/zeek/bin/zeekctl deploy
deploy does three things in one shot: checks your config, installs scripts, and starts all nodes.

Oh no. We have an error.
standalone mode only allows a single node definition. When we add a second block, Zeek complains. We need to restructure node.cfg to use proper multi-node mode.
Let’s go back into the node.cfg file
sudo nano /opt/zeek/etc/node.cfg
Replace everything with this:
[manager]
type=manager
host=localhost
[proxy-1]
type=proxy
host=localhost
[worker-eth1]
type=worker
host=localhost
interface=eth1
[worker-eth2]
type=worker
host=localhost
interface=eth2
We now have a proper local cluster: a manager (coordinates logging), a proxy (routes events between nodes), and two workers (one per interface). All four processes run on localhost. Nothing leaves the Pi, it's just Zeek's internal architecture for handling multiple capture points.
Save and exit, then run deploy again:

Now let’s verify everything is actually running and generating logs:
sudo /opt/zeek/bin/zeekctl status
ls /opt/zeek/logs/current/


All four processes running, and the logs directory is already populated.
Before moving on, Let me explain the error, and correction.
When we first configured node.cfg, the top block was set to type=standalone. Standalone mode is Zeek's simplified single-process mode — it's designed for the most basic use case: one Zeek process, one interface, one machine. Because it's a single process doing everything, Zeek's standalone mode has a hard rule: only one node definition is allowed in the config file. The moment you added [worker-eth2] as a second block, Zeek saw two node definitions and threw the error: "more than one node defined in standalone node config."
The fix was to switch to cluster mode, which is Zeek’s architecture for running multiple processes — even when all of them live on the same physical machine. In cluster mode, responsibilities are divided:
- The manager handles log writing and coordinates the other processes
- The proxy routes events and data between nodes
- The workers do the actual packet capture, one per interface
This might sound like overkill for a home lab, but it’s actually the correct and supported way to monitor multiple interfaces on a single Zeek installation. The key thing to understand is that “cluster” doesn’t mean multiple physical machines here — all four processes (manager, proxy, worker-eth1, worker-eth2) run on the same Pi, on localhost. It's just Zeek's internal division of labour.
That being said, let’s peek inside a couple to confirm they’re capturing real data:
sudo tail -5 /opt/zeek/logs/current/conn.log
sudo tail -5 /opt/zeek/logs/current/dns.log

We’re almost done. Let’s move to the two remaining important tasks for Stage 8: Zeek start automatically on boot and log rotating.
Stage 8: Auto-start on boot
Zeek doesn’t create a systemd service by default. We need to make one:
sudo nano /etc/systemd/system/zeek.service
Paste this:
[Unit]
Description=Zeek Network Security Monitor
After=network.target
[Service]
Type=forking
ExecStart=/opt/zeek/bin/zeekctl start
ExecStop=/opt/zeek/bin/zeekctl stop
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target

Save and exit, then enable it:
sudo systemctl daemon-reload
sudo systemctl enable zeek
sudo systemctl status zeek

The service file looks correct and the symlink was created successfully. The inactive (dead) status is expected — we enabled it for boot but haven't started it via systemd yet. Zeek is actually already running from the zeekctl deploy we did earlier, so there's no conflict.
One thing worth noting: zeekctl manages its own process lifecycle internally, which means systemd's view of "active/inactive" doesn't always reflect reality. Let's verify Zeek is still actually running:
sudo /opt/zeek/bin/zeekctl status

It still show all four nodes running from our earlier deploy. The service unit will correctly call zeekctl start on next boot and pick up where it left off.
Stage 9: Logrotate
Zeek actually handles its own log rotation internally via zeekctl — it rotates logs every hour by default and archives them to /opt/zeek/logs/ with date stamped folders. But we want to make sure old archives get cleaned up so they don't fill your SSD over time.
Check the default rotation interval:
grep -i logrotate /opt/zeek/etc/zeekctl.cfg
Then set a retention limit:
sudo nano /opt/zeek/etc/zeekctl.cfg
Find the LogExpireInterval line and set it:
LogExpireInterval = 7
This tells zeekctl to automatically delete logs older than 7 days. Save and apply:
sudo /opt/zeek/bin/zeekctl deploy


You’ll notice that the zeekctl.cfg logrotate didn’t exist from the first command? I ran it again with the proper variable name. worked.
LogRotationInterval = 3600 This controls how often Zeek closes the current log file and starts a new one. At 3600 seconds (1 hour), Zeek archives the current logs every hour into a timestamped folder under /opt/zeek/logs/. This is Zeek's own internal rotation, completely separate from the Linux logrotate utility.
LogExpireInterval = 7 This is the retention policy. It tells zeekctl cron to automatically delete archived log folders older than 7 days. Without this set, logs accumulate forever.
That’s Part 06 complete! :D Here’s a final summary of everything we accomplished:

Thank you for joining me in Part 06 of Building a SOC series. :) I hope to see you in the next section where we install OpenCanary.
I hope to see you there. ❤
메타데이터
- post_id
- 89180cfc0fa7
- slug
- part-06-building-a-security-operations-center-homelab-installing-zeek-on-thegatewatcher-89180cfc0fa7
- url
- https://medium.com/@seancnelsonis/part-06-building-a-security-operations-center-homelab-installing-zeek-on-thegatewatcher-89180cfc0fa7
- canonical_url
- https://medium.com/@seancnelsonis/part-06-building-a-security-operations-center-homelab-installing-zeek-on-thegatewatcher-89180cfc0fa7
- author_url
- https://medium.com/@seancnelsonis
- status
- ok
- fetched_at
- 2026-06-22 05:41:33