VLAN Hardening Execution Checklist¶
Date: 2026-03-11 Status: Pending (requires home access) Prerequisite: OPNsense web UI via SSH tunnel or LAN Reference: vlan-design.md Risk level: Medium — incorrect rules can isolate cameras or break guest WiFi
Current State¶
- VLAN interfaces created: IOT (vtnet1.10, 192.168.10.1/24), GUEST (vtnet1.20, 192.168.20.1/24)
- DHCP configured per VLAN
- MokerLink switch trunks configured (P1, P7 trunk; P6 IoT access)
- TP-Link PoE switch connected to MokerLink P6 (IoT VLAN)
- Firewall rules: NOT applied — VLANs exist but have no rules (default deny)
- Cameras: Still on Management VLAN (192.168.0.110, .111, .101) — not yet moved to IoT VLAN
Before You Start¶
- Open OPNsense web UI via SSH tunnel:
-
Backup OPNsense config (System → Configuration → Backups → Download): Save as
config-pre-vlan-rules-YYYYMMDD.xml -
Have a rollback plan: If you lose access, connect directly to MokerLink P8 (Management VLAN) via Ethernet from MacBook. OPNsense LAN is always reachable at 192.168.0.1.
Phase 1: Create Aliases¶
Aliases simplify rule management. Create these before writing rules.
Firewall → Aliases → Add¶
| Name | Type | Content | Description |
|---|---|---|---|
| RFC1918 | Network(s) | 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 | All private networks |
| PiHole_DNS | Host(s) | 192.168.0.10 | Docker VM (Pi-hole) |
Note: OPNsense may already have a built-in RFC1918 alias — check before creating.
Apply changes after adding aliases.
Phase 2: LAN (Management) Rules¶
LAN should already have a default "allow all" rule. Verify:
Firewall → Rules → LAN¶
| # | Action | Source | Dest | Port | Protocol | Description |
|---|---|---|---|---|---|---|
| 1 | Pass | LAN net | any | any | any | Default allow all |
This should exist from the initial OPNsense setup. If not, create it.
Anti-lockout rule: OPNsense has a built-in anti-lockout rule (System → Settings → Administration). Ensure it's enabled — prevents accidentally blocking web UI access.
Phase 3: IoT VLAN Rules¶
Firewall → Rules → IOT¶
Rules are evaluated top to bottom, first match wins. Order matters.
| # | Action | Protocol | Source | Dest | Port | Description |
|---|---|---|---|---|---|---|
| 1 | Pass | TCP/UDP | IOT net | PiHole_DNS | 53 | Allow DNS to Pi-hole | | 2 | Pass | UDP | IOT net | IOT address | 123 | Allow NTP from OPNsense gateway | | 3 | Block | any | IOT net | RFC1918 | any | Block all private networks | | 4 | Block | any | IOT net | any | any | Block internet (default deny) |
Why no "Cameras → Frigate" rule?Frigate (on Docker VM/LAN)pulls RTSP streams from cameras — it initiates the connection. LAN has "allow all", so Frigate → Camera traffic is permitted. Return traffic (camera responses) is handled by OPNsense's stateful firewall. Cameras never need to initiate connections to Frigate.
NTP (rule 2): Points to IOT address (192.168.10.1 — OPNsense on the IOT interface) so cameras can sync time. OPNsense forwards NTP upstream. Using the gateway avoids opening a path to the LAN subnet.
How to create each rule¶
Firewall → Rules → IOT → Add (+ icon at top for first position)¶
For each rule:
- Action: Pass or Block
- Interface: IOT
- Direction: in
- TCP/IP Version: IPv4
- Protocol: as specified
- Source: Select alias or "IOT net"
- Destination: Select alias, "IOT address", or "any"
- Destination port range: as specified (53, 123, or "any")
- Description: as specified
- Log: Enable on Block rules (helps debugging)
Click Apply Changes after adding all rules.
Verification¶
Expected behavior:
✓ IoT device → Pi-hole DNS (53) = PASS
✓ IoT device → OPNsense NTP (123) = PASS
✓ Frigate → Camera RTSP (554) = PASS (initiated from LAN, stateful return)
✗ IoT device → internet = BLOCKED
✗ IoT device → NAS = BLOCKED
✗ IoT device → Docker VM = BLOCKED
✗ Camera → camera = BLOCKED (no IoT→IoT rule)
Phase 4: Guest VLAN Rules¶
Firewall → Rules → GUEST¶
| # | Action | Protocol | Source | Dest | Port | Description |
|---|---|---|---|---|---|---|
| 1 | Pass | TCP/UDP | GUEST net | PiHole_DNS | 53 | Allow DNS to Pi-hole | | 2 | Block | any | GUEST net | RFC1918 | any | Block all private networks | | 3 | Pass | TCP | GUEST net | any | 80 | Allow HTTP | | 4 | Pass | TCP | GUEST net | any | 443 | Allow HTTPS | | 5 | Block | any | GUEST net | any | any | Block all else |
Apply Changes¶
Verification¶
Expected behavior:
✓ Guest → Pi-hole DNS (53) = PASS
✓ Guest → internet HTTP/HTTPS = PASS
✗ Guest → LAN devices = BLOCKED
✗ Guest → Docker VM = BLOCKED
✗ Guest → NAS = BLOCKED
✗ Guest → SSH anywhere = BLOCKED
Phase 5: Move Cameras to IoT VLAN¶
This is the disruptive step. Cameras will lose connectivity until re-IPed.
5a. Verify physical path¶
Cameras are connected via:
This means cameras already receive VLAN 10 at Layer 2. They just need VLAN 10 IPs.
5b. Reserve static IPs in OPNsense DHCP¶
Services → DHCPv4 → IOT → Static Mappings¶
| MAC Address | IP | Hostname | Description |
|---|---|---|---|
| (from cam 1) | 192.168.10.101 | front-door | Reolink front door | | (from cam 2) | 192.168.10.102 | back-yard | Reolink back yard |
Get MAC addresses from current DHCP leases:
- Services → DHCPv4 → Leases → find 192.168.0.110 and .111
Tapo C110 (192.168.0.101) is WiFi — it's on VLAN 1 via the TP-Link AP (HomeNet SSID). Moving it to IoT VLAN requires either:
- A separate IoT SSID on the AP (needs OpenWrt or multi-SSID support)
- Leave it on Management VLAN for now (still accessible to Frigate)
5c. Re-IP Reolink cameras¶
Option A: Via Reolink app/web UI (simplest)
- Access each camera's web UI (192.168.0.110, .111)
- Settings → Network → change IP to 192.168.10.101/.102, gateway 192.168.10.1, DNS 192.168.0.10
- Camera reboots on new IP
Option B: Let DHCP handle it¶
- Cameras already connected to PoE switch on VLAN 10
- If cameras use DHCP, they'll get 192.168.10.x from OPNsense IOT DHCP
- Static mapping ensures they always get .101/.102
5d. Update Frigate config¶
After cameras move to 192.168.10.x, update the go2rtc stream URLs in frigate.yml:
Change camera IPs in the go2rtc.streams section:
go2rtc:
streams:
front_door_sub:
- "rtsp://{FRIGATE_REOLINK_USER}:{FRIGATE_FRONT_PASS}@192.168.10.101:554/..." # was .0.110
front_door_main:
- "rtsp://{FRIGATE_REOLINK_USER}:{FRIGATE_FRONT_PASS}@192.168.10.101:554/..." # was .0.110
back_yard_sub:
- "rtsp://{FRIGATE_REOLINK_USER}:{FRIGATE_BACK_PASS}@192.168.10.102:554/..." # was .0.111
back_yard_main:
- "rtsp://{FRIGATE_REOLINK_USER}:{FRIGATE_BACK_PASS}@192.168.10.102:554/..." # was .0.111
indoor:
# Leave as-is — Tapo stays on Management VLAN (WiFi, no IoT SSID)
Note: Camera IPs only appear in go2rtc.streams, not in the cameras: section (cameras reference go2rtc via 127.0.0.1:8554).
Deploy: ssh docker-vm "cd /opt/homelab/repo/docker/fixed/docker-vm/security && docker compose restart frigate"
5e. Verify camera feeds¶
- Check Frigate UI (
taguato.cronova.dev) — all streams should reconnect - Check Firewall → Log Files → Live View — camera traffic should match IoT rules
- Verify no internet access: Firewall logs should show BLOCKED for camera → WAN
Phase 6: Test Everything¶
From Management VLAN (MacBook on LAN)¶
# Should work
ping 192.168.0.1 # OPNsense
ping 192.168.0.10 # Docker VM
curl https://jara.cronova.dev # HA
From IoT VLAN (camera perspective)¶
Check OPNsense Firewall → Live Log, filter by IOT interface:
- DNS queries to 192.168.0.10:53 → PASS ✓
- NTP to 192.168.10.1:123 → PASS ✓
- Any other outbound → BLOCK ✓
From LAN → IoT (Frigate pulling cameras)¶
- Frigate RTSP pull from 192.168.10.101:554 → PASS ✓ (LAN allows all, stateful return)
- Check Frigate UI: all camera feeds reconnect after IP change
From Guest VLAN¶
Connect a phone to Guest WiFi (if configured) or test later:
nslookup google.com→ should resolve (DNS passes) ✓curl <https://google.com> → should work (443 passes) ✓ping 192.168.0.10→ should fail (RFC1918 blocked) ✓
Post-Execution¶
- [ ] Update
security-hardening.md: change "IoT VLAN (10): Configured, rules pending" → active - [ ] Update
vlan-design.md: fix Phase 1 checklist if needed, mark Phase 3 done - [ ] Update Frigate config in repo if camera IPs changed
- [ ] Update Uptime Kuma monitors if camera IPs changed
- [ ] Take OPNsense config backup (post-rules)
Rollback¶
If something breaks:
- Cameras offline in Frigate: Frigate pulls RTSP from cameras — check LAN "allow all" rule still exists. If cameras moved to IoT VLAN, verify Frigate config has new IPs (192.168.10.x). Check OPNsense Live Log for blocked traffic.
- Cameras can't resolve DNS: Check IOT rule 1 (DNS to Pi-hole) is above the RFC1918 block rule
- Guest WiFi broken: Check GUEST rules — DNS (rule 1) must be above RFC1918 block (rule 2)
- Nuclear option: Firewall → Rules → IOT/GUEST → delete all rules → Apply. Returns to default deny (safe but cameras lose DNS/NTP)
- Restore config: System → Configuration → Backups → restore pre-rules XML