This Ansible playbook automates the configuration of firewall rules on a Unifi Cloud Gateway using a data-driven approach. Rules are defined in YAML format for easy maintenance and updates.
The playbook creates a secure firewall ruleset that:
- Implements a default deny policy for inter-VLAN traffic
- Allows the default network to access all other networks
- Grants specific IoT devices controlled access based on their requirements
- Maintains established and related connections
Key Features:
- Data-driven design: All rules defined in YAML format
- Easy maintenance: Add/remove/modify rules by editing
vars/firewall_rules.yml - Idempotent: Can be run multiple times safely
- Flexible: Supports all Unifi firewall rule parameters
- Ansible 2.9 or higher
- Python 3.6 or higher
- Access to Unifi Cloud Gateway with the API key of a Super Admin (needed for rules creation)
IMPORTANT: Regular "Admin" users can read via API but cannot create/modify rules.
.
├── configure_firewall.yml # Main playbook
├── vars/
│ └── firewall_rules.yml # Firewall rules definition
| VLAN ID | Name | Subnet | Purpose |
|---|---|---|---|
| 1 | default | 192.168.1.0/24 | Main trusted network |
| 2 | lab | 192.168.2.0/24 | Homelab network |
| 10 | iot | 192.168.10.0/24 | IoT devices |
| 11 | cameras | 192.168.11.0/24 | Security cameras |
| 100 | untrusted | 192.168.100.0/24 | Isolated, untrustred network |
| Hostname | IP Address | Access Level | Description |
|---|---|---|---|
| automation-hub | 192.168.10.3 | All VLANs | Home automation hub |
| robot1 | 192.168.10.60 | Internet only | Robot vacuum |
| robot2 | 192.168.10.61 | Internet only | Robot mower |
The configure_firewall.yml playbook:
- Loads rules from
vars/firewall_rules.yml - Removes existing ANSIBLE-managed rules
- Creates all rules defined in the configuration
- Provides detailed output and summaries
The vars/firewall_rules.yml file contains:
- VLANs: Network definitions for reference
- Special Hosts: Host-specific access requirements
- Firewall Rules: List of all firewall rules to create
Each rule in vars/firewall_rules.yml follows this structure:
firewall_rules:
- name: "ANSIBLE-Rule-Name" # Required: Rule name (must start with ANSIBLE-)
description: "Rule description" # Optional: Human-readable description
ruleset: "LAN_IN" # Required: LAN_IN, LAN_OUT, WAN_IN, WAN_OUT
rule_index: 2000 # Required: Priority (lower = higher priority)
enabled: true # Optional: Enable/disable (default: true)
action: "accept" # Required: accept, drop, reject
protocol: "all" # Optional: all, tcp, udp, icmp (default: all)
# Optional source/destination
src_address: "192.168.1.0/24" # Source IP or CIDR
dst_address: "192.168.2.0/24" # Destination IP or CIDR
src_port: "1024-65535" # Source port(s)
dst_port: "80,443" # Destination port(s)
# Optional VLAN references
src_networkconf_id: "1" # Source VLAN ID
dst_networkconf_id: "10" # Destination VLAN ID
# Optional connection states
state_established: true # Allow established connections
state_related: true # Allow related connections
state_new: false # Allow new connections
state_invalid: false # Allow invalid connections
# Optional logging
logging: true # Enable logging for this rule| Parameter | Type | Description | Example |
|---|---|---|---|
name |
string | Rule name (must start with "ANSIBLE-") | "ANSIBLE-Allow-SSH" |
description |
string | Human-readable description | "Allow SSH from admin network" |
ruleset |
string | Rule direction | "LAN_IN", "LAN_OUT", "WAN_IN", "WAN_OUT" |
rule_index |
integer | Priority (lower = higher) | 2000 |
enabled |
boolean | Enable/disable rule | true, false |
action |
string | What to do with matching traffic | "accept", "drop", "reject" |
protocol |
string | Network protocol | "all", "tcp", "udp", "icmp" |
src_address |
string | Source IP/CIDR | "192.168.1.0/24" |
dst_address |
string | Destination IP/CIDR | "10.0.0.0/8" |
src_port |
string | Source port(s) | "1024-65535", "22,80,443" |
dst_port |
string | Destination port(s) | "80,443" |
src_networkconf_id |
string | Source VLAN ID | "1", "20" |
dst_networkconf_id |
string | Destination VLAN ID | "21", "100" |
state_established |
boolean | Match established connections | true, false |
state_related |
boolean | Match related connections | true, false |
state_new |
boolean | Match new connections | true, false |
state_invalid |
boolean | Match invalid connections | true, false |
logging |
boolean | Log matching traffic | true, false |
icmp_typename |
string | ICMP type | "echo-request", "echo-reply" |
git clone <your-repo-url>
cd unifi-firewall-configSet the following environment variables:
export UNIFI_CONTROLLER="192.168.1.1" # IP/hostname of Unifi controller
export UNIFI_USERNAME="admin" # Admin username
export UNIFI_PASSWORD="your_password" # Admin password
export UNIFI_SITE="default" # Site name (optional, defaults to "default")
export UNIFI_PORT="443" # HTTPS port (optional, defaults to "443")Edit vars/firewall_rules.yml to match your requirements:
vim vars/firewall_rules.ymlansible-playbook configure_firewall.yml# Set environment variables
export UNIFI_CONTROLLER="192.168.1.1"
export UNIFI_API_TOKEN="Blahblahblah"
# Run the playbook
ansible-playbook configure_firewall.ymlCreate a vault file:
# Create vault file
ansible-vault create vault.ymlAdd credentials:
---
vault_unifi_api_token: "Blahblahblah"Modify the playbook to use vault variables:
vars:
unifi_username: "{{ vault_unifi_api_token }}"
Run with vault:
ansible-playbook configure_firewall.yml --ask-vault-passansible-playbook configure_firewall.yml --checkansible-playbook configure_firewall.yml -vvvTo add a new firewall rule, edit vars/firewall_rules.yml and add a new entry to the firewall_rules list:
firewall_rules:
# ... existing rules ...
- name: "ANSIBLE-Allow-SSH-from-Admin"
description: "Allow SSH access from default network to lab"
ruleset: "LAN_IN"
rule_index: 2100
enabled: true
action: "accept"
protocol: "tcp"
src_address: "192.168.1.0/24"
dst_address: "192.168.2.0/24"
dst_port: "22"
logging: false - name: "ANSIBLE-Lab-to-Cameras-RTSP"
description: "Allow lab network to access camera RTSP streams"
ruleset: "LAN_IN"
rule_index: 2101
enabled: true
action: "accept"
protocol: "tcp"
src_address: "192.168.2.0/24"
dst_address: "192.168.21.0/24"
dst_port: "554,8554"
logging: false - name: "ANSIBLE-IoT-to-DNS"
description: "Allow IoT devices to access DNS server"
ruleset: "LAN_IN"
rule_index: 2102
enabled: true
action: "accept"
protocol: "udp"
src_address: "192.168.10.0/24"
dst_address: "192.168.1.1"
dst_port: "53"
logging: false - name: "ANSIBLE-Block-Suspicious-Device"
description: "Block suspicious IoT device from internet"
ruleset: "LAN_OUT"
rule_index: 2103
enabled: true
action: "drop"
protocol: "all"
src_address: "192.168.10.100"
logging: trueAfter adding rules, simply re-run the playbook:
ansible-playbook configure_firewall.ymlSet enabled: false in the rule definition:
- name: "ANSIBLE-Robot1-Internet"
description: "Allow robot1 to access internet"
enabled: false # Temporarily disabled
# ... rest of rule ...Modify the rule_index value (remember: lower = higher priority):
- name: "ANSIBLE-HomeAssistant-to-All"
rule_index: 1999 # Changed from 2002 to run earlierSimply delete or comment out the rule in vars/firewall_rules.yml:
# Removed - no longer needed
# - name: "ANSIBLE-Old-Rule"
# ruleset: "LAN_IN"
# ...My current configuration implements the following hierarchy (evaluated in order):
-
Allow Established/Related (Priority: 2000)
- Permits return traffic for existing connections
- Essential for bidirectional communication
-
Allow Default Network to All (Priority: 2001)
- Source: 192.168.1.0/24 (default VLAN)
- Destination: All networks
- Reason: Admin network needs full access
-
Allow Home Assistant to All VLANs (Priority: 2002)
- Source: 192.168.20.3
- Destination: All networks
- Reason: Home automation requires device access
-
Block Robots to RFC1918 (Priority: 2003-2004)
- Source: 192.168.20.60, 192.168.20.61
- Destination: Private networks (this requires to create a group, query the API to get the group id, and put this is dsp_firewallgroup_ids. Or, create one rule per private subnet :) )
- Action: Drop
- Reason: Restrict to internet-only
-
Allow Robots to Internet (Priority: 2005-2006)
- Source: 192.168.20.60, 192.168.20.61
- Destination: Any (after RFC1918 block)
- Reason: Cloud connectivity required. Some robots require access to a cloud account to work (yuk :( next robot : valetudo enabled, with a local map only))
-
Deny Inter-VLAN Traffic (Priority: 2999)
- Source: Any RFC1918 address (see comment for point #4)
- Destination: Any RFC1918 address
- Action: Drop with logging
- Reason: Default deny policy
- Default Deny Policy: All inter-VLAN traffic is denied unless explicitly allowed
- Least Privilege: Devices receive only the minimum access required
- Logging: Important deny rules log traffic for auditing
- Stateful Firewall: Established and related connections are tracked
- Rule Ordering: Priority is critical - more specific rules should have lower indexes
- Documentation: Use the
descriptionfield to explain why each rule exists
- Default Network: Acts as management/admin network with full access
- Home Assistant: Broad access required; consider additional application-level security
- Robots: Internet-only access prevents lateral movement
- Cameras: Isolated unless explicitly allowed
- Airgap Lab: Should remain isolated for sensitive testing
Consider adding these rules for enhanced security:
# Allow NTP for time synchronization
- name: "ANSIBLE-Allow-NTP"
description: "Allow all networks to access NTP"
ruleset: "LAN_IN"
rule_index: 2010
action: "accept"
protocol: "udp"
dst_port: "123"
# Allow DHCP
- name: "ANSIBLE-Allow-DHCP"
description: "Allow DHCP traffic"
ruleset: "LAN_IN"
rule_index: 2011
action: "accept"
protocol: "udp"
src_port: "68"
dst_port: "67"
# Rate limit ICMP
- name: "ANSIBLE-Rate-Limit-ICMP"
description: "Allow ICMP but rate limited"
ruleset: "LAN_IN"
rule_index: 2012
action: "accept"
protocol: "icmp"
# Note: Rate limiting configured in Unifi UI# Test controller connectivity
curl -k -X GET 'https://192.168.1.1/proxy/network/api/s/default/rest/firewallrule' -H 'X-API-KEY: <put your API token here>' -H 'Accept: application/json'
# Verify credentials
ansible-playbook configure_firewall.yml -vvv- Log into Unifi Controller web interface
- Navigate to Settings > Security > Firewall
- Look for rules prefixed with "ANSIBLE-"
- Verify rule order and configuration
| Error | Cause | Solution |
|---|---|---|
| Authentication failed | Invalid credentials | Verify you're using the right token and that the environment variable or vault is properly set |
| Connection refused | Wrong controller IP/port | Check UNIFI_CONTROLLER and UNIFI_PORT |
| 403 Forbidden on rule creation | Insufficient API permissions | The key must be created by a user that can create firewall rules in the webUI |
| Rule creation failed | Invalid rule syntax | Check vars/firewall_rules.yml for syntax errors |
| Rules not appearing | Wrong site name | Verify UNIFI_SITE matches your site name |
| SSL certificate error | Self-signed certificate | Playbook uses validate_certs: false |
-
Enable verbose output:
ansible-playbook configure_firewall.yml -vvv 2>&1 | tee ansible.log
-
Validate YAML syntax:
ansible-playbook configure_firewall.yml --syntax-check
-
Check rule file syntax:
python3 -c "import yaml; yaml.safe_load(open('vars/firewall_rules.yml'))" -
Test rule logic:
# From source network, test connectivity ping <destination_ip> curl -v <destination_ip>:<port>
-
Check Unifi logs:
# SSH to controller tail -f /var/log/unifi/server.log
- Check rule order: Lower rule_index has higher priority
- Check for overlapping rules: More specific rules should come first
- Enable logging: Set
logging: trueto see what's being blocked - Verify stateful rules: Ensure established/related rule is first
- Check Unifi UI: Manually verify rules appear correctly
- Edit
vars/firewall_rules.yml - Re-run the playbook
- Existing ANSIBLE-* rules are automatically removed and recreated
vim vars/firewall_rules.yml
ansible-playbook configure_firewall.ymlBefore making changes, backup your Unifi configuration:
To remove all ANSIBLE-managed rules, delete all rules from vars/firewall_rules.yml and run:
# Make firewall_rules an empty list
echo "firewall_rules: []" > vars/firewall_rules_empty.yml
# Run with empty rules
ansible-playbook configure_firewall.yml -e @vars/firewall_rules_empty.ymlOr manually delete via Unifi UI by searching for "ANSIBLE-" prefix.
-
Verify Default Network Access
# From 192.168.1.x host ping 192.168.2.1 # Should succeed ping 192.168.20.1 # Should succeed ping 192.168.21.1 # Should succeed
-
Verify IoT Isolation
# From 192.168.20.x host (not home-assistant) ping 192.168.1.1 # Should fail ping 192.168.2.1 # Should fail
-
Verify Home Assistant Access
# From 192.168.20.3 ping 192.168.1.1 # Should succeed ping 192.168.2.1 # Should succeed curl 192.168.21.100 # Should succeed
-
Verify Robot Internet-Only Access
# From 192.168.20.60 or 192.168.20.61 ping 8.8.8.8 # Should succeed ping 192.168.1.1 # Should fail ping 192.168.2.1 # Should fail
-
Verify Logging
# Check Unifi logs for blocked traffic # Settings > System > Logs > Events # Filter by "Firewall"
Create a test script:
#!/bin/bash
# test_firewall.sh
echo "Testing firewall rules..."
# Test from various networks
for src in 192.168.1.10 192.168.20.3 192.168.20.60; do
echo "Testing from $src"
ssh user@$src "ping -c 1 192.168.1.1"
ssh user@$src "ping -c 1 8.8.8.8"
doneCreate reusable rule templates in vars/firewall_rules.yml:
# Define template anchor
rule_templates:
allow_web: &allow_web
protocol: "tcp"
dst_port: "80,443"
action: "accept"
firewall_rules:
# Use template
- name: "ANSIBLE-Lab-Web-Access"
<<: *allow_web
ruleset: "LAN_IN"
rule_index: 2200
src_address: "192.168.2.0/24"Maintain different rule sets for different environments:
# Production rules
ansible-playbook configure_firewall.yml -e @vars/firewall_rules_prod.yml
# Development rules
ansible-playbook configure_firewall.yml -e @vars/firewall_rules_dev.yml# .gitlab-ci.yml or .github/workflows/deploy.yml
deploy_firewall:
script:
- ansible-playbook configure_firewall.yml
only:
changes:
- vars/firewall_rules.ymlunifi-firewall-config/
├── configure_firewall.yml # Main Ansible playbook
├── vars/
│ └── firewall_rules.yml # Firewall rules definition
├── README.md # This file
Primary configuration file containing:
- Network definitions (vlans, subnets)
- Special host definitions
- Complete firewall rules list
Main playbook that:
- Loads configuration from vars/
- Authenticates to Unifi Controller
- Manages firewall rules lifecycle
- Provides detailed logging
This playbook is provided as-is for education purposes only.