Zeek (formerly Bro) is a passive network security monitor. Instead of matching signatures like an IDS, it parses traffic into structured logs of connections, DNS queries, HTTP requests, TLS handshakes and more, and runs scripts over that data to raise alerts. In this tutorial you will install Zeek on Ubuntu 24.04 from the official package repository, monitor a network interface in standalone mode, read the logs it produces, and add a custom detection script and a threat intelligence feed.

Prerequisites

To follow this tutorial you need:

  • A server running Ubuntu 24.04 LTS with at least 2 CPU cores, 4 GB of RAM and 20 GB of free disk space for logs, for example a CubePath VPS.
  • A non-root user with sudo privileges.
  • The name of the interface to monitor. On a single server this is its own network interface. To monitor a whole network, the interface must receive a copy of the traffic from a switch mirror (SPAN) port or a network tap.

Find the interface name with:

ip -br link
lo               UNKNOWN        00:00:00:00:00:00 <LOOPBACK,UP,LOWER_UP>
eth0             UP             52:54:00:3a:1c:7e <BROADCAST,MULTICAST,UP,LOWER_UP>

This guide uses eth0. Replace it with your interface name.

Step 1 - Adding the Zeek repository

Zeek is not in the Ubuntu archive in a current version, so the Zeek project publishes packages on the openSUSE Build Service. Install the tools needed to fetch the signing key:

sudo apt update
sudo apt install curl gpg

Download the repository key and store it as a dedicated keyring:

curl -fsSL https://download.opensuse.org/repositories/security:zeek/xUbuntu_24.04/Release.key | sudo gpg --dearmor -o /etc/apt/keyrings/zeek.gpg

Add the repository, restricted to that key:

echo 'deb [signed-by=/etc/apt/keyrings/zeek.gpg] https://download.opensuse.org/repositories/security:/zeek/xUbuntu_24.04/ /' | sudo tee /etc/apt/sources.list.d/zeek.list

Refresh the package index and confirm the package is available:

sudo apt update
apt policy zeek

The output lists a candidate version from download.opensuse.org.

Step 2 - Installing Zeek

The repository offers zeek (the current feature release) and zeek-lts (the long-term support release). For a production sensor, the LTS release changes less often:

sudo apt install zeek-lts

During installation, apt may pull in Postfix as a mail dependency and ask how to configure it. Choose Local only if you do not plan to send Zeek reports by email.

Zeek installs everything under /opt/zeek. Add its bin directory to your PATH so you can run zeek and zeekctl without full paths:

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

Verify the installation:

zeek --version
zeek version 8.0.1

Your version number may differ.

Step 3 - Configuring the node and local networks

zeekctl manages Zeek processes using three files in /opt/zeek/etc. First, tell Zeek which interface to sniff:

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

The default file already defines a standalone node. Set interface to your interface:

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

Next, declare which address ranges belong to your network. Zeek uses this list to decide what is local and what is remote, which many scripts depend on:

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

Replace the example entries with your own ranges, one per line, followed by a description. Include your server's public IP or subnet if you monitor the server itself:

10.0.0.0/8          Private IP space
172.16.0.0/12       Private IP space
192.168.0.0/16      Private IP space
203.0.113.10/32     Public IP of this server

Finally, review the general settings:

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

The defaults are sensible. The two values worth checking are the log retention and the report recipient:

LogRotationInterval = 3600
LogExpireInterval = 30day
MailTo = root@localhost

With these values, logs rotate every hour and archives older than 30 days are deleted.

Step 4 - Preparing the site policy

The file /opt/zeek/share/zeek/site/local.zeek is where you choose which scripts Zeek loads. Open it:

sudo nano /opt/zeek/share/zeek/site/local.zeek

Add these lines at the end:

# Write logs as JSON, easier to ship to a SIEM.
@load policy/tuning/json-logs.zeek

# Needed when Zeek sniffs the host's own interface: outgoing packets
# have checksums filled in by the NIC, so Zeek sees them as invalid.
redef ignore_checksums = T;

The checksum setting only matters when Zeek monitors traffic of the machine it runs on. On a dedicated sensor fed by a mirror port, you can leave it out.

The default local.zeek already loads useful detections, such as SSH brute-force detection (protocols/ssh/detect-bruteforcing) and software version tracking.

Step 5 - Deploying Zeek

zeekctl deploy checks the scripts for errors, installs the configuration and starts Zeek in one step:

sudo /opt/zeek/bin/zeekctl deploy
checking configurations ...
installing ...
removing old policies in /opt/zeek/spool/installed-scripts-do-not-touch/site ...
removing old policies in /opt/zeek/spool/installed-scripts-do-not-touch/auto ...
creating policy directories ...
installing site policies ...
generating standalone-layout.zeek ...
generating local-networks.zeek ...
generating zeekctl-config.zeek ...
generating zeekctl-config.sh ...
stopping ...
stopping zeek ...
starting ...
starting zeek ...

Check that it is running:

sudo /opt/zeek/bin/zeekctl status
Name         Type       Host          Status    Pid    Started
zeek         standalone localhost     running   4127   25 Sep 10:31:08

zeekctl does not install a systemd unit. Its cron command restarts crashed nodes, rotates logs and expires old ones, so schedule it every five minutes in root's crontab:

sudo crontab -e
*/5 * * * * /opt/zeek/bin/zeekctl cron

This also starts Zeek again after a reboot, within five minutes.

Step 6 - Reading the Zeek logs

The logs for the current hour are in /opt/zeek/logs/current, and rotated ones in dated directories under /opt/zeek/logs. Generate some traffic and list the files:

curl -s https://example.com > /dev/null
ls /opt/zeek/logs/current
conn.log  dns.log  loaded_scripts.log  packet_filter.log  ssl.log  stats.log  stderr.log  stdout.log  telemetry.log

The most important logs are:

LogContent
conn.logOne line per connection: endpoints, ports, protocol, duration, bytes
dns.logDNS queries and answers
http.logHTTP requests: method, host, URI, user agent, status
ssl.logTLS handshakes: version, server name (SNI), certificate validation
notice.logAlerts raised by scripts
weird.logProtocol anomalies

Since the logs are JSON, use jq to query them:

sudo apt install jq

Show the last five connections with source, destination, port and service:

tail -n 5 /opt/zeek/logs/current/conn.log | jq -c '{src: .["id.orig_h"], dst: .["id.resp_h"], port: .["id.resp_p"], proto, service}'
{"src":"203.0.113.10","dst":"93.184.215.14","port":443,"proto":"tcp","service":"ssl"}
{"src":"203.0.113.10","dst":"185.12.64.1","port":53,"proto":"udp","service":"dns"}

List the TLS server names that were contacted:

jq -r '.server_name // empty' /opt/zeek/logs/current/ssl.log | sort | uniq -c | sort -rn

Step 7 - Writing a custom detection script

Zeek scripts react to events generated while parsing traffic. As an example, this script raises a notice when a host on your network opens an SSH connection to a server outside it that is not on an allow list, a common sign of lateral movement or data exfiltration.

Create the script in the site directory:

sudo nano /opt/zeek/share/zeek/site/outbound-ssh.zeek
@load base/frameworks/notice

module OutboundSSH;

export {
    redef enum Notice::Type += { Unexpected_Outbound_SSH };

    # External SSH servers that local hosts may connect to.
    option allowed_servers: set[addr] = {};
}

event connection_established(c: connection)
    {
    if ( c$id$resp_p != 22/tcp )
        return;

    if ( ! Site::is_local_addr(c$id$orig_h) || Site::is_local_addr(c$id$resp_h) )
        return;

    if ( c$id$resp_h in allowed_servers )
        return;

    NOTICE([$note=Unexpected_Outbound_SSH,
            $conn=c,
            $msg=fmt("Outbound SSH from %s to %s", c$id$orig_h, c$id$resp_h),
            $identifier=cat(c$id$orig_h, c$id$resp_h)]);
    }

The $identifier field lets the notice framework suppress duplicates for the same pair of hosts (for one hour by default).

Load the script from local.zeek by adding this line at the end:

sudo nano /opt/zeek/share/zeek/site/local.zeek
@load ./outbound-ssh

Check the configuration for errors, then deploy it:

sudo /opt/zeek/bin/zeekctl check
sudo /opt/zeek/bin/zeekctl deploy

zeekctl check prints zeek scripts are ok. when the scripts parse correctly. Test the detection by opening an SSH connection to an external host, for example GitHub:

nc -zv github.com 22

After a few seconds, the notice appears:

jq -c '{note, msg}' /opt/zeek/logs/current/notice.log
{"note":"OutboundSSH::Unexpected_Outbound_SSH","msg":"Outbound SSH from 203.0.113.10 to 140.82.121.4"}

Step 8 - Adding threat intelligence

The Intel framework matches indicators (IP addresses, domains, URLs, file hashes) against everything Zeek sees and writes hits to intel.log. Indicators are read from tab-separated files.

Create an indicator file with printf, which guarantees real tab characters between columns:

printf '#fields\tindicator\tindicator_type\tmeta.source\n' | sudo tee /opt/zeek/share/zeek/site/intel.dat
printf 'example.org\tIntel::DOMAIN\tlocal-test\n' | sudo tee -a /opt/zeek/share/zeek/site/intel.dat

Tell Zeek to load the framework and read the file. Add these lines to the end of local.zeek:

sudo nano /opt/zeek/share/zeek/site/local.zeek
@load frameworks/intel/seen
redef Intel::read_files += { "/opt/zeek/share/zeek/site/intel.dat" };

Deploy again and trigger the indicator with a DNS lookup:

sudo /opt/zeek/bin/zeekctl deploy
dig +short example.org
jq -c '{seen: .["seen.indicator"], where: .["seen.where"], sources}' /opt/zeek/logs/current/intel.log
{"seen":"example.org","where":"DNS::IN_REQUEST","sources":["local-test"]}

Replace the test entry with indicators from a real feed. Zeek re-reads the file automatically when it changes.

Troubleshooting

zeekctl status shows crashed. Run sudo /opt/zeek/bin/zeekctl diag, which prints the end of stderr.log and the reason for the crash. A wrong interface name in node.cfg is the most common cause.

Almost no logs apart from conn.log. If weird.log shows many bad_TCP_checksum entries, Zeek is discarding packets with offloaded checksums. Add redef ignore_checksums = T; as shown in step 4 and deploy again.

High packet loss on a busy link. Check sudo /opt/zeek/bin/zeekctl netstats. A single standalone process handles a few hundred Mbit/s at most. For more, switch to a cluster layout in node.cfg with several workers using AF_PACKET load balancing.

The disk fills up. Lower LogExpireInterval in zeekctl.cfg, run zeekctl deploy, and make sure the zeekctl cron job is present, since it is what deletes expired logs.

Conclusion

You installed Zeek on Ubuntu 24.04 from the official repository, configured it to monitor an interface with JSON logs, analysed connections, DNS and TLS activity with jq, and extended it with a custom detection script and a threat intelligence file. As next steps, ship the JSON logs to a SIEM such as Wazuh or Elasticsearch, install community packages with zkg, or feed Zeek from a mirror port to monitor a whole network segment.