← Back to blog

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

9 min read
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/unifi provider — 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.tfvars to git. Use a secrets manager or environment variables (UNIFI_USERNAME, UNIFI_PASSWORD, or UNIFI_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 plan output. A wrong firewall rule can lock you out of your own network
  • Self-signed certs — The allow_insecure = true flag 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.