Deploying Suricata IDS Behind pfSense: Network Intrusion Detection for Self-Hosted Infrastructure

A properly configured firewall blocks unauthorized connections. An intrusion detection system (IDS) tells you what is happening inside the connections your firewall allows. These are complementary tools, not alternatives. If your firewall allows HTTPS inbound to your web servers, it cannot inspect the content of those connections for SQL injection, command injection, or data exfiltration. That is where Suricata comes in.

This guide covers deploying Suricata as a network IDS on a dedicated sensor behind a pfSense firewall, tuning it for performance, writing custom rules, and shipping alerts to a SIEM.

Architecture: Span Port Deployment

The cleanest deployment puts Suricata on a dedicated sensor that receives a copy of all traffic via a mirror/span port on your switch. This is passive — Suricata sees everything but cannot block anything (IDS mode, not IPS).

Internet
    |
[pfSense FW] --- Port 1 (WAN)
    |
    +--- Port 2 (LAN) --- [Internal Network]
    |
    +--- Port 3 (SPAN/Mirror of Port 2) --- [sensor01 - Suricata]

Configure your managed switch to mirror all traffic on the LAN port to the Suricata sensor port. On a Cisco switch:

interface GigabitEthernet0/3
  description Suricata SPAN destination
  switchport mode access
  switchport access vlan 1

monitor session 1 source interface Gi0/2 both
monitor session 1 destination interface Gi0/3

On the Suricata sensor, the monitoring interface should have no IP address — it is a receive-only tap:

# /etc/network/interfaces (on sensor01)
auto eth1
iface eth1 inet manual
    up ip link set eth1 up promisc on
    down ip link set eth1 down

IDS vs IPS Mode

IDS mode (passive monitoring) is safer for initial deployment. Suricata inspects a copy of traffic and generates alerts without affecting live connections. You cannot accidentally break production by misconfiguring a rule.

IPS mode (inline) places Suricata in the traffic path. It can drop malicious packets in real time, but a misconfigured rule or an overloaded Suricata instance can cause outages.

Start with IDS. Move to IPS only after you have thoroughly tuned your ruleset and are confident in your false-positive rate.

Installation and Base Configuration

On Debian/Ubuntu:

sudo apt-get install suricata suricata-update

The primary configuration file is /etc/suricata/suricata.yaml. Key sections to configure:

Network Variables

Tell Suricata what your internal network looks like:

vars:
  address-groups:
    HOME_NET: "[10.0.70.0/24, 10.0.80.0/24, 172.16.0.0/16]"
    EXTERNAL_NET: "!$HOME_NET"
    HTTP_SERVERS: "[10.0.70.20, 10.0.70.21]"
    DNS_SERVERS: "[10.0.70.2, 10.0.70.3]"
    SMTP_SERVERS: "[10.0.70.25]"

  port-groups:
    HTTP_PORTS: "[80, 443, 8080, 8443]"
    SSH_PORTS: "[22, 30022]"
    DNS_PORTS: "[53]"

Capture Configuration

For high-throughput networks, use AF_PACKET with multiple threads:

af-packet:
  - interface: eth1
    threads: 4
    cluster-id: 99
    cluster-type: cluster_flow
    defrag: yes
    use-mmap: yes
    ring-size: 200000
    block-size: 262144
    buffer-size: 67108864  # 64 MB

The cluster_flow type ensures all packets from the same flow go to the same thread, which is essential for stateful inspection. The ring-size and buffer-size parameters prevent packet drops under load.

Threading and CPU Affinity

On a multi-core sensor, pin Suricata threads to specific cores to avoid cache thrashing:

threading:
  set-cpu-affinity: yes
  cpu-affinity:
    - management-cpu-set:
        cpu: [0]
    - receive-cpu-set:
        cpu: [1, 2, 3, 4]
    - worker-cpu-set:
        cpu: [5, 6, 7, 8, 9, 10, 11]

Dedicate core 0 to management (logging, stats), cores 1-4 to packet capture, and the remaining cores to rule matching workers. Adjust based on your hardware.

Rule Management

Use suricata-update to fetch and manage rulesets:

# Fetch ET Open rules (free)
sudo suricata-update

# List available rule sources
sudo suricata-update list-sources

# Enable additional free sources
sudo suricata-update enable-source oisf/trafficid
sudo suricata-update enable-source etnetera/aggressive

# Update and reload
sudo suricata-update
sudo systemctl reload suricata

Writing Custom Rules

ET Open covers common threats, but you need custom rules for your specific environment. Suricata rules follow a structured syntax:

action protocol src_ip src_port -> dst_ip dst_port (rule_options)

Detect internal lateral movement via SMB:

alert tcp $HOME_NET any -> $HOME_NET 445 (msg:"INTERNAL Lateral movement - SMB connection between workstations"; flow:to_server,established; content:"|ff|SMB"; offset:0; depth:4; classtype:policy-violation; sid:1000001; rev:1;)

Detect DNS tunneling (unusually long DNS queries):

alert dns $HOME_NET any -> any 53 (msg:"SUSPICIOUS DNS query over 100 bytes - possible tunneling"; dns.query; content:"."; pcre:"/^[a-zA-Z0-9-.]{100,}/"; classtype:bad-unknown; sid:1000002; rev:1;)

Detect potential C2 beacon (periodic HTTP POST to uncommon port):

alert http $HOME_NET any -> $EXTERNAL_NET !$HTTP_PORTS (msg:"SUSPICIOUS HTTP POST to non-standard port - possible C2"; flow:to_server,established; http.method; content:"POST"; http.host; pcre:"/^d+.d+.d+.d+$/"; classtype:trojan-activity; sid:1000003; rev:1;)

Detect base64-encoded PowerShell in HTTP traffic:

alert http any any -> $HOME_NET $HTTP_PORTS (msg:"ATTACK Potential base64 encoded PowerShell payload"; flow:to_server,established; http.request_body; content:"powershell"; nocase; content:"-enc"; nocase; distance:0; within:20; classtype:attempted-admin; sid:1000004; rev:1;)

Place custom rules in /etc/suricata/rules/local.rules and reference them in suricata.yaml:

rule-files:
  - suricata.rules    # managed by suricata-update
  - local.rules       # your custom rules

EVE JSON Logging

Suricata’s EVE (Extensible Event Format) JSON log is the primary output for SIEM integration. Configure it in suricata.yaml:

outputs:
  - eve-log:
      enabled: yes
      filetype: regular
      filename: /var/log/suricata/eve.json
      types:
        - alert:
            tagged-packets: yes
            payload: yes
            payload-printable: yes
            http-body: yes
            http-body-printable: yes
        - http:
            extended: yes
        - dns:
            query: yes
            answer: yes
        - tls:
            extended: yes
        - files:
            force-magic: yes
            force-hash: [md5, sha256]
        - stats:
            totals: yes
            threads: no
            deltas: yes

Shipping to SIEM

Wazuh Integration

If you run Wazuh, install the Wazuh agent on sensor01 and configure it to read Suricata’s EVE log:

<!-- /var/ossec/etc/ossec.conf on sensor01 -->
<ossec_config>
  <localfile>
    <log_format>json</log_format>
    <location>/var/log/suricata/eve.json</location>
  </localfile>
</ossec_config>

Wazuh has built-in decoders and rules for Suricata alerts. They appear in the Wazuh dashboard under the “Threat Detection” module.

Filebeat to Elasticsearch/OpenSearch

Alternatively, ship EVE JSON directly to your indexer:

# /etc/filebeat/filebeat.yml on sensor01
filebeat.inputs:
  - type: log
    enabled: true
    paths:
      - /var/log/suricata/eve.json
    json.keys_under_root: true
    json.add_error_key: true

output.elasticsearch:
  hosts: ["https://siem01.internal.example-corp.com:9200"]
  ssl.certificate_authorities: ["/etc/ssl/internal/ca.crt"]
  index: "suricata-%{+yyyy.MM.dd}"

False Positive Tuning

The initial deployment will generate noise. Tune aggressively:

Suppress List

Suppress specific signatures for specific sources/destinations:

# /etc/suricata/threshold.config

# Suppress ET SCAN rules for your vulnerability scanner
suppress gen_id 1, sig_id 2010935, track by_src, ip 10.0.80.50

# Suppress DNS query alerts for your internal DNS resolvers
suppress gen_id 1, sig_id 2027863, track by_src, ip 10.0.70.2
suppress gen_id 1, sig_id 2027863, track by_src, ip 10.0.70.3

Threshold Rules

Reduce alert volume for noisy-but-valid signatures:

# Only alert once per minute per source IP for port scans
threshold gen_id 1, sig_id 2010937, type both, track by_src, count 1, seconds 60

# Rate limit SSH brute force alerts
threshold gen_id 1, sig_id 2001219, type threshold, track by_src, count 10, seconds 60

Performance Monitoring

Watch for packet drops — they mean Suricata cannot keep up:

# Check Suricata stats
sudo suricatasc -c "dump-counters" | python3 -m json.tool | grep -E "capture.kernel_drops|decoder.pkts"

# Check interface drops
ethtool -S eth1 | grep -i drop

# Monitor CPU usage per thread
top -H -p $(pgrep suricata)

If you see persistent drops:

  1. Increase ring-size and buffer-size in the AF_PACKET config
  2. Add more worker threads (needs more CPU cores)
  3. Disable expensive protocol parsers you do not need (e.g., if you do not inspect SMB traffic, disable the SMB parser)
  4. Consider hardware: a dedicated IDS sensor benefits from high core count CPUs and 10 Gbps NICs with RSS (Receive Side Scaling)
# Disable parsers you don't need
app-layer:
  protocols:
    smb:
      enabled: no
    nfs:
      enabled: no
    tftp:
      enabled: no

Suricata, properly deployed and tuned, gives you deep visibility into network activity that a firewall alone cannot provide. Start in IDS mode, tune your rules over two to four weeks, then decide whether inline IPS makes sense for your environment.

Scroll to Top