If you are building a home lab or a small enterprise-style SOC environment, one of the most practical things you can do is deploy passive network monitoring. It costs nothing beyond compute resources, it does not interfere with production traffic, and it gives you visibility into exactly what is moving across your network at all times. This guide walks through one of the most effective approaches: using pfSense’s built-in bridge SPAN capability to mirror network traffic to Zeek, a passive network analysis engine that converts raw packets into structured, human-readable logs.

The result is a completely invisible sensor. Your Zeek machine has no IP address on the capture interface, generates no traffic of its own on the monitoring segment, and cannot be seen or scanned by anything on the network. From an attacker’s perspective, it simply does not exist. From a defender’s perspective, it records everything.

What Is a SPAN Port and Why Does It Matter?

A SPAN port (Switched Port Analyzer), also called a mirror port, copies all traffic passing through specified interfaces and sends a duplicate stream to a designated monitoring port. The monitoring device receives every packet but never injects any traffic back onto the wire. It is entirely passive.

In physical network environments, SPAN is a feature found on managed switches. In a virtualized lab, pfSense can replicate the same behaviour through its bridging engine, allowing you to mirror traffic between any combination of virtual interfaces to a silent monitoring port, without touching a single physical cable.

What Is Zeek?

Zeek (formerly known as Bro) is a passive network analysis framework. It reads raw packets off the wire and produces structured, human-readable log files covering connection records, DNS queries, HTTP transactions, TLS sessions, file transfers, and much more. It does not block anything, generate real-time alerts by default, or modify traffic in any way. It simply observes and logs.

What makes Zeek particularly valuable in a SOC context is not just the volume of data it produces, but the quality of it. Rather than storing raw PCAP files that require a full packet analysis tool to interpret, Zeek outputs clean, tab-separated log files that are immediately queryable, parseable, and ingestible into SIEMs like Wazuh, Elastic, or Splunk. You can also query them directly from the command line in seconds.  

How to Set Up Passive Network Monitoring With Zeek and pfSense SPAN Port in VMware

How to Set Up Passive Network Monitoring With Zeek and pfSense SPAN Port in VMware

 

 

 

 

 

 

 

 

 

 

Passive Network Monitoring with pfSense and Zeek in VMware – Step-by-Step Guide:

Together, pfSense and Zeek create a powerful passive monitoring pipeline. pfSense bridges your monitored network segments and mirrors a copy of all traffic out through a dedicated silent port. Zeek sits on the other end of that mirror, captures every duplicated packet through a NIC with no IP address, and writes it to log files. The Zeek sensor is completely invisible to every host on the network because it never transmits anything.

What You Need

  • pfSense running as your gateway or firewall, physical, VM, or cloud-based, with web GUI access.
  • A Linux server for Zeek, this guide uses Ubuntu Server 26.04. The machine needs two NICs: one for management traffic and one for passive capture.
  • A virtualisation platform, this guide uses VMware Workstation. The same concepts apply to Proxmox or VirtualBox with minor differences in network naming.
  • At least one LAN or VLAN interface on pfSense whose traffic you want to monitor.

Phase 1: Create the Silent Monitoring Network

The first step is to build an isolated virtual network that will carry only mirrored traffic. No DHCP, no host adapter, no IP addresses assigned to anything that lives on it. Think of it as a dedicated wire that carries a copy of all your network traffic directly to Zeek and nothing else. Nothing on this segment can talk back.

Step 1: Open the Virtual Network Editor

In VMware Workstation, go to Edit → Virtual Network Editor. Click Change Settings if prompted for administrator access.

Step 2: Add a New Network

Click Add Network. Choose an unused VMnet slot. For this guide we use VMnet6, but any unused slot works. Click OK.

Step 3: Configure It as a Silent Monitoring Lane

Select your new VMnet from the list and apply the following settings:

  • Type: Host-Only
  • Subnet: any private range (for example, 192.168.50.0 / 255.255.255.0)
  • Uncheck “Connect a host virtual adapter to this network”
  • Uncheck “Use local DHCP service to distribute IP addresses”

Click Apply. This network is now completely silent. It carries no traffic of its own and will only ever carry mirrored packets pushed to it by pfSense.

Phase 2: Add and Configure a Mirror Interface on pfSense

pfSense needs a new NIC attached to the silent monitoring network. Once added, you assign it as a dedicated interface inside pfSense, then configure a bridge that copies all traffic from your monitored segments out through that port. This is the software equivalent of plugging a network tap into a switch’s SPAN port.

Step 4: Add the NIC to the pfSense VM

  1. Shut down the pfSense VM cleanly.
  2. Open VM Settings and click Add → Network Adapter.
  3. Set the connection type to Custom and select your monitoring VMnet (VMnet6).
  4. Click Finish, then boot pfSense.

Step 5: Assign the Interface in the pfSense GUI

  1. Log into the pfSense web GUI.
  2. Go to Interfaces → Assignments.
  3. At the bottom of the page, find the Available Network Ports dropdown and select the newly added NIC.
  4. Click Add. pfSense will assign it a name such as OPT1, OPT2, OPT3, or OPT4 depending on how many interfaces you already have.

Step 6: Configure the Interface as MONITOR_SPAN

Click on the newly assigned interface to open its settings page and configure the following:

  • Enable Interface: Checked
  • Description: MONITOR_SPAN
  • IPv4 Configuration Type: None
  • IPv6 Configuration Type: None

Click Save, then Apply Changes. The interface is now defined but carries no IP addressing of any kind, which is exactly what we want for a silent monitoring port.

Step 7: Navigate to the Bridges Menu

In the pfSense web GUI, go to Interfaces → Assignments. Look at the sub-tabs row at the top of the page and click Bridges.

Step 8: Create the Network Tap (Bridge with SPAN)

This is the core of the whole setup. A pfSense bridge with a Span Port configured will copy every packet passing between the bridged member interfaces and push a duplicate out through the designated monitor port. Nothing on the network is aware this is happening.

  1. Click the green + Add button to create a new bridge.
  2. Under Member Interfaces, select the interfaces whose traffic you want to monitor. Hold Ctrl to select multiple interfaces at once (for example, your LAN segment and any additional VLANs).
  3. Click Display Advanced (or Show Advanced Options).
  4. Find the Span Port dropdown and select MONITOR_SPAN.
  5. In the Description field, enter: Zeek_Network_Tap
  6. Click Save.

From this point on, pfSense will duplicate every packet passing between your selected interfaces and push a copy out through MONITOR_SPAN, which connects directly to your silent VMnet6, and from there to the Zeek sensor NIC.

Phase 3: Set Up the Zeek Ubuntu Server

The Zeek machine needs two NICs. The first handles management: SSH access, package updates, and log forwarding to your SIEM. The second is the passive capture interface, connected to the silent monitoring network. The capture NIC will have no IP address and will never transmit any traffic.

Step 9: Install Ubuntu Server and Add Both NICs

During the Ubuntu Server installation, it is easiest to have only the management NIC attached so the installer picks it up cleanly and assigns a network path. Once the OS is fully installed, shut the VM down and add the second NIC for passive capture:

  1. Open VM Settings for your Zeek VM.
  2. Click Add → Network Adapter.
  3. Set it to Custom and choose your monitoring VMnet (VMnet6).
  4. Click Finish and boot the VM.

Your final NIC layout should be:

  • Network Adapter 1 connected to your management network, which will get a static IP (for example, 192.168.5.70)
  • Network Adapter 2 connected to VMnet6 (the monitoring segment), with no IP address and promiscuous mode enabled

Step 10: Configure Netplan for Both Interfaces

Open the Netplan configuration file:

sudo nano /etc/netplan/00-installer-config.yaml

Delete all existing content and replace it with the following. Adjust the interface names, IP address, and gateway to match your environment:

network:
  version: 2
  ethernets:

    ens33:
      addresses:
        - 192.168.5.70/24
      routes:
        - to: default
          via: 192.168.5.1
      nameservers:
        addresses:
          - 8.8.8.8
          - 8.8.4.4

    ens37:
      dhcp4: false
      dhcp6: false
      accept-ra: false

Important: YAML is extremely strict about indentation. Never use the Tab key anywhere in this file. Use the spacebar only, and keep the structure exactly as shown above. A single misaligned space will cause Netplan to fail silently or throw a parse error at apply time.

Save the file with Ctrl + O then Enter, and exit with Ctrl + X. Apply the changes:

sudo netplan apply

Step 11: Verify the Capture Interface Has No IP

ip a show ens37

The output should show the interface state as UP, but there should be no inet line, meaning no IPv4 address is assigned. If you see an IP address here, go back and check your Netplan config. The capture interface must remain address-free for this setup to work correctly.

Note on interface names: Interface names vary depending on your platform and NIC order. Run ip link show to list your actual interface names and substitute them throughout this guide wherever you see ens33 or ens37.

Note on cloning: If you later clone this VM to deploy additional Zeek sensors across different segments, interface names may shift. After every clone, run ip link to check the actual names and update only the interface name keys in the Netplan file if needed. This is a ten-second check that saves a lot of debugging time.

Phase 4: Install Zeek

Step 12: Verify the Repository Exists for Your Ubuntu Version

The Zeek project publishes repositories for specific Ubuntu releases. Before installing, confirm your version is supported:

curl -s https://download.opensuse.org/repositories/security:/zeek/ | grep xUbuntu

You should see your Ubuntu version in the output (for example, xUbuntu_26.04). If your version is not listed, check the Zeek download page for the closest available release and substitute the version string in the commands that follow.

Step 13: Install Dependencies

sudo apt update
sudo apt install curl gnupg apt-transport-https software-properties-common libpcap-dev tcpdump -y

Step 14: Import the Zeek GPG Key

Replace xUbuntu_26.04 with your Ubuntu version if it differs:

curl -fsSL https://download.opensuse.org/repositories/security:zeek/xUbuntu_26.04/Release.key \
  | gpg --dearmor \
  | sudo tee /etc/apt/trusted.gpg.d/security_zeek.gpg > /dev/null

Step 15: Add the Zeek Repository

echo "deb [signed-by=/usr/share/keyrings/zeek.gpg] http://download.opensuse.org/repositories/security:/zeek/xUbuntu_26.04/ /" \
  | sudo tee /etc/apt/sources.list.d/zeek.list

Step 16: Install Zeek

sudo apt update
sudo apt install zeek -y

Step 17: Add Zeek to Your PATH

echo 'export PATH=$PATH:/opt/zeek/bin' >> ~/.bashrc
source ~/.bashrc

Verify the installation completed correctly:

zeek --version

Phase 5: Configure Zeek

Step 18: Define Your Internal Networks

Zeek needs to know which subnets are considered internal so it can correctly classify traffic direction: internal to internal, external to internal, internal to external, and so on. This classification feeds directly into Zeek’s detection scripts and the output quality of logs like notice.log and conn.log. Open the networks config file:

sudo nano /opt/zeek/etc/networks.cfg

Delete the existing content and add your protected subnets, one per line, with a descriptive label:

192.168.5.0/24   MGMT_Network
192.168.10.0/24  USERS_Network
192.168.20.0/24  SERVERS_Network

Add whichever subnets you want Zeek to treat as internal. Any subnet not listed here will be treated as an external (untrusted) source. This is actually useful for lab environments: if you have a dedicated attack machine on a separate subnet and you leave that subnet out of this file, attacks from it will appear in Zeek’s logs as external-to-internal, which is more realistic for SOC analysis and threat hunting practice.

Step 19: Set the Capture Interface

Open the Zeek node configuration file:

sudo nano /opt/zeek/etc/node.cfg

Find the [zeek] section and update the interface line to point to your capture NIC, the one with no IP address:

[zeek]
type=standalone
host=localhost
interface=ens37

Important: Make sure you enter the capture NIC name here (the interface with no IP, connected to VMnet6), not the management NIC. Using the management NIC would only capture management traffic and miss all the mirrored data coming through the SPAN port.

Phase 6: Enable Promiscuous Mode on the Capture Interface

By default, a network card only passes packets addressed to its own MAC address up to the operating system. Since the Zeek capture NIC has no IP and receives traffic destined for many different machines via the SPAN mirror, it must be put into promiscuous mode. In promiscuous mode, the NIC accepts and passes every packet it physically receives to the OS, regardless of the destination MAC address. Without this, Zeek would drop most of the mirrored traffic before it even reaches the capture engine.

Step 20: Enable Promiscuous Mode Immediately

sudo ip link set ens37 promisc on

Verify it is active:

ip link show ens37

Look for the word PROMISC in the interface flags line of the output, for example: <BROADCAST,MULTICAST,PROMISC,UP,LOWER_UP>. If you see it, promiscuous mode is enabled and the interface is ready to receive all mirrored traffic.

Step 21: Make Promiscuous Mode Permanent

The command above does not survive a reboot. Create a systemd service to re-enable promiscuous mode automatically every time the server starts:

sudo nano /etc/systemd/system/zeek-promisc.service

Paste the following into the file:

[Unit]
Description=Enable Promiscuous Mode for Zeek
After=network.target

[Service]
Type=oneshot
ExecStart=/usr/sbin/ip link set ens37 promisc on
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target

Save and exit, then enable and start the service:

sudo systemctl daemon-reload
sudo systemctl enable zeek-promisc.service
sudo systemctl start zeek-promisc.service

Confirm it is running:

sudo systemctl status zeek-promisc.service

Expected output:

● zeek-promisc.service - Enable Promiscuous Mode for Zeek
   Loaded: loaded (/etc/systemd/system/zeek-promisc.service)
   Active: active (exited)

active (exited) is the correct and expected status for a oneshot service. It ran once, succeeded, and will run again automatically on every subsequent boot. There is nothing wrong with this output.

Phase 7: Validate, Deploy, and Verify Zeek

Step 22: Validate the Configuration

Before deploying, run zeekctl’s built-in config checker. This catches errors in scripts, interface settings, or configuration files before they cause a failed start:

sudo /opt/zeek/bin/zeekctl check

Expected output:

zeek scripts OK

If you see errors, review your node.cfg (is the interface name correct and does it match what ip link shows?) and networks.cfg (is the CIDR notation valid with no trailing spaces?).

Step 23: Deploy Zeek

sudo /opt/zeek/bin/zeekctl deploy

Step 24: Check Running Status

sudo /opt/zeek/bin/zeekctl status

Expected output:

Name         Type       Host       Status
zeek         standalone localhost  running

Step 25: Confirm Logs Are Being Written

sudo ls -lah /opt/zeek/logs/current/

You should see log files being actively created. Here is what each one contains and why it matters:

  • conn.log — Every network connection, including source IP, destination IP, port, protocol, duration, and bytes transferred. This is the most important log for baseline analysis and anomaly detection.
  • dns.log — All DNS queries and responses, including the resolved IP addresses. DNS logs are invaluable for detecting beaconing, data exfiltration over DNS, and command-and-control communication.
  • http.log — HTTP requests showing method, host, URI, user-agent string, and response code. These expose plaintext web traffic, suspicious URIs, and unusual user-agent strings used by malware.
  • ssl.log — TLS and SSL handshakes, including certificate subject, issuer, version, and JA3 fingerprint hashes. JA3 hashes can match known malicious TLS client fingerprints even when the payload is encrypted.
  • weird.log — Malformed or unusual traffic that does not match expected protocol behaviour. Entries here often indicate scanning tools, protocol violations, or evasion attempts.
  • notice.log — Policy notices generated by Zeek’s built-in detection scripts, such as port scans, address scans, or known bad signatures.

Step 26: Run Diagnostics if Zeek Is Not Running

If the status shows crashed or stopped, run:

sudo /opt/zeek/bin/zeekctl diag

This prints detailed error output and is the first place to look when Zeek fails to start. The most common causes are an incorrect interface name in node.cfg, the capture interface not being in promiscuous mode, or a YAML syntax error in a custom script.

Testing the Full Pipeline

The best way to confirm everything is working end to end is to generate some traffic on a monitored network and watch Zeek log it in real time. Open one terminal and start watching the connection log:

sudo tail -f /opt/zeek/logs/current/conn.log

From any machine on your monitored network, generate some traffic:

ping 192.168.1.100
curl http://192.168.1.100
nmap 192.168.1.100

Entries should appear in the first terminal immediately as traffic is generated, showing source IP, destination IP, protocol, and connection metadata. If you see entries populating in real time, your full pipeline is confirmed and working. The full chain looks like this:

  • Traffic flows normally through pfSense between your monitored interfaces.
  • The pfSense bridge duplicates every packet and sends a copy out through MONITOR_SPAN.
  • MONITOR_SPAN pushes the copy into the silent monitoring VMnet (VMnet6).
  • Zeek’s capture NIC (no IP, promiscuous mode) receives every duplicated packet.
  • Zeek writes the session data to conn.log, dns.log, http.log, ssl.log, and the other log files.

Useful Commands for Day-to-Day Use

# Watch live connections
sudo tail -f /opt/zeek/logs/current/conn.log

# Watch live DNS queries
sudo tail -f /opt/zeek/logs/current/dns.log

# Restart Zeek after config changes
sudo /opt/zeek/bin/zeekctl deploy

# Stop Zeek
sudo /opt/zeek/bin/zeekctl stop

# Start Zeek without redeploying scripts
sudo /opt/zeek/bin/zeekctl start

SOC Perspective: How Analysts Use This Setup

Getting Zeek running is one thing. Understanding how to operationalise it from a SOC analyst’s perspective is another. This section covers what the setup looks like in practice when someone is actively using it for monitoring, threat hunting, or incident investigation.

Zeek Logs as Your Primary Data Source

In a real SOC environment, analysts rarely stare directly at Zeek log files from the command line. Instead, Zeek logs are forwarded to a SIEM or log aggregation platform. In a home lab or small enterprise setup, common choices are Wazuh, Elastic Stack (ELK), or Splunk. Zeek’s tab-separated log format is natively supported by all three, and the structured field names (like id.orig_h for source IP and id.resp_p for destination port) make queries straightforward to write.

Once ingested into a SIEM, Zeek data becomes the backbone of a wide range of detection use cases. You can build correlation rules that fire when a single source IP makes connections to an unusually high number of distinct destination ports in a short window, which is a classic port scan signature visible in conn.log. You can track DNS queries for newly registered domains, which appear in dns.log and are a common indicator of C2 infrastructure. You can hunt for JA3 hashes in ssl.log that match known malicious TLS client fingerprints without ever needing to decrypt the traffic.

Lateral Movement Detection

One of the most practical uses of this monitoring setup in a lab environment is lateral movement detection. Because the pfSense bridge mirrors traffic between all your monitored segments, Zeek sees every east-west connection: a machine on your USERS subnet talking to your SERVERS subnet, an attacker on your ATTACK subnet pivoting to a compromised USERS machine, or an internal host suddenly initiating connections to unusual ports or protocols it has never used before.

Lateral movement often looks quiet at the network level. An attacker using legitimate protocols like SMB, WMI, or RDP generates exactly the kind of connections that blend in with normal traffic at first glance. Zeek’s conn.log captures every one of these connections with full metadata. Over time, analysts learn the baseline of what normal east-west traffic looks like in their environment, and any deviation becomes immediately visible.

Command and Control (C2) Identification

Command-and-control traffic is where Zeek’s dns.log and ssl.log become particularly valuable. Modern malware often uses DNS-based C2, where beaconing traffic is encoded in DNS query strings and responses. Zeek logs every single DNS transaction, including the full query name and response records, which makes it straightforward to identify hosts generating high volumes of DNS queries to unusual domains, or queries with random-looking subdomains characteristic of domain generation algorithms (DGAs).

For encrypted C2 traffic over TLS, Zeek’s ssl.log captures JA3 and JA3S fingerprints. JA3 is a hash of specific TLS handshake parameters from the client side. Because malware tends to use particular TLS libraries with fixed parameters, the resulting JA3 hash is often consistent across infections and can be matched against known bad fingerprint databases without needing to decrypt anything. This is one of the more powerful detection techniques available to analysts working with network data, and it is available out of the box with a standard Zeek installation.

The Value of the Invisible Sensor

From a pure security architecture standpoint, the most important property of this setup is that the Zeek sensor is completely undetectable by anything on the monitored network. The capture NIC has no IP address. It transmits nothing. There is no ARP entry for it, no response to pings, no open ports. An attacker who has compromised a machine on your USERS or SERVERS subnet cannot scan for the monitoring sensor because, from a network perspective, it does not exist.

This is fundamentally different from agent-based monitoring, where a software agent running on each host is visible to an attacker who has compromised that host. With a passive SPAN-based sensor, the monitoring infrastructure is entirely out of band. Even if an attacker achieves full control of every endpoint on the network, the Zeek sensor remains intact and continues capturing their activity.

Integration with Threat Intelligence

Zeek’s logging output pairs naturally with threat intelligence feeds. Because every external IP address that your monitored hosts communicate with appears in conn.log and dns.log with full session metadata, you can run automated lookups against threat intelligence sources to flag connections to known bad infrastructure. This can be done at the SIEM level via lookup tables or enrichment rules, or directly within Zeek using the Intel framework, which allows you to feed in indicators of compromise (IOCs) like IP addresses, domain names, URLs, and file hashes, and have Zeek generate notice.log entries whenever a match is observed in live traffic.

What This Setup Does Not Replace

It is worth being clear about what passive network monitoring does not give you. Zeek sees network metadata, not host-level events. It will not tell you which process on a compromised machine initiated a connection, which user account was used for authentication, or what commands were run in a shell session. For that level of visibility, you need endpoint detection and response (EDR) tooling or a host-based agent like Wazuh’s endpoint agent running on each monitored machine.

A mature SOC uses both. The network layer (Zeek, network flow data) gives you breadth: visibility across every host and every connection without requiring any software to be installed or maintained on endpoints. The host layer (EDR, SIEM agents) gives you depth: process-level, user-level, and file-level visibility on the machines you care most about. Used together, they close most of the gaps that either approach leaves on its own.

The setup described in this guide is the network layer. It is a solid foundation to build on, and for many lab environments and small teams, it provides more visibility than most organisations actually have in production.

Final Architecture Overview

Here is what the completed setup looks like from end to end:

  • pfSense sits at the edge, handling all routing and firewall rules as usual. Nothing about its behaviour toward normal traffic changes.
  • A bridge inside pfSense spans your monitored interfaces (USERS, SERVERS, or whichever segments you chose) with the MONITOR_SPAN interface as the span port.
  • MONITOR_SPAN connects via VMnet6 to the capture NIC on the Zeek server. This NIC has no IP address and operates in promiscuous mode.
  • The Zeek management NIC connects to your management network with a static IP, allowing SSH access and log forwarding.
  • Zeek runs continuously in standalone mode, capturing all mirrored traffic through the capture NIC and writing structured logs to /opt/zeek/logs/current/.

The monitoring infrastructure is entirely invisible to the monitored network. Traffic flows normally. Hosts behave normally. Attackers behave normally. And Zeek records all of it.