OPNsenseXLIII · Ansible-Driven Firewall ConfigurationAnsible foundations
OPNsense Ansible collection — ansibleguy.opnsense modules, installation, and version pinning
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
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 area | Modules |
|---|---|
| Aliases | alias, alias_multi, alias_purge |
| Firewall rules | rule, rule_multi, rule_purge, rule_interface_group |
| NAT | nat_source, nat_one_to_one |
| Interfaces | interface_vlan, interface_vip, interface_lagg, interface_bridge, interface_gif, interface_gre, interface_vxlan, interface_loopback |
| Routing | route, gateway, neighbor |
| DNS — Unbound | unbound_general, unbound_host, unbound_host_alias, unbound_forward, unbound_dot, unbound_acl, unbound_dnsbl |
| DNS — Dnsmasq and BIND | dnsmasq_general, dnsmasq_host, dnsmasq_domain, dnsmasq_range, bind_domain, bind_record, bind_acl, bind_blocklist |
| DHCP | dhcp_general, dhcp_subnet, dhcp_reservation, dhcrelay_relay, dhcrelay_destination |
| VPN | wireguard_server, wireguard_peer, wireguard_general, openvpn_server, openvpn_client, ipsec_connection, ipsec_child, ipsec_psk, ipsec_vti |
| Users and access | user, group, privilege |
| System | system, service, package, cron, syslog, monit_service, monit_test, monit_alert |
| IDS and proxy | ids_general, ids_rule, ids_policy, webproxy_general, webproxy_acl, webproxy_forward |
| Traffic shaping | shaper_pipe, shaper_queue, shaper_rule |
| Generic | list, 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.
$ ansible-doc -l ansibleguy.opnsense | head -12ansibleguy.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 servicesIllustrative 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:
- Use
collections/next to the play. Many production repositories have acollections/directory at the top level, withcollections_path = ./collectionsinansible.cfg. Run from the repository root. - Install in the user’s default location.
ansible-galaxy collection install ... -p ~/.ansible/collectionsputs the collection in the user’s default search path. This is the dev-machine pattern.
$ ansible-galaxy collection install -r collections/requirements.yml -p collections/; echo '---'; ANSIBLE_COLLECTIONS_PATH=./collections ansible-doc ansibleguy.opnsense.rule | head -24Starting 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: listIllustrative 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:
- The collection version is pinned. Verify in
collections/requirements.yml. Re-running the install must not change the version. - The modules work against the running firmware.
--check --diffon one firewall surfaces the mismatches that version numbers cannot predict, and it does it without writing anything. - 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.
$ ansible-playbook playbooks/firewall-rules.yml --limit fw-canary --check --diffPLAY [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=0Illustrative output
Summary
ansibleguy.opnsenseis 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, plusrawfor anything with no module yet.- It runs on the controller and needs
httpxthere. Nothing is installed on the firewall. - Install from Galaxy with an explicit version pin recorded in
collections/requirements.yml, or from a Git tag.latestis not a pin. - There is no published collection-to-firmware compatibility table, and the maintainer does not track API changes actively. A
--check --diffdry 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
Q1. You are about to run an Ansible playbook against 30 OPNsense firewalls. Which installation approach is the most production-safe?
Q2. The ansibleguy.opnsense modules run on the OPNsense firewall itself, so the firewall needs Python and the collection installed on it.
Q3. Which of the following are valid steps to validate the collection before a fleet run? Select all that apply.
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.