Suricata and Zeek (formerly Bro) are the two open-source pillars of network security monitoring. Suricata is a high-performance IDS/IPS engine with signature-based detection. Zeek is a network analysis framework that produces rich protocol logs and supports scripted detection logic. They are complementary, not competitive, and most mature SOCs run both. This guide walks through deploying them side by side on a RamNode KVM VPS to monitor the host's own traffic, ship logs off-box, and tune for the constraints of a VPS environment.
Deployment Model and Sizing
In a traditional enterprise deployment, Suricata and Zeek sit on a tap or span port and see all traffic from a network segment. On a cloud VPS, that model does not apply. What you get instead is host-based network monitoring: the sensor watches the traffic in and out of the VPS itself, on its primary network interface. This is genuinely useful for monitoring exposed services, detecting lateral movement attempts against your VPS, and producing forensic logs of inbound and outbound flows.
Plan the following on RamNode:
- 4 GB RAM minimum, 8 GB recommended once you load full Emerging Threats Pro or commercial rules
- 4 vCPU minimum; both engines parallelize across cores
- 80 GB disk minimum if you retain logs locally; budget more if you index with Elasticsearch on-box
- Ubuntu 24.04 LTS
If you intend to ship logs to a remote SIEM, you can run on a smaller plan since you are not retaining the corpus locally. If you intend to index logs on the same host with Elasticsearch or OpenSearch, you need a much larger plan and should consider whether a separate analytics VPS makes more sense.
Initial Setup
Update the base system and install supporting tools:
apt update && apt -y full-upgrade
apt -y install ufw jq curl gnupg2 software-properties-common chrony
systemctl enable --now chronyAccurate time is non-negotiable for network monitoring. Confirm:
chronyc trackingConfigure UFW to permit only what the host needs. Note that neither Suricata nor Zeek requires inbound ports; they read traffic off the interface in promiscuous mode (which on a KVM VPS is more accurately "all traffic destined for this VM's MAC").
ufw default deny incoming
ufw default allow outgoing
ufw allow 22/tcp
ufw --force enableInstalling Suricata
The OISF maintains a current PPA for Ubuntu. Use it rather than the distro package, which tends to lag by major versions:
add-apt-repository -y ppa:oisf/suricata-stable
apt update
apt -y install suricata jqIdentify the network interface Suricata should monitor:
ip -o -4 addr show | awk '{print $2": "$4}'You are looking for the interface with the VPS's public IPv4. On RamNode KVM, this is typically eth0 or ens3. Edit /etc/suricata/suricata.yaml and set the AF_PACKET interface accordingly:
af-packet:
- interface: ens3
threads: auto
cluster-id: 99
cluster-type: cluster_flow
defrag: yes
use-mmap: yes
tpacket-v3: yes
ring-size: 200000
block-size: 1048576Set the home network. For a VPS, the home network is just the host itself:
vars:
address-groups:
HOME_NET: "[YOUR.VPS.PUB.IP/32]"
EXTERNAL_NET: "!$HOME_NET"Substitute your actual public IP. If you have multiple IPs assigned to the VPS, include all of them.
Suricata Rules Management
The signature corpus is what makes Suricata useful. The standard tool is suricata-update, which is bundled with the PPA package:
suricata-update update-sources
suricata-update enable-source et/open
suricata-update enable-source oisf/trafficid
suricata-update enable-source ptresearch/attackdetection
suricata-updateValidate the config and rules before restarting:
suricata -T -c /etc/suricata/suricata.yaml -vIf the test passes, enable and start the service:
systemctl enable --now suricata
systemctl status suricataSchedule daily rule updates via cron. Edit root's crontab:
15 3 * * * /usr/bin/suricata-update --quiet && /usr/bin/systemctl kill -s USR2 suricataThe USR2 signal triggers a live rule reload without dropping packets.
Suricata Performance Tuning
The two settings that matter most on a VPS are thread count and flow timeouts. For thread count, threads: auto will use one worker per available core. On a 4 vCPU plan that is correct. If you start dropping packets, check:
jq '.suricata.capture.kernel_drops' /var/log/suricata/stats.logA non-zero kernel_drops count means the kernel is dropping packets before Suricata sees them. Fix this by increasing the ring-size or the af-packet block-size, in that order.
Flow timeouts default to values appropriate for a corporate LAN, not a public-facing VPS that sees TCP scan noise constantly. In /etc/suricata/suricata.yaml:
flow-timeouts:
default:
new: 10
established: 100
closed: 5
bypassed: 50
emergency-new: 5
emergency-established: 50
emergency-closed: 2
emergency-bypassed: 25
tcp:
new: 30
established: 300
closed: 10Tighter timeouts mean lower memory pressure from the flow table at the cost of merging some long-lived flows.
Installing Zeek
Zeek ships current packages via the OBS repository:
echo 'deb http://download.opensuse.org/repositories/security:/zeek/xUbuntu_24.04/ /' \
> /etc/apt/sources.list.d/security:zeek.list
curl -fsSL https://download.opensuse.org/repositories/security:zeek/xUbuntu_24.04/Release.key \
| gpg --dearmor > /etc/apt/trusted.gpg.d/security_zeek.gpg
apt update
apt -y install zeekAdd Zeek to PATH for all users:
echo 'export PATH=/opt/zeek/bin:$PATH' > /etc/profile.d/zeek.sh
source /etc/profile.d/zeek.shConfigure the node. Edit /opt/zeek/etc/node.cfg:
[zeek]
type=standalone
host=localhost
interface=ens3Edit /opt/zeek/etc/networks.cfg to define the local network. On a VPS this is just the host:
YOUR.VPS.PUB.IP/32 VPS hostEdit /opt/zeek/etc/zeekctl.cfg for log management:
LogRotationInterval = 3600
LogExpireInterval = 7day
MailTo =
MailConnectionSummary = 0The empty MailTo and disabled connection summary email are deliberate. RamNode does not allow mail services, so Zeek's email reporting is non-functional. Use log shipping for alerts instead.
Running Zeek Alongside Suricata
Both engines need to read from the same interface. AF_PACKET cluster mode allows this without contention. Suricata already uses cluster-flow mode; Zeek's default uses pcap directly, which is fine for a single-process deployment but performs poorly under load. For a single-VPS deployment with modest traffic this is acceptable.
Validate and deploy Zeek:
zeekctl deploy
zeekctl statusLogs land in /opt/zeek/logs/current/. The key log files are conn.log (connection summaries), dns.log, http.log, ssl.log, notice.log, and weird.log. The richness of these logs versus what Suricata produces is the whole reason to run Zeek.
Zeek Scripting and Detection Packages
Zeek's real power is its scripting language. The community maintains detection packages installable via zkg:
zkg autoconfig
zkg install zeek/cybera/zeek-sniffpass
zkg install zeek/corelight/zeek-community-id
zkg install zeek/salesforce/ja3The Community ID plugin produces a flow hash that correlates Zeek and Suricata events for the same connection, which is essential if you ship both into a SIEM. JA3 produces TLS client fingerprints that are useful for malware identification.
After installing packages, reload Zeek:
zeekctl deployLog Shipping
Local logs are a starting point, not a destination. Ship them to a central platform. Vector is the cleanest agent for this; Filebeat is the traditional choice if you are running Elastic. A minimal Vector config for both engines at /etc/vector/vector.yaml:
sources:
suricata_eve:
type: file
include:
- /var/log/suricata/eve.json
read_from: end
zeek_logs:
type: file
include:
- /opt/zeek/logs/current/*.log
read_from: end
transforms:
suricata_parse:
type: remap
inputs: [suricata_eve]
source: |
. = parse_json!(.message)
.source_type = "suricata"
.host = get_hostname!()
zeek_parse:
type: remap
inputs: [zeek_logs]
source: |
.source_type = "zeek"
.host = get_hostname!()
sinks:
remote_siem:
type: http
inputs: [suricata_parse, zeek_parse]
uri: https://siem.example.org/ingest
encoding:
codec: json
auth:
strategy: bearer
token: "${SIEM_TOKEN}"Install Vector:
bash -c "$(curl -L https://setup.vector.dev)"
apt install -y vector
systemctl enable --now vectorLog Rotation and Disk Management
Even with shipping enabled, the local logs need rotation. Suricata uses its built-in rotation; verify in suricata.yaml that outputs.eve-log.filetype: regular is set and that you have a logrotate entry:
/etc/logrotate.d/suricata:
/var/log/suricata/*.log /var/log/suricata/*.json {
daily
rotate 14
compress
delaycompress
missingok
notifempty
postrotate
/bin/kill -HUP $(cat /var/run/suricata.pid 2>/dev/null) 2>/dev/null || true
endscript
}Zeek rotates internally based on LogRotationInterval in zeekctl.cfg, and LogExpireInterval controls retention of compressed archives. Confirm /opt/zeek/logs/ is being pruned by checking dated subdirectories.
Operational Validation
Confirm both engines are seeing traffic. For Suricata:
tail -f /var/log/suricata/eve.json | jq .You should see flow events accumulating. Trigger an alert deliberately for validation:
curl http://testmynids.org/uid/index.htmlThe Emerging Threats GPL test rule should fire and produce an alert event in eve.json within seconds.
For Zeek:
tail -f /opt/zeek/logs/current/conn.logThe Zeek logs are tab-separated by default. Pipe through zeek-cut for readability:
zeek-cut id.orig_h id.resp_h id.resp_p service < /opt/zeek/logs/current/conn.log | headMonitoring the Monitors
Both engines need their own monitoring. Watch for:
- Suricata kernel_drops increasing (indicates packet loss)
- Suricata memuse approaching the configured limit
- Zeek capture_loss notices in
notice.log - Disk usage on
/var/log/suricataand/opt/zeek/logs - Both services running under systemd
A simple health check script for cron:
#!/usr/bin/env bash
systemctl is-active --quiet suricata || echo "ALERT: suricata down"
pgrep -x zeek > /dev/null || echo "ALERT: zeek down"
DROPS=$(jq -r '.suricata.capture.kernel_drops' /var/log/suricata/stats.log 2>/dev/null | tail -1)
[ "${DROPS:-0}" -gt 1000 ] && echo "ALERT: suricata kernel drops at $DROPS"What to Read Next
The natural next step is detection engineering: writing custom Suricata rules for your environment and Zeek scripts that codify your threat model. After that, the integration layer (correlating Suricata alerts to Zeek protocol context via Community ID, then enriching with MISP threat intel) is what turns raw events into actionable signal. Each of those deserves its own guide.
