UniFi
The UniFi controller runs the switching fabric: VLANs on switch ports, DHCP Guard, IGMP snooping and mDNS scope. It is not the gateway and not a DHCP server here. The Firewalla routes and serves DHCPv6 on every network plus DHCPv4 on Guest and Telework; the Kea pair on the resolvers serves DHCPv4 on the other networks. A setting on the controller can still veto work on those layers: on 2026-08-24, Main’s DHCP Guard trusted only the Firewalla and silently dropped every Kea offer.
Controller
Section titled “Controller”| URL | https://unifi.fzymgc.house (192.168.40.2, Core). Use the hostname; the certificate does not cover the IP |
| Network application | 10.6.101 |
| API | UniFi OS login POST /api/auth/login; Network API under /proxy/network/api/s/default/ |
Credentials
Section titled “Credentials”| Path | Account | Role | Used by |
|---|---|---|---|
secret/fzymgc-house/infrastructure/unifi/readonly |
automation-readonly |
Network: View Only | Agents and ad-hoc probes; the controller itself rejects writes |
secret/fzymgc-house/infrastructure/unifi/admin |
automation-admin |
Network: Full Management | The main-cluster-unifi workspace only (policy terraform-unifi) |
Humans use their own logins (the local admin is in 1Password).
What Terraform manages
Section titled “What Terraform manages”tf/unifi (workspace main-cluster-unifi, agent mode, auto-apply on merge) declares every network
on the controller as a unifi_network, adopted with import {} blocks. The provider is our fork,
app.terraform.io/fzymgc-house/unifi (runbook).
- Change the fabric through a PR, not the UI. A UI change shows up as drift on the next plan.
- DHCP Guard is
dhcp_guarding = { enabled, servers }, at most three servers. Any cut that brings a new DHCP server onto a guarded VLAN adds its address first. - On the
corporatenetworks the subnet, domain and DHCP fields are inert leftovers: UniFi serves DHCP nowhere. Only the VLAN, DHCP Guard and basic settings are live anywhere. - switch-main’s ports are
unifi_device_ports.switch_mainintf/unifi/ports.tf, which holds every port definition on the switch.- A port that isn’t listed runs the controller’s default configuration, so a port changed in the UI shows up as drift.
- Each port states only the settings that differ from port to port: name, VLANs, LAG topology,
PoE, and a few per-port ones. Settings the UI stamps identically everywhere (port security,
isolation, storm control, LLDP-MED, STP mode, keepalive, egress rate limiting, autoneg) are
left out. The controller keeps them, and Terraform does not assert them, so a UI change to one
of those is not drift. State one in
ports.tfif it ever needs to be. - The resolver trunks are port 12 (sandiego) and port 13 (portland): Core native, every VLAN
tagged (
tagged_vlan_mgmt = "auto"). unifi_deviceis not used, and itsport_overrideblock is deprecated in our fork.
- Not yet managed: the other switches’ ports and the port profiles. AllMainDefault is used by three other switches.
Changing a port
Section titled “Changing a port”- Where to edit: the port’s entry in
ports.tf. The plan shows the keys you changed. It also shows “known after apply” for keys the controller sets as a side effect, such aslag_idxon a new aggregation lead. - VLANs:
native_networkconf_idplustagged_vlan_mgmt. The values areauto(every network tagged),block_all(nothing tagged), orcustomtogether withexcluded_networkconf_ids.- The controller derives the legacy
forwardfield and ignores writes to it. - A
customport that excludes every VLAN network is stored asblock_all, so declareblock_allfor that case.
- The controller derives the legacy
- Link aggregation: declare the group on its lead port only, with
op_mode = "aggregate"andaggregate_members. The switch-main groups are 6→7, 23→24 and 25→26. - Removing a setting from
ports.tfleaves its value on the controller. To change a value, state the new one. - Never delete the resource. Destroying it resets every port on the switch, uplinks included,
and
prevent_destroyblocks that plan. To stop managing the switch, useremoved { from = unifi_device_ports.switch_main; lifecycle { destroy = false } }.
| Network | Purpose | VLAN | DHCP Guard |
|---|---|---|---|
| Core | corporate (leftover) | untagged | off |
| Lab | corporate (leftover) | 1000 | off |
| BackupNet | vlan-only, disabled | 2000 | off |
| Main | vlan-only | 3000 | on: .20.6, .20.7, .20.5 |
| MainWireless | vlan-only | 3001 | off |
| IoT | vlan-only | 3020 | off |
| Guest | corporate (leftover) | 3100 | off |
| Telework | corporate (leftover) | 3200 | off |
| Thirdparty | corporate (leftover) | 3300 | off |
Reading state off the controller
Section titled “Reading state off the controller”- DHCP Guard is
dhcpguard_enabledplus the trusted list indhcpd_ip_1,dhcpd_ip_2,dhcpd_ip_3on eachrest/networkconfobject. There is nodhcpguard_ipkey; reading one returns nothing, whatever is configured. vlanis an integer on most networks, a string on some ("1000"on Lab), and absent on the untagged one.- Change history:
POST /proxy/network/v2/api/site/default/system-log/admin-activitywith{timestampFrom, timestampTo, pageSize, pageNumber}returns who changed which field, when, from which app, with old and new values. The View Only account may run it..planning/spikes/001-networkconf-guard-fields/probe_admin_activity.pywraps it. - The iOS app has written DHCP Guard fields (2026-08-30 it disabled Main’s guard); check the activity log when the fabric behaves unexpectedly.