Rule

STATE: unstable

TESTS: Playbook

API Docs: Core - Firewall

Service Docs: Rules

Prerequisites

You need to install the following plugin as OPNSense has no core-api for managing its firewall rules:

os-firewall

You can also install it using the ansibleguy.opnsense.package module.

Limitations

This plugin has some limitations you need to know of:

  • ports don’t support aliases

  • each of these parameters only takes ONE value per rule:

    • port

    • protocol (or ‘any’; ‘TCP/UDP’ is NOT valid)

    • ip-protocol (IPv4/IPv6)

    • direction

  • gateway-groups are not valid yet => see OPNSense Forum or OPNSense Issue

  • the ruleset managed by this plugin is SEPARATE from the default WEB-UI rules (Firewall - Rules) - combined usage might bring complications

  • interfaces must be provided as used in the network config (p.e. ‘opt1’ instead of ‘DMZ’)

    • per example see menu: ‘Interface - Assignments - Interface ID (in brackets)’

    • this brings problems if the interface-names are not the same on both nodes when using HA-setups

Info

Savepoint

You can prevent lockout-situations using the savepoint systems:

Mass-Manage

If you want to mass-manage rules - take a look at the ansibleguy.opnsense.rule_multi module. It scales better for that use-case!

Web-UI

These rules are shown in the separate WEB-UI table.

Menu: ‘Firewall - Automation - Filter’

Definition

Definition

Parameter

Type

Required

Default

Aliases

Comment

match_fields

list

true

-

-

Fields that are used to match configured rules with the running config - if any of those fields are changed, the module will think it’s a new rule. At least one of: ‘sequence’, ‘action’, ‘interface’, ‘direction’, ‘ip_protocol’, ‘protocol’, ‘source_invert’, ‘source_net’, ‘source_port’, ‘destination_invert’, ‘destination_net’, ‘destination_port’, ‘gateway’, ‘description’, ‘uuid’

sequence

int

false

1

seq

Sequence for rule processing, Integer between 1 and 1000000

action

string

false

‘pass’

a

Rule action. One of: ‘pass’, ‘block’ or ‘reject’

quick

boolean

false

true

q

When set to quick, the rule is handled on “first match” basis, which means that the first rule matching the packet will take precedence over rules following in sequence.

interface

list

false

[‘lan’]

i, int

One or multiple interfaces use this rule on

direction

string

false

‘in’

d, dir

Direction of the traffic. Traffic IN is coming into the firewall interface, while traffic OUT is going out of the firewall interface. In visual terms: [Source] -> IN -> [Firewall] -> OUT -> [Destination]. The default policy is to filter inbound traffic, which means the policy applies to the interface on which the traffic is originally received by the firewall from the source. This is more efficient from a traffic processing perspective. In most cases, the default policy will be the most appropriate.

ip_protocol

string

false

‘inet’

ipp, ip_proto

IP protocol to match. One of: ‘inet’, ‘inet6’ (IPv4 = ‘inet’, IPv6 = ‘inet6’)

protocol

string

false

‘any’

p, proto

Protocol like ‘TCP’, ‘UDP’, ‘ICMP’ and so on. For options see the WEB-UI. ‘TCP/UDP’ is NOT valid!

source_invert

boolean

false

false

si, src_inv, src_not

Inverted matching of the source

source_net

string

false

‘any’

s, src, source

Host, network, alias or ‘any’

source_port

string

false

-

sp, src_port

Leave empty to allow all, alias not supported

destination_invert

boolean

false

false

di, dest_inv, dest_not

Inverted matching of the destination

destination_net

string

false

‘any’

d, dest, destination

Host, network, alias or ‘any’

destination_port

string

false

-

dp, dest_port

Leave empty to allow all, alias not supported

gateway

string

false

-

g, gw

Existing gateway to use

log

boolean

false

true

l

If rule matches should be shown in the firewall logs

description

string

false

-

desc

Description for the rule

state

string

false

‘present’

st

State of the rule. One of: ‘present’, ‘absent’

enabled

boolean

false

true

en

If the rule should be en- or disabled

uuid

string

false

-

-

Optionally you can supply the uuid of an existing rule

reload

boolean

false

true

apply

If the running config should be reloaded on change - this may take some time. For mass-managing items you might want to reload it ‘manually’ after all changes are done => using the ansibleguy.opnsense.reload module.

For basic parameters see: Basic

Usage

First you will have to know about rule-matching.

The module somehow needs to link the configured and existing rules to manage them.

You need to set how this matching is done by setting the ‘match_fields’ parameter!

It is recommended to use/set unique identifiers like ‘description’ to make sure rules can be matched without overlapping.

You could also use the UUID of existing rules as ID - but you would have to pull (list) and configure those ‘manually’.

Examples

Basic

- hosts: localhost
  gather_facts: no
  module_defaults:
    group/ansibleguy.opnsense.all:
      firewall: 'opnsense.template.ansibleguy.net'
      api_credential_file: '/home/guy/.secret/opn.key'

    ansibleguy.opnsense.list:
      target: 'rule'

  tasks:
    - name: Example
      ansibleguy.opnsense.rule:
        source_net: '192.168.0.0/24'  # host, network, alias or 'any'
        destination_net: '192.168.10.0/24'
        destination_port: 443  # alias not supported, leave unset for 'any'
        protocol: 'TCP'
        description: 'Generic test'
        match_fields: ['description']
        # sequence: 1
        # action: 'pass'
        # quick: true
        # interface: 'lan'
        # direction: 'in'
        # ip_protocol: 'inet' or 'inet6'
        # source_invert: false
        # source_port: ''
        # destination_invert: false
        # log: true
        # gateway: 'LAN_GW'
        # state: 'present'
        # enabled: true
        # uuid: 'a9d85c00-0aa2-4705-b855-96aae16e05d7'  # optionally use uuid to identify existing rules
        # debug: true
        # reload: true

    - name: Listing
      ansibleguy.opnsense.list:
      #  target: 'rule'
      register: existing_entries

    - name: Printing rules
      ansible.bultin.debug:
        var: existing_entries.data

With inventory config

- hosts: localhost
  gather_facts: no
  module_defaults:
    group/ansibleguy.opnsense.all:
      firewall: 'opnsense.template.ansibleguy.net'
      api_credential_file: '/home/guy/.secret/opn.key'

    ansibleguy.opnsense.rule:
      match_fields: ['description']  # setting description as unique-id field

  # you may want to configure your rules inside the inventory
  vars:
    rules:
      wan_deny_tor_exit_nodes_ipv4:
        src: 'ALIAS_URLTABLE_TOR_EXIT_NODES'
        int: 'wan'
        action: 'block'
      wan_deny_tor_exit_nodes_ipv6:
        src: 'ALIAS_URLTABLE_TOR_EXIT_NODES'
        int: 'wan'
        action: 'block'
        ip_proto: 'inet6'
      lan_to_dmz_https:
        src: 'LAN_net'
        dest: 'DMZ_net'
        dest_port: 443
      lan_to_dmz_http:
        src: 'LAN_net'
        dest: 'DMZ_net'
        dest_port: 80
      internal_to_inet_http:
        src: '172.16.0.0/16'
        dest_invert: true
        dest: 'bogons'
        dest_port: 80
      internal_to_inet_https:
        src: '172.16.0.0/16'
        dest_invert: true
        dest: 'bogons'
        dest_port: 443

  tasks:
    - name: Test
      ansibleguy.opnsense.rule:
        description: "{{ rule_id }}"

        action: "{{ rule.action | default(omit) }}"
        interface: "{{ rule.int | default(omit) }}"
        direction: "{{ rule.dir | default(omit) }}"
        ip_protocol: "{{ rule.ip_proto | default(omit) }}"
        protocol: "{{ rule.proto | default(omit) }}"

        source_invert: "{{ rule.src_invert | default(omit) }}"
        source_net: "{{ rule.src | default(omit) }}"
        source_port: "{{ rule.src_port | default(omit) }}"
        destination_invert: "{{ rule.dest_invert | default(omit) }}"
        destination_net: "{{ rule.dest | default(omit) }}"
        destination_port: "{{ rule.dest_port | default(omit) }}"

        sequence: "{{ rule.seq | default(omit) }}"
        quick: "{{ rule.quick | default(omit) }}"
        log: "{{ rule.log | default(omit) }}"
        gateway: "{{ rule.gw | default(omit) }}"
        state: "{{ rule.state | default(omit) }}"
        enabled: "{{ rule.enabled | default(omit) }}"
        # debug: "{{ rule.debug | default(omit) }}"

      vars:
        rule: "{{ rule_item.value }}"
        rule_id: "{{ rule_item.key }}"

      loop_control:
        loop_var: rule_item
      with_dict: "{{ rules }}"

Purging

If you want to delete all existing rules that are NOT CONFIGURED.

You can also use the ansibleguy.opnsense.rule_purge module to do this in a cleaner way.

- hosts: localhost
  gather_facts: no
  module_defaults:
    group/ansibleguy.opnsense.all:
      firewall: 'opnsense.template.ansibleguy.net'
      api_credential_file: '/home/guy/.secret/opn.key'

    ansibleguy.opnsense.list:
      target: 'rule'

    ansibleguy.opnsense.rule:
      match_fields: ['description']

  vars:
    rules: {...}

  tasks:
    - name: Pulling existing rules
      ansibleguy.opnsense.list:
      #  target: 'rule'
      register: existing_entries

    - name: Purging unconfigured rules
      ansibleguy.opnsense.rule:
        state: 'absent'
        description: "{{ existing_rule_id }}"

      when: existing_rule_id not in rules

      vars:
        existing_rule_id: "{{ existing_rule_item.value.description }}"

      loop_control:
        loop_var: existing_rule_item
      with_dict: "{{ existing_entries.data }}"