Deploying Zeek using systemd
Added in version 8.1.
Zeek includes a systemd generator
that reads the <PREFIX>/etc/zeek/zeek.conf configuration file and instantiates systemd
unit files for the manager, logger, proxy and worker processes accordingly.
The generator is installed at <PREFIX>/bin/zeek-systemd-generator.
systemd automatically runs generators during system startup, or on demand (see below).
Note
zeek.conf file’s focus is a system-level Zeek deployment tailored for
appliance or container use cases. It presents an alternative to a
ZeekControl managed deployment.
Installation
After installing Zeek on your system, link Zeek’s systemd generator into one of the standard generator directories (create it if it does not exist):
# mkdir -p /etc/systemd/system-generators/
# ln -s <PREFIX>/bin/zeek-systemd-generator /etc/systemd/system-generators/
Single Host Walkthrough
For a small Zeek deployment running two AF_PACKET worker processes on eth1, one logger, one proxy, and the manager, the following configuration is sufficient:
interface = af_packet::eth1
workers = 2
manager = 1
loggers = 1
proxies = 1
This minimal format is termed the section-less configuration style. See the Multiple Interfaces section for the more verbose INI-style configuration that is required when you want to listen on multiple interfaces.
After running systemctl daemon-reload, which executes zeek-systemd-generator,
the generated unit files are found in the /run/systemd/generator directory of
the system:
# tree /run/systemd/generator | grep zeek
├── zeek-archiver.service
├── zeek-logger@.service
├── zeek-manager.service
├── zeek-proxy@.service
├── zeek-setup.service
├── zeek.target
├── zeek.target.wants
│ ├── zeek-archiver.service -> ../zeek-archiver.service
│ ├── zeek-logger@1.service -> ../zeek-logger@.service
│ ├── zeek-manager.service -> ../zeek-manager.service
│ ├── zeek-proxy@1.service -> ../zeek-proxy@.service
│ ├── zeek-setup.service -> ../zeek-setup.service
│ ├── zeek-worker@1.service -> ../zeek-worker@.service
│ └── zeek-worker@2.service -> ../zeek-worker@.service
├── zeek-worker@1.service.d
│ └── 10-zeek-systemd-generator.conf
├── zeek-worker@2.service.d
│ └── 10-zeek-systemd-generator.conf
└── zeek-worker@.service
If you’re curious about all these different unit files and processes, take a look at Cluster Architectures and Spawning a Cluster. Zeek is a relatively complex multi-process application.
Note
Running systemctl daemon-reload does not provide immediate feedback about
failures when running generators. If you do not see service unit files created,
check for error messages using journalctl.
# journalctl -f
Jul 27 17:10:31 tinkyx1 systemd[1]: Reloading...
Jul 27 17:10:31 tinkyx1 /etc/systemd/system-generators/zeek-systemd-generator: config /opt/zeek-dev-prod/etc/zeek/zeek.conf is invalid
Jul 27 17:10:31 tinkyx1 /etc/systemd/system-generators/zeek-systemd-generator: invalid workers value: '16x'
Jul 27 17:10:31 tinkyx1 (sd-exec-[2365550]: /etc/systemd/system-generators/zeek-systemd-generator failed with exit status 1.
You can also invoke the generator, optionally passing –config to validate the
zeek.conf file beforehand. The units will be generated into directory provided
as the last argument. If --config is not provided, it defaults to <PREFIX/etc/zeek/zeek.confg.
# <PREFIX>/bin/zeek-systemd-generator --config <PREFIX>/etc/zeek/zeek.conf /tmp/out
config <PREFIX>/etc/zeek/zeek.conf is invalid
invalid workers value: '1typo'
If you have AppArmor or SELinux enabled, watch out for messages in the system
logs; there may be extra configuration required to allow running the
zeek-systemd-generator and zeek-cluster-layout-generator executables
from where you installed them.
Start Zeek using systemctl with the zeek.target unit:
# systemctl start zeek.target
Inspect the status of the individual processes with systemd-cgtop zeek.slice
or systemctl status zeek.slice:
# systemctl status zeek.slice
● zeek.slice - Slice /zeek
Loaded: loaded
Active: active since Mon 2026-04-20 17:40:28 CEST; 3 days ago
Tasks: 90
Memory: 477.8M (peak: 2.5G swap: 0B swap peak: 999.1M)
CPU: 1h 9min 36.236s
CGroup: /zeek.slice
├─zeek-archiver.slice
│ └─zeek-archiver.service
│ └─2601102 /opt/zeek/bin/zeek-archiver /opt/zeek/var/spool/zeek/log-queue /opt/zeek/var/logs/zeek
├─zeek-loggers.slice
│ └─zeek-logger@1.service
│ └─2601103 /opt/zeek/bin/zeek policy/misc/systemd-generator local frameworks/cluster/backend/zeromq
├─zeek-manager.slice
│ └─zeek-manager.service
│ └─2601104 /opt/zeek/bin/zeek policy/misc/systemd-generator local frameworks/cluster/backend/zeromq
├─zeek-proxies.slice
│ └─zeek-proxy@1.service
│ └─2601112 /opt/zeek/bin/zeek policy/misc/systemd-generator local frameworks/cluster/backend/zeromq
└─zeek-workers.slice
├─zeek-worker@1.service
│ └─2601123 /opt/zeek/bin/zeek -i af_packet::eth1 policy/misc/systemd-generator local frameworks/cluster/backend/zeromq
└─zeek-worker@2.service
└─2601107 /opt/zeek/bin/zeek -i af_packet::eth1 policy/misc/systemd-generator local frameworks/cluster/backend/zeromq
Individual Zeek processes are started as user zeek and group zeek. This is
configurable with the user and group keys in the zeek.conf file.
To inspect the logs of the individual Zeek processes, use journalctl, maybe
together with --reverse or --follow:
# journalctl --follow -u 'zeek*'
Apr 24 13:42:14 tinkyx1 systemd[1]: Starting zeek-worker@1.service - Zeek Worker 1...
Apr 24 13:42:14 tinkyx1 systemd[1]: Started zeek-worker@2.service - Zeek Worker 2.
Apr 24 13:42:14 tinkyx1 systemd[1]: Started zeek-worker@1.service - Zeek Worker 1.
Apr 24 13:42:14 tinkyx1 systemd[1]: Reached target zeek.target - The Zeek Network Security Monitor.
Apr 24 13:42:15 tinkyx1 zeek-worker-2[2602816]: listening on eth1
Apr 24 13:42:15 tinkyx1 zeek-worker-1[2602825]: listening on eth1
Apr 24 13:46:44 tinkyx1 systemd[1]: zeek-worker@1.service: Main process exited, code=dumped, status=6/ABRT
Apr 24 13:46:44 tinkyx1 systemd[1]: zeek-worker@1.service: Failed with result 'core-dump'.
Apr 24 13:46:45 tinkyx1 systemd[1]: zeek-worker@1.service: Scheduled restart job, restart counter is at 2.
Apr 24 13:46:45 tinkyx1 systemd[1]: Starting zeek-worker@1.service - Zeek Worker 1...
Apr 24 13:46:45 tinkyx1 systemd[1]: Started zeek-worker@1.service - Zeek Worker 1.
Apr 24 13:46:45 tinkyx1 zeek-worker-1[2603512]: listening on eth1
This output shows zeek-worker@1 terminating due to a SIGABRT (here, kill -SIGABRT
was used for demonstration purposes) and systemd automatically restarting the process
and reporting the current restart counter value. Use systemctl show zeek-worker@1
to inspect this counter and various other details.
For debugging of crashes and coredump handling in general, we recommend installing
systemd-coredump
if your distro doesn’t automatically do so and setting up /etc/systemd/coredump.conf as needed.
Inspecting coredumps is then possible with coredumpctl list, coredumpctl debug, etc:
# PAGER= coredumpctl info | head -20
PID: 2603455 (zeek)
UID: 997 (zeek)
GID: 995 (zeek)
Signal: 6 (ABRT)
Timestamp: Fri 2026-04-24 13:46:43 CEST (48min ago)
Command Line: /opt/zeek/bin/zeek -i af_packet::eth1 policy/misc/systemd-generator local frameworks/cluster/backend/zeromq
Executable: /opt/zeek/bin/zeek
Control Group: /zeek.slice/zeek-workers.slice/zeek-worker@1.service
Unit: zeek-worker@1.service
Slice: zeek-workers.slice
Boot ID: 0d20f470e7a9413d92f7324c27866992
Machine ID: c9dbccfd439e43b598fa6153f5fa9e3e
Hostname: tinkyx1
Storage: /var/lib/systemd/coredump/core.zeek.997.0d20f470e7a9413d92f7324c27866992.2603455.1777031203000000.zst (present)
Size on Disk: 10.4M
Message: Process 2603455 (zeek) of user 997 dumped core.
Multiple Interfaces
Added in version 9.0.
To monitor multiple interfaces, the zeek.conf file supports INI-style sections
where there is one section per interface. Each interface section is named. The
name of the secution is used in systemd’s unit files, working directories, and also
the cluster node name. The names used in the sample configuration below are eth1
and eth2, but could also be internal and external or anything
descriptive.
When using the INI-style format, place non-interface options into the [zeek]
section, conventionally put at the top of the file:
[zeek]
loggers = 3
proxies = 7
[interface eth1]
interface = af_packet::eth1
workers = 4
workers_cpu_list = 4-7
worker_args = AF_Packet::fanout_id=42
[interface eth2]
interface = af_packet::eth2
workers = 4
workers_cpu_list = 8-11
worker_args = AF_Packet::fanout_id=4711
worker_env =
LD_PRELOAD=/usr/local/lib/libjemalloc.so
MALLOC_CONF=prof:true,prof_prefix:jeprof.out,prof_final:true,lg_prof_interval:26
With AF_PACKET, the respective workers have to use different fanout groups, so we need
to pass an explicit AF_Packet::fanout_id setting via the worker_args option.
This configuration also pins workers sequentially onto CPUs 4 through 11
and enables jemalloc profiling for all workers listening on eth2 via the
worker_env option.
You will find that the worker unit names include the section names when
configured like this. I.e., instead of zeek-worker@1, the first worker
for each interface is zeek-worker-eth1@1 and zeek-worker-eth2@2.
The CLUSTER_NODE and Cluster::node values change accordingly:
worker-1 is worker-eth1-1 and worker-eth2-1, respectively.
You can see these values in the generated cluster-layout.zeek file
at <PREFIX>/var/spool/zeek/generated-scripts/.
Note
You may wonder why there’s even a section-less configuration style.
It stems mostly from the initial zeek.conf version only supporting
a single interface and not having sections. Further, working directories,
unit names and the CLUSTER_NODE value for worker processes exclude
the interface section name as an identifier to make them a bit shorter.
The INI-style format works with a single interface just as well, so you can just use that exclusively and ignore the section-less style.
Worker Interface and Environment Templating
For worker processes specifically, the interface and worker_env options
support templating. For example, selecting a worker-specific Napatech stream
for the interface can be expressed as follows:
[interface napatech]
interface = napatech::${worker_index0}
workers = 32
The ${worker_index0} variable expands to the zero-based index of each worker
for this interface (0 through 31) and so every worker receives an individual
interface napatech::0, napatech::1 through napatech::31. This can
also be useful to select a specific NETMAP pipe per worker.
Templating also works for the single interface section-less configuration style:
interface = netmap::eth1{${worker_index0}
workers = 32
manager = 1
loggers = 2
proxies = 4
Following is a list of the supported variables.
|
The worker index starting at 1. Resets to 1 per interface section. |
|
The worker index starting at 0. Resets to 0 per interface section. |
|
The worker index within the host. Starts at 1 and doesn’t reset. |
|
The worker index within the host. Starts at 0 and doesn’t reset. |
|
The CPU a worker will be pinned on. Only available if |
|
The name part from the |
To use templating in environment variables, use the same variable names
in the worker_env:
[zeek]
loggers = 3
proxies = 7
[interface eth1]
interface = af_packet::${interface_section_name}
workers = 8
workers_cpu_list = 4-11
worker_env =
INTF=${interface_section_name}
CPU=${worker_cpu}
Multi Host Walkthrough
Added in version 9.0.
Note
Multi-host support using the zeek.conf is still being refined. We plan to finalize this towards Zeek 10.0. We welcome testing and feedback!
Two assumptions for running a multi-host Zeek cluster exist. First, all nodes use compatible Zeek versions. This usually means deploying the exact same Zeek build, plugins and packages everywhere. Second, name resolution on each system allows to find the other hosts addresses.
To create a Zeek cluster that spans multiple hosts, the format of the zeek.conf
file does not change. However, you place the configuration files of all hosts of the
cluster at <PREFIX>/etc/zeek/cluster/<hostname>.zeek.conf on each host.
That means that every host has visibility into every other host’s configuration.
Use some configuration management tool to keep the <PREFIX>/etc/zeek/cluster
directories in sync. In a container environment, a volume mount is a viable
alternative, too.
When no <PREFIX>/etc/zeek/zeek.conf file exists, the zeek-systemd-generator
will attempt to open <PREFIX>/etc/zeek/cluster/<hostname>.zeek.conf using the
hostname of the system it is running on. If the file is found, it’ll use it to configure
the appropriate systemd services. The zeek-cluster-layout-generator executable uses
the contents of <PREFIX>/etc/cluster to create the cluster-layout.zeek file.
For a minimal three host cluster, where host c-mgr runs manager, 3 loggers,
2 proxies and the archiver and two hosts c-w-01 and c-w-02 that each
run 4 workers, listening on their eth1 interface using AF_PACKET, the following
configuration files are sufficient.
# <PREFIX>/etc/zeek/cluster/c-mgr.zeek.conf
[zeek]
manager = 1
loggers = 3
proxies = 2
archiver = 1
cluster_backend_args = misc/zeromq-multi-host-auto-setup
# <PREFIX>/etc/zeek/cluster/c-w-01.zeek.conf
[zeek]
cluster_backend_args = misc/zeromq-multi-host-auto-setup
[interface eth1]
interface = af_packet::eth1
workers = 4
# <PREFIX>/etc/zeek/cluster/c-w-02.zeek.conf
[zeek]
cluster_backend_args = misc/zeromq-multi-host-auto-setup
[interface eth1]
interface = af_packet::eth1
workers = 4
You synchronize the full <PREFIX>/etc/zeek/cluster directory with these
three files to all systems:
etc
`-- zeek
`-- cluster
|-- c-mgr.zeek.conf
|-- c-w-01.zeek.conf
`-- c-w-02.zeek.conf
Then, run the following commands on each system to start all processes on each of the hosts:
# systemctl daemon-reload
# systemctl start zeek.target
On the c-mgr host, in <PREFIX>/var/spool/zeek/logger-{1,2,3}/cluster.log,
you should see each of the Zeek nodes saying hello to each other node.
# for f in logger-*; do echo $f/cluster.log; head -3 $f/cluster.log; done
logger-1/cluster.log
{"ts":1784135106.521939,"node":"c-mgr-logger-1","message":"got hello from c-w-01-worker-eth1-1 (zeromq_c-w-01-worker-eth1-1_c-w-01_55_NfzaVa4ZA3BAmvqPx9)"}
{"ts":1784135106.521939,"node":"c-mgr-logger-1","message":"got hello from c-w-01-worker-eth1-2 (zeromq_c-w-01-worker-eth1-2_c-w-01_56_NL4dCr4F5Hca3bxLKe)"}
{"ts":1784135106.521939,"node":"c-mgr-logger-1","message":"got hello from manager (zeromq_manager_c-mgr_91_NIIFQV17DZe55r15Bd)"}
logger-2/cluster.log
{"ts":1784135106.446018,"node":"c-mgr-logger-2","message":"got hello from c-w-01-worker-eth1-3 (zeromq_c-w-01-worker-eth1-3_c-w-01_57_NIupbB37F2DKh2SDCg)"}
{"ts":1784135106.446018,"node":"c-mgr-logger-2","message":"got hello from manager (zeromq_manager_c-mgr_91_NIIFQV17DZe55r15Bd)"}
{"ts":1784135106.446263,"node":"c-mgr-logger-2","message":"got hello from c-mgr-logger-3 (zeromq_c-mgr-logger-3_c-mgr_90_Nig6uU2hdCFqaJr0c6)"}
logger-3/cluster.log
{"ts":1784135106.413847,"node":"c-mgr-logger-3","message":"got hello from c-w-01-worker-eth1-3 (zeromq_c-w-01-worker-eth1-3_c-w-01_57_NIupbB37F2DKh2SDCg)"}
{"ts":1784135106.413847,"node":"c-mgr-logger-3","message":"got hello from manager (zeromq_manager_c-mgr_91_NIIFQV17DZe55r15Bd)"}
{"ts":1784135106.445795,"node":"c-mgr-logger-3","message":"got hello from c-mgr-logger-2 (zeromq_c-mgr-logger-2_c-mgr_89_NAfhZd445aDO9AY8ed)"}
# cat logger-*/cluster.log | jq .node | sort | uniq -c
13 "c-mgr-logger-1"
13 "c-mgr-logger-2"
13 "c-mgr-logger-3"
13 "c-mgr-proxy-1"
13 "c-mgr-proxy-2"
13 "c-w-01-worker-eth1-1"
13 "c-w-01-worker-eth1-2"
13 "c-w-01-worker-eth1-3"
13 "c-w-01-worker-eth1-4"
13 "c-w-02-worker-eth1-1"
13 "c-w-02-worker-eth1-2"
13 "c-w-02-worker-eth1-3"
13 "c-w-02-worker-eth1-4"
13 "manager"
You successfully started up a multi-host Zeek cluster!
Cluster Layout Notes
Note
Some of the current requirements around the static cluster-layout.zeek
file are historic and there’s ideas to relax some of them.
As of version 9.0, each host needs to be statically aware of all other hosts in
a cluster and able to resolve their IP addresses though the hostname as used in
the configuration filenames. Adding the required entries into /etc/hosts on
the respective systems is one way to achieve this, DNS is another. Container
orchestrators usually allow specifying hostnames for containers or pods,
and provide name resolution services as well.
The zeek-cluster-layout-generator executable creates the contents
of a cluster-layout.zeek file.
You can point the zeek-cluster-layout-generator executable at a
<PREFIX>/etc/zeek/cluster directory and inspect the generated layout
for a multi-host Zeek cluster.
If you do not specify a cluster_address option within the individual
<hostname>.zeek.conf files, the generated cluster-layout.zeek file
contains extra script code to use Zeek’s DNS functionality to lookup the
addresses of the involved hosts.
# zeek-cluster-layout-generator -C ./cluster/
# Auto-generated by zeek-cluster-layout-generator
...
const hosts: table[string] of Host &ordered &redef;
redef Cluster::hosts += {
["c-mgr"] = [$ip=(blocking_lookup_hostname("c-mgr") as vector of addr)[0]],
["c-w-01"] = [$ip=(blocking_lookup_hostname("c-w-01") as vector of addr)[0]],
["c-w-02"] = [$ip=(blocking_lookup_hostname("c-w-02") as vector of addr)[0]],
};
redef Cluster::nodes += {
["manager"] = [$node_type=Cluster::MANAGER, $ip=hosts["c-mgr"]$ip, $p=27760/tcp, $metrics_port=9991/tcp],
["c-mgr-logger-1"] = [$node_type=Cluster::LOGGER, $ip=hosts["c-mgr"]$ip, $p=27761/tcp, $manager="manager", $metrics_port=9992/tcp],
}
This means that each Zeek process will determine the addresses of all other
hosts in a cluster when loading the generated cluster-layout.zeek script
during startup. That’s why it is important to have a functional /etc/hosts
or DNS setup. Alternatively, set cluster_address to the host’s address
in the respective <hostname>.zeek.conf.
You can debug the resulting layout using Zeek itself:
# zeek -b <PREFIX>/var/spool/zeek/generated-scripts/cluster-layout.zeek -e 'print Cluster::nodes'
{
[c-w-02-worker-vxlan-4] = [node_type=Cluster::WORKER, ip=fd00:dead:beef::12, zone_id=, p=0/unknown, manager=manager, id=<uninitialized>, metrics_port=9994/tcp],
[c-w-01-worker-vxlan-3] = [node_type=Cluster::WORKER, ip=fd00:dead:beef::11, zone_id=, p=0/unknown, manager=manager, id=<uninitialized>, metrics_port=9993/tcp],
...
}
For the single-host case, the invocation of zeek-cluster-layout-generator contains
explicit logger, proxy, worker counts rather than reading zeek.conf files:
# zeek-cluster-layout-generator -W 1 -a 192.168.0.1 -m 0 -p 5000
# Auto-generated by zeek-cluster-layout-generator
redef Cluster::nodes += {
["manager"] = [$node_type=Cluster::MANAGER, $ip=192.168.0.1, $p=5000/tcp, $metrics_port=0/tcp],
["logger-1"] = [$node_type=Cluster::LOGGER, $ip=192.168.0.1, $p=5001/tcp, $manager="manager", $metrics_port=0/tcp],
["proxy-1"] = [$node_type=Cluster::PROXY, $ip=192.168.0.1, $p=5002/tcp, $manager="manager", $metrics_port=0/tcp],
["worker-1"] = [$node_type=Cluster::WORKER, $ip=192.168.0.1, $manager="manager", $metrics_port=0/tcp],
};
...
You can use the zeek-cluster-layout-generator executable in non-Linux
and non-systemd environments as well.
If you want to use a custom cluster-layout.zeek file, it is possible
to use cluster_layout = /path/to/cluster-layout.zeek within zeek.conf.