2026年11月14日

2026年11月14日

How to Write Snort Rules: Syntax, local.rules and Testing

This article is a localized edition of the Japanese original. Read the Japanese original

A custom Snort rule is written in two parts, a header (what, and from where to where) and options (what to match and how to record it), and the rules are collected in local.rules and loaded from the configuration file. If you follow the order of passing a syntax check and a pcap replay test before putting a rule into production, you can find missed detections and false positives early. This article assumes Snort 3 and explains the rule syntax, practical rules using WordPress as an example, and the stumbling points in testing and operation.

Prerequisite: differences between Snort 3 and Snort 2

Snort 3 is the current mainstream version (as of October 2026). Official support for the Snort 2 series is shrinking, so Snort 3 is the safer choice for a new installation. The basic form of the rules is the same in both, but they differ in the following ways.

  • The configuration file changed from snort.conf to snort.lua, in Lua format
  • The way of specifying HTTP URIs and headers changed from "modifiers" to "sticky buffers" (described below)
  • In Snort 3 you can split the inside of the rule's parentheses across multiple lines (in Snort 2 a \ is required at the end of each line)

Many explanations online are still written in Snort 2 syntax, so check which syntax they use before copying.

Basic structure of a rule

One rule expresses one detection condition. Here is a minimal example.

alert icmp any any -> $HOME_NET any (msg:"LOCAL ICMP echo request"; itype:8; sid:1000001; rev:1;)

What comes before the parentheses is the header, and what is inside the parentheses is the options.

Header

The header is arranged in the order "action protocol source-IP source-port direction destination-IP destination-port."

  • Action: Start by writing alert (detect and log). drop and reject make sense only when Snort runs inline (in the path of the traffic); in a monitoring-only setup they cannot stop traffic
  • Protocol: tcp, udp, icmp, ip and so on. In Snort 3 there is also a syntax that uses a service name such as http
  • IP and port: You can use any, 192.0.2.0/24, or variables such as $HOME_NET. Variables are defined in snort.lua (or in the default-value file it loads)
  • Direction: -> is one-way and <> is bidirectional

Many environments leave $HOME_NET set to any. That makes it impossible to tell inside from outside and changes what the rules mean, so set the range of your own network first.

Options

Options are written in the form key:value;, and the final semicolon is never omitted. These are the commonly used ones.

  • msg: The description shown in the alert. Write a sentence that the person reading the log later will understand
  • sid: The rule's identification number. Numbers below 1,000,000 are reserved for distributed rules, so use 1000001 and above for custom rules
  • rev: The rule's revision number. Increase it by one when you change the content
  • content: A string contained in the packet payload. nocase makes it case-insensitive
  • flow: Narrows the direction and state of the connection, as in to_server,established
  • classtype: The classification. It also affects the default priority

A rule using WordPress as the example

Example: Detect a burst of POSTs to wp-login.php

This example detects brute-force attempts against the login screen as a short-term burst from the same source.

alert tcp $EXTERNAL_NET any -> $HOME_NET $HTTP_PORTS (
    msg:"LOCAL WordPress wp-login.php POST burst";
    flow:to_server,established;
    http_method; content:"POST";
    http_uri; content:"/wp-login.php", nocase;
    detection_filter:track by_src, count 10, seconds 60;
    classtype:attempted-user;
    sid:1000010; rev:1;
)

http_method; and http_uri; are sticky buffers, which mean the content that follows is matched within the "method" or "URI" scope. In Snort 2 you attached a modifier after it, as in content:"/wp-login.php"; http_uri;. detection_filter is the condition to raise an alert "when there are more than 10 from the same source within 60 seconds."

You cannot see inside HTTPS traffic

This is the point where people most often get stuck. Snort cannot read the contents of encrypted traffic. If the site uses HTTPS and Snort sits between the internet and the server, the URI matching in the rule above will not match.

The countermeasure depends on your setup.

  • Terminate TLS at a load balancer or reverse proxy, and monitor the plaintext segment behind it with Snort
  • Limit yourself to rules that can be written with header information such as connection counts, ports and source IPs, rather than contents
  • Leave application-layer detection to a WAF or server logs (access logs, fail2ban and so on)

If you draw a diagram of which segment of traffic Snort is watching before deciding the rule policy, you avoid waste.

Tips for making rules run fast

  • Include at least one content. Snort narrows down candidates by content before checking the detailed conditions, so a rule with only pcre is slow
  • Choose a long, uncommon string for content. With only a short string such as GET, almost all traffic becomes a candidate
  • Narrow the direction with flow. Many rules do not need to inspect the response-side traffic

Add it to local.rules and load it

Collect custom rules in a file separate from the distributed rules (for example local.rules). This prevents them from being overwritten by updates to the distributed rules and lets you list the ones you wrote.

Load the rule file in the ips section of snort.lua. Change the path to match your environment.

ips =
{
    variables = default_variables,
    include = '/usr/local/etc/rules/local.rules',
}

If you already load the distributed rules through rules or similar, you add local.rules to that setting. Back up the configuration file before changing it, and then check where and what is being loaded.

Test it

1. Syntax check

Check that the configuration and rules can be loaded before starting in production.

snort -c /usr/local/etc/snort/snort.lua -T

If an error appears, look at the rule at the line number shown. Common causes are a forgotten semicolon, an unclosed quotation mark, and a duplicate sid.

2. Replay a pcap to confirm the match

Before trying on production traffic, confirm with a pcap file that records the traffic you want to detect. Generate the access in a test environment and save it with tcpdump.

sudo tcpdump -i eth0 -w test.pcap port 80

snort -c /usr/local/etc/snort/snort.lua -R /usr/local/etc/rules/local.rules \
  -r test.pcap -A alert_fast -k none

-r reads the pcap, and -A alert_fast prints alerts to the screen one per line. -k none skips checksum verification, so detection also works on a pcap recorded in an environment where the network card fills in checksums afterward.

There are two things to check: that an alert fires on the traffic you want to detect, and that none fires on normal everyday traffic (admin screen operations, plugin updates and so on). If you put a rule into production without checking the latter, a flood of false-positive alerts buries the real anomalies.

3. After going into production, watch it with alert first

Even in an inline setup, run with alert for a few days at first, and switch to drop after looking at the count and content. Find false positives that would block legitimate users while you are only detecting.

Stumbling points in operation

  • Conflicts between rule updates and custom rules: Distributed rules (such as the Talos rules) are generally updated with a tool such as PulledPork 3. If you keep custom rules in a separate file, updates will not erase them
  • Managing sids: Keep a record of who wrote what and why, in msg and in a separate record (such as Git). If you change the content without increasing rev, people reading the logs get confused
  • Suppressing false positives: When you want to exclude only a specific IP, consider whether Snort's suppress settings can handle it before rewriting the rule
  • Log volume: If there are too many alerts, narrow the rule's conditions or adjust how alerts are raised with detection_filter and similar

Summary

  • A rule is written with a header (action, protocol, IP, port, direction) and options (msg, content, sid, rev and so on). The sid of custom rules starts at 1000001
  • In Snort 3 you specify the matching scope with sticky buffers. Do not mix it with Snort 2 syntax
  • You cannot see inside HTTPS, so decide which segment Snort monitors before writing rules
  • Check syntax with -T, and use pcap replay to confirm that what should be detected is detected and that normal traffic does not trigger it, before going into production
  • In production, run with alert first, eliminate false positives, and then move on to blocking

Questions about WordPress? The publisher of this site is based in Japan and replies in Japanese. Contact (Japanese)

お気軽にご相談ください

お見積りへ お問い合わせへ
▲