OPNsense Firewall

This role manages firewall rules across multiple network interfaces on OPNsense via the REST API. It supports configuration for VLANs (VLAN10-22), WAN, LAN, and WireGuard interfaces.

Ansible Bash Centreon DNS Grafana HTTPS InfluxDB JSON

OPNsense Firewall Role

Overview

This role manages firewall rules across multiple network interfaces on OPNsense via the REST API. It supports configuration for VLANs (VLAN10-22), WAN, LAN, and WireGuard interfaces. The role provides full lifecycle management: creating new rules, updating existing rules when configuration changes, and deleting orphaned rules that exist on the firewall but are no longer defined in the vars files.

Purpose

  • Multi-Interface Management: Configure rules for 10+ interfaces
  • Code as Configuration: Define firewall rules in YAML
  • Centralized Control: Manage all firewall rules from Ansible
  • API-Based: Reliable automation via OPNsense REST API
  • Full Lifecycle Management: Create, update, and delete rules
  • Idempotent: Safe to run multiple times using sequence-based idempotency
  • Automatic Cleanup: Removes orphaned rules not defined in vars
  • Network Segmentation: Enforce VLAN security policies
  • Logging: Optional traffic logging per rule

Requirements

  • Ansible 2.9 or higher
  • OPNsense firewall with API access enabled
  • API key and secret stored in Ansible Vault
  • Network connectivity to OPNsense (VLAN10)
  • OPNsense user with firewall rule permissions

What are Firewall Rules?

Firewall rules control network traffic by:

  • Allowing or blocking packets
  • Based on source, destination, protocol, port
  • Processed in order (first match wins with quick rules)
  • Applied per network interface

Rule example: “Allow HTTPS from VLAN12 to Internet”

Role Variables

Required Variables

VariableRequiredDescription
vault_opnsense_bjoffrey_user_api_keyYesOPNsense API key (in vault)
vault_opnsense_bjoffrey_user_api_secretYesOPNsense API secret (in vault)
opnsense_firewall_rulesYesDictionary of rules by interface

Optional Variables

VariableDefaultDescription
opnsense_firewall_validate_certstrueValidate SSL certificates
opnsense_firewall_vlan10_rules[]VLAN10 rules (management)
opnsense_firewall_vlan12_rules[]VLAN12 rules (servers)
opnsense_firewall_vlan14_rules[]VLAN14 rules (desktops)
opnsense_firewall_vlan16_rules[]VLAN16 rules (WiFi trusted)
opnsense_firewall_vlan18_rules[]VLAN18 rules (WiFi guest)
opnsense_firewall_vlan20_rules[]VLAN20 rules (WiFi CCTV)
opnsense_firewall_vlan22_rules[]VLAN22 rules (ethernet guest)
opnsense_firewall_wg0_rules[]WireGuard VPN rules
opnsense_firewall_lan_rules[]LAN interface rules
opnsense_firewall_wan_rules[]WAN interface rules

Variable Details

opnsense_firewall_rules

Combined dictionary of all firewall rules organized by interface.

Structure:

opnsense_firewall_rules:
  vlan10:
    - rule1
    - rule2
  vlan12:
    - rule1
  wan:
    - rule1

Typically constructed in playbook:

opnsense_firewall_rules:
  vlan10: "{{ opnsense_firewall_vlan10_rules }}"
  vlan12: "{{ opnsense_firewall_vlan12_rules }}"
  wan: "{{ opnsense_firewall_wan_rules }}"

Rule Structure

Each rule in a list has these fields:

Required fields:

- sequence: "1"                # Unique sequence number for idempotency
  action: pass|block|reject
  interface: vlan10|vlan12|lan|wan|wg0
  description: "Rule description"

Optional fields:

  enabled: "1"               # Enable (1) or disable (0)
  quick: "1"                 # Stop processing on match
  direction: in|out          # Traffic direction
  ipprotocol: inet|inet6     # IPv4 or IPv6
  protocol: tcp|udp|icmp     # Protocol
  source_net: any|alias|CIDR # Source address
  source_port: port          # Source port
  source_not: "0"            # Invert source match
  destination_net: any|alias|CIDR  # Destination
  destination_port: port     # Destination port
  destination_not: "0"       # Invert dest match
  log: "1"                   # Log matched packets
  statetype: keep|sloppy|synproxy|none  # State tracking
  categories: "CategoryName" # Assign rule to a category (by name)

Complete example:

opnsense_firewall_vlan12_rules:
  - sequence: "1"
    action: pass
    interface: vlan12
    description: "Allow VLAN12 to Internet HTTPS"
    protocol: tcp
    source_net: vlan12
    destination_net: any
    destination_port: "443"
    log: "1"
    categories: "Web"

  - sequence: "2"
    action: block
    interface: vlan12
    description: "Block VLAN12 to VLAN10"
    source_net: vlan12
    destination_net: vlan10
    log: "1"
    categories: "Block"

sequence

The sequence number is the key field for idempotency. It uniquely identifies a rule within an interface.

Purpose:

  • Maps rules in vars to rules on the firewall
  • Enables create/update/delete logic
  • Must be unique per interface (can reuse across interfaces)

Example:

opnsense_firewall_vlan12_rules:
  - sequence: "1"   # First rule for VLAN12
    action: pass
    ...
  - sequence: "2"   # Second rule for VLAN12
    action: pass
    ...
  - sequence: "99"  # Block rule at end
    action: block
    ...

Important:

  • Sequence numbers don’t need to be consecutive
  • Use gaps (e.g., 10, 20, 30) to allow inserting rules later
  • Removing a rule from vars (but keeping its sequence on the firewall) will delete it
  • Changing a sequence number creates a new rule and orphans the old one (which gets deleted)

Dependencies

This role has no dependencies on other Ansible roles, but requires:

  • OPNsense firewall with API enabled
  • API key with firewall management permissions
  • Ansible Vault for storing API credentials
  • Firewall aliases defined (if using aliases in rules)
  • Firewall categories created (if using categories in rules) - see opnsense_firewall_categories role

Example Playbook

Basic Usage

---
- name: Configure OPNsense Firewall Rules
  hosts: localhost
  become: false

  vars:
    opnsense_firewall_rules:
      vlan12:
        - sequence: "1"
          action: pass
          interface: vlan12
          description: "Allow VLAN12 HTTPS to Internet"
          protocol: tcp
          destination_port: "443"

  roles:
    - opnsense_firewall

Multiple Interfaces

---
- name: Configure Multi-Interface Firewall
  hosts: localhost
  become: false

  vars:
    opnsense_firewall_vlan10_rules:
      - sequence: "1"
        action: pass
        interface: vlan10
        description: "Allow management to all"
        source_net: vlan10

    opnsense_firewall_vlan12_rules:
      - sequence: "1"
        action: pass
        interface: vlan12
        description: "Allow servers to Internet"
        protocol: tcp
        destination_port: "443"

      - sequence: "2"
        action: block
        interface: vlan12
        description: "Block servers to management"
        destination_net: vlan10
        log: "1"

    opnsense_firewall_rules:
      vlan10: "{{ opnsense_firewall_vlan10_rules }}"
      vlan12: "{{ opnsense_firewall_vlan12_rules }}"

  roles:
    - opnsense_firewall

Using Aliases

---
- name: Configure Firewall with Aliases
  hosts: localhost
  become: false

  vars:
    opnsense_firewall_vlan12_rules:
      - sequence: "1"
        action: pass
        interface: vlan12
        description: "Allow servers to web ports"
        source_net: Internal_Servers  # Alias
        destination_port: Web_Ports   # Alias
        protocol: tcp

  roles:
    - opnsense_aliases  # Create aliases first
    - opnsense_firewall # Then create rules using them

What This Role Does

  1. Iterates through interfaces:

    • vlan10, vlan12, vlan14, vlan16, vlan18, vlan20, vlan22
    • wg0, lan, wan
  2. For each interface with defined rules:

    • Fetches existing categories via /api/firewall/category/searchItem and builds name → UUID mapping
    • Fetches existing rules via /api/firewall/filter/search_rule
    • Builds sequence → UUID mapping for idempotency
    • Creates new rules (sequence doesn’t exist) via /api/firewall/filter/addRule
    • Updates existing rules (sequence exists but fields differ) via /api/firewall/filter/setRule
    • Deletes orphaned rules (sequence exists on firewall but not in vars) via /api/firewall/filter/delRule
    • Applies configuration via /api/firewall/filter/apply
  3. Displays summary: Lists interfaces configured

OPNsense API Endpoints

Add Firewall Rule

POST /api/firewall/filter/addRule
Authorization: Basic (API key:secret)
Content-Type: application/json

Request body:

{
  "rule": {
    "enabled": "1",
    "action": "pass",
    "quick": "1",
    "interface": "vlan12",
    "direction": "in",
    "ipprotocol": "inet",
    "protocol": "tcp",
    "source_net": "any",
    "destination_net": "any",
    "destination_port": "443",
    "log": "1",
    "description": "Allow HTTPS",
    "statetype": "keep",
    "categories": "category-uuid-here"
  }
}

Response:

{
  "result": "saved",
  "uuid": "12345678-1234-1234-1234-123456789abc"
}

Update Firewall Rule

POST /api/firewall/filter/setRule/{uuid}
Authorization: Basic (API key:secret)
Content-Type: application/json

Request body: Same as addRule

Response:

{
  "result": "saved"
}

Delete Firewall Rule

POST /api/firewall/filter/delRule/{uuid}
Authorization: Basic (API key:secret)

Response:

{
  "result": "deleted"
}

Search Rules by Interface

GET /api/firewall/filter/search_rule?interface={interface_id}
Authorization: Basic (API key:secret)

Returns all rules for an interface with their UUIDs and sequence numbers.

Apply Configuration

POST /api/firewall/filter/apply

Activates pending firewall rule changes.

Firewall Rule Fields

action

What to do with matching traffic.

Values:

  • pass: Allow traffic
  • block: Silently drop traffic
  • reject: Drop and send rejection (RST/ICMP)

Example:

action: pass  # Allow traffic

interface

Which network interface this rule applies to.

Values: vlan10, vlan12, vlan14, vlan16, vlan18, vlan20, vlan22, wg0, lan, wan

Homelab network design:

  • VLAN10: Management (192.168.x.x/24)
  • VLAN12: Servers (192.168.x.x/24)
  • VLAN14: Desktops (192.168.x.x/24)
  • VLAN16: WiFi Trusted (192.168.x.x/24)
  • VLAN18: WiFi Guest (192.168.x.x/24)
  • VLAN20: WiFi CCTV (192.168.x.x/24)
  • VLAN22: Ethernet Guest (192.168.x.x/24)
  • wg0: WireGuard VPN
  • lan: Internal LAN (172.16.x.x/24)
  • wan: Internet-facing

quick

Stop processing rules after first match.

Default: "1" (enabled)

Values:

  • "1": First match wins (typical)
  • "0": Continue processing (rare)

direction

Traffic direction relative to interface.

Default: in

Values:

  • in: Incoming to interface
  • out: Outgoing from interface

protocol

Network protocol.

Common values: tcp, udp, icmp, icmp6

Omit for any protocol.

source_net / destination_net

Source or destination address.

Values:

  • any: Any address
  • vlan10: Interface subnet
  • Alias name: Internal_Servers
  • CIDR: 192.168.x.x/24
  • Single IP: 192.168.x.x

source_port / destination_port

TCP/UDP port numbers.

Values:

  • Single port: "443"
  • Port range: "8000-8999"
  • Alias: Web_Ports

Omit for any port.

log

Log matched packets.

Default: "1" (enabled)

Values:

  • "1": Log matches
  • "0": Don’t log

Logs location: OPNsense → Firewall → Log Files

statetype

Connection state tracking.

Default: keep

Values:

  • keep: Stateful (track connections)
  • sloppy: Relaxed state matching
  • synproxy: Proxy TCP handshake
  • none: Stateless

categories

Assign a rule to a firewall category for organization and filtering.

Usage: Specify the category name (not UUID). The role automatically looks up the UUID.

Prerequisites: Categories must exist in OPNsense before assigning them to rules. Use the opnsense_firewall_categories role to create them first.

Example categories:

  • DNS: DNS and NTP related rules
  • Web: HTTP/HTTPS access rules
  • Management: SSH, RDP, SMB, SFTP rules
  • Monitoring: Grafana, Centreon, InfluxDB, SNMP rules
  • Block: All block/deny rules

Example:

- sequence: "1"
  action: pass
  interface: vlan12
  description: "Allow DNS requests"
  protocol: udp
  destination_port: "53"
  categories: "DNS"

- sequence: "2"
  action: pass
  interface: vlan12
  description: "Allow HTTPS to Grafana"
  protocol: tcp
  destination_port: "3000"
  categories: "Monitoring"

Benefits:

  • Filter rules by category in OPNsense UI
  • Color-coded rule visualization
  • Easier rule auditing and management

Rule Processing Order

OPNsense processes rules top to bottom:

  1. First matching rule with quick: "1" wins
  2. If no quick rules match, last matching rule wins
  3. Default rule: Block all (implicit at end)

Example:

# Rule 1 (quick): Allow HTTPS
- sequence: "1"
  action: pass
  quick: "1"
  destination_port: "443"

# Rule 2: Block all
- sequence: "2"
  action: block
  quick: "1"

Result: HTTPS allowed (Rule 1), everything else blocked (Rule 2)

Network Segmentation Example

Typical homelab segmentation:

# Management VLAN: Allow all
opnsense_firewall_vlan10_rules:
  - sequence: "1"
    action: pass
    interface: vlan10
    description: "Allow management to everything"

# Servers VLAN: Internet only, block inter-VLAN
opnsense_firewall_vlan12_rules:
  - sequence: "1"
    action: pass
    interface: vlan12
    description: "Allow servers to Internet"
    destination_net: "!vlan10,!vlan12,!vlan14"

  - sequence: "2"
    action: block
    interface: vlan12
    description: "Block servers inter-VLAN"
    log: "1"

# Guest VLAN: Internet only, block everything else
opnsense_firewall_vlan18_rules:
  - sequence: "1"
    action: pass
    interface: vlan18
    description: "Allow guest to Internet only"
    destination_net: "!vlan10,!vlan12,!vlan14,!vlan16"

  - sequence: "2"
    action: block
    interface: vlan18
    description: "Block guest to internal"
    log: "1"

Using Aliases in Rules

Aliases simplify rules:

Without alias:

source_net: "192.168.x.x,192.168.x.x,192.168.x.x"

With alias:

source_net: Internal_Servers

Prerequisites: Create aliases first (see opnsense_aliases role).

Logging Considerations

Enable logging (log: "1") for:

  • ✅ Block rules (see what’s denied)
  • ✅ Security-critical rules
  • ✅ Troubleshooting rules

Disable logging (log: "0") for:

  • ❌ High-traffic rules (performance)
  • ❌ Well-established rules

View logs: OPNsense → Firewall → Log Files → Live View

Idempotency

Role uses sequence-based idempotency:

  • Each rule has a unique sequence number per interface
  • Sequence numbers map to UUIDs on the firewall
  • Create: New sequence → creates rule
  • Update: Existing sequence with changed fields → updates rule
  • Delete: Sequence on firewall but not in vars → deletes rule
  • Only applies configuration if changes were made
  • Safe to run repeatedly

Important: Removing a rule from your vars file will delete it from the firewall on the next run.

Security Considerations

  • API Credentials: Stored in Ansible Vault
  • HTTPS: Uses SSL/TLS for API calls
  • Basic Auth: API key/secret authentication
  • Certificate Validation: Enabled by default
  • Least Privilege: Only open necessary ports
  • Default Deny: Implicit block all at end

Tags

This role does not define any tags. Use playbook-level tags if needed:

- hosts: localhost
  roles:
    - opnsense_firewall
  tags:
    - opnsense
    - firewall
    - rules
    - security

Notes

  • become: false recommended (no root needed)
  • Rules processed per interface in main.yml loop
  • Configuration applied per interface after any changes
  • Sequence numbers must be unique per interface
  • Removing a rule from vars will delete it from the firewall
  • Rule order matters (first match wins with quick)
  • Default implicit rule: Block all

Troubleshooting

”Authentication failed” errors

Cause: Invalid API key/secret

Solution: Verify credentials in vault and test API manually.

Rules not appearing in firewall

Cause: Apply configuration not called

Solution: Check role output for apply task execution.

Traffic blocked unexpectedly

Check rule order:

# Via OPNsense UI
# Firewall → Rules → Interface
# Review rule order

Check logs:

# Firewall → Log Files → Live View
# Look for blocked traffic

Rule conflicts

Cause: Multiple rules matching same traffic

Solution: Review rule order, ensure quick rules are correct.

Testing the Role

Verify Rules Created

Via OPNsense UI:

  1. Log into OPNsense
  2. Firewall → Rules → [Interface]
  3. Should see configured rules

Via API:

curl -u "key:secret" \
  https://opnsense-ip/api/firewall/filter/searchRule | jq

Test Traffic

# Test allowed traffic
curl https://example.com

# Test blocked traffic (should timeout/fail)
curl http://blocked-host

# Check firewall logs
# Firewall → Log Files → Live View

Best Practices

  1. Document rules: Use descriptive descriptions
  2. Default deny: Block by default, allow explicitly
  3. Log blocks: Monitor denied traffic
  4. Use aliases: Simplify rule management
  5. Group related rules: Organize by purpose
  6. Test incrementally: Add rules gradually
  7. Review logs: Monitor firewall activity
  8. Least privilege: Only allow necessary traffic
  9. Network segmentation: Isolate VLANs
  10. Version control: Store rules in git
  11. Sequence management: Use consistent sequence numbers per interface
  12. Review before running: Removing rules from vars will delete them from the firewall

This role is often used with:

  • opnsense_aliases: Create aliases before rules
  • opnsense_firewall_categories: Create categories before assigning them to rules
  • opnsense_source_nat: Configure outbound NAT

License

MIT

Author

Created for homelab infrastructure management.