24.2. Firewall Ruleset Examples

Note

In Suricata 8 the firewall mode is experimental and subject to change.

24.2.1. HTTP

In this example a simple HTTP ruleset will be shown. It will allow HTTP to flow as long as:

  • method is GET or POST

  • User-Agent is "curl"

  • Status code is 200.

It starts by allowing the TCP port 80 traffic.

accept:hook tcp:all any any <> any 80 (sid:10;)

The stream tracking combined with the default exception policy handling will enforce a proper TCP handshake, etc.

The HTTP rules need to accept each state:

# allow traffic before the request line is complete
accept:hook http1:request_started any any -> any any (sid:100;)
# allow GET
accept:hook http1:request_line any any -> any any ( \
        http.method; content:"GET"; sid:101;)
# or allow POST
accept:hook http1:request_line any any -> any any ( \
        http.method; content:"POST"; sid:102;)
# allow User-Agent curl
accept:hook http1:request_headers any any -> any any ( \
        http.user_agent; content:"curl"; sid:103;)
# allow the body, if any
accept:hook http1:request_body any any -> any any (sid:104;)
# allow trailers, if any
accept:hook http1:request_trailer any any -> any any (sid:105;)
# allow completion
accept:hook http1:request_complete any any -> any any (sid:106;)

# allow traffic before the response line is complete
accept:hook http1:response_started any any -> any any (sid:200;)
# allow the 200 ok stat code.
accept:hook http1:response_line any any -> any any ( \
        http.stat_code; content:"200"; sid:201;)
# allow all other states
accept:hook http1:response_headers any any -> any any (sid:202;)
accept:hook http1:response_body any any -> any any (sid:203;)
accept:hook http1:response_trailer any any -> any any (sid:204;)
accept:hook http1:response_complete any any -> any any (sid:205;)

Each state needs an accept rule. Each state is evaluated at least once.

24.2.1.1. HTTP example with partially using default policies

In the example below: the config auto accepts various hooks, leaving just http1:request_line, http1:request_headers and http1:response_line for the ruleset to accept.

firewall:
  policies:
    app:
      http1:
        request-started:
          - "accept:hook"
        request-line:
          - "drop:flow"
          - "alert"
        request-headers:
          - "drop:flow"
          - "alert"
        request-body:
          - "accept:hook"
        request-trailer:
          - "accept:hook"
        request-complete:
          - "accept:hook"

        response-started:
          - "accept:hook"
        response-line:
          - "drop:flow"
          - "alert"
        response-headers:
          - "accept:hook"
        response-body:
          - "accept:hook"
        response-trailer:
          - "accept:hook"
        response-complete:
          - "accept:hook"
# allow GET
accept:hook http1:request_line any any -> any any ( \
        http.method; content:"GET"; sid:101;)
# or allow POST
accept:hook http1:request_line any any -> any any ( \
        http.method; content:"POST"; sid:102;)
# allow User-Agent curl
accept:hook http1:request_headers any any -> any any ( \
        http.user_agent; content:"curl"; sid:103;)

# allow the 200 ok stat code.
accept:hook http1:response_line any any -> any any ( \
        http.stat_code; content:"200"; sid:201;)

24.2.2. TLS SNI with complex TCP rules

In this example the packet:filter rules will be more opinionated about the traffic:

# allow 3-way handshake
accept:hook tcp:all $HOME_NET any -> $EXTERNAL_NET 443 (flags:S; \
        flow:not_established; flowbits:set,syn; sid:1;)
accept:hook tcp:all $EXTERNAL_NET 443 -> $HOME_NET any (flags:SA; \
        flow:not_established; flowbits:isset,syn; flowbits:set,synack; sid:2;)
accept:hook tcp:all $HOME_NET any -> $EXTERNAL_NET 443 (flags:A; \
        flow:not_established; flowbits:isset,synack;             \
        flowbits:unset,syn; flowbits:unset,synack; sid:3;)
# allow established
accept:hook tcp:all $HOME_NET any <> $EXTERNAL_NET 443 (flow:established; sid:4;)

Then on the TLS level this will be a TLS SNI firewall. Migrating from the old TLS state names covers migrating rules and config keys from the pre-phase state names.

Again all the states need to be accepted. Only in the client_hello state will there be additional constraints:

accept:hook tls:client_started $HOME_NET any -> $EXTERNAL_NET any (sid:100;)
# allow the good sites
accept:flow tls:client_hello $HOME_NET any -> $EXTERNAL_NET any (tls.sni; \
        pcre:"/^(suricata.io|oisf.net)$/R"; sid:101;)
# drop every ClientHello that carried an SNI the rule above does
# not allow; the SNI buffer is required, so a fragment still in
# flight (no SNI buffer yet) cannot match
drop:flow tls:client_hello $HOME_NET any -> $EXTERNAL_NET any (alert; tls.sni; \
        pcre:!"/^(suricata.io|oisf.net)$/R"; sid:102;)
# hello packets that carry no verdict of their own: the in-flight
# fragments
accept:hook tls:client_hello $HOME_NET any -> $EXTERNAL_NET any (sid:103;)
# no accept at the certificate state: a hello without an SNI matches
# no SNI rule and is decided by the default policy of the states it
# walked over (the implicit drop:flow) - the same outcome the base
# release gave it at the hello-completion milestone. If you want to
# accept SNI-less hellos, add an explicit accept here and audit the
# relaxation.
accept:hook tls:client_data $HOME_NET any -> $EXTERNAL_NET any (sid:105;)
accept:hook tls:client_finished $HOME_NET any -> $EXTERNAL_NET any (sid:106;)

accept:hook tls:server_started $EXTERNAL_NET any -> $HOME_NET any (sid:200;)
accept:hook tls:server_hello $EXTERNAL_NET any -> $HOME_NET any (sid:201;)
accept:hook tls:server_cert $EXTERNAL_NET any -> $HOME_NET any (sid:202;)
accept:hook tls:server_data $EXTERNAL_NET any -> $HOME_NET any (sid:204;)
accept:hook tls:server_finished $EXTERNAL_NET any -> $HOME_NET any (sid:205;)

The client_hello state names the ClientHello being parsed: it is entered at the first byte of the message, and its completion moves the state to client_cert. The SNI buffer is owned by the hello state, so tls.sni rules must live in client_hello even though the record that completes the hello is decided at client_cert: a flow that matches the SNI accept is carried over by the flow-scoped accept (101) and a flow whose SNI is not allowed is dropped by 102 on that same record - the drop is rule-scoped and alerts, where the implicit default policy would drop silently. The plain accept:hook at 103 carries the fragments in flight (the SNI buffer is not available before the message completes, so it also cannot gate them). A ClientHello that carries no SNI at all matches no SNI rule either - and with no accept at the certificate state it is decided by the default policy of the states it walked over (the implicit drop:flow), the same outcome the base release gave it at the hello-completion milestone. A flow whose hello is accepted by 101 is carried by the flow-scoped accept: the flow action is checked before any further firewall rule of a packet is evaluated, so the later states of that flow never reach the implicit default. client_started covers the packets before the hello.

24.2.2.1. TLS SNI with auto-accept logic

Rule that has the same effect as the 11 TLS rules above:

accept:flow tls:<client_hello $HOME_NET any -> $EXTERNAL_NET any (tls.sni; \
        pcre:"/^(suricata.io|oisf.net)$/R"; sid:101;)

Explanation: accept:flow accepts all of the TLS flow from the moment the rule has matched. The tls:client_started hook is auto-accepted by the use of the < modifier in the hook tls:<client_hello. The < modifier is only valid for firewall rules: a threat detection rule using it fails to load.

24.2.2.2. TLS SNI with auto-accept logic, plus disabling TD matching

To allow-list a connection to a specific SNI, w/o threat detection rules matching on this flow either, the example above can be extended by adding pass:flow as a secondary action:

accept:flow,pass:flow tls:<client_hello $HOME_NET any -> $EXTERNAL_NET any \
    (tls.sni; pcre:"/^(suricata.io|oisf.net)$/; sid:101;)

Explanation: as soon as this rule fully matches at the tls:client_hello hook, a pass is applied to the flow effectively bypassing the threat detection engine.