OPNsense Destination Nat
This role manages Destination NAT (port forwarding) rules on OPNsense via the REST API. It enables forwarding external traffic to internal hosts and redirecting traffic between interfaces.
OPNsense Destination NAT Role
Overview
This role manages Destination NAT (port forwarding) rules on OPNsense via the REST API. It enables forwarding external traffic to internal hosts and redirecting traffic between 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
- Port Forwarding: Forward external traffic to internal services
- Traffic Redirection: Redirect traffic between interfaces (e.g., NTP redirect)
- Code as Configuration: Define DNAT rules in YAML
- Centralized Control: Manage all port forwarding 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
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 NAT rule permissions
What is Destination NAT?
Destination NAT (DNAT) modifies the destination address/port of incoming packets:
- Port Forwarding: External IP:port → Internal IP:port
- Traffic Redirection: Redirect traffic to a different destination
Example use cases:
- Forward WAN port 25565 to internal Minecraft server
- Redirect all NTP traffic from IoT devices to the firewall
- Expose internal web servers to the internet
Role Variables
Required Variables
| Variable | Required | Description |
|---|---|---|
vault_opnsense_bjoffrey_user_api_key | Yes | OPNsense API key (in vault) |
vault_opnsense_bjoffrey_user_api_secret | Yes | OPNsense API secret (in vault) |
opnsense_destination_nat_rules | Yes | List of DNAT rules |
Optional Variables
| Variable | Default | Description |
|---|---|---|
opnsense_destination_nat_validate_certs | true | Validate SSL certificates |
Rule Structure
Each rule in the list has these fields:
Required fields:
- sequence: "100" # Unique sequence number for idempotency
interface: "wan" # Incoming interface
destination_port: "25565" # External port to match
target: "192.168.x.x" # Internal IP to forward to
local_port: "25565" # Internal port to forward to
description: "Rule description"
Optional fields:
disabled: "0" # Disabled state (0=active, 1=disabled)
ipprotocol: "inet" # IP version (inet=IPv4, inet6=IPv6)
protocol: "tcp" # Protocol (tcp, udp, tcp/udp)
source_network: "" # Source network to match (empty=any)
source_port: "" # Source port to match (empty=any)
source_not: "0" # Invert source match
destination_network: "" # Destination address to match (WAN IP)
destination_not: "0" # Invert destination match
log: "1" # Log matched packets
Complete example:
opnsense_destination_nat_rules:
# Port forward from WAN to internal server
- sequence: "100"
disabled: "0"
interface: "wan"
protocol: "tcp/udp"
destination_network: "192.168.x.x"
destination_port: "25565"
target: "192.168.x.x"
local_port: "25565"
log: "1"
description: "Minecraft server"
# Redirect NTP traffic to firewall
- sequence: "200"
disabled: "0"
interface: "opt6"
protocol: "udp"
source_network: "opt6"
destination_port: "123"
target: "192.168.x.x"
local_port: "123"
log: "1"
description: "Redirect cameras NTP to firewall"
sequence
The sequence number is the key field for idempotency. It uniquely identifies a rule.
Purpose:
- Maps rules in vars to rules on the firewall
- Enables create/update/delete logic
- Must be unique across all DNAT rules
Important:
- Sequence numbers don’t need to be consecutive
- Use gaps (e.g., 100, 200, 300) to allow inserting rules later
- Removing a rule from vars will delete it from the firewall
- Changing a sequence number creates a new rule and orphans the old one
disabled vs enabled
Note: DNAT rules use disabled instead of enabled (inverted logic):
disabled: "0"= Rule is activedisabled: "1"= Rule is disabled
Dependencies
This role has no dependencies on other Ansible roles, but requires:
- OPNsense firewall with API enabled
- API key with NAT rule permissions
- Ansible Vault for storing API credentials
Example Playbook
Basic Usage
---
- name: Configure OPNsense Destination NAT Rules
hosts: mint-vm
gather_facts: false
vars_files:
- ../../roles/opnsense_destination_nat/vars/dnat_rules.yml
tasks:
- name: Configure destination NAT rules on OPNsense
ansible.builtin.include_role:
name: opnsense_destination_nat
Port Forwarding Example
---
- name: Configure Port Forwarding
hosts: mint-vm
gather_facts: false
vars:
opnsense_destination_nat_rules:
# Web server on port 443
- sequence: "100"
interface: "wan"
protocol: "tcp"
destination_port: "443"
target: "192.168.x.x"
local_port: "443"
log: "1"
description: "HTTPS to web server"
# SSH on non-standard port
- sequence: "200"
interface: "wan"
protocol: "tcp"
destination_port: "2222"
target: "192.168.x.x"
local_port: "22"
log: "1"
description: "SSH to admin server"
roles:
- opnsense_destination_nat
Traffic Redirection Example
---
- name: Configure Traffic Redirection
hosts: mint-vm
gather_facts: false
vars:
opnsense_destination_nat_rules:
# Force all DNS to firewall
- sequence: "100"
interface: "opt5"
protocol: "udp"
source_network: "opt5"
destination_port: "53"
target: "192.168.x.x"
local_port: "53"
log: "0"
description: "Redirect guest DNS to firewall"
# Force all NTP to firewall
- sequence: "200"
interface: "opt6"
protocol: "udp"
source_network: "opt6"
destination_port: "123"
target: "192.168.x.x"
local_port: "123"
log: "1"
description: "Redirect CCTV NTP to firewall"
roles:
- opnsense_destination_nat
What This Role Does
- Fetches existing rules via
/api/firewall/d_nat/search_rule - Builds sequence → UUID mapping for idempotency
- Creates new rules (sequence doesn’t exist) via
/api/firewall/d_nat/addRule - Updates existing rules (sequence exists but fields differ) via
/api/firewall/d_nat/setRule - Deletes orphaned rules (sequence exists on firewall but not in vars) via
/api/firewall/d_nat/delRule - Applies configuration via
/api/firewall/d_nat/apply - Displays summary of configured rules
OPNsense API Endpoints
Add DNAT Rule
POST /api/firewall/d_nat/addRule
Authorization: Basic (API key:secret)
Content-Type: application/json
Request body:
{
"rule": {
"disabled": "0",
"sequence": "100",
"interface": "wan",
"ipprotocol": "inet",
"protocol": "tcp",
"source.network": "",
"source.port": "",
"source.not": "0",
"destination.network": "192.168.x.x",
"destination.port": "25565",
"destination.not": "0",
"target": "192.168.x.x",
"local-port": "25565",
"log": "1",
"descr": "Minecraft server"
}
}
Response:
{
"result": "saved",
"uuid": "12345678-1234-1234-1234-123456789abc"
}
Update DNAT Rule
POST /api/firewall/d_nat/setRule/{uuid}
Authorization: Basic (API key:secret)
Content-Type: application/json
Request body: Same as addRule
Response:
{
"result": "saved"
}
Delete DNAT Rule
POST /api/firewall/d_nat/delRule/{uuid}
Authorization: Basic (API key:secret)
Response:
{
"result": "deleted"
}
Search Rules
GET /api/firewall/d_nat/search_rule
Authorization: Basic (API key:secret)
Returns all DNAT rules with their UUIDs and sequence numbers.
Apply Configuration
POST /api/firewall/d_nat/apply
Activates pending DNAT rule changes.
Idempotency
Role uses sequence-based idempotency:
- Each rule has a unique
sequencenumber - 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
- Firewall Rules: Remember to create corresponding firewall rules for forwarded traffic
- Logging: Enable logging for security-sensitive port forwards
Notes
become: falserecommended (no root needed)- Sequence numbers must be unique
- Removing a rule from vars will delete it from the firewall
- DNAT rules require corresponding firewall rules to allow the traffic
- Uses
disabledinstead ofenabled(inverted logic) - API field names use dots (
source.network) and hyphens (local-port)
Common Use Cases
Expose Web Server
- sequence: "100"
interface: "wan"
protocol: "tcp"
destination_port: "443"
target: "192.168.x.x"
local_port: "443"
description: "HTTPS to web server"
Game Server with Non-Standard Port
- sequence: "200"
interface: "wan"
protocol: "tcp/udp"
destination_port: "25565"
target: "192.168.x.x"
local_port: "25565"
description: "Minecraft server"
Redirect IoT DNS to Pi-hole
- sequence: "300"
interface: "opt6"
protocol: "udp"
source_network: "opt6"
destination_port: "53"
target: "192.168.x.x"
local_port: "53"
description: "Redirect IoT DNS to Pi-hole"
Force NTP to Firewall
- sequence: "400"
interface: "opt6"
protocol: "udp"
source_network: "opt6"
destination_port: "123"
target: "192.168.x.x"
local_port: "123"
description: "Force CCTV NTP to firewall"
Troubleshooting
”Authentication failed” errors
Cause: Invalid API key/secret
Solution: Verify credentials in vault and test API manually.
Rules not working
Check:
- Firewall rules exist to allow the forwarded traffic
- Target IP is correct and reachable
- Service is listening on the target port
- NAT reflection settings if accessing from internal network
Traffic not being forwarded
Check firewall logs:
- OPNsense → Firewall → Log Files → Live View
- Look for blocked traffic on the target port
Best Practices
- Use descriptive names: Make descriptions clear
- Log sensitive rules: Enable logging for external-facing services
- Disable unused rules: Set
disabled: "1"instead of deleting - Document purpose: Add comments in vars files
- Sequence gaps: Use 100, 200, 300 to allow insertions
- Review before running: Removing rules from vars deletes them
- Firewall rules: Always create matching firewall rules
- Version control: Store rules in git
Related Roles
This role is often used with:
- opnsense_firewall: Create firewall rules for forwarded traffic
- opnsense_source_nat: Configure outbound NAT
- opnsense_aliases: Use aliases in rules
License
MIT
Author
Created for homelab infrastructure management.