VyOS VLAN Fast-Track Migration (20, 30, 40)
Fast-track migration of VLANs 20 (VOICE), 30 (GUEST), 40 (IOT) to vyos-02 due to rogue DHCP incident on 2026-03-04.
|
Context: vyos-02 DHCP was active for all VLANs including DATA (10), causing rogue DHCP and client outages. This runbook migrates VLANs 20, 30, 40 to vyos-02 intentionally while keeping DATA (10) on pfSense until full cutover. |
Current State
| VLAN | Name | Current Gateway | Target Gateway |
|---|---|---|---|
10 |
DATA |
pfSense (10.50.10.1) |
pfSense (until full cutover) |
20 |
VOICE |
pfSense (10.50.20.1) |
vyos-02 (10.50.20.1) |
30 |
GUEST |
pfSense (10.50.30.1) |
vyos-02 (10.50.30.1) |
40 |
IOT |
pfSense (10.50.40.1) |
vyos-02 (10.50.40.1) |
Prerequisites
-
Console access to vyos-02:
ssh kvm-02 "sudo virsh console vyos-02 --force" -
pfSense WebUI or netapi access
-
vyos-02 has VLAN interfaces configured (eth0.20, eth0.30, eth0.40)
Phase 1: Validate vyos-02 Readiness
|
DO NOT proceed to Phase 2 until ALL validations in Phase 1 pass. Each section has a PASS/FAIL checkbox. If ANY check fails, STOP and fix before continuing. |
1.2 Verify VLAN Interfaces Up and Correct IPs
show interfaces | grep -E 'eth0\.(20|30|40)'
eth0.20 10.50.20.1/24 ... u/u VOICE eth0.30 10.50.30.1/24 ... u/u GUEST eth0.40 10.50.40.1/24 ... u/u IOT
| Validation | Status |
|---|---|
eth0.20 has IP 10.50.20.1/24 and state u/u |
[ ] PASS / [ ] FAIL |
eth0.30 has IP 10.50.30.1/24 and state u/u |
[ ] PASS / [ ] FAIL |
eth0.40 has IP 10.50.40.1/24 and state u/u |
[ ] PASS / [ ] FAIL |
1.3 Verify DHCP Pools Configured
show configuration commands | grep -E 'dhcp-server.*shared-network-name.*(VOICE|GUEST|IOT)' | head -20
set service dhcp-server shared-network-name VOICE subnet 10.50.20.0/24 ... set service dhcp-server shared-network-name GUEST subnet 10.50.30.0/24 ... set service dhcp-server shared-network-name IOT subnet 10.50.40.0/24 ...
set service dhcp-server shared-network-name GUEST subnet 10.50.30.0/24 lease '3600' set service dhcp-server shared-network-name GUEST subnet 10.50.30.0/24 option default-router '10.50.30.1' set service dhcp-server shared-network-name GUEST subnet 10.50.30.0/24 option domain-name 'inside.domusdigitalis.dev' set service dhcp-server shared-network-name GUEST subnet 10.50.30.0/24 option name-server '10.50.1.90' set service dhcp-server shared-network-name GUEST subnet 10.50.30.0/24 option name-server '10.50.1.91' set service dhcp-server shared-network-name GUEST subnet 10.50.30.0/24 range 0 start '10.50.30.100' set service dhcp-server shared-network-name GUEST subnet 10.50.30.0/24 range 0 stop '10.50.30.199' set service dhcp-server shared-network-name IOT subnet 10.50.40.0/24 lease '86400' set service dhcp-server shared-network-name IOT subnet 10.50.40.0/24 option default-router '10.50.40.1' set service dhcp-server shared-network-name IOT subnet 10.50.40.0/24 option domain-name 'inside.domusdigitalis.dev' set service dhcp-server shared-network-name IOT subnet 10.50.40.0/24 option name-server '8.8.8.8' set service dhcp-server shared-network-name IOT subnet 10.50.40.0/24 option name-server '1.1.1.1' set service dhcp-server shared-network-name IOT subnet 10.50.40.0/24 range 0 start '10.50.40.100' set service dhcp-server shared-network-name IOT subnet 10.50.40.0/24 range 0 stop '10.50.40.199' set service dhcp-server shared-network-name VOICE subnet 10.50.20.0/24 lease '86400' set service dhcp-server shared-network-name VOICE subnet 10.50.20.0/24 option default-router '10.50.20.1' set service dhcp-server shared-network-name VOICE subnet 10.50.20.0/24 option domain-name 'inside.domusdigitalis.dev' set service dhcp-server shared-network-name VOICE subnet 10.50.20.0/24 option name-server '10.50.1.90' set service dhcp-server shared-network-name VOICE subnet 10.50.20.0/24 option name-server '10.50.1.91'
| Validation | Status |
|---|---|
VOICE pool exists with subnet 10.50.20.0/24 |
[x] PASS |
GUEST pool exists with subnet 10.50.30.0/24 |
[x] PASS |
IOT pool exists with subnet 10.50.40.0/24 |
[x] PASS |
Validate before proceeding
# 1. Verify pfSense DHCP DISABLED on opt2/3/4
echo "=== pfSense DHCP Status ==="
curl -sk -X GET "https://$PFSENSE_HOST/api/v2/services/dhcp_servers" \
-H "X-API-Key: $PFSENSE_API_SECRET" \
| jq -r '
.data[]
| select(.id | IN("opt2", "opt3", "opt4"))
| "\(.id)\t\(.interface)\t\(if .enable then "\u001b[31mENABLED ✗\u001b[0m" else "\u001b[32mDISABLED ✓\u001b[0m" end)\t\(.range_from) - \(.range_to)"
' | column -t
=== pfSense DHCP Status === opt2 opt2 DISABLED ✓ 10.50.20.100 - 10.50.20.200 opt3 opt3 DISABLED ✓ 10.50.30.100 - 10.50.30.150 opt4 opt4 DISABLED ✓ 10.50.40.100 - 10.50.40.200
# 2. Verify pfSense Interfaces (still ENABLED - not disabled yet)
echo "=== pfSense Interface Status ==="
curl -sk -X GET "https://$PFSENSE_HOST/api/v2/interfaces" \
-H "X-API-Key: $PFSENSE_API_SECRET" \
| jq -r '
.data
| map(select(.id | test("opt[234]")))
| .[]
| "\(.id)\t\(.descr)\t\(if .enable then "\u001b[33mENABLED (pending disable)\u001b[0m" else "\u001b[32mDISABLED ✓\u001b[0m" end)\t\(.ipaddr // "none")"
' | column -t -s $'\t'
=== pfSense Interface Status === opt2 VOICE_VLAN DISABLED ✓ 10.50.20.1 opt3 GUEST_VLAN DISABLED ✓ 10.50.30.1 opt4 RESEARCH_VLAN DISABLED ✓ 10.50.40.1
1.4 Verify DHCP Pool Ranges
show configuration commands | grep -E 'dhcp-server.*range'
set service dhcp-server shared-network-name DATA subnet 10.50.10.0/24 range 0 start '10.50.10.100' set service dhcp-server shared-network-name DATA subnet 10.50.10.0/24 range 0 stop '10.50.10.199' set service dhcp-server shared-network-name GUEST subnet 10.50.30.0/24 range 0 start '10.50.30.100' set service dhcp-server shared-network-name GUEST subnet 10.50.30.0/24 range 0 stop '10.50.30.199' set service dhcp-server shared-network-name IOT subnet 10.50.40.0/24 range 0 start '10.50.40.100' set service dhcp-server shared-network-name IOT subnet 10.50.40.0/24 range 0 stop '10.50.40.199' set service dhcp-server shared-network-name VOICE subnet 10.50.20.0/24 range 0 start '10.50.20.100' set service dhcp-server shared-network-name VOICE subnet 10.50.20.0/24 range 0 stop '10.50.20.199'
| Validation | Status |
|---|---|
VOICE range starts at 10.50.20.100 or similar |
[x] PASS |
GUEST range starts at 10.50.30.100 or similar |
[x] PASS |
IOT range starts at 10.50.40.100 or similar |
[x] PASS |
1.5 Verify DHCP Default Gateway (Router Option)
show configuration commands | grep -E 'dhcp-server.*default-router'
set service dhcp-server shared-network-name VOICE subnet 10.50.20.0/24 default-router 10.50.20.1 set service dhcp-server shared-network-name GUEST subnet 10.50.30.0/24 default-router 10.50.30.1 set service dhcp-server shared-network-name IOT subnet 10.50.40.0/24 default-router 10.50.40.1
set service dhcp-server shared-network-name DATA subnet 10.50.10.0/24 option default-router '10.50.10.1' set service dhcp-server shared-network-name GUEST subnet 10.50.30.0/24 option default-router '10.50.30.1' set service dhcp-server shared-network-name IOT subnet 10.50.40.0/24 option default-router '10.50.40.1' set service dhcp-server shared-network-name VOICE subnet 10.50.20.0/24 option default-router '10.50.20.1'
| Validation | Status |
|---|---|
VOICE default-router is 10.50.20.1 |
[x] PASS |
GUEST default-router is 10.50.30.1 |
[x] PASS |
IOT default-router is 10.50.40.1 |
[x] PASS |
1.6 Verify DHCP DNS Servers
show configuration commands | grep -E 'dhcp-server.*name-server'
set service dhcp-server shared-network-name VOICE subnet ... name-server 10.50.10.1 set service dhcp-server shared-network-name GUEST subnet ... name-server 10.50.10.1 set service dhcp-server shared-network-name IOT subnet ... name-server 10.50.10.1
set service dhcp-server shared-network-name DATA subnet 10.50.10.0/24 option name-server '10.50.1.90' set service dhcp-server shared-network-name DATA subnet 10.50.10.0/24 option name-server '10.50.1.91' set service dhcp-server shared-network-name GUEST subnet 10.50.30.0/24 option name-server '10.50.1.90' set service dhcp-server shared-network-name GUEST subnet 10.50.30.0/24 option name-server '10.50.1.91' set service dhcp-server shared-network-name IOT subnet 10.50.40.0/24 option name-server '8.8.8.8' set service dhcp-server shared-network-name IOT subnet 10.50.40.0/24 option name-server '1.1.1.1' set service dhcp-server shared-network-name VOICE subnet 10.50.20.0/24 option name-server '10.50.1.90' set service dhcp-server shared-network-name VOICE subnet 10.50.20.0/24 option name-server '10.50.1.91'
| Validation | Status |
|---|---|
DNS servers configured for all pools |
[x] PASS |
1.7 Verify NAT/Masquerade Rules
show configuration commands | grep -E 'nat.*source.*rule'
set nat source rule ... outbound-interface eth0 set nat source rule ... source address 10.50.20.0/24 set nat source rule ... translation address masquerade ...
set nat source rule 100 description 'SNAT INFRA → WAN' set nat source rule 100 outbound-interface name 'eth1' set nat source rule 100 source group network-group 'NET_INFRA' set nat source rule 100 translation address 'masquerade' set nat source rule 110 description 'SNAT DATA → WAN' set nat source rule 110 outbound-interface name 'eth1' set nat source rule 110 source group network-group 'NET_DATA' set nat source rule 110 translation address 'masquerade' set nat source rule 120 description 'SNAT VOICE → WAN' set nat source rule 120 outbound-interface name 'eth1' set nat source rule 120 source group network-group 'NET_VOICE' set nat source rule 120 translation address 'masquerade' set nat source rule 130 description 'SNAT GUEST → WAN' set nat source rule 130 outbound-interface name 'eth1' set nat source rule 130 source group network-group 'NET_GUEST' set nat source rule 130 translation address 'masquerade' set nat source rule 140 description 'SNAT IOT → WAN' set nat source rule 140 outbound-interface name 'eth1' set nat source rule 140 source group network-group 'NET_IOT' set nat source rule 140 translation address 'masquerade' set nat source rule 150 description 'SNAT SECURITY → WAN' set nat source rule 150 outbound-interface name 'eth1' set nat source rule 150 source group network-group 'NET_SECURITY' set nat source rule 150 translation address 'masquerade' set nat source rule 160 description 'SNAT SERVICES → WAN' set nat source rule 160 outbound-interface name 'eth1' set nat source rule 160 source group network-group 'NET_SERVICES' set nat source rule 160 translation address 'masquerade'
show nat source rules
Rule Source Destination Proto Out-Int Translation
------ --------------- ------------- ------- --------- -------------
100 @N_NET_INFRA 0.0.0.0/0 any eth1 masquerade
sport any dport any
110 @N_NET_DATA 0.0.0.0/0 any eth1 masquerade
sport any dport any
120 @N_NET_VOICE 0.0.0.0/0 any eth1 masquerade
sport any dport any
130 @N_NET_GUEST 0.0.0.0/0 any eth1 masquerade
sport any dport any
140 @N_NET_IOT 0.0.0.0/0 any eth1 masquerade
sport any dport any
150 @N_NET_SECURITY 0.0.0.0/0 any eth1 masquerade
sport any dport any
160 @N_NET_SERVICES 0.0.0.0/0 any eth1 masquerade
sport any dport any
| Validation | Status |
|---|---|
NAT masquerade rule exists for 10.50.20.0/24 (VOICE) |
[x] PASS |
NAT masquerade rule exists for 10.50.30.0/24 (GUEST) |
[x] PASS |
NAT masquerade rule exists for 10.50.40.0/24 (IOT) |
[x] PASS |
1.8 Verify Default Route Exists
show ip route 0.0.0.0/0
S>* 0.0.0.0/0 [1/0] via 10.50.1.1, eth0, ...
Routing entry for 0.0.0.0/0 Known via "static", distance 210, metric 0, tag 210, best Last update 17:53:07 ago Flags: Recursion Selected RR Distance Status: Installed * 192.168.1.254, via eth1, weight 1
| Validation | Status |
|---|---|
Default route exists via upstream gateway |
[x] PASS |
1.9 Verify Internet Connectivity from vyos-02
ping -c 3 8.8.8.8
PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data. 64 bytes from 8.8.8.8: icmp_seq=1 ttl=... time=... ... 3 packets transmitted, 3 received, 0% packet loss
vyos@vyos-02:~$ ping 8.8.8.8 PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data. 64 bytes from 8.8.8.8: icmp_seq=1 ttl=118 time=4.78 ms ^C --- 8.8.8.8 ping statistics --- 1 packets transmitted, 1 received, 0% packet loss, time 0ms rtt min/avg/max/mdev = 4.775/4.775/4.775/0.000 ms
ping -c 3 google.com
PING google.com (...) 56(84) bytes of data. 64 bytes from ...: icmp_seq=1 ttl=... time=...
PING google.com (142.250.68.46) 56(84) bytes of data. 64 bytes from lax17s46-in-f14.1e100.net (142.250.68.46): icmp_seq=1 ttl=118 time=3.99 ms 64 bytes from lax17s46-in-f14.1e100.net (142.250.68.46): icmp_seq=2 ttl=118 time=3.98 ms ^C --- google.com ping statistics --- 2 packets transmitted, 2 received, 0% packet loss, time 1001ms rtt min/avg/max/mdev = 3.976/3.984/3.992/0.008 ms
| Validation | Status |
|---|---|
ping 8.8.8.8 succeeds (0% packet loss) |
[x] PASS |
ping google.com succeeds (DNS + connectivity) |
[x] PASS |
1.10 Verify Firewall Allows VLAN Traffic
show configuration commands | grep -E 'firewall.*name'
set firewall ipv4 name ALLOW_ESTABLISHED default-action 'drop' set firewall ipv4 name ALLOW_ESTABLISHED rule 10 action 'accept' set firewall ipv4 name ALLOW_ESTABLISHED rule 10 state 'established' set firewall ipv4 name ALLOW_ESTABLISHED rule 10 state 'related' set firewall ipv4 name ALLOW_ESTABLISHED rule 20 action 'drop' set firewall ipv4 name ALLOW_ESTABLISHED rule 20 state 'invalid' set firewall ipv4 name DATA_LOCAL default-action 'drop' set firewall ipv4 name DATA_LOCAL rule 10 action 'accept' set firewall ipv4 name DATA_LOCAL rule 10 state 'established' set firewall ipv4 name DATA_LOCAL rule 10 state 'related' set firewall ipv4 name DATA_LOCAL rule 20 action 'accept' set firewall ipv4 name DATA_LOCAL rule 20 description 'SSH from ADMINS only' set firewall ipv4 name DATA_LOCAL rule 20 destination port '22' set firewall ipv4 name DATA_LOCAL rule 20 protocol 'tcp' set firewall ipv4 name DATA_LOCAL rule 20 source group address-group 'ADMINS' set firewall ipv4 name DATA_LOCAL rule 25 action 'accept' set firewall ipv4 name DATA_LOCAL rule 25 description 'HTTPS API from ADMINS only' set firewall ipv4 name DATA_LOCAL rule 25 destination port '443' set firewall ipv4 name DATA_LOCAL rule 25 protocol 'tcp' set firewall ipv4 name DATA_LOCAL rule 25 source group address-group 'ADMINS' set firewall ipv4 name DATA_LOCAL rule 30 action 'accept' set firewall ipv4 name DATA_LOCAL rule 30 destination port '67' set firewall ipv4 name DATA_LOCAL rule 30 protocol 'udp' set firewall ipv4 name DATA_LOCAL rule 40 action 'accept' set firewall ipv4 name DATA_LOCAL rule 40 destination port '53' set firewall ipv4 name DATA_LOCAL rule 40 protocol 'tcp_udp' set firewall ipv4 name DATA_LOCAL rule 50 action 'accept' set firewall ipv4 name DATA_LOCAL rule 50 protocol 'icmp' set firewall ipv4 name DATA_MGMT default-action 'drop' set firewall ipv4 name DATA_MGMT rule 10 action 'accept' set firewall ipv4 name DATA_MGMT rule 10 state 'established' set firewall ipv4 name DATA_MGMT rule 10 state 'related' set firewall ipv4 name DATA_MGMT rule 20 action 'accept' set firewall ipv4 name DATA_MGMT rule 20 description 'Allow DNS' set firewall ipv4 name DATA_MGMT rule 20 destination group address-group 'DNS_SERVERS' set firewall ipv4 name DATA_MGMT rule 20 destination group port-group 'DNS_PORTS' set firewall ipv4 name DATA_MGMT rule 20 protocol 'tcp_udp' set firewall ipv4 name DATA_MGMT rule 30 action 'accept' set firewall ipv4 name DATA_MGMT rule 30 description 'Allow Wazuh agent' set firewall ipv4 name DATA_MGMT rule 30 destination group address-group 'WAZUH_MANAGER' set firewall ipv4 name DATA_MGMT rule 30 destination group port-group 'WAZUH_PORTS' set firewall ipv4 name DATA_MGMT rule 30 protocol 'tcp_udp' set firewall ipv4 name DATA_MGMT rule 40 action 'accept' set firewall ipv4 name DATA_MGMT rule 40 description 'Allow K8s services' set firewall ipv4 name DATA_MGMT rule 40 destination group network-group 'K8S_SERVICES' set firewall ipv4 name DATA_MGMT rule 40 destination port '80,443' set firewall ipv4 name DATA_MGMT rule 40 protocol 'tcp' set firewall ipv4 name DATA_SECURITY default-action 'drop' set firewall ipv4 name DATA_SECURITY rule 10 action 'accept' set firewall ipv4 name DATA_SECURITY rule 10 state 'established' set firewall ipv4 name DATA_SECURITY rule 10 state 'related' set firewall ipv4 name DATA_SECURITY rule 20 action 'accept' set firewall ipv4 name DATA_SECURITY rule 20 description 'Vault API' set firewall ipv4 name DATA_SECURITY rule 20 destination port '8200' set firewall ipv4 name DATA_SECURITY rule 20 protocol 'tcp' set firewall ipv4 name DATA_SECURITY rule 30 action 'accept' set firewall ipv4 name DATA_SECURITY rule 30 description 'RADIUS auth/acct' set firewall ipv4 name DATA_SECURITY rule 30 destination port '1812,1813' set firewall ipv4 name DATA_SECURITY rule 30 protocol 'tcp' set firewall ipv4 name DATA_SERVICES default-action 'drop' set firewall ipv4 name DATA_SERVICES rule 10 action 'accept' set firewall ipv4 name DATA_SERVICES rule 10 state 'established' set firewall ipv4 name DATA_SERVICES rule 10 state 'related' set firewall ipv4 name DATA_SERVICES rule 20 action 'accept' set firewall ipv4 name DATA_SERVICES rule 20 description 'Web services' set firewall ipv4 name DATA_SERVICES rule 20 destination port '80,443' set firewall ipv4 name DATA_SERVICES rule 20 protocol 'tcp' set firewall ipv4 name DATA_SERVICES rule 30 action 'accept' set firewall ipv4 name DATA_SERVICES rule 30 description 'LDAP/LDAPS' set firewall ipv4 name DATA_SERVICES rule 30 destination port '389,636' set firewall ipv4 name DATA_SERVICES rule 30 protocol 'tcp' set firewall ipv4 name DATA_WAN default-action 'accept' set firewall ipv4 name GUEST_DATA default-action 'drop' set firewall ipv4 name GUEST_IOT default-action 'drop' set firewall ipv4 name GUEST_LOCAL default-action 'drop' set firewall ipv4 name GUEST_LOCAL rule 10 action 'accept' set firewall ipv4 name GUEST_LOCAL rule 10 state 'established' set firewall ipv4 name GUEST_LOCAL rule 10 state 'related' set firewall ipv4 name GUEST_LOCAL rule 30 action 'accept' set firewall ipv4 name GUEST_LOCAL rule 30 destination port '67' set firewall ipv4 name GUEST_LOCAL rule 30 protocol 'udp' set firewall ipv4 name GUEST_LOCAL rule 40 action 'accept' set firewall ipv4 name GUEST_LOCAL rule 40 destination port '53' set firewall ipv4 name GUEST_LOCAL rule 40 protocol 'tcp_udp' set firewall ipv4 name GUEST_MGMT default-action 'drop' set firewall ipv4 name GUEST_VOICE default-action 'drop' set firewall ipv4 name GUEST_WAN default-action 'accept' set firewall ipv4 name IOT_DATA default-action 'drop' set firewall ipv4 name IOT_GUEST default-action 'drop' set firewall ipv4 name IOT_LOCAL default-action 'drop' set firewall ipv4 name IOT_LOCAL rule 10 action 'accept' set firewall ipv4 name IOT_LOCAL rule 10 state 'established' set firewall ipv4 name IOT_LOCAL rule 10 state 'related' set firewall ipv4 name IOT_LOCAL rule 20 action 'accept' set firewall ipv4 name IOT_LOCAL rule 20 description 'Allow ICMP to gateway' set firewall ipv4 name IOT_LOCAL rule 20 protocol 'icmp' set firewall ipv4 name IOT_LOCAL rule 30 action 'accept' set firewall ipv4 name IOT_LOCAL rule 30 destination port '67' set firewall ipv4 name IOT_LOCAL rule 30 protocol 'udp' set firewall ipv4 name IOT_LOCAL rule 40 action 'accept' set firewall ipv4 name IOT_LOCAL rule 40 destination port '53' set firewall ipv4 name IOT_LOCAL rule 40 protocol 'tcp_udp' set firewall ipv4 name IOT_MGMT default-action 'drop' set firewall ipv4 name IOT_MGMT rule 10 action 'accept' set firewall ipv4 name IOT_MGMT rule 10 state 'established' set firewall ipv4 name IOT_MGMT rule 10 state 'related' set firewall ipv4 name IOT_MGMT rule 20 action 'accept' set firewall ipv4 name IOT_MGMT rule 20 description 'Allow Wazuh agent' set firewall ipv4 name IOT_MGMT rule 20 destination group address-group 'WAZUH_MANAGER' set firewall ipv4 name IOT_MGMT rule 20 destination group port-group 'WAZUH_PORTS' set firewall ipv4 name IOT_MGMT rule 20 protocol 'tcp_udp' set firewall ipv4 name IOT_VOICE default-action 'drop' set firewall ipv4 name IOT_WAN default-action 'drop' set firewall ipv4 name IOT_WAN rule 10 action 'accept' set firewall ipv4 name IOT_WAN rule 10 state 'established' set firewall ipv4 name IOT_WAN rule 10 state 'related' set firewall ipv4 name IOT_WAN rule 20 action 'accept' set firewall ipv4 name IOT_WAN rule 20 description 'Allow HTTP/HTTPS' set firewall ipv4 name IOT_WAN rule 20 destination port '80,443' set firewall ipv4 name IOT_WAN rule 20 protocol 'tcp' set firewall ipv4 name IOT_WAN rule 30 action 'accept' set firewall ipv4 name IOT_WAN rule 30 destination port '123' set firewall ipv4 name IOT_WAN rule 30 protocol 'udp' set firewall ipv4 name IOT_WAN rule 40 action 'accept' set firewall ipv4 name IOT_WAN rule 40 description 'Allow ICMP outbound' set firewall ipv4 name IOT_WAN rule 40 protocol 'icmp' set firewall ipv4 name IOT_WAN rule 50 action 'accept' set firewall ipv4 name IOT_WAN rule 50 description 'Allow DNS to internet' set firewall ipv4 name IOT_WAN rule 50 destination port '53' set firewall ipv4 name IOT_WAN rule 50 protocol 'udp' set firewall ipv4 name IOT_WAN rule 60 action 'accept' set firewall ipv4 name IOT_WAN rule 60 description 'Allow CAPWAP control (OEAP)' set firewall ipv4 name IOT_WAN rule 60 destination port '5246' set firewall ipv4 name IOT_WAN rule 60 protocol 'udp' set firewall ipv4 name IOT_WAN rule 70 action 'accept' set firewall ipv4 name IOT_WAN rule 70 description 'Allow CAPWAP data (OEAP)' set firewall ipv4 name IOT_WAN rule 70 destination port '5247' set firewall ipv4 name IOT_WAN rule 70 protocol 'udp' set firewall ipv4 name LOCAL_MGMT default-action 'accept' set firewall ipv4 name LOCAL_MGMT description 'Router to MGMT infrastructure' set firewall ipv4 name LOCAL_WAN default-action 'accept' set firewall ipv4 name LOCAL_WAN description 'Router outbound to WAN' set firewall ipv4 name MGMT_DATA default-action 'accept' set firewall ipv4 name MGMT_GUEST default-action 'accept' set firewall ipv4 name MGMT_IOT default-action 'accept' set firewall ipv4 name MGMT_LOCAL default-action 'drop' set firewall ipv4 name MGMT_LOCAL rule 10 action 'accept' set firewall ipv4 name MGMT_LOCAL rule 10 state 'established' set firewall ipv4 name MGMT_LOCAL rule 10 state 'related' set firewall ipv4 name MGMT_LOCAL rule 20 action 'accept' set firewall ipv4 name MGMT_LOCAL rule 20 description 'SSH' set firewall ipv4 name MGMT_LOCAL rule 20 destination port '22' set firewall ipv4 name MGMT_LOCAL rule 20 protocol 'tcp' set firewall ipv4 name MGMT_LOCAL rule 30 action 'accept' set firewall ipv4 name MGMT_LOCAL rule 30 description 'DHCP' set firewall ipv4 name MGMT_LOCAL rule 30 destination port '67' set firewall ipv4 name MGMT_LOCAL rule 30 protocol 'udp' set firewall ipv4 name MGMT_LOCAL rule 40 action 'accept' set firewall ipv4 name MGMT_LOCAL rule 40 description 'DNS' set firewall ipv4 name MGMT_LOCAL rule 40 destination port '53' set firewall ipv4 name MGMT_LOCAL rule 40 protocol 'tcp_udp' set firewall ipv4 name MGMT_LOCAL rule 50 action 'accept' set firewall ipv4 name MGMT_LOCAL rule 50 description 'ICMP' set firewall ipv4 name MGMT_LOCAL rule 50 protocol 'icmp' set firewall ipv4 name MGMT_LOCAL rule 60 action 'accept' set firewall ipv4 name MGMT_LOCAL rule 60 description 'HTTPS API' set firewall ipv4 name MGMT_LOCAL rule 60 destination port '443' set firewall ipv4 name MGMT_LOCAL rule 60 protocol 'tcp' set firewall ipv4 name MGMT_LOCAL rule 70 action 'accept' set firewall ipv4 name MGMT_LOCAL rule 70 description 'BGP from Cilium' set firewall ipv4 name MGMT_LOCAL rule 70 destination group port-group 'BGP' set firewall ipv4 name MGMT_LOCAL rule 70 protocol 'tcp' set firewall ipv4 name MGMT_LOCAL rule 70 source group address-group 'K3S_NODES' set firewall ipv4 name MGMT_SECURITY default-action 'accept' set firewall ipv4 name MGMT_SERVICES default-action 'accept' set firewall ipv4 name MGMT_VOICE default-action 'accept' set firewall ipv4 name MGMT_WAN default-action 'accept' set firewall ipv4 name SECURITY_LOCAL default-action 'drop' set firewall ipv4 name SECURITY_LOCAL rule 10 action 'accept' set firewall ipv4 name SECURITY_LOCAL rule 10 state 'established' set firewall ipv4 name SECURITY_LOCAL rule 10 state 'related' set firewall ipv4 name SECURITY_LOCAL rule 20 action 'accept' set firewall ipv4 name SECURITY_LOCAL rule 20 description 'SSH from ADMINS only' set firewall ipv4 name SECURITY_LOCAL rule 20 destination port '22' set firewall ipv4 name SECURITY_LOCAL rule 20 protocol 'tcp' set firewall ipv4 name SECURITY_LOCAL rule 20 source group address-group 'ADMINS' set firewall ipv4 name SECURITY_LOCAL rule 30 action 'accept' set firewall ipv4 name SECURITY_LOCAL rule 30 destination port '67' set firewall ipv4 name SECURITY_LOCAL rule 30 protocol 'udp' set firewall ipv4 name SECURITY_LOCAL rule 40 action 'accept' set firewall ipv4 name SECURITY_LOCAL rule 40 destination port '53' set firewall ipv4 name SECURITY_LOCAL rule 40 protocol 'tcp_udp' set firewall ipv4 name SECURITY_LOCAL rule 50 action 'accept' set firewall ipv4 name SECURITY_LOCAL rule 50 protocol 'icmp' set firewall ipv4 name SECURITY_SERVICES default-action 'drop' set firewall ipv4 name SECURITY_SERVICES rule 10 action 'accept' set firewall ipv4 name SECURITY_SERVICES rule 10 state 'established' set firewall ipv4 name SECURITY_SERVICES rule 10 state 'related' set firewall ipv4 name SECURITY_SERVICES rule 20 action 'accept' set firewall ipv4 name SECURITY_SERVICES rule 20 description 'LDAP/LDAPS' set firewall ipv4 name SECURITY_SERVICES rule 20 destination port '389,636' set firewall ipv4 name SECURITY_SERVICES rule 20 protocol 'tcp' set firewall ipv4 name SERVICES_LOCAL default-action 'drop' set firewall ipv4 name SERVICES_LOCAL rule 10 action 'accept' set firewall ipv4 name SERVICES_LOCAL rule 10 state 'established' set firewall ipv4 name SERVICES_LOCAL rule 10 state 'related' set firewall ipv4 name SERVICES_LOCAL rule 20 action 'accept' set firewall ipv4 name SERVICES_LOCAL rule 20 description 'SSH from ADMINS only' set firewall ipv4 name SERVICES_LOCAL rule 20 destination port '22' set firewall ipv4 name SERVICES_LOCAL rule 20 protocol 'tcp' set firewall ipv4 name SERVICES_LOCAL rule 20 source group address-group 'ADMINS' set firewall ipv4 name SERVICES_LOCAL rule 30 action 'accept' set firewall ipv4 name SERVICES_LOCAL rule 30 destination port '67' set firewall ipv4 name SERVICES_LOCAL rule 30 protocol 'udp' set firewall ipv4 name SERVICES_LOCAL rule 40 action 'accept' set firewall ipv4 name SERVICES_LOCAL rule 40 destination port '53' set firewall ipv4 name SERVICES_LOCAL rule 40 protocol 'tcp_udp' set firewall ipv4 name SERVICES_LOCAL rule 50 action 'accept' set firewall ipv4 name SERVICES_LOCAL rule 50 protocol 'icmp' set firewall ipv4 name SERVICES_SECURITY default-action 'drop' set firewall ipv4 name SERVICES_SECURITY rule 10 action 'accept' set firewall ipv4 name SERVICES_SECURITY rule 10 state 'established' set firewall ipv4 name SERVICES_SECURITY rule 10 state 'related' set firewall ipv4 name SERVICES_SECURITY rule 20 action 'accept' set firewall ipv4 name SERVICES_SECURITY rule 20 description 'Vault API' set firewall ipv4 name SERVICES_SECURITY rule 20 destination port '8200' set firewall ipv4 name SERVICES_SECURITY rule 20 protocol 'tcp' set firewall ipv4 name VOICE_LOCAL default-action 'drop' set firewall ipv4 name VOICE_LOCAL rule 10 action 'accept' set firewall ipv4 name VOICE_LOCAL rule 10 state 'established' set firewall ipv4 name VOICE_LOCAL rule 10 state 'related' set firewall ipv4 name VOICE_LOCAL rule 30 action 'accept' set firewall ipv4 name VOICE_LOCAL rule 30 destination port '67' set firewall ipv4 name VOICE_LOCAL rule 30 protocol 'udp' set firewall ipv4 name VOICE_LOCAL rule 40 action 'accept' set firewall ipv4 name VOICE_LOCAL rule 40 destination port '53' set firewall ipv4 name VOICE_LOCAL rule 40 protocol 'tcp_udp' set firewall ipv4 name VOICE_MGMT default-action 'drop' set firewall ipv4 name VOICE_MGMT rule 10 action 'accept' set firewall ipv4 name VOICE_MGMT rule 10 state 'established' set firewall ipv4 name VOICE_MGMT rule 10 state 'related' set firewall ipv4 name VOICE_MGMT rule 20 action 'accept' set firewall ipv4 name VOICE_MGMT rule 20 destination group address-group 'DNS_SERVERS' set firewall ipv4 name VOICE_MGMT rule 20 destination group port-group 'DNS_PORTS' set firewall ipv4 name VOICE_MGMT rule 20 protocol 'tcp_udp' set firewall ipv4 name VOICE_WAN default-action 'accept' set firewall ipv4 name WAN_IN default-action 'drop' set firewall ipv4 name WAN_IN rule 10 action 'accept' set firewall ipv4 name WAN_IN rule 10 state 'established' set firewall ipv4 name WAN_IN rule 10 state 'related' set firewall ipv4 name WAN_LOCAL default-action 'drop' set firewall ipv4 name WAN_LOCAL rule 10 action 'accept' set firewall ipv4 name WAN_LOCAL rule 10 state 'established' set firewall ipv4 name WAN_LOCAL rule 10 state 'related' set firewall ipv4 name WAN_LOCAL rule 20 action 'accept' set firewall ipv4 name WAN_LOCAL rule 20 icmp type-name 'echo-request' set firewall ipv4 name WAN_LOCAL rule 20 limit rate '5/second' set firewall ipv4 name WAN_LOCAL rule 20 protocol 'icmp' set firewall zone DATA from GUEST firewall name 'GUEST_DATA' set firewall zone DATA from IOT firewall name 'IOT_DATA' set firewall zone DATA from MGMT firewall name 'MGMT_DATA' set firewall zone DATA from WAN firewall name 'WAN_IN' set firewall zone GUEST from MGMT firewall name 'MGMT_GUEST' set firewall zone GUEST from WAN firewall name 'WAN_IN' set firewall zone IOT from MGMT firewall name 'MGMT_IOT' set firewall zone IOT from WAN firewall name 'WAN_IN' set firewall zone LOCAL from DATA firewall name 'DATA_LOCAL' set firewall zone LOCAL from GUEST firewall name 'GUEST_LOCAL' set firewall zone LOCAL from IOT firewall name 'IOT_LOCAL' set firewall zone LOCAL from MGMT firewall name 'MGMT_LOCAL' set firewall zone LOCAL from SECURITY firewall name 'SECURITY_LOCAL' set firewall zone LOCAL from SERVICES firewall name 'SERVICES_LOCAL' set firewall zone LOCAL from VOICE firewall name 'VOICE_LOCAL' set firewall zone LOCAL from WAN firewall name 'WAN_LOCAL' set firewall zone MGMT from DATA firewall name 'DATA_MGMT' set firewall zone MGMT from GUEST firewall name 'GUEST_MGMT' set firewall zone MGMT from IOT firewall name 'IOT_MGMT' set firewall zone MGMT from LOCAL firewall name 'LOCAL_MGMT' set firewall zone MGMT from VOICE firewall name 'VOICE_MGMT' set firewall zone MGMT from WAN firewall name 'WAN_IN' set firewall zone SECURITY from DATA firewall name 'DATA_SECURITY' set firewall zone SECURITY from MGMT firewall name 'MGMT_SECURITY' set firewall zone SECURITY from SERVICES firewall name 'SERVICES_SECURITY' set firewall zone SECURITY from WAN firewall name 'WAN_IN' set firewall zone SERVICES from DATA firewall name 'DATA_SERVICES' set firewall zone SERVICES from MGMT firewall name 'MGMT_SERVICES' set firewall zone SERVICES from SECURITY firewall name 'SECURITY_SERVICES' set firewall zone SERVICES from WAN firewall name 'WAN_IN' set firewall zone VOICE from MGMT firewall name 'MGMT_VOICE' set firewall zone VOICE from WAN firewall name 'WAN_IN' set firewall zone WAN from DATA firewall name 'DATA_WAN' set firewall zone WAN from GUEST firewall name 'GUEST_WAN' set firewall zone WAN from IOT firewall name 'IOT_WAN' set firewall zone WAN from LOCAL firewall name 'LOCAL_WAN' set firewall zone WAN from MGMT firewall name 'MGMT_WAN' set firewall zone WAN from VOICE firewall name 'VOICE_WAN'
|
If no firewall rules exist, default is ACCEPT (permissive). If rules exist, verify VLANs 20, 30, 40 can reach internet. |
| Validation | Status |
|---|---|
Firewall allows VOICE/GUEST/IOT to reach internet (or no firewall = permissive) |
[x] PASS |
1.11 Phase 1 Summary - GO/NO-GO Decision
| Check | Description | Status |
|---|---|---|
1.2 |
VLAN interfaces up with correct IPs |
[x] PASS |
1.3 |
DHCP pools configured |
[x] PASS |
1.4 |
DHCP ranges configured |
[x] PASS |
1.5 |
DHCP default-router correct |
[x] PASS |
1.6 |
DHCP DNS servers configured |
[x] PASS |
1.7 |
NAT masquerade rules exist |
[x] PASS |
1.8 |
Default route exists |
[x] PASS |
1.9 |
Internet connectivity works |
[x] PASS |
1.10 |
Firewall permits traffic |
[x] PASS |
|
ALL checks must PASS before proceeding. If ANY check fails, STOP HERE and fix the configuration before continuing to Phase 1.12. |
1.12 Disable DHCP for DATA (Prevent Rogue DHCP)
|
This prevents vyos-02 from acting as rogue DHCP on VLAN 10 (DATA). |
configure
delete service dhcp-server shared-network-name DATA
commit
save
show configuration commands | grep -E 'dhcp-server.*DATA'
(empty - no DATA DHCP config)
Phase 2: Disable VLANs 20, 30, 40 on pfSense
2.2 List pfSense VLAN Interfaces
netapi pfsense interfaces
Network Interfaces ┏━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓ ┃ Name ┃ Status ┃ IP Address ┃ Description ┃ ┡━━━━━━╇━━━━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩ │ wan │ up │ 192.168.1.184 │ WAN │ │ lan │ up │ N/A │ TRUNK │ │ opt1 │ up │ 10.50.10.1 │ DATA_VLAN │ │ opt2 │ up │ 10.50.20.1 │ VOICE_VLAN │ │ opt3 │ up │ 10.50.30.1 │ GUEST_VLAN │ │ opt4 │ up │ 10.50.40.1 │ RESEARCH_VLAN │ │ opt5 │ up │ 10.50.1.1 │ MGMT │ └──────┴────────┴───────────────┴───────────────┘
Interface mapping from output:
| Interface | Description | IP |
|---|---|---|
opt2 |
VOICE_VLAN |
10.50.20.1 |
opt3 |
GUEST_VLAN |
10.50.30.1 |
opt4 |
RESEARCH_VLAN |
10.50.40.1 |
2.3 Disable DHCP on pfSense for VLANs 20, 30, 40
|
netapi does not have a |
curl -sk -X PATCH "https://$PFSENSE_HOST/api/v2/services/dhcp_server" \
-H "X-API-Key: $PFSENSE_API_SECRET" \
-H "Content-Type: application/json" \
-d '{"id": "opt2", "enable": false}' \
| jq '{interface: .data.id, enabled: .data.enable, range: "\(.data.range_from // "n/a") - \(.data.range_to // "n/a")"}'
{
"interface": "opt2",
"enabled": false,
"range": "10.50.20.100 - 10.50.20.200"
}
curl -sk -X PATCH "https://$PFSENSE_HOST/api/v2/services/dhcp_server" \
-H "X-API-Key: $PFSENSE_API_SECRET" \
-H "Content-Type: application/json" \
-d '{"id": "opt3", "enable": false}' \
| jq '{interface: .data.id, enabled: .data.enable, range: "\(.data.range_from // "n/a") - \(.data.range_to // "n/a")"}'
curl -sk -X PATCH "https://$PFSENSE_HOST/api/v2/services/dhcp_server" \
-H "X-API-Key: $PFSENSE_API_SECRET" \
-H "Content-Type: application/json" \
-d '{"id": "opt4", "enable": false}' \
| jq '{interface: .data.id, enabled: .data.enable, range: "\(.data.range_from // "n/a") - \(.data.range_to // "n/a")"}'
curl -sk -X POST "https://$PFSENSE_HOST/api/v2/services/dhcp_server/apply" \
-H "X-API-Key: $PFSENSE_API_SECRET" \
-H "Content-Type: application/json" \
-d '{}' \
| jq '{status: .status, code: .code, message: .message // "success"}'
# Get ALL DHCP servers, filter to our interfaces, format as table
curl -sk -X GET "https://$PFSENSE_HOST/api/v2/services/dhcp_servers" \
-H "X-API-Key: $PFSENSE_API_SECRET" \
| jq -r '
.data[]
| select(.id == "opt2" or .id == "opt3" or .id == "opt4")
| "\(.id)\t\(if .enable then "\u001b[32mENABLED\u001b[0m" else "\u001b[31mDISABLED\u001b[0m" end)\t\(.range_from // "n/a")"
' | column -t
opt2 DISABLED 10.50.20.100 opt3 DISABLED 10.50.30.100 opt4 DISABLED 10.50.40.100
jq patterns used: // (alternative operator for null), \() (string interpolation), select() (filter), -r (raw output for piping to column)
|
=== opt2 === Enabled No === opt3 === Enabled No === opt4 === Enabled No
| Validation | Status |
|---|---|
opt2 (VOICE) DHCP disabled |
[x] PASS |
opt3 (GUEST) DHCP disabled |
[x] PASS |
opt4 (RESEARCH) DHCP disabled |
[x] PASS |
2.4 Disable pfSense VLAN Interfaces (Release .1 IPs)
|
netapi does not have an |
curl -sk -X PATCH "https://$PFSENSE_HOST/api/v2/interface" \
-H "X-API-Key: $PFSENSE_API_SECRET" \
-H "Content-Type: application/json" \
-d '{"id": "opt2", "enable": false}' \
| jq '{
id: .data.id,
descr: .data.descr,
status: (if .data.enable then "UP" else "DOWN" end),
ipaddr: .data.typev4
}'
{
"id": "opt2",
"descr": "VOICE_VLAN",
"status": "DOWN",
"ipaddr": "static"
}
curl -sk -X PATCH "https://$PFSENSE_HOST/api/v2/interface" \
-H "X-API-Key: $PFSENSE_API_SECRET" \
-H "Content-Type: application/json" \
-d '{"id": "opt3", "enable": false}' \
| jq '{id: .data.id, descr: .data.descr, status: (if .data.enable then "UP" else "DOWN" end)}'
curl -sk -X PATCH "https://$PFSENSE_HOST/api/v2/interface" \
-H "X-API-Key: $PFSENSE_API_SECRET" \
-H "Content-Type: application/json" \
-d '{"id": "opt4", "enable": false}' \
| jq '{id: .data.id, descr: .data.descr, status: (if .data.enable then "UP" else "DOWN" end)}'
curl -sk -X POST "https://$PFSENSE_HOST/api/v2/interface/apply" \
-H "X-API-Key: $PFSENSE_API_SECRET" \
-H "Content-Type: application/json" \
-d '{}' \
| jq '{status: .status, code: .code, message: .message // "applied"}'
# Get all interfaces, filter to opt2/3/4, format as summary
curl -sk -X GET "https://$PFSENSE_HOST/api/v2/interfaces" \
-H "X-API-Key: $PFSENSE_API_SECRET" \
| jq -r '
.data
| map(select(.id | test("opt[234]")))
| .[]
| "\(.id)\t\(.descr)\t\(if .enable then "\u001b[32mENABLED\u001b[0m" else "\u001b[31mDISABLED\u001b[0m" end)\t\(.ipaddr // "none")"
' | column -t -s $'\t'
opt2 VOICE_VLAN DISABLED 10.50.20.1 opt3 GUEST_VLAN DISABLED 10.50.30.1 opt4 RESEARCH_VLAN DISABLED 10.50.40.1
jq patterns used: if/then/else/end (ternary), map() (transform array), test() (regex match), @tsv (tab-separated output)
|
(pending execution)
| Validation | Status |
|---|---|
opt2 (VOICE) interface disabled/down |
[ ] PENDING |
opt3 (GUEST) interface disabled/down |
[ ] PENDING |
opt4 (RESEARCH) interface disabled/down |
[ ] PENDING |
|
Disabling the interface releases 10.50.20.1, 10.50.30.1, 10.50.40.1 so vyos-02 owns them exclusively. |
2.5 Switch Preparation (VLAN Isolation)
|
Prevent ARP/DHCP race conditions by isolating VLANs at switch layer before testing. |
2.5.1 Remove VLAN 10 from kvm-02 Trunk (Prevent Rogue DHCP)
This prevents vyos-02 from receiving DATA VLAN traffic entirely.
conf t
interface <kvm-02-trunk-port>
switchport trunk allowed vlan remove 10
end
LAB-3560CX-01(config-if)#switchport trunk allowed vlan remove 10 LAB-3560CX-01(config-if)#
2.5.2 Remove VLANs 20,30,40 from pfSense Trunk
This prevents pfSense from seeing VOICE/GUEST/IOT traffic (vyos-02 is now authoritative).
conf t
interface TenGigabitEthernet1/0/2
switchport trunk allowed vlan remove 20,30,40
end
LAB-3560CX-01(config-if)#int t1/0/2 LAB-3560CX-01(config-if)#switchport trunk allowed vlan remove 20,30,40 LAB-3560CX-01(config-if)#
2.5.3 Configure Test Port for IOT VLAN (No DOT1X)
Move g1/0/4 from DATA (VLAN 10) to IOT (VLAN 40) and disable 802.1X for testing.
interface GigabitEthernet1/0/4
description [DOT1X] AIR-AP1815T-BK9-RF
switchport access vlan 10
switchport mode access
ip arp inspection trust
source template DefaultWiredDot1xClosedAuth
spanning-tree portfast edge
end
conf t
interface GigabitEthernet1/0/4
switchport access vlan 40
no source template DefaultWiredDot1xClosedAuth
end
interface GigabitEthernet1/0/4
description [DOT1X] AIR-AP1815T-BK9-RF
switchport access vlan 40
switchport mode access
ip arp inspection trust
spanning-tree portfast edge
end
2.5.4 Verify Switch State
show ip device tracking interface g1/0/4
show ip dhcp snooping binding interface g1/0/4
| Validation | Status |
|---|---|
VLAN 10 removed from kvm-02 trunk |
[x] PASS |
VLANs 20,30,40 removed from pfSense trunk |
[x] PASS |
Test port g1/0/4 on VLAN 40 (IOT) |
[x] PASS |
DOT1X disabled on test port |
[x] PASS |
2.6 pfSense Static Routes (Return Traffic)
|
CRITICAL: After removing VLANs 20,30,40 from pfSense trunk, return traffic from MGMT hosts (bind-01, ipa-01, etc.) cannot reach these networks. pfSense needs static routes pointing to vyos-02. |
2.6.1 Create VYOS02_MGMT Gateway
curl -sk -X POST "https://$PFSENSE_HOST/api/v2/routing/gateway" \
-H "X-API-Key: $PFSENSE_API_SECRET" \
-H "Content-Type: application/json" \
-d '{
"name": "VYOS02_MGMT",
"interface": "opt5",
"gateway": "10.50.1.3",
"ipprotocol": "inet",
"descr": "VyOS-02 for VLAN routing"
}' \
| jq '{name: .data.name, gateway: .data.gateway, interface: .data.interface}'
{
"name": "VYOS02_MGMT",
"gateway": "10.50.1.3",
"interface": "opt5"
}
2.6.2 Remove IPv4 from pfSense VLAN Interfaces
pfSense interfaces still have the network assigned (even if disabled). Remove to allow static route.
curl -sk -X PATCH "https://$PFSENSE_HOST/api/v2/interface" \
-H "X-API-Key: $PFSENSE_API_SECRET" \
-H "Content-Type: application/json" \
-d '{"id": "opt4", "typev4": "none"}' \
| jq '{id: .data.id, descr: .data.descr, typev4: .data.typev4}'
curl -sk -X PATCH "https://$PFSENSE_HOST/api/v2/interface" \
-H "X-API-Key: $PFSENSE_API_SECRET" \
-H "Content-Type: application/json" \
-d '{"id": "opt3", "typev4": "none"}' \
| jq '{id: .data.id, descr: .data.descr, typev4: .data.typev4}'
curl -sk -X PATCH "https://$PFSENSE_HOST/api/v2/interface" \
-H "X-API-Key: $PFSENSE_API_SECRET" \
-H "Content-Type: application/json" \
-d '{"id": "opt2", "typev4": "none"}' \
| jq '{id: .data.id, descr: .data.descr, typev4: .data.typev4}'
2.6.3 Add Static Routes
curl -sk -X POST "https://$PFSENSE_HOST/api/v2/routing/static_route" \
-H "X-API-Key: $PFSENSE_API_SECRET" \
-H "Content-Type: application/json" \
-d '{
"network": "10.50.40.0/24",
"gateway": "VYOS02_MGMT",
"descr": "IOT via vyos-02"
}' \
| jq '{network: .data.network, gateway: .data.gateway}'
curl -sk -X POST "https://$PFSENSE_HOST/api/v2/routing/static_route" \
-H "X-API-Key: $PFSENSE_API_SECRET" \
-H "Content-Type: application/json" \
-d '{
"network": "10.50.30.0/24",
"gateway": "VYOS02_MGMT",
"descr": "GUEST via vyos-02"
}' \
| jq '{network: .data.network, gateway: .data.gateway}'
curl -sk -X POST "https://$PFSENSE_HOST/api/v2/routing/static_route" \
-H "X-API-Key: $PFSENSE_API_SECRET" \
-H "Content-Type: application/json" \
-d '{
"network": "10.50.20.0/24",
"gateway": "VYOS02_MGMT",
"descr": "VOICE via vyos-02"
}' \
| jq '{network: .data.network, gateway: .data.gateway}'
2.6.4 Apply Routing Changes
curl -sk -X POST "https://$PFSENSE_HOST/api/v2/routing/apply" \
-H "X-API-Key: $PFSENSE_API_SECRET" \
-H "Content-Type: application/json" \
-d '{}' \
| jq -r 'if .status == "ok" then "\u001b[32m✓ Routes applied\u001b[0m" else "\u001b[31m✗ ERROR: \(.message)\u001b[0m" end'
2.6.5 Verify Static Routes
curl -sk -X GET "https://$PFSENSE_HOST/api/v2/routing/static_routes" \
-H "X-API-Key: $PFSENSE_API_SECRET" \
| jq -r '.data[] | select(.gateway == "VYOS02_MGMT") | "\(.network) → \(.gateway) (\(.descr))"'
10.50.40.0/24 → VYOS02_MGMT (IOT via vyos-02) 10.50.30.0/24 → VYOS02_MGMT (GUEST via vyos-02) 10.50.20.0/24 → VYOS02_MGMT (VOICE via vyos-02)
Phase 3: Verify vyos-02 is Now Gateway
Phase 4: Client Validation
4.1 Force DHCP Renewal on Test Client
From a device on VLAN 20, 30, or 40:
# Linux
sudo dhclient -r && sudo dhclient
# Or NetworkManager
nmcli connection down <conn> && nmcli connection up <conn>
Rollback Procedure
If issues occur, re-enable pfSense:
Re-enable pfSense VLAN Interfaces
for iface in opt2 opt3 opt4; do
echo "=== Enabling $iface ==="
curl -sk -X PATCH "https://$PFSENSE_HOST/api/v2/interface" \
-H "X-API-Key: $PFSENSE_API_SECRET" \
-H "Content-Type: application/json" \
-d "{\"id\": \"$iface\", \"enable\": true}" \
| jq '{id, descr, enabled: .enable}'
done
curl -sk -X POST "https://$PFSENSE_HOST/api/v2/interface/apply" \
-H "X-API-Key: $PFSENSE_API_SECRET" \
-H "Content-Type: application/json" \
-d '{}' \
| jq '{status: .status, code: .code, message: .message // "interfaces restored"}'
curl -sk -X GET "https://$PFSENSE_HOST/api/v2/interfaces" \
-H "X-API-Key: $PFSENSE_API_SECRET" \
| jq -r '
.data
| map(select(.id | IN("opt2", "opt3", "opt4")))
| sort_by(.id)
| .[]
| "\(.id) \(.descr) \(if .enable then "\u001b[32m✓ UP\u001b[0m" else "\u001b[31m✗ DOWN\u001b[0m" end)"
'
Re-enable pfSense DHCP
for iface in opt2 opt3 opt4; do
echo "=== Enabling DHCP on $iface ==="
curl -sk -X PATCH "https://$PFSENSE_HOST/api/v2/services/dhcp_server" \
-H "X-API-Key: $PFSENSE_API_SECRET" \
-H "Content-Type: application/json" \
-d "{\"id\": \"$iface\", \"enable\": true}" \
| jq '{id: .data.id, enabled: .data.enable, range: "\(.data.range_from) - \(.data.range_to)"}'
done
curl -sk -X POST "https://$PFSENSE_HOST/api/v2/services/dhcp_server/apply" \
-H "X-API-Key: $PFSENSE_API_SECRET" \
-H "Content-Type: application/json" \
-d '{}' \
| jq -r 'if .status == "ok" then "\u001b[32m✓ DHCP applied successfully\u001b[0m" else "\u001b[31m✗ ERROR: \(.message)\u001b[0m" end'
curl -sk -X GET "https://$PFSENSE_HOST/api/v2/services/dhcp_servers" \
-H "X-API-Key: $PFSENSE_API_SECRET" \
| jq -r '
.data
| map(select(.id | IN("opt2", "opt3", "opt4")))
| group_by(.enable)
| map({
status: (if .[0].enable then "ENABLED" else "DISABLED" end),
count: length,
interfaces: [.[].id] | join(", ")
})
'
[
{
"status": "ENABLED",
"count": 3,
"interfaces": "opt2, opt3, opt4"
}
]
jq patterns used: IN() (membership test), sort_by() (ordering), group_by() (aggregation), string interpolation with conditionals
|
Completion Checklist
| Step | Description | Status |
|---|---|---|
1.1-1.10 |
Phase 1 vyos-02 readiness validated |
[x] DONE |
1.12 |
vyos-02 DATA DHCP disabled (no rogue DHCP) |
[~] SKIPPED (accepted risk) |
2.3 |
pfSense DHCP disabled for VLANs 20, 30, 40 |
[x] DONE |
2.4 |
pfSense interfaces disabled for VLANs 20, 30, 40 |
[ ] PENDING |
2.5 |
Switch Preparation (VLAN isolation at switch layer) |
[x] DONE |
2.6 |
pfSense Static Routes (return traffic via vyos-02) |
[x] DONE (IOT only) |
2.7 |
Firewall Rule Fixes (IOT_LOCAL ICMP, IOT_WAN ICMP) |
[x] DONE |
2.7a |
IOT_MGMT DNS rule (bind-01 port 53) |
[ ] PENDING - See Session Log 2026-03-04 |
3.1 |
vyos-02 showing DHCP leases for VOICE/GUEST/IOT |
[x] DONE (IOT: 10.50.40.103) |
3.3 |
vyos-02 can reach internet (ping 8.8.8.8) |
[x] DONE (validated in 1.9) |
4.2 |
Client has vyos-02 as gateway |
[x] DONE (IOT: 10.50.40.1) |
4.3 |
Client can reach internet (ping 8.8.8.8, curl https) |
[x] DONE (IOT: verified) |
5.1 |
ISE/Switch/WLC VLAN alignment (IOT_VLAN) |
[x] DONE (2026-03-04) |
5.1a |
IOT Wired Validation (modestus-aw via MAB) |
[x] DONE (2026-03-04) |
5.1b |
OEAP Wired Validation (04:5F:B9:78:02:20 → DATA_VLAN) |
[x] DONE (2026-03-04) |
5.2 |
iPSK device MAC registered |
[ ] PENDING |
5.3 |
iPSK wireless device test (Domus-IoT SSID) |
[ ] PENDING |
5.4 |
iPSK wireless validation checklist complete |
[ ] PENDING |
5.5 |
IOT_MGMT DNS firewall rule |
[ ] PENDING |
Phase 5: IOT Wireless iPSK Validation
|
IOT wireless devices use iPSK (Identity PSK) - each device has a unique WPA2-PSK registered in ISE via iPSK Manager. Authentication flow:
1. Device connects to |
5.1 Prerequisite Alignment (Completed 2026-03-04)
| Component | Change | Value | Status |
|---|---|---|---|
Switch (3560CX) |
Renamed VLAN 40 |
|
[x] DONE |
ISE |
Updated authorization profile |
|
[x] DONE |
WLC |
Verified VLAN name |
VLAN 40 = |
[x] DONE |
# Switch VLAN rename
LAB-3560CX-01(config)#vlan 40
LAB-3560CX-01(config-vlan)#name IOT_VLAN
# ISE profile update
netapi ise update-authz-profile "Domus_IoT_Profile" --vlan IOT_VLAN
# ISE verification
netapi ise get-authz-profile "Domus_IoT_Profile" | grep -i vlan
# Output: vlan {'nameID': 'IOT_VLAN', 'tagID': 1}
# WLC verification (no change needed)
netapi wlc run "show vlan brief | include 40"
# Output: 40 IOT_VLAN active
5.2 iPSK Device Registration Check
Before testing, verify the IOT device MAC is registered in iPSK Manager.
dsource d000 dev/network
# List iPSK endpoints (look for your test device MAC)
netapi ise get-endpoints | grep -i "<device-mac>"
URL: https://{ipsk-mgr-hostname}:8443
Add device MAC + generate unique PSK
5.3 Test IOT Wireless Device
5.3.1 Connect Device to WiFi
On IOT device (phone, smart device, etc.):
1. Connect to SSID: Domus-IoT
2. Enter device-specific PSK from iPSK Manager
5.3.2 Verify ISE Authentication
# Check ISE RADIUS live log for the device MAC
netapi ise mnt sessions | grep -i "<device-mac>"
MAC: <device-mac> Auth Method: MAB Authorization Profile: Domus_IoT_Profile VLAN: IOT_VLAN (40)
5.3.3 Verify DHCP from vyos-02
From vyos-02 console:
ssh kvm-02 "sudo virsh console vyos-02 --force"
show dhcp server leases | grep '10.50.40'
10.50.40.1xx <mac> <hostname> <pool> <expiry>
5.3.4 Verify Device IP Configuration
On the IOT device (if accessible):
ip addr show | awk '/inet.*10\.50\.40/ {print $2}'
10.50.40.1xx/24
ip route | awk '/default/ {print "Gateway:", $3}'
Gateway: 10.50.40.1
5.3.5 Verify DNS Configuration
cat /etc/resolv.conf | awk '/nameserver/ {print}'
nameserver 8.8.8.8 nameserver 1.1.1.1
5.3.6 Verify Internet Access
ping -c 3 8.8.8.8
3 packets transmitted, 3 received, 0% packet loss
curl -I https://google.com 2>/dev/null | awk '/^HTTP/ {print}'
HTTP/2 200
5.3.7 Verify Zero-Trust Isolation (RFC1918 Blocked)
IOT devices should NOT reach internal networks per VyOS firewall policy.
# Should FAIL - IOT cannot reach MGMT
ping -c 1 -W 2 10.50.1.90
1 packets transmitted, 0 received, 100% packet loss
# Should FAIL - IOT cannot reach DATA
ping -c 1 -W 2 10.50.10.1
1 packets transmitted, 0 received, 100% packet loss
5.4 iPSK Wireless Validation Checklist
| Step | Validation | Status |
|---|---|---|
5.2 |
Device MAC registered in iPSK Manager |
[ ] |
5.3.1 |
Device connected to Domus-IoT |
[ ] |
5.3.2 |
ISE shows MAB auth with Domus_IoT_Profile |
[ ] |
5.3.3 |
vyos-02 issued DHCP lease (10.50.40.x) |
[ ] |
5.3.4 |
Device IP in 10.50.40.0/24, gateway 10.50.40.1 |
[ ] |
5.3.5 |
DNS = 8.8.8.8, 1.1.1.1 (public) |
[ ] |
5.3.6 |
Internet access working (ping 8.8.8.8, curl google) |
[ ] |
5.3.7 |
RFC1918 blocked (cannot reach 10.50.1.x, 10.50.10.x) |
[ ] |
5.5 Troubleshooting iPSK
Device Not Getting IP
netapi wlc run "show wireless client summary | include <mac-pattern>"
netapi ise dc query "SELECT USERNAME, CALLING_STATION_ID, FAILURE_REASON FROM RADIUS_AUTHENTICATIONS WHERE CALLING_STATION_ID LIKE '%<mac>%' AND PASSED = 0 ORDER BY TIMESTAMP_TIMEZONE DESC FETCH FIRST 5 ROWS ONLY"
Session Log
2026-03-04: IOT Wired + OEAP Validation
IOT Wired Testing (modestus-aw)
Objective: Validate wired device in IOT VLAN via vyos-02.
Endpoint Details:
MAC |
08:92:04:38:11:9C |
Identity Group |
Research_Onboard (static) |
Profile |
Dell-Device (profiler) |
Switch Port |
G1/0/4 (LAB-3560CX-01) |
ISE Configuration Created:
netapi ise create-authz-profile "IOT_Test_Profile" --vlan IOT_VLAN --descr "Test profile for trusted devices in IOT VLAN"
netapi ise add-authz-rule "Domus_MAB" "IOT_Test_Device" "IOT_Test_Profile" \
--and "IdentityGroup:Name:equals:Endpoint Identity Groups:Research_Onboard" \
--rank 1
Validation Results:
netapi ise dc query "
SELECT
TO_CHAR(acs_timestamp, 'HH24:MI:SS') as time,
policy_set_name,
selected_azn_profiles,
passed as status
FROM mnt.radius_auth_48_live
WHERE calling_station_id LIKE '%08%92%04%38%11%9C%'
ORDER BY acs_timestamp DESC
FETCH FIRST 3 ROWS ONLY"
┏━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━┓ ┃ TIME ┃ POLICY_SET_NAME ┃ SELECTED_AZN_PROFILES ┃ STATUS ┃ ┡━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━┩ │ 19:10:05 │ Domus_MAB │ IOT_Test_Profile │ 1 │ │ 19:09:59 │ Domus_MAB │ IOT_Test_Profile │ 1 │ │ 16:23:32 │ Domus_8021X │ Domus_Research_Profile │ 1 │ └──────────┴─────────────────┴────────────────────────┴────────┘
Result: [x] PASS - Device authorized via IOT_Test_Profile, VLAN 40 assigned.
Work OEAP Validation
Objective: Confirm work Office Extend AP authenticates and gets DATA VLAN.
Endpoint Details:
MAC |
04:5F:B9:78:02:20 |
Switch Port |
G1/0/5 (LAB-3560CX-01) |
Device Type |
Cisco OEAP (Office Extend Access Point) |
Validation Commands:
netapi ise get-endpoint 04:5F:B9:78:02:20
netapi ise dc query "
SELECT
TO_CHAR(acs_timestamp, 'HH24:MI:SS') as time,
policy_set_name,
selected_azn_profiles,
passed as status
FROM mnt.radius_auth_48_live
WHERE calling_station_id LIKE '%04%5F%B9%78%02%20%'
ORDER BY acs_timestamp DESC
FETCH FIRST 5 ROWS ONLY"
netapi ios exec "show access-session int g1/0/5 d"
netapi ios exec "show mac address-table int g1/0/5"
Validation Results:
netapi ise get-endpoint 04:5F:B9:78:02:20 ╭─ Endpoint: 04:5F:B9:78:02:20 ────────────────────────────────────────────────╮ │ MAC Address : 04:5F:B9:78:02:20 │ │ Name : 04:5F:B9:78:02:20 │ │ Identity Group : Trusted_Access_Points │ │ Profile : Cisco-Device │ │ Static Assignment : True │ │ Static Group : True │ ╰──────────────────────────────────────────────────────────────────────────────╯
┏━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━┓ ┃ TIME ┃ POLICY_SET_NAME ┃ SELECTED_AZN_PROFILES ┃ STATUS ┃ ┡━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━┩ │ 19:11:47 │ Domus_MAB │ Domus_Secure_Profile │ 1 │ │ 19:11:41 │ Domus_MAB │ Domus_Secure_Profile │ 1 │ │ 16:23:34 │ Domus_MAB │ Domus_Secure_Profile │ 1 │ └──────────┴─────────────────┴────────────────────────┴────────┘
netapi ios exec "show access-session int g1/0/5 d"
Interface: GigabitEthernet1/0/5
IIF-ID: 0x18C7DE00
MAC Address: 045f.b978.0220
IPv6 Address: Unknown
IPv4 Address: 10.50.10.103
User-Name: 04-5F-B9-78-02-20
Status: Authorized
Domain: DATA
Oper host mode: multi-auth
Oper control dir: both
Session timeout: 28800s (local), Remaining: 28735s
Common Session ID: 0A32010B000000720035A8C4
Acct Session ID: 0x00000076
Handle: 0x44000073
Current Policy: POLICY_Gi1/0/5
Local Policies:
Service Template: DEFAULT_LINKSEC_POLICY_SHOULD_SECURE (priority 150)
Security Policy: Should Secure
Security Status: Link Unsecure
Server Policies:
Vlan Group: Vlan: 10
Security Policy: None
Security Status: Link Unsecure
Method status list:
Method State
mab Authc Success
Result: [x] PASS - OEAP authorized via Domus_Secure_Profile (Trusted_Access_Points), DATA_VLAN (10) assigned.
IOT_MGMT DNS Status
Issue Identified: Firewall logs showed DNS drops from IOT zone:
[ipv4-NAM-IOT_MGMT-default-D] SRC=10.50.40.103 DST=10.50.1.90 DPT=53
Root Cause: IOT_MGMT ruleset has no DNS rule. Runbook marked "DONE" but rule not present.
Current IOT_MGMT Rules:
Rule 10: established/related Rule 20: Wazuh agent traffic (NO DNS rule to bind-01)
Resolution Options:
-
Fix VyOS firewall - Add DNS rule:
configure set firewall ipv4 name IOT_MGMT rule 15 action accept set firewall ipv4 name IOT_MGMT rule 15 description "Allow DNS to bind-01" set firewall ipv4 name IOT_MGMT rule 15 destination address 10.50.1.90 set firewall ipv4 name IOT_MGMT rule 15 destination port 53 set firewall ipv4 name IOT_MGMT rule 15 protocol tcp_udp commit save -
Fix DHCP DNS server - Point to vyos-02 (10.50.40.1) instead of bind-01
Status: [ ] PENDING - Choose resolution approach
Pending: iPSK Wireless Validation
IOT_Test_Profile and authz rule work for wireless (iPSK hits Domus_MAB policy set).
netapi ise dc query "
SELECT policy_set_name, COUNT(*) as hits
FROM mnt.radius_auth_48_live
WHERE calling_station_id LIKE '%64%32%A8%C4%C7%19%'
GROUP BY policy_set_name"
# Result: Domus_MAB (11 hits)
Next Steps:
-
Add iPSK device (64:32:A8:C4:C7:19) to Research_Onboard group
-
Reconnect to Domus-IoT SSID
-
Verify IOT_Test_Profile applied
Post-Migration
After VLANs 20, 30, 40 are stable on vyos-02 (24-48 hours):
-
Continue with main runbook: VyOS Migration
-
Phase 7+ for security hardening
-
Phase 14 for full cutover (DATA VLAN + INFRA)
Appendix: jq Patterns Reference
Quick reference for all jq patterns used in this runbook.
Object Construction
# Basic - select specific fields
jq '{id, descr, enabled: .enable}'
# Rename fields
jq '{interface: .data.id, status: .data.enable}'
# Nested access with null safety (pfSense uses range_from, not range.from)
jq '{range: "\(.data.range_from // "n/a") - \(.data.range_to // "n/a")"}'
Conditional Expressions
# Ternary (if/then/else)
jq 'if .enable then "UP" else "DOWN" end'
# Inline conditional in string
jq '"\(.id) is \(if .enable then "active" else "inactive" end)"'
# With error message
jq 'if .applied then "✓ success" else "✗ ERROR: \(.message)" end'
Array Operations
# pfSense API wraps data in .data - always access that first
jq '.data[] | select(.id == "opt2")'
# Multiple values with IN()
jq '.data | map(select(.id | IN("opt2", "opt3", "opt4")))'
# Regex match with test()
jq '.data | map(select(.id | test("opt[234]")))'
# Sort
jq '.data | sort_by(.id)'
# Group and aggregate
jq '.data | group_by(.enable) | map({status: .[0].enable, count: length})'
pfSense API v2 envelope: All responses wrap data in {"code": 200, "status": "ok", "data": […]}. Always access .data first.
|
Output Formatting
# Tab-separated for column (note: access .data first for arrays)
jq -r '.data[] | [.id, .descr, .enable] | @tsv' | column -t
# Custom formatted lines
jq -r '.data[] | "\(.id)\t\(.descr)\t\(.enable)"'
# Join array to string
jq '.data | [.[].id] | join(", ")'
ANSI Color Output
# Red for errors/disabled, green for success/enabled
jq -r 'if .enable then "\u001b[32mENABLED\u001b[0m" else "\u001b[31mDISABLED\u001b[0m" end'
# Color in string interpolation
jq -r '"\(.id)\t\(if .enable then "\u001b[32m✓ UP\u001b[0m" else "\u001b[31m✗ DOWN\u001b[0m" end)"'
# Success/error messages
jq -r 'if .status == "ok" then "\u001b[32m✓ Success\u001b[0m" else "\u001b[31m✗ Error: \(.message)\u001b[0m" end'
| Code | Color | Use For |
|---|---|---|
|
Red |
Errors, DISABLED, DOWN |
|
Green |
Success, ENABLED, UP |
|
Yellow |
Warnings, pending |
|
Blue |
Info, labels |
|
Magenta |
Highlights |
|
Cyan |
Secondary info |
|
Bold |
Emphasis |
|
Reset |
End color (required!) |
Common Patterns Summary
| Pattern | Use Case | Example |
|---|---|---|
|
Null safety |
|
|
Conditional |
Status UP/DOWN |
|
Filter single |
Find opt2 |
|
Filter array |
Find multiple interfaces |
|
Membership test |
Match opt2/3/4 |
|
Regex match |
Match opt[234] |
|
Tabular output |
Pipe to column |
|
Aggregate |
Count by status |
|
Order results |
Alphabetical |
|
Collect values |
List interfaces |