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:
- Protocol-aware analysis: Zeek understands over 40 application protocols including HTTP, HTTPS, DNS, SSL/TLS, SSH, FTP, SMTP, and SMB. It can extract metadata from these protocols even when the payload is encrypted.
- Comprehensive logging: Zeek generates structured logs in TSV or JSON format covering connections, DNS, HTTP, SSL, files, weird anomalies, and more. These logs become the foundation for threat hunting and SIEM correlation.
- File extraction: Zeek can automatically extract files transferred over network protocols, enabling malware analysis and data loss prevention.
- Extensibility: The Zeek scripting language lets you write custom detectors for organization-specific threats without modifying the core engine.
- Integration ecosystem: Zeek integrates with tools like ELK Stack, Splunk, Suricata, and threat intelligence platforms through its flexible log output and Broker framework.
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:
- Use PF_RING or AF_PACKET: For multi-gigabit networks, use kernel bypass technologies like PF_RING to distribute packet capture across multiple CPU cores efficiently.
- Right-size your cluster: Allocate one worker process per CPU core on capture interfaces. Monitor CPU usage and add workers or hosts as needed.
- Separate logger and manager nodes: On busy networks, dedicate separate hosts for logging and management to prevent I/O bottlenecks from affecting packet processing.
- Optimize script performance: Avoid expensive operations in hot paths. Use tables with expiration instead of unbounded growth. Profile scripts with
zeek -r capture.pcap --profile. - Tune packet filtering: Use BPF filters to exclude traffic you don't need to analyze, such as backup traffic or internal monitoring traffic.
# 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:
- Use JSON format for SIEM integration: JSON logs are easier to parse and ingest into platforms like Splunk, ELK, or commercial SIEMs.
- Configure log rotation: Set appropriate rotation intervals based on log volume. Hourly rotation is common for high-traffic environments.
- Compress old logs: Use a post-processor script to compress rotated logs with gzip to save disk space.
- Forward logs to centralized storage: Don't rely on local disk for long-term retention. Forward logs to a SIEM or data lake.
- Monitor log generation health: Set up alerts for when log generation stops or drops significantly, which may indicate Zeek problems.
# 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:
- Run as non-root: Configure Zeek to run as a dedicated unprivileged user. Use capabilities (CAP_NET_RAW) or setuid on the capture binary rather than running everything as root.
- Restrict network access: Limit which hosts can connect to Zeek's Broker ports and management interfaces. Use firewalls to protect the cluster communication.
- Encrypt cluster communication: Use TLS for Broker communication between cluster nodes, especially if they span network segments.
- Secure log storage: Encrypt log volumes at rest. Implement access controls on log directories. Logs contain sensitive data about your network.
- Validate script inputs: When loading external scripts or intelligence feeds, validate the sources and review the content for potential issues.
# 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.