Skip to main content
RunBook Academy

OPNsenseXLIII · Ansible-Driven Firewall ConfigurationAnsible foundations

OPNsense Ansible collection — ansibleguy.opnsense modules, installation, and version pinning

Intermediate⏱ ~14 minansibleansible-galaxygitansible-lint

What you'll learn

  • Install ansibleguy.opnsense from Galaxy or from source and pin its version reproducibly
  • List the modules that ship with the collection and the configuration area each one addresses
  • Explain how match_fields gives the modules their idempotency, and what breaks when it changes
  • Validate the collection against the running OPNsense version before committing to a fleet run

Prerequisites

Verified against OPNsense 25.x · FreeBSD 14.x · PF (FreeBSD packet filter) FreeBSD 14.x · Unbound 1.20+ · Kea DHCP OPNsense 25.x plugin · WireGuard in-kernel + OPNsense plugin · strongSwan (IPsec plugin) OPNsense 25.x plugin · OpenVPN 2.6.x · Suricata 7.x · 2026-08-14

Not yet marked complete on this device.

The collection in production use is ansibleguy.opnsense — a community-maintained set of modules that wrap the OPNsense REST API into Ansible’s task model. It is not OPNsense’s first-party code, and OPNsense does not publish an Ansible collection of its own. As with any community module, the discipline is: pin a known version, validate against the running OPNsense firmware, and watch for breaking changes when upgrading either side.

This lesson covers what the collection provides, how to install it, why version pinning matters for production safety, and how to validate the collection against the OPNsense version before committing to a fleet run.

What the collection provides

The collection is broad — roughly 150 modules — and organised by the configuration area each one addresses. The FQCN is ansibleguy.opnsense.<module>:

Configuration areaModules
Aliasesalias, alias_multi, alias_purge
Firewall rulesrule, rule_multi, rule_purge, rule_interface_group
NATnat_source, nat_one_to_one
Interfacesinterface_vlan, interface_vip, interface_lagg, interface_bridge, interface_gif, interface_gre, interface_vxlan, interface_loopback
Routingroute, gateway, neighbor
DNS — Unboundunbound_general, unbound_host, unbound_host_alias, unbound_forward, unbound_dot, unbound_acl, unbound_dnsbl
DNS — Dnsmasq and BINDdnsmasq_general, dnsmasq_host, dnsmasq_domain, dnsmasq_range, bind_domain, bind_record, bind_acl, bind_blocklist
DHCPdhcp_general, dhcp_subnet, dhcp_reservation, dhcrelay_relay, dhcrelay_destination
VPNwireguard_server, wireguard_peer, wireguard_general, openvpn_server, openvpn_client, ipsec_connection, ipsec_child, ipsec_psk, ipsec_vti
Users and accessuser, group, privilege
Systemsystem, service, package, cron, syslog, monit_service, monit_test, monit_alert
IDS and proxyids_general, ids_rule, ids_policy, webproxy_general, webproxy_acl, webproxy_forward
Traffic shapingshaper_pipe, shaper_queue, shaper_rule
Genericlist, reload, raw

The last row is the escape hatch. list reads any supported object type, reload triggers a service reload, and raw issues an arbitrary API call for the areas that have no module yet — useful, and correspondingly unprotected: raw gives up the idempotency and parameter validation that make the other modules worth using.

Modules carry a development state — stable, unstable, or testing — and the README states it per module. rule and alias are stable; the purge variants are marked unstable. That marking is worth reading before a fleet run, because it is the maintainer’s own statement about how much production exposure a module has had.

Read-only / Safelist collection modules
$ ansible-doc -l ansibleguy.opnsense | head -12
ansibleguy.opnsense.alias              Manage OPNsense aliases
ansibleguy.opnsense.alias_multi        Manage multiple OPNsense aliases
ansibleguy.opnsense.alias_purge        Purge OPNsense aliases
ansibleguy.opnsense.cron               Configure cron jobs
ansibleguy.opnsense.gateway            Configure routing gateways
ansibleguy.opnsense.list               List configured items
ansibleguy.opnsense.raw                Manage OPNsense configuration items using an unsupported module
ansibleguy.opnsense.reload             Reload running configuration of a specific module
ansibleguy.opnsense.route              Configure static routes
ansibleguy.opnsense.rule               Manage OPNsense firewall rules
ansibleguy.opnsense.rule_multi         Manage multiple OPNsense firewall rules
ansibleguy.opnsense.service            Manage services

Illustrative output

Installation

The collection talks to the firewall over HTTPS using the Python httpx library, so that dependency has to be present on the controller — not on the firewall, which never has Ansible or Python-for-Ansible installed on it at all:

python3 -m pip install --upgrade httpx

Then the collection itself, pinned:

ansible-galaxy collection install ansibleguy.opnsense:1.2.16 -p collections/

Without the :1.2.16 suffix, ansible-galaxy installs whatever is latest at that moment, which is not a thing a production controller should depend on. The same pin belongs in collections/requirements.yml so the install is reproducible:

---
collections:
  - name: ansibleguy.opnsense
    version: 1.2.16

For the playbook to find the collection, two routes:

  1. Use collections/ next to the play. Many production repositories have a collections/ directory at the top level, with collections_path = ./collections in ansible.cfg. Run from the repository root.
  2. Install in the user’s default location. ansible-galaxy collection install ... -p ~/.ansible/collections puts the collection in the user’s default search path. This is the dev-machine pattern.
Read-only / Safeinstall from requirements and read the module
$ ansible-galaxy collection install -r collections/requirements.yml -p collections/; echo '---'; ANSIBLE_COLLECTIONS_PATH=./collections ansible-doc ansibleguy.opnsense.rule | head -24
Starting galaxy collection install process
Installing 'ansibleguy.opnsense:1.2.16' to '/srv/fw-iac/collections/ansible_collections/ansibleguy/opnsense'
ansibleguy.opnsense:1.2.16 was installed successfully
---
> ANSIBLEGUY.OPNSENSE.RULE    (.../plugins/modules/rule.py)

      Manage OPNsense firewall rules

OPTIONS (= is mandatory):

= description
      Unique description used to identify existing rules
      type: str

= match_fields
      Fields that are used to match configured rules with the
      running config
      choices: [sequence, action, interface, direction,
                ip_protocol, protocol, source_invert,
                source_net, source_port, destination_invert,
                destination_net, destination_port, gateway,
                description, uuid]
      type: list

Illustrative output

Installation from source

ansible-galaxy will install straight from the repository, which is how you get a fix that is merged but not yet released:

# latest commit on the default branch
ansible-galaxy collection install git+https://github.com/O-X-L/ansible_opnsense.git

# a specific tag
ansible-galaxy collection install git+https://github.com/O-X-L/ansible_opnsense.git,1.2.16

Production discipline: pin a tag, not a branch. A branch pin moves without anyone deciding that it should, and the move arrives on whichever controller next refreshes its collections — which is not a change window anyone scheduled.

Version compatibility

There is no published table mapping collection versions to OPNsense releases, and it is worth understanding why rather than looking for one.

The collection targets the current OPNsense release. The maintainer states plainly that the project is unfunded and does not actively track API changes, so when OPNsense alters an endpoint or a field name, the collection follows only once someone reports the breakage. The compatibility question is therefore not “which version pairs with which” but “has anyone exercised this module against the firmware I am running”.

Two consequences for a production estate:

  • Newer collection, older firmware is the common shape after a controller update, and it fails in the direction of the module sending a field the older API does not accept.
  • Older collection, newer firmware is what happens after a firewall upgrade, and it fails more quietly: the module’s payload can still be accepted while silently omitting something the newer model expects.

Neither is detectable by reading version numbers. Both are detectable by a dry run against one firewall, which is why the validation step below is not optional ceremony.

Validate before a fleet run

Three checks before running a playbook against a fleet:

  1. The collection version is pinned. Verify in collections/requirements.yml. Re-running the install must not change the version.
  2. The modules work against the running firmware. --check --diff on one firewall surfaces the mismatches that version numbers cannot predict, and it does it without writing anything.
  3. The credentials have the privileges the modules need. A dry run authenticates and reads, which proves the pair works but not that it can write. A canary firewall — one real run, one real change, reverted — is what proves that.
Read-only / Safedry run against one firewall
$ ansible-playbook playbooks/firewall-rules.yml --limit fw-canary --check --diff
PLAY [firewalls] ***************************************************

TASK [Allow CI runners to the build cache] *************************
--- before
+++ after
@@ -1,3 +1,7 @@
-{}
+{
+    "description": "CHG-2026-1314 ci runners",
+    "destination_net": "10.20.0.0/24",
+    "source_net": "ci_runners"
+}
changed: [fw-canary]

PLAY RECAP *********************************************************
fw-canary   : ok=1  changed=1  unreachable=0  failed=0

Illustrative output

Summary

  • ansibleguy.opnsense is the community collection in production use. It wraps the REST API into around 150 modules covering rules, aliases, NAT, interfaces, routing, DNS, DHCP, VPN, users, services and IDS, plus raw for anything with no module yet.
  • It runs on the controller and needs httpx there. Nothing is installed on the firewall.
  • Install from Galaxy with an explicit version pin recorded in collections/requirements.yml, or from a Git tag. latest is not a pin.
  • There is no published collection-to-firmware compatibility table, and the maintainer does not track API changes actively. A --check --diff dry run against one firewall is what stands in for one.
  • Idempotency comes from match_fields, which defines how a module recognises the object it manages. Choose it deliberately and keep it stable.

Knowledge check · 4 questions

  1. Q1. You are about to run an Ansible playbook against 30 OPNsense firewalls. Which installation approach is the most production-safe?

  2. Q2. The ansibleguy.opnsense modules run on the OPNsense firewall itself, so the firewall needs Python and the collection installed on it.

  3. Q3. Which of the following are valid steps to validate the collection before a fleet run? Select all that apply.

  4. Q4. A playbook has managed a set of rules for months with match_fields: [description]. Someone tidies the descriptions in the playbook and re-runs it. What happens?

Passing score: 75%. Answers are checked in this browser.