IoT VLAN 94 — Segmentation, Kea DHCP, Firewall Rules, and Device Migration
Summary
TheBurrow's flat 192.168.95.0/24 network had no segmentation — smart home devices, 3D printers, cameras, and Roombas shared the same broadcast domain as servers, Pis, and the management network. IoT devices have a poor security track record: weak firmware, unencrypted protocols, and cloud dependencies that make them attractive pivot points.
VLAN 94 (192.168.94.0/24) was built as the first segmented zone in a planned nine-VLAN architecture. This lab note documents the full implementation: VLAN design, Kea DHCP, OPNsense firewall rules, DNS enforcement, Omada SSID tagging, and the device migration process.
Architecture
VLAN design
| VLAN | Subnet | Purpose |
|---|---|---|
| 94 | 192.168.94.0/24 | IoT — smart devices, cameras, printers |
| 95 | 192.168.95.0/24 | LAN — servers, Pis (legacy, migration pending) |
Gateway for IoT: 192.168.94.1 (nevermore igc1.94 subinterface)
DNS for IoT: 192.168.95.250, 192.168.95.251 (Pi-hole VIPs via NAT)
Trust model
IoT devices on VLAN 94 can:
- Reach the internet (via NAT on nevermore)
- Reach DNS (forced through Pi-hole cluster via NAT redirect)
- Reach Home Assistant on fablehaven (explicit allow rule)
IoT devices cannot:
- Initiate connections to LAN (192.168.95.0/24)
- Bypass DNS enforcement (port 53 redirected, DoT port 853 blocked)
- Reach each other on the IoT subnet (no intra-VLAN rules added yet)
Wireless isolation
TheBurrow-IoT SSID on Omada is tagged to VLAN 94, 2.4 GHz only.
IoT devices connect to this SSID and receive 192.168.94.x addresses
from Kea. The main TheBurrow SSID remains on the untagged LAN.
Notes
OPNsense VLAN interface
Created VLAN 94 interface on nevermore via Interfaces → Other Types → VLAN:
- Parent:
igc1(LAN physical interface) - VLAN tag:
94 - Description:
IoT
Assigned the VLAN interface as igc1.94 and configured:
- IPv4: Static,
192.168.94.1/24 - IPv6: Disabled
- Block private networks: Disabled (it is a private network)
Kea DHCP subnet
Added IoT subnet to Kea via OPNsense Services → Kea DHCP → Subnets:
{
"subnet": "192.168.94.0/24",
"interface": "igc1.94",
"pools": [{ "pool": "192.168.94.100-192.168.94.199" }],
"option-data": [
{ "name": "routers", "data": "192.168.94.1" },
{ "name": "domain-name-servers", "data": "192.168.95.250, 192.168.95.251" }
]
}
Dynamic pool: .100–.199 (100 addresses)
Static reservations: below .100
DNS option-data serves the Pi-hole VIPs (.250, .251), not raw Pi-hole IPs — consistent with the VRRP topology design.
Static reservations
31 IoT devices migrated to static reservations. Each reservation:
{
"hw-address": "4c:b9:ea:08:3f:a2",
"ip-address": "192.168.94.100",
"hostname": "winky"
}
Devices were identified by cross-referencing:
- OPNsense DHCP lease table (existing LAN leases)
- Home Assistant device registry (for friendly names)
- Physical inspection for unlabeled devices
Home Assistant WebSocket API for friendly names
The HA WebSocket API provided the cleanest mapping from MAC address to friendly name. Connection pattern:
import websocket
import json
ws = websocket.WebSocket()
ws.connect("ws://192.168.95.6:8123/api/websocket")
## Authenticate
ws.send(json.dumps({
"type": "auth",
"access_token": vault_secrets_homeassistant.long_lived_token
}))
## Fetch device registry
ws.send(json.dumps({
"id": 1,
"type": "config/device_registry/list"
}))
The response includes connections arrays with MAC addresses and
name_by_user for friendly names. Cross-referenced against the DHCP
lease table to build the reservation list.
CSV import workflow
Reservations were built as a CSV, edited in Numbers, and imported to Kea via a Python script using the OPNsense Kea REST API:
hostname,mac,ip
winky,4c:b9:ea:08:3f:a2,192.168.94.100
warden-luccio,xx:xx:xx:xx:xx:xx,192.168.94.70
...
The import script called:
POST /api/kea/dhcpv4/addReservation for each row.
After import, applied the configuration:
POST /api/kea/service/reconfigure
OPNsense firewall rules for IoT VLAN
Rules applied to the IoT interface (igc1.94), evaluated
top-to-bottom:
| Action | Protocol | Source | Destination | Port | Description |
|---|---|---|---|---|---|
| Allow | TCP/UDP | IoT net | Pi-hole VIPs | 53, 8080 | DNS and Pi-hole API |
| Allow | TCP | IoT net | fablehaven | 8123 | Home Assistant |
| Allow | TCP/UDP | IoT net | any | 80, 443 | Internet HTTP/HTTPS |
| Allow | UDP | IoT net | any | 123 | NTP |
| Block | any | IoT net | RFC1918 | any | Block LAN access |
| Allow | any | IoT net | any | any | Internet egress |
Order matters: the RFC1918 block must precede the internet allow rule.
DNS enforcement NAT rules
Five NAT port-forward rules added for the IoT interface, mirroring the existing LAN rules:
## Redirect DNS to Pi-hole VIP
IoT net → any:53 → redirect to 192.168.95.250:53
## Block DNS-over-TLS
IoT net → any:853 → block
## Redirect DNS-over-HTTPS candidates
IoT net → any:443 (DoH hosts) → block
The no rdr exclusion pattern exempts nevermore (192.168.95.1) and
the Pi-hole nodes themselves from DNS redirection — same pattern as
the LAN interface rules.
Omada SSID configuration
TheBurrow-IoT SSID configured in Omada (Coruscant controller):
- Band: 2.4 GHz only (IoT devices rarely support 5 GHz)
- VLAN: 94 (tagged)
- Security: WPA2-Personal
- Client isolation: Enabled (clients cannot reach each other)
- Band steering: Disabled
The SSID is active on all three EAP720 APs but serves only 2.4 GHz clients. 5 GHz associations are rejected.
Exception: Vivint smart panel requires 5 GHz WiFi and cannot use TheBurrow-IoT. It remains on the main TheBurrow SSID (currently LAN, pending VLAN 95 Trusted cutover).
NetBox IoT device import
IoT devices were imported to NetBox via pynetbox after the VLAN was live. A Python script using the netbox-tools pynetbox environment created device records with:
- Site: the-burrow
- Location: by room (living room, kitchen, etc.)
- Device role: iot-device
- IP address: from Kea reservation
- Custom field:
vlan: 94
Import executed via:
~/src/netbox-tools/.venv/bin/python scripts/import_iot_devices.py
Insights
Kea DHCP requires explicit interface binding
The Kea subnet must specify the interface field matching the VLAN
subinterface name (igc1.94). Without this, Kea does not know which
interface to listen on for DHCP requests from the new subnet. The
subnet will be configured but DHCP discovery packets will not be
answered.
OPNsense FreeBSD shell limitations
OPNsense runs on FreeBSD with csh as the default shell. Standard bash
heredocs and sed -i quoting patterns do not work. For any Kea
configuration that cannot be done via the UI, use the REST API
directly. Do not attempt to edit config.xml or Kea JSON files via
shell scripts on nevermore.
Firewall rule order is security-critical
The RFC1918 block rule must appear before the general internet allow rule. OPNsense evaluates rules top-to-bottom, first match wins. An IoT device attempting to reach 192.168.95.x would otherwise match the general allow rule and succeed.
DNS option-data must serve VIPs not raw IPs
The Kea domain-name-servers option must serve the Pi-hole VIPs
(.250, .251), never the raw Pi-hole host IPs (.3, .6, .158).
This ensures DNS traffic passes through the VRRP layer and failover
works correctly. Serving raw IPs bypasses VRRP entirely.
Device discovery is the hard part
The technical implementation of VLAN 94 took a few hours. Identifying all 31 IoT devices, mapping MAC addresses to friendly names, and deciding which devices belonged on IoT vs. LAN took longer. The HA device registry was the most useful source — it had friendly names and MAC addresses for everything that had ever touched the network.
Results
- IoT VLAN 94 live with 31 device reservations
- All IoT devices receiving 192.168.94.x addresses from Kea
- DNS enforcement active: port 53 redirected to Pi-hole VIPs
- TheBurrow-IoT SSID operational on all three EAP720 APs
- IoT devices blocked from reaching LAN (192.168.95.0/24)
- Home Assistant reachable from IoT (explicit allow rule)
- NetBox updated with IoT device inventory
- Dobby (deceased Roomba) reservation pending removal from Kea
Related
- newcomb-labs/engineering-journal-kb #453 — ADR: IoT VLAN segmentation strategy
- newcomb-labs/engineering-journal-kb #451 — ADR: No RDR exclusion rule
- newcomb-labs/opnsense-lab #19 (IoT VLAN isolation issue)
- mistborn-ops/ansible #280 (IoT VLAN design issue)
- newcomb-labs/engineering-journal-kb #459 (this issue)