← Back to DevBytes

Zeek: Setup, Configuration, and Best Practices

Introduction to Zeek

Zeek (formerly known as Bro) is a powerful open-source network security monitoring (NSM) framework that provides deep inspection of network traffic. Unlike traditional intrusion detection systems (IDS) that rely primarily on signature matching, Zeek takes a semantic approach — it analyzes network behavior and produces rich, structured logs that give security teams unprecedented visibility into what's actually happening on their networks.

Originally developed at Lawrence Berkeley National Laboratory in the 1990s by Vern Paxson, Zeek has evolved into one of the most widely deployed network monitoring platforms in the world. It's used by major universities, government agencies, financial institutions, and enterprises to detect threats, investigate incidents, and maintain comprehensive audit trails of network activity.

What Makes Zeek Different

Zeek sits at the intersection of several security disciplines. It functions as an IDS, but it's also a network traffic analysis (NTA) platform, a forensics tool, and a programmable network monitoring framework. The key differentiator is Zeek's scripting language, which allows analysts to write custom logic to detect specific behaviors, extract artifacts, and trigger alerts based on complex conditions.

When Zeek processes network traffic, it doesn't just look for known bad patterns. Instead, it decodes protocols, reconstructs sessions, tracks application-layer semantics, and generates dozens of log types that capture everything from DNS queries to SSL certificate details to file transfers. This depth of analysis makes Zeek invaluable for both real-time detection and post-incident investigation.

Why Zeek Matters

Modern network threats are increasingly sophisticated. Attackers use encrypted channels, living-off-the-land techniques, and slow-moving exfiltration methods that evade traditional perimeter defenses. Zeek addresses these challenges by providing:

Installing Zeek

Zeek can be installed on Linux systems through package repositories, Docker containers, or by building from source. The most common approach for production deployments is using the official package repositories.

Installing on Ubuntu/Debian

# Add the Zeek repository
sudo apt-get update
sudo apt-get install -y curl gnupg lsb-release

# Add the OpenSUSE Zeek repository
echo 'deb http://download.opensuse.org/repositories/security:/zeek/xUbuntu_22.04/ /' \
  | sudo tee /etc/apt/sources.list.d/security:zeek.list

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

# Install Zeek
sudo apt-get update
sudo apt-get install -y zeek

# Add Zeek to your PATH
export PATH=$PATH:/opt/zeek/bin
echo 'export PATH=$PATH:/opt/zeek/bin' >> ~/.bashrc

Installing on CentOS/RHEL

# Install the Zeek repository
sudo dnf install -y epel-release
sudo dnf config-manager --add-repo \
  https://download.opensuse.org/repositories/security:zeek/CentOS_8/security:zeek.repo

# Install Zeek
sudo dnf install -y zeek

# Add to PATH
export PATH=$PATH:/opt/zeek/bin
echo 'export PATH=$PATH:/opt/zeek/bin' >> ~/.bashrc

Running Zeek via Docker

For quick testing or containerized deployments, the official Zeek Docker image is convenient:

# Pull the official Zeek image
docker pull zeek/zeek:latest

# Run Zeek on a PCAP file
docker run --rm -v $(pwd)/pcaps:/pcaps zeek/zeek:latest \
  zeek -r /pcaps/sample.pcap

# The logs will be generated in the container's working directory
# To capture them, mount an output directory
docker run --rm -v $(pwd)/pcaps:/pcaps -v $(pwd)/logs:/logs \
  -w /logs zeek/zeek:latest zeek -r /pcaps/sample.pcap

Verifying the Installation

# Check Zeek version
zeek --version

# Verify key binaries are available
which zeek
which zeekctl

# Test on a sample PCAP (download one if needed)
zeek -r sample.pcap

# List generated logs
ls -la *.log

After running Zeek against a PCAP file, you should see multiple log files in TSV format, including conn.log, dns.log, http.log, and others depending on the traffic in the capture.

Configuring Zeek

Zeek's configuration lives in the /opt/zeek/etc/ directory. The main configuration files you'll work with are node.cfg (for cluster topology), networks.cfg (for defining local networks), and zeekctl.cfg (for Zeek control settings). Additionally, you'll customize behavior through Zeek scripts in the site/ directory.

Configuring the Cluster Topology

For production deployments, Zeek runs as a cluster with multiple processes handling different roles. The node.cfg file defines this topology:

# /opt/zeek/etc/node.cfg

# Manager node - handles log management and coordination
[manager]
type=manager
host=10.0.0.10

# Logger node - receives and writes all logs
[logger]
type=logger
host=10.0.0.11

# Proxy node - distributes work and maintains shared state
[proxy]
type=proxy
host=10.0.0.12

# Worker nodes - perform the actual traffic analysis
# Each worker typically binds to one CPU core
[worker-1]
type=worker
host=10.0.0.20
interface=eth1
lb_method=pf_ring
lb_procs=4

[worker-2]
type=worker
host=10.0.0.21
interface=eth1
lb_method=pf_ring
lb_procs=4

For a single-host standalone deployment (common for testing or small networks), the configuration is simpler:

# /opt/zeek/etc/node.cfg - Standalone mode

[zeek-standalone]
type=standalone
host=localhost
interface=eth1

Defining Local Networks

The networks.cfg file tells Zeek which networks are "local" to your organization. This distinction is important because Zeek applies different analysis logic to local versus remote traffic:

# /opt/zeek/etc/networks.cfg

# Corporate network
10.0.0.0/8        Corporate LAN

# DMZ network
192.168.100.0/24  DMZ

# Server network
172.16.0.0/16     Server Farm

# VPN client pool
10.99.0.0/16      VPN Clients

Enabling and Disabling Script Packages

Zeek ships with a large collection of script packages that can be enabled or disabled. The local.zeek file is where you customize which scripts load and add your own logic:

# /opt/zeek/share/zeek/site/local.zeek

# Load the standard scripts
@load base/protocols/conn
@load base/protocols/dns
@load base/protocols/http
@load base/protocols/ssl
@load base/protocols/files

# Load the framework scripts
@load base/frameworks/intel
@load base/frameworks/notice
@load base/frameworks/signatures

# Load additional analysis packages
@load policy/protocols/ssh/detect-bruteforcing
@load policy/protocols/ssl/validate-certs
@load policy/protocols/http/detect-sqli
@load policy/frameworks/intel/do_notice

# Load file analysis
@load policy/frameworks/files/hash-all-files
@load policy/protocols/http/file-extract

# Load your custom scripts
@load ./custom-detections.zeek

Configuring Log Output

By default, Zeek writes logs in TSV format to /opt/zeek/logs/. For integration with modern SIEM platforms, JSON format is often preferred:

# /opt/zeek/share/zeek/site/local.zeek

# Output all logs in JSON format
@load policy/tuning/json-logs.zeek

# Alternatively, configure specific log streams
redef LogAscii::json_timestamps = JSON::TS_ISO8601;
redef LogAscii::use_json = T;

You can also configure logs to be sent to external systems:

# Send logs to a remote syslog server
redef Log::default_rotation_postprocessor_cmd = 
  "/opt/zeek/bin/send-logs-to-siem.sh";

# Configure log rotation interval
redef Log::default_rotation_interval = 3600;  # 1 hour in seconds

Running Zeek

Processing PCAP Files

The simplest way to use Zeek is to analyze saved packet captures. This is useful for testing, training, and forensic analysis:

# Basic PCAP analysis
zeek -r capture.pcap

# Analyze with custom scripts
zeek -r capture.pcap my-script.zeek

# Analyze with specific scripts loaded
zeek -r capture.pcap policy/protocols/http/detect-sqli

# Output logs to a specific directory
zeek -r capture.pcap -w /path/to/output/

# Process multiple PCAP files
zeek -r capture1.pcap -r capture2.pcap

Live Traffic Monitoring

For production use, Zeek monitors live network traffic using zeekctl, the Zeek control utility:

# Deploy the configuration
zeekctl deploy

# Start Zeek monitoring
zeekctl start

# Check status of all nodes
zeekctl status

# Stop Zeek
zeekctl stop

# Restart Zeek (applies configuration changes)
zeekctl restart

# View logs in real-time
zeekctl top

Understanding Zeek Logs

Zeek generates over 50 log types. Here are the most important ones you'll encounter:

# conn.log - Records every network connection
# Fields include: ts, uid, id.orig_h, id.orig_p, id.resp_h, id.resp_p,
#   proto, service, duration, orig_bytes, resp_bytes, conn_state

# dns.log - DNS queries and responses
# Fields include: ts, uid, id.orig_h, id.resp_h, query, qclass, 
#   qtype, rcode, rdata, TTLs

# http.log - HTTP request/response details
# Fields include: ts, uid, id.orig_h, id.resp_h, method, host, uri,
#   referrer, version, user_agent, status_code, status_msg

# ssl.log - SSL/TLS handshake information
# Fields include: ts, uid, id.orig_h, id.resp_h, version, cipher,
#   curve, server_name, resumed, last_alert

# files.log - Files transferred over the network
# Fields include: ts, fuid, uid, source, depth, analyzers, 
#   mime_type, filename, seen.bytes, total_bytes, md5, sha1, sha256

Here's an example of examining a connection log:

# View recent connections
cat /opt/zeek/logs/current/conn.log | zeek-cut id.orig_h id.resp_h \
  service duration orig_bytes resp_bytes

# Find connections to suspicious ports
cat conn.log | zeek-cut id.orig_h id.resp_h id.resp_p service | \
  awk '$3 != 80 && $3 != 443 && $3 != 53 {print}'

# Count connections by destination IP
cat conn.log | zeek-cut id.resp_h | sort | uniq -c | sort -rn | head -20

Writing Custom Zeek Scripts

The Zeek scripting language is what sets Zeek apart from other monitoring tools. It's a domain-specific language designed for network analysis. Here's a progression of scripts from simple to advanced.

A Simple Connection Counter

# connection-counter.zeek
# Counts total connections and prints summary on shutdown

global connection_count: count = 0;

event new_connection(c: connection)
    {
    ++connection_count;
    }

event zeek_done()
    {
    print fmt("Total connections observed: %d", connection_count);
    }

Detecting DNS Tunneling

DNS tunneling is a common exfiltration technique. This script detects unusually long DNS queries that may indicate encoded data being transmitted through DNS:

# dns-tunnel-detect.zeek
# Detects potential DNS tunneling based on query length and entropy

@load base/protocols/dns/main

module DNS_TUNNEL;

export {
    redef enum Notice::Type += {
        Long_DNS_Query,
        Excessive_DNS_Queries
    };

    # Threshold for query length (in characters)
    const query_length_threshold = 50 &redef;
    
    # Threshold for queries per host per minute
    const query_rate_threshold = 100 &redef;
}

# Track DNS query counts per source host
global dns_query_counts: table[addr] of count &default=0 
    &create_expire=1min;

event DNS::log_dns(rec: DNS::Info)
    {
    # Check for unusually long queries
    if (|rec$query| > query_length_threshold)
        {
        NOTICE([$note=Long_DNS_Query,
                $msg=fmt("Long DNS query from %s: %s", 
                    rec$id.orig_h, rec$query),
                $sub=rec$query,
                $src=rec$id.orig_h]);
        }

    # Track query rate per host
    ++dns_query_counts[rec$id.orig_h];
    
    if (dns_query_counts[rec$id.orig_h] > query_rate_threshold)
        {
        NOTICE([$note=Excessive_DNS_Queries,
                $msg=fmt("Excessive DNS queries from %s: %d in 1 minute",
                    rec$id.orig_h, dns_query_counts[rec$id.orig_h]),
                $src=rec$id.orig_h]);
        }
    }

Detecting Beaconing Behavior

Beaconing — regular, periodic connections to a command-and-control server — is a hallmark of malware infections. This script detects hosts making connections at regular intervals:

# beacon-detect.zeek
# Detects potential C2 beaconing through connection timing analysis

@load base/protocols/conn

module BEACON;

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

    # Minimum number of connections to consider beaconing
    const min_connections = 10 &redef;
    
    # Maximum allowed deviation in inter-arrival time (as fraction)
    const jitter_threshold = 0.3 &redef;
}

# Track connection timestamps per source-destination pair
global connection_times: table[addr, addr] of vector of time 
    &default=vector();

event new_connection(c: connection)
    {
    local orig = c$id$orig_h;
    local resp = c$id$resp_h;
    
    connection_times[orig, resp] += c$start_time;
    
    # Only analyze after we have enough data points
    if (|connection_times[orig, resp]| >= min_connections)
        {
        local times = connection_times[orig, resp];
        local intervals: vector of double = vector();
        
        # Calculate inter-arrival times
        for (i in times)
            {
            if (i > 0)
                intervals += double_to_interval(times[i] - times[i-1]);
            }
        
        # Calculate mean and standard deviation
        local sum = 0.0;
        for (interval in intervals)
            sum += interval;
        
        local mean = sum / |intervals|;
        
        local variance = 0.0;
        for (interval in intervals)
            variance += (interval - mean) ^ 2;
        
        local stddev = sqrt(variance / |intervals|);
        
        # If jitter is low relative to mean, flag as potential beacon
        if (mean > 0 && (stddev / mean) < jitter_threshold)
            {
            NOTICE([$note=Possible_Beacon,
                    $msg=fmt("Possible beaconing from %s to %s: " +
                        "mean=%.1fs, stddev=%.1fs, count=%d",
                        orig, resp, mean, stddev, |times|),
                    $src=orig,
                    $dst=resp]);
            }
        }
    }

File Extraction Script

Zeek can extract files transferred over the network for malware analysis or DLP purposes:

# file-extract.zeek
# Extracts executable files from HTTP traffic

@load base/files/hash/main
@load base/protocols/http/main

export {
    # Directory for extracted files
    const extract_dir = "/opt/zeek/extracted" &redef;
}

event file_state_remove(f: fa_file)
    {
    if (f$source == "HTTP" && f?$info && f$info?$mime_type)
        {
        # Extract executable files
        if (f$info$mime_type in ["application/x-dosexec", 
                                  "application/x-executable",
                                  "application/x-msdownload"])
            {
            print fmt("Extracted executable: %s (SHA256: %s)",
                f$info$filename, f$info$sha256);
            }
        }
    }

Integrating Threat Intelligence

Zeek's Intelligence Framework lets you consume threat feeds and match them against observed network activity. This is one of the most powerful features for operational security teams.

Loading Threat Intelligence Data

# threat-intel.zeek
# Loads threat intelligence feeds and generates alerts on matches

@load base/frameworks/intel
@load policy/frameworks/intel/do_notice
@load policy/frameworks/intel/do_expire

module THREAT_INTEL;

export {
    # Path to threat intelligence data files
    const intel_data_dir = "/opt/zeek/share/zeek/intel" &redef;
}

event zeek_init()
    {
    # Load IP-based threat intelligence
    Intel::read_files(fmt("%s/bad_ips.intel", intel_data_dir));
    
    # Load domain-based threat intelligence
    Intel::read_files(fmt("%s/bad_domains.intel", intel_data_dir));
    
    # Load URL-based threat intelligence
    Intel::read_files(fmt("%s/bad_urls.intel", intel_data_dir));
    
    # Load file hash-based threat intelligence
    Intel::read_files(fmt("%s/bad_hashes.intel", intel_data_dir));
    }

Threat Intelligence File Format

Zeek intelligence files use a simple TSV format:

# /opt/zeek/share/zeek/intel/bad_ips.intel
# Fields: indicator indicator_type meta.source meta.desc meta.url

192.168.1.100    Intel::ADDR    internal-feed    Known C2 server    https://threat-feed.example.com/ip1
10.10.10.50      Intel::ADDR    internal-feed    Malicious host     https://threat-feed.example.com/ip2

# /opt/zeek/share/zeek/intel/bad_domains.intel
evil-domain.com    Intel::DOMAIN    external-feed    Phishing domain    https://threat-feed.example.com/dom1
c2-server.net      Intel::DOMAIN    external-feed    C2 domain          https://threat-feed.example.com/dom2

# /opt/zeek/share/zeek/intel/bad_hashes.intel
d41d8cd98f00b204e9800998ecf8427e    Intel::FILE_HASH    malware-db    Known malware MD5    https://malware-db.example.com/h1

Consuming Real-Time Threat Feeds

For dynamic threat intelligence, you can use the Broker framework to receive updates in real time:

# dynamic-intel.zeek
# Receives threat intelligence updates via Broker

@load base/frameworks/intel
@load base/frameworks/broker

event zeek_init()
    {
    # Listen for threat intel updates
    Broker::subscribe("zeek/threat-intel");
    
    # Connect to threat intel publisher
    Broker::peer("127.0.0.1", 9999/tcp);
    }

event threat_intel_update(indicator: string, indicator_type: string, 
                          source: string, description: string)
    {
    # Insert into Intel framework
    Intel::insert([$indicator=indicator,
                   $indicator_type=to_indicator_type(indicator_type),
                   $meta=[$source=source, $desc=description]]);
    
    print fmt("Added threat intel: %s (%s) - %s", 
        indicator, indicator_type, description);
    }

Best Practices

Performance Tuning

Zeek performance is critical for high-traffic environments. These practices help ensure Zeek keeps up with network demands:

# Example: BPF filter to exclude backup traffic
# In node.cfg, add to worker configuration:
[worker-1]
type=worker
host=10.0.0.20
interface=eth1
lb_method=pf_ring
lb_procs=4
env_vars=ZEEK_CAPTURE_FILTER=not port 873 and not host 10.0.0.50

Log Management

Zeek generates substantial log volume. Proper management is essential:

# Log rotation and compression configuration
# In local.zeek:

redef Log::default_rotation_interval = 3600;  # 1 hour
redef Log::default_rotation_postprocessor_cmd = 
    "gzip -9 $1 && mv $1.gz /opt/zeek/logs/archive/";

# Alternative: forward to syslog
@load policy/tuning/logs-to-syslog
redef Syslog::send_json = T;
redef Syslog::dest_address = "10.0.0.100";  # SIEM IP
redef Syslog::dest_port = 514;

Security Hardening

Zeek itself must be secured, as it processes potentially malicious traffic and holds sensitive network data:

# Running Zeek as non-root user
# Create dedicated user
sudo useradd -r -s /sbin/nologin zeek

# Set ownership on Zeek directories
sudo chown -R zeek:zeek /opt/zeek/logs
sudo chown -R zeek:zeek /opt/zeek/spool

# Grant capture capability
sudo setcap cap_net_raw,cap_net_admin=eip /opt/zeek/bin/zeek

# Configure in node.cfg
[worker-1]
type=worker
host=10.0.0.20
interface=eth1
user=zeek

Monitoring and Alerting

Zeek's Notice framework is the primary mechanism for generating alerts. Configure it properly to ensure actionable alerts reach the right people:

# notice-config.zeek
# Configures alert routing and escalation

@load base/frameworks/notice
@load policy/frameworks/notice/actions/email

# Email configuration for critical alerts
redef Notice::email_dest = "soc@company.com";
redef Notice::email_from = "zeek@company.com";
redef Notice::email_subject_prefix = "Zeek Alert";

# Route specific notice types to email
hook Notice::policy(n: Notice::Info)
    {
    # Send email for critical notices
    if (n$note in [Intel::Notice::Intel_Hit, 
                   SSH::Password_Guessing,
                   SSL::Invalid_Server_Cert])
        add n$actions[Notice::ACTION_EMAIL];
    
    # Log all notices
    add n$actions[Notice::ACTION_LOG];
    }

Testing and Validation

Always test configuration changes before deploying to production:

# Test configuration syntax
zeekctl check

# Test scripts against sample PCAP
zeek -r test.pcap /opt/zeek/share/zeek/site/local.zeek

# Validate cluster configuration
zeekctl config

# Dry-run deployment (shows what would change)
zeekctl deploy --dry-run

# Performance testing with tcpreplay
sudo tcpreplay --intf1=eth1 --mbps=1000 capture.pcap

Keeping Zeek Updated

Regular updates are important for security and functionality:

# Check current version
zeek --version

# Update Zeek (Ubuntu/Debian)
sudo apt-get update
sudo apt-get upgrade zeek

# After updating, redeploy
zeekctl deploy

# Review changelog for breaking changes
# https://github.com/zeek/zeek/releases

# Test custom scripts against new version
zeek -r test.pcap custom-scripts.zeek

Conclusion

Zeek is a remarkably powerful network monitoring platform that provides depth of analysis unmatched by traditional IDS tools. By understanding its architecture, mastering its configuration, and learning its scripting language, security teams can build a network monitoring capability that detects both known threats and novel attack behaviors. The key to success with Zeek is starting simple — deploy it, understand the logs it produces, and gradually add custom detections as you learn the scripting language. Invest time in proper cluster sizing, log management, and integration with your existing security stack, and Zeek will become an indispensable component of your security operations. As network threats continue to evolve, the visibility and analytical power that Zeek provides will only become more critical to maintaining a strong security posture.

— Ad —

Google AdSense will appear here after approval

← Back to all articles