Infrastructure as Code for Your UCG-Max: Managing UniFi with Terraform and Claude Code

The Unifi Cloud Gateway Max (UCG-Max) is a capable little gateway — but like most consumer-grade networking gear, it expects you to click through a web GUI every time you want to change something. Fine for a single home network. Unacceptable if you treat your homelab like infrastructure.
This guide shows how to manage your UCG-Max configuration as code using Terraform and Claude Code. Every VLAN, firewall rule, SSID, and port profile becomes a versioned, reviewable, reproducible artifact.
Why IaC for a Home Gateway?
You might think Infrastructure as Code is overkill for a home network. It is not. Here is why:
- Reproducibility — Factory reset your UCG-Max and rebuild the entire config in minutes
- Version control — Every network change is a git commit with a meaningful message
- Peer review — Firewall rule changes go through pull requests, not ad-hoc GUI clicks
- Documentation — The .tf files are the documentation. No more "what VLAN is that?"
- Consistency — No configuration drift between what you think is configured and what actually is
The Stack
┌─────────────────────────────────────────┐
│ Your Workstation │
│ │
│ Claude Code ──→ Writes/edits ──→ .tf │
│ │
│ Terraform ──→ UniFi Provider ──→ API │
│ │
└────────────────────┬────────────────────┘
│ HTTPS
▼
┌──────────────┐
│ UCG-Max │
│ (UniFi OS) │
└──────────────┘
- Terraform with the
filipowm/unifiprovider — declarative state management for networks, firewall rules, WLANs, and more - Claude Code — AI assistant that writes the Terraform based on your intent
Prerequisites
- A UCG-Max (or any UniFi gateway) running UniFi Network Application 6.x+
- A local admin account on the controller (or API key on 9.0.108+)
- Terraform installed
- Claude Code installed
Creating Credentials
The provider supports two authentication methods:
- Username/password — works with all supported versions. Use a dedicated local-only admin account
- API key — requires UniFi OS 9.0.108+. More secure, no session management
For API key authentication, go to Settings → Control Plane → Integrations in the UniFi Network console and generate a key.
Project Setup with Claude Code
Instead of reading provider documentation and writing boilerplate, describe what you want:
$ cd ~/homelab/unifi-iac
$ claude
You: Set up a Terraform project for managing my UCG-Max at
192.168.1.1. Use the filipowm/unifi provider with
username/password auth. Create separate files for
networks, firewall rules, and wireless.
Claude Code creates the project structure:
unifi-iac/
├── main.tf # Provider configuration
├── variables.tf # Input variables
├── terraform.tfvars # Credentials (gitignored)
├── networks.tf # VLANs and subnets
├── firewall.tf # Firewall rules and groups
├── wireless.tf # SSIDs and WiFi config
├── port_profiles.tf # Switch port profiles
└── outputs.tf # Useful outputs
Provider Configuration
terraform {
required_providers {
unifi = {
source = "filipowm/unifi"
version = "~> 1.0"
}
}
}
provider "unifi" {
username = var.unifi_username
password = var.unifi_password
api_url = var.unifi_api_url
allow_insecure = true # Self-signed cert
}
variable "unifi_username" {
type = string
}
variable "unifi_password" {
type = string
sensitive = true
}
variable "unifi_api_url" {
type = string
description = "UniFi controller URL (e.g. https://192.168.1.1)"
}
Or with API key authentication (UniFi OS 9.0.108+):
provider "unifi" {
api_key = var.unifi_api_key
api_url = var.unifi_api_url
allow_insecure = true
}
Building Your Network
VLANs
The core of any segmented network. Describe your VLANs to Claude Code:
You: Create VLANs for my homelab:
- Servers (VLAN 10, 192.168.10.0/24, DHCP .100-.199)
- IoT (VLAN 15, 192.168.15.0/24, DHCP .100-.199, isolated)
- Trusted clients (VLAN 20, 192.168.20.0/24, DHCP .100-.199)
- Guest WiFi (VLAN 30, 192.168.30.0/24, DHCP .100-.199, isolated)
Claude Code generates:
resource "unifi_network" "servers" {
name = "Servers"
purpose = "corporate"
subnet = "192.168.10.1/24"
vlan_id = 10
dhcp_start = "192.168.10.100"
dhcp_stop = "192.168.10.199"
dhcp_enabled = true
dhcp_dns = ["192.168.10.1"]
}
resource "unifi_network" "iot" {
name = "IoT"
purpose = "corporate"
subnet = "192.168.15.1/24"
vlan_id = 15
dhcp_start = "192.168.15.100"
dhcp_stop = "192.168.15.199"
dhcp_enabled = true
dhcp_dns = ["192.168.15.1"]
network_isolation_enabled = true
}
resource "unifi_network" "clients" {
name = "Trusted Clients"
purpose = "corporate"
subnet = "192.168.20.1/24"
vlan_id = 20
dhcp_start = "192.168.20.100"
dhcp_stop = "192.168.20.199"
dhcp_enabled = true
dhcp_dns = ["192.168.10.1"]
}
resource "unifi_network" "guests" {
name = "Guest WiFi"
purpose = "corporate"
subnet = "192.168.30.1/24"
vlan_id = 30
dhcp_start = "192.168.30.100"
dhcp_stop = "192.168.30.199"
dhcp_enabled = true
dhcp_dns = ["192.168.30.1"]
network_isolation_enabled = true
internet_access_enabled = true
}
The network_isolation_enabled flag automatically blocks inter-VLAN traffic for that network — the UniFi equivalent of a deny-all starting point.
Firewall Rules
Network isolation covers the basics, but you need specific allow rules for legitimate cross-VLAN traffic. First, create firewall groups to keep rules clean:
You: Create firewall groups for my RFC1918 ranges and common
service ports (DNS, NTP, HTTPS). Then create rules to:
1. Allow established/related connections
2. Let trusted clients reach the server VLAN
3. Let IoT devices reach Home Assistant (192.168.10.30) on 443 and 8123
4. Block IoT from reaching anything else internal
5. Let all VLANs reach DNS on the gateway
resource "unifi_firewall_group" "rfc1918" {
name = "RFC1918"
type = "address-group"
members = ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"]
}
resource "unifi_firewall_group" "ha_ports" {
name = "HomeAssistant-Ports"
type = "port-group"
members = ["443", "8123"]
}
resource "unifi_firewall_rule" "allow_established" {
name = "Allow established/related"
action = "accept"
ruleset = "LAN_IN"
rule_index = 2000
protocol = "all"
state_established = true
state_related = true
}
resource "unifi_firewall_rule" "clients_to_servers" {
name = "Clients to Servers"
action = "accept"
ruleset = "LAN_IN"
rule_index = 2010
protocol = "all"
src_network_id = unifi_network.clients.id
src_network_type = "network"
dst_network_id = unifi_network.servers.id
dst_network_type = "network"
}
resource "unifi_firewall_rule" "iot_to_ha" {
name = "IoT to Home Assistant"
action = "accept"
ruleset = "LAN_IN"
rule_index = 2020
protocol = "all"
src_network_id = unifi_network.iot.id
src_network_type = "network"
dst_address = "192.168.10.30"
dst_firewall_group_ids = [unifi_firewall_group.ha_ports.id]
}
resource "unifi_firewall_rule" "block_iot_internal" {
name = "Block IoT to internal"
action = "drop"
ruleset = "LAN_IN"
rule_index = 2030
protocol = "all"
src_network_id = unifi_network.iot.id
src_network_type = "network"
dst_firewall_group_ids = [unifi_firewall_group.rfc1918.id]
logging = true
}
Rule ordering matters — UniFi processes rules by rule_index, lowest first. That is why the allow rules come before the block rules. Use ranges like 2000-2999 for custom LAN_IN rules to avoid conflicts with system-generated rules.
Zone-Based Firewall (UniFi OS 9.0+)
If you are running UniFi OS 9.0 or later, you can use the newer zone-based firewall model instead of traditional rulesets. Zones group networks together, and policies define traffic flow between zones:
resource "unifi_firewall_zone" "trusted" {
name = "Trusted"
networks = [unifi_network.clients.id, unifi_network.servers.id]
}
resource "unifi_firewall_zone" "iot_zone" {
name = "IoT"
networks = [unifi_network.iot.id]
}
resource "unifi_firewall_zone" "guest_zone" {
name = "Guests"
networks = [unifi_network.guests.id]
}
resource "unifi_firewall_zone_policy" "iot_to_trusted" {
name = "IoT to Trusted - HA only"
action = "ALLOW"
protocol = "tcp_udp"
enabled = true
source = {
zone_id = unifi_firewall_zone.iot_zone.id
}
destination = {
zone_id = unifi_firewall_zone.trusted.id
ips = ["192.168.10.30"]
port_group_id = unifi_firewall_group.ha_ports.id
}
}
resource "unifi_firewall_zone_policy" "block_guests" {
name = "Block Guests to Internal"
action = "BLOCK"
enabled = true
source = {
zone_id = unifi_firewall_zone.guest_zone.id
}
destination = {
zone_id = unifi_firewall_zone.trusted.id
}
}
Wireless Networks
SSIDs mapped to VLANs, with proper security settings:
You: Create three WiFi networks:
1. "Home" on the clients VLAN, WPA2/WPA3, visible
2. "IoT" on the IoT VLAN, WPA2, hidden SSID
3. "Guest" on the guest VLAN, WPA2, client isolation
data "unifi_ap_group" "default" {}
data "unifi_user_group" "default" {}
resource "unifi_wlan" "home" {
name = "Home"
security = "wpapsk"
passphrase = var.wifi_home_password
network_id = unifi_network.clients.id
ap_group_ids = [data.unifi_ap_group.default.id]
user_group_id = data.unifi_user_group.default.id
wpa3_support = true
wpa3_transition = true
pmf_mode = "optional"
}
resource "unifi_wlan" "iot_wifi" {
name = "IoT"
security = "wpapsk"
passphrase = var.wifi_iot_password
network_id = unifi_network.iot.id
ap_group_ids = [data.unifi_ap_group.default.id]
user_group_id = data.unifi_user_group.default.id
hide_ssid = true
}
resource "unifi_wlan" "guest_wifi" {
name = "Guest"
security = "wpapsk"
passphrase = var.wifi_guest_password
network_id = unifi_network.guests.id
ap_group_ids = [data.unifi_ap_group.default.id]
user_group_id = data.unifi_user_group.default.id
is_guest = true
l2_isolation = true
}
Note the WPA3 transition mode on the home network — existing WPA2 devices still connect while WPA3-capable devices get stronger encryption.
Switch Port Profiles
If you have UniFi switches, manage port assignments as code:
resource "unifi_port_profile" "server_access" {
name = "Server-Access"
native_networkconf_id = unifi_network.servers.id
poe_mode = "auto"
}
resource "unifi_port_profile" "iot_access" {
name = "IoT-Access"
native_networkconf_id = unifi_network.iot.id
poe_mode = "auto"
forward = "native"
}
The Claude Code Workflow
Day-to-day, the workflow is straightforward:
1. Describe the Change
$ claude
You: I added a new camera VLAN (VLAN 45, 192.168.45.0/24).
Cameras should only be able to reach the NVR at
192.168.10.50 on TCP 554 and 80. Create the network,
firewall rules, and a port profile for camera ports.
2. Review the Diff
Claude Code creates the .tf files. Review the diff — it is a standard git diff, readable by anyone.
3. Plan and Apply
$ terraform plan # See what will change
$ terraform apply # Apply to UCG-Max
4. Commit
You: Commit this change with a descriptive message.
Importing Existing Configuration
If you already have a configured UCG-Max, you do not start from zero. Use terraform import to bring existing resources under management:
$ terraform import unifi_network.servers <network-id>
$ terraform import unifi_wlan.home <wlan-id>
$ terraform import unifi_firewall_rule.allow_established <rule-id>
You can find resource IDs through the UniFi API or by inspecting URLs in the controller GUI. Claude Code can help write the import commands and generate matching .tf resources from your existing configuration.
Useful Data Sources
The provider includes data sources for referencing existing objects:
data "unifi_ap_group" "default" {}
data "unifi_user_group" "default" {}
# Reference in WLAN resources
ap_group_ids = [data.unifi_ap_group.default.id]
user_group_id = data.unifi_user_group.default.id
Security Considerations
- Credential storage — Never commit
terraform.tfvarsto git. Use a secrets manager or environment variables (UNIFI_USERNAME,UNIFI_PASSWORD, orUNIFI_API_KEY) - State file — Terraform state contains WiFi passwords and other sensitive data. Encrypt it or use remote state with encryption at rest
- API access — Use a dedicated admin account with only the permissions Terraform needs
- Plan before apply — Always review
terraform planoutput. A wrong firewall rule can lock you out of your own network - Self-signed certs — The
allow_insecure = trueflag is necessary for self-signed certificates but disables TLS verification. Use a real certificate in production environments
Common Pitfalls
1. The Default Network
UniFi creates a default network that cannot be deleted. Import it into Terraform rather than trying to recreate it. Attempting to create a new default network will fail.
2. Rule Index Conflicts
UniFi has system-generated firewall rules in certain index ranges. Use 2000-2999 or 4000-4999 for custom rules to avoid conflicts. If you see unexpected rule behavior, check for index collisions.
3. WLAN Dependencies
WLANs require references to AP groups and user groups. Always use data sources to reference the defaults rather than hardcoding IDs, since these change between controller installations.
4. Network Isolation vs Firewall Rules
network_isolation_enabled is the simplest way to block inter-VLAN traffic, but it is an all-or-nothing setting per network. For granular control (like allowing IoT to reach only Home Assistant), you need explicit firewall rules with network isolation disabled, or use zone-based firewall on UniFi OS 9.0+.
5. Provider Version Compatibility
The filipowm/unifi provider (v1.0+) is the actively maintained fork. The older paultyng/unifi provider is no longer actively developed. Make sure you are using the correct provider source.
What the Provider Covers
The filipowm/unifi provider supports 35 resource types including:
- Networks and VLANs
- Firewall rules, groups, zones, and zone policies
- WLANs (SSIDs)
- Port profiles
- Static routes
- DNS records
- Port forwarding
- RADIUS profiles
- Dynamic DNS
- Various system settings (IPS, DPI, NTP, syslog, etc.)
Notably absent: device adoption, firmware management, and Protect/Access configuration. These remain GUI-only operations.
Conclusion
Your UCG-Max is more than a consumer router — it runs a full UniFi OS with a capable API. Treating its configuration as code means you can rebuild your entire network from a git repository, review changes before they go live, and never wonder "who changed that firewall rule" again.
Start with your VLANs and basic firewall rules, then expand to WLANs and port profiles as you get comfortable. The investment in setting up Terraform pays off the first time you need to rebuild after a factory reset — or explain to someone exactly what your network looks like.