Suricata IDS on Proxmox for Network Threat Detection

Deploy Suricata 7 on a Proxmox LXC container to monitor all bridge traffic in real time. Covers tc mirroring, ET Open rules, alert tuning, and log integration.

Proxmox Pulse Proxmox Pulse
9 min read
Holographic hawk monitoring glowing network data streams in a dark server room.

Running Suricata 7.x as a network intrusion detection system on Proxmox gives you full visibility into inter-VM traffic without modifying a single guest. By the end of this guide you will have a dedicated Suricata LXC container watching a mirrored copy of your bridge traffic, writing structured JSON alerts you can query in real time or ship to your existing log stack.

Key Takeaways

  • Traffic mirroring: Use tc mirred to clone packets from vmbr0 to a dummy interface with zero impact on VM throughput
  • Container type: A privileged LXC container on Debian 12 is the simplest path; Suricata needs NET_RAW and NET_ADMIN capabilities
  • Free ruleset: Emerging Threats Open is updated daily and covers the vast majority of homelab threat scenarios at no cost
  • Day-one noise: A fresh ET Open install fires 200 to 500 false positive alerts in the first 24 hours—budget time to tune
  • JSON-native: Suricata's eve.log is newline-delimited JSON, feeding directly into Loki, Elasticsearch, or a jq one-liner

What You Need Before Starting

This guide targets Proxmox VE 9.1 with a standard Linux bridge (vmbr0). Before starting you need:

  • At least 2 GB of RAM to spare for the Suricata container
  • The Debian 12 LXC template downloaded on your node
  • Internet access from within the container to pull rulesets

Download the template if you do not have it:

pveam update && pveam download local debian-12-standard

If you have already configured VLANs following the guide to configuring VLANs on Proxmox with Linux bridges, the same tc mirroring approach works on VLAN-aware bridges—mirror each bridge independently and list all interfaces in suricata.yaml.

If you are already running CrowdSec for brute-force defense, Suricata fills a different layer. CrowdSec analyzes application logs; Suricata inspects raw packets. They complement each other rather than overlap.

How Traffic Mirroring Works on a Linux Bridge

Suricata needs raw packet access but must never sit in the forwarding path where it could drop traffic or add latency. The solution is a passive mirror: Linux tc clones every packet traversing vmbr0 to a dedicated dummy interface, and Suricata reads from that copy in promiscuous mode.

vmbr0 (bridge) ---> tc mirred clone ---> dummy0 ---> Suricata (read-only)

Create the mirror on the Proxmox host:

# Load the dummy kernel module
modprobe dummy

# Create and activate the mirror interface
ip link add dummy0 type dummy
ip link set dummy0 promisc on
ip link set dummy0 up

# Mirror ingress traffic on vmbr0
tc qdisc add dev vmbr0 handle ffff: ingress
tc filter add dev vmbr0 parent ffff: protocol all u32 match u8 0 0 \
  action mirred egress mirror dev dummy0

# Mirror egress traffic on vmbr0
tc qdisc add dev vmbr0 handle 1: root prio
tc filter add dev vmbr0 parent 1: protocol all u32 match u8 0 0 \
  action mirred egress mirror dev dummy0

Make the mirroring survive reboots by adding post-up hooks to /etc/network/interfaces:

auto vmbr0
iface vmbr0 inet static
    address 192.168.1.10/24
    gateway 192.168.1.1
    bridge-ports enp3s0
    bridge-stp off
    bridge-fd 0
    post-up modprobe dummy
    post-up ip link add dummy0 type dummy || true
    post-up ip link set dummy0 promisc on
    post-up ip link set dummy0 up
    post-up tc qdisc add dev vmbr0 handle ffff: ingress
    post-up tc filter add dev vmbr0 parent ffff: protocol all u32 match u8 0 0 action mirred egress mirror dev dummy0
    post-up tc qdisc add dev vmbr0 handle 1: root prio
    post-up tc filter add dev vmbr0 parent 1: protocol all u32 match u8 0 0 action mirred egress mirror dev dummy0

Gotcha: If you already have a root qdisc on vmbr0 from a previous QoS setup, the handle 1: root prio line fails with RTNETLINK answers: File exists. Check first with tc qdisc show dev vmbr0 and choose a different handle number.

Installing Suricata in a Proxmox LXC Container

Create a privileged container with 2 GB RAM. Suricata 7 with ET Open loaded idles at around 800 MB, so 2 GB gives comfortable headroom for traffic spikes.

pct create 200 local:vztmpl/debian-12-standard_12.7-1_amd64.tar.zst \
  --hostname suricata-ids \
  --memory 2048 \
  --cores 2 \
  --rootfs local-lvm:8 \
  --net0 name=eth0,bridge=vmbr0,ip=192.168.1.200/24,gw=192.168.1.1 \
  --privileged 1

Before starting the container, grant the capabilities Suricata requires and pass dummy0 into the container namespace:

echo 'lxc.cap.keep = net_bind_service net_raw net_admin sys_nice' >> /etc/pve/lxc/200.conf

cat >> /etc/pve/lxc/200.conf << 'EOF'
lxc.net.1.type = phys
lxc.net.1.link = dummy0
lxc.net.1.name = suricata0
lxc.net.1.flags = up
EOF

Start the container and enter it:

pct start 200 && pct enter 200

Inside the container, install Suricata 7 from the official OISF stable repository:

apt-get update && apt-get install -y curl gnupg

curl -fsSL https://www.openinfosecfoundation.org/download/suricata-stable-debian.gpg \
  | gpg --dearmor -o /usr/share/keyrings/suricata.gpg

echo 'deb [signed-by=/usr/share/keyrings/suricata.gpg] https://ppa.launchpad.net/oisf/suricata-stable/ubuntu jammy main' \
  > /etc/apt/sources.list.d/suricata.list

apt-get update && apt-get install -y suricata suricata-update

Verify:

suricata --build-info | grep 'Suricata version'
# Suricata version 7.0.x RELEASE

Configuring Suricata to Watch the Mirror Interface

Edit /etc/suricata/suricata.yaml inside the container. The two critical sections are af-packet capture and eve-log output:

# /etc/suricata/suricata.yaml (relevant excerpts)
af-packet:
  - interface: suricata0
    cluster-id: 99
    cluster-type: cluster_flow
    defrag: yes
    use-mmap: yes
    tpacket-v3: yes

vars:
  address-groups:
    HOME_NET: "[192.168.0.0/16,10.0.0.0/8,172.16.0.0/12]"
    EXTERNAL_NET: "!$HOME_NET"

outputs:
  - eve-log:
      enabled: yes
      filetype: regular
      filename: /var/log/suricata/eve.json
      types:
        - alert:
            payload: yes
            payload-printable: yes
        - dns:
            query: yes
            answer: yes
        - http:
            extended: yes
        - tls:
            extended: yes

Setting HOME_NET correctly matters: rules classify traffic direction based on this value, and getting it wrong produces alerts pointing the wrong way.

Loading and Updating Emerging Threats Open Rules

suricata-update is the official rule manager and ships with Suricata. Run it to fetch ET Open:

suricata-update

The first run fetches roughly 40,000 rules and takes a minute or two on a typical connection. Set up a daily cron to keep rules fresh:

echo '0 3 * * * root /usr/bin/suricata-update && systemctl reload suricata' > /etc/cron.d/suricata-update

Enable and start Suricata:

systemctl enable --now suricata
systemctl status suricata

If Suricata fails to start, the most common cause is that suricata0 is not visible yet inside the container because the lxc.net.1 config changes require a container restart to take effect. Run ip link show inside the container to confirm the interface appears before debugging further.

Reading Eve JSON Alerts in Real Time

Suricata writes all events to /var/log/suricata/eve.json in newline-delimited JSON. Watch alerts as they arrive:

tail -f /var/log/suricata/eve.json | jq 'select(.event_type == "alert")'

A typical alert entry looks like this:

{
  "timestamp": "2026-05-31T14:22:03.112345+0000",
  "event_type": "alert",
  "src_ip": "192.168.1.45",
  "src_port": 49823,
  "dest_ip": "185.220.101.47",
  "dest_port": 443,
  "proto": "TCP",
  "alert": {
    "action": "allowed",
    "signature_id": 2027865,
    "signature": "ET TOR Known Tor Exit Node Traffic",
    "category": "Misc Attack",
    "severity": 2
  }
}

Look up any rule by its SID to understand what triggered it:

grep 'sid:2027865' /var/lib/suricata/rules/suricata.rules

Why Alert Volume Is High on Day One and How to Tune It

Expect 200 to 500 alerts in the first 24 hours on a typical homelab. Most will be legitimate-looking noise: DNS queries hitting CDN IPs that share address space with known scanners, TLS certificate fingerprints ET flagged years ago, or your NAS phoning home through a service that looks suspicious to a generic ruleset.

Identify the noisiest rule IDs first:

cat /var/log/suricata/eve.json \
  | jq -r 'select(.event_type == "alert") | .alert.signature_id' \
  | sort | uniq -c | sort -rn | head -10

For each SID you confirm is a false positive on your network, add it to a suppress list:

# /etc/suricata/disable.conf
2027865
2013028
2021001

Re-run the update with the suppress list:

suricata-update --disable-conf /etc/suricata/disable.conf
systemctl reload suricata

Do this tuning pass in the first week. A noisy IDS that fires constantly gets ignored—which is strictly worse than no IDS at all. The goal is a signal-to-noise ratio where each alert is worth reading.

Shipping Suricata Alerts to a Centralized Log Stack

The eve.json format is natively supported by Loki, Elasticsearch, Wazuh, and Graylog. For a quick Loki integration, install Promtail inside the container and point it at the log file:

# /etc/promtail/config.yaml (inside Suricata LXC)
server:
  http_listen_port: 9080

positions:
  filename: /tmp/positions.yaml

clients:
  - url: http://192.168.1.50:3100/loki/api/v1/push

scrape_configs:
  - job_name: suricata
    static_configs:
      - targets: [localhost]
        labels:
          job: suricata
          host: proxmox-node-01
          __path__: /var/log/suricata/eve.json
    pipeline_stages:
      - json:
          expressions:
            event_type: event_type
            src_ip: src_ip
      - labels:
          event_type:
          src_ip:

In Grafana's Explore view, filter to alerts with:

{job="suricata"} | json | event_type = "alert"

A bar chart of alerts by category over 24-hour windows is enough to spot behavioral changes at a glance. If you are running Cloudflare Tunnel on Proxmox for zero-trust remote access, pairing that with Suricata gives you coverage at both the perimeter and inside your bridge fabric.

Performance Impact and When This Setup Is Worth It

On a Proxmox node handling 500 Mbps of inter-VM traffic, Suricata 7 with 40,000 ET Open rules uses roughly 1 to 2 CPU cores and 800 MB RAM. On an Intel N100 mini PC that leaves plenty of headroom for typical homelab workloads.

If CPU pressure is an issue, configure Suricata to use multiple AF_PACKET worker threads pinned to specific cores:

# /etc/suricata/suricata.yaml
threading:
  set-cpu-affinity: yes
  cpu-affinity:
    - management-cpu-set:
        cpu: [ 0 ]
    - worker-cpu-set:
        cpu: [ "1-3" ]
        mode: exclusive
        threads: 3

This setup earns its resource cost for any node that exposes services to the internet or hosts multiple VMs that could be compromised and used as lateral movement footholds. For a completely air-gapped lab, the benefit is marginal and you can skip it.

Conclusion

You now have Suricata 7 passively watching your Proxmox bridge traffic, writing structured alerts to eve.json, and tuned to cut through the day-one false positive flood. The mirroring approach means no in-path latency and no risk of Suricata taking down your network if it restarts. The logical next step is pairing this with CrowdSec on your Proxmox cluster to act on Suricata-flagged source IPs at the firewall level, closing the loop from detection to automated response.

Share
Proxmox Pulse

Written by

Proxmox Pulse

Sysadmin-driven guides for getting the most out of Proxmox VE in production and homelab environments.

Related Articles

View all →