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.1 Connect to vyos-02 Console

ssh kvm-02 "sudo virsh console vyos-02 --force"

1.2 Verify VLAN Interfaces Up and Correct IPs

show interfaces | grep -E 'eth0\.(20|30|40)'
Expected Output
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
Your Output (paste below)

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
Expected: Lines showing VOICE, GUEST, IOT pools with subnet and range
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 ...
Your Output (2026-03-04)
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
output
=== 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'
output
=== 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'
Your Output (paste below)
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'
Expected: Each pool points to its .1 gateway
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
Execution Output (2026-03-04)
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'
Expected: DNS servers configured (pfSense or external)
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
Execution Output (2026-03-04)
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'
Expected: NAT rules for outbound traffic from VLANs
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
...
Your Output (paste below)
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
Your Output (paste below)
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
Expected: Default route to upstream gateway (pfSense or ISP)
S>* 0.0.0.0/0 [1/0] via 10.50.1.1, eth0, ...
Your Output (paste below)
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
Expected: 3 packets transmitted, 3 received
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
Your Output (paste below)
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
Expected: DNS resolution works AND ping succeeds
PING google.com (...) 56(84) bytes of data.
64 bytes from ...: icmp_seq=1 ttl=... time=...
Your Output (paste below)
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'
Your Output (paste below)
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
Verify DATA DHCP Removed
show configuration commands | grep -E 'dhcp-server.*DATA'
Expected Output
(empty - no DATA DHCP config)

Phase 2: Disable VLANs 20, 30, 40 on pfSense

2.1 Load Credentials

dsource d000 dev/network

2.2 List pfSense VLAN Interfaces

netapi pfsense interfaces
expected output
               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 dhcp disable command. Use pfSense REST API v2 directly.

Disable DHCP on opt2 (VOICE_VLAN) - jq extracts confirmation
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")"}'
Expected Output
{
  "interface": "opt2",
  "enabled": false,
  "range": "10.50.20.100 - 10.50.20.200"
}
Disable DHCP on opt3 (GUEST_VLAN) - jq object construction
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")"}'
Disable DHCP on opt4 (RESEARCH_VLAN / IOT) - same pattern
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")"}'
Apply DHCP Changes (required after disabling) - jq for status
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"}'
Verify DHCP Disabled - jq with select() filter
# 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
Expected Output
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)
Execution Output (2026-03-04)
=== 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 interface disable command. Use pfSense REST API v2 directly.

Disable opt2 (VOICE_VLAN) Interface - jq with ternary
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
  }'
Expected Output
{
  "id": "opt2",
  "descr": "VOICE_VLAN",
  "status": "DOWN",
  "ipaddr": "static"
}
Disable opt3 (GUEST_VLAN) Interface - same jq pattern
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)}'
Disable opt4 (RESEARCH_VLAN) Interface
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)}'
Apply Interface Changes - jq for apply confirmation
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"}'
Verify Interfaces Disabled - jq array filter with map()
# 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'
Expected Output
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)
Your Output (paste below)
(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
Execution Output (2026-03-04)
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
Execution Output (2026-03-04)
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.

Before (VLAN 10 with DOT1X)
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
Configuration Commands
conf t
interface GigabitEthernet1/0/4
 switchport access vlan 40
 no source template DefaultWiredDot1xClosedAuth
end
After (VLAN 40 without DOT1X)
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}'
Expected Output
{
  "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.

IOT (opt4)
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}'
GUEST (opt3)
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}'
VOICE (opt2)
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

IOT Route (10.50.40.0/24)
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}'
GUEST Route (10.50.30.0/24)
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}'
VOICE Route (10.50.20.0/24)
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))"'
Expected Output
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

3.1 From vyos-02 - Check DHCP Leases

show dhcp server leases

Look for leases in VOICE, GUEST, IOT pools (10.50.20.x, 10.50.30.x, 10.50.40.x).

3.2 From vyos-02 - Ping Test Devices

If you have known device IPs on VLANs 20, 30, 40:

ping 10.50.40.100

3.3 From vyos-02 - Verify Routing to Internet

ping 8.8.8.8
show ip route

3.4 From vyos-02 - Verify NAT Working

show nat source translations

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>

4.2 Verify Client Got vyos-02 as Gateway

ip route | grep default
Expected Output
default via 10.50.20.1 dev ...   # (or 10.50.30.1 or 10.50.40.1)

4.3 Test Internet from Client

ping 8.8.8.8
curl -I https://google.com

Rollback Procedure

If issues occur, re-enable pfSense:

Load Credentials

dsource d000 dev/network

Re-enable pfSense VLAN Interfaces

Enable all 3 interfaces in a loop - jq extracts status
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
Apply Interface Changes - jq confirms
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"}'
Verify Interfaces Re-enabled - jq with keys_unsorted
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

Enable DHCP on all 3 interfaces - jq confirms each
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
Apply DHCP Changes - jq with error handling
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'
Verify DHCP Re-enabled - jq with group_by summary
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(", ")
    })
'
Expected Output (rollback successful)
[
  {
    "status": "ENABLED",
    "count": 3,
    "interfaces": "opt2, opt3, opt4"
  }
]
jq patterns used: IN() (membership test), sort_by() (ordering), group_by() (aggregation), string interpolation with conditionals

Disable vyos-02 DHCP (Temporary)

ssh kvm-02 "sudo virsh console vyos-02 --force"
configure
delete service dhcp-server shared-network-name VOICE
delete service dhcp-server shared-network-name GUEST
delete service dhcp-server shared-network-name IOT
commit
save

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 Domus-IoT with device-specific PSK 2. WLC sends MAC to ISE for iPSK/MAB authentication 3. ISE queries iPSK Manager (10.50.1.30) for MAC → PSK mapping 4. ISE returns Domus_IoT_Profile with VLAN IOT_VLAN (40) 5. WLC tags traffic to VLAN 40 6. Device gets DHCP from vyos-02 (10.50.40.1)


5.1 Prerequisite Alignment (Completed 2026-03-04)

Component Change Value Status

Switch (3560CX)

Renamed VLAN 40

RESEARCH_VLANIOT_VLAN

[x] DONE

ISE

Updated authorization profile

Domus_IoT_Profile VLAN: GUEST_VLANIOT_VLAN

[x] DONE

WLC

Verified VLAN name

VLAN 40 = IOT_VLAN (already correct)

[x] DONE

Commands Executed
# 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>"
If device not registered, add via iPSK Manager WebUI:
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>"
Expected: Session shows MAB auth with Domus_IoT_Profile
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'
Expected: Lease in 10.50.40.100-199 range
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}'
Expected Output
10.50.40.1xx/24
ip route | awk '/default/ {print "Gateway:", $3}'
Expected Output
Gateway: 10.50.40.1

5.3.5 Verify DNS Configuration

cat /etc/resolv.conf | awk '/nameserver/ {print}'
Expected Output (public DNS per vyos-02 DHCP config)
nameserver 8.8.8.8
nameserver 1.1.1.1

5.3.6 Verify Internet Access

ping -c 3 8.8.8.8
Expected: 0% packet loss
3 packets transmitted, 3 received, 0% packet loss
curl -I https://google.com 2>/dev/null | awk '/^HTTP/ {print}'
Expected Output
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
Expected: No response (timeout or 100% packet loss)
1 packets transmitted, 0 received, 100% packet loss
# Should FAIL - IOT cannot reach DATA
ping -c 1 -W 2 10.50.10.1
Expected: No response
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

Check WLC client status
netapi wlc run "show wireless client summary | include <mac-pattern>"
Check ISE RADIUS failures
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"

Wrong VLAN Assigned

Check ISE authorization profile
netapi ise get-authz-profile "Domus_IoT_Profile" | awk '/vlan/ {print}'
Expected: IOT_VLAN (not GUEST_VLAN)
vlan                       {'nameID': 'IOT_VLAN', 'tagID': 1}

iPSK PSK Rejected

Verify MAC in iPSK Manager
# iPSK Manager WebUI or API
curl -sk "https://{ipsk-mgr-ip}:8443/api/endpoints" | jq '.[] | select(.mac | test("<mac>"))'

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:

Authorization Profile
netapi ise create-authz-profile "IOT_Test_Profile" --vlan IOT_VLAN --descr "Test profile for trusted devices in IOT VLAN"
Authorization Rule (Domus_MAB policy set)
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:

ISE DataConnect Query
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"
Output (2026-03-04 19:10)
┏━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━┓
┃ 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:

1. Check ISE Endpoint
netapi ise get-endpoint 04:5F:B9:78:02:20
2. Check ISE Auth History
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"
3. Check Switch Access-Session
netapi ios exec "show access-session int g1/0/5 d"
4. Check Switch MAC Table
netapi ios exec "show mac address-table int g1/0/5"

Validation Results:

1. ISE Endpoint Details (2026-03-04 19:12)
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                                                     │
╰──────────────────────────────────────────────────────────────────────────────╯
2. ISE DataConnect Auth History (2026-03-04 19:12)
┏━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━┓
┃ 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      │
└──────────┴─────────────────┴────────────────────────┴────────┘
3. Switch Access-Session (2026-03-04 19:13)
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:

  1. 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
  2. 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).

Verify iPSK device 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:

  1. Add iPSK device (64:32:A8:C4:C7:19) to Research_Onboard group

  2. Reconnect to Domus-IoT SSID

  3. Verify IOT_Test_Profile applied


Post-Migration

After VLANs 20, 30, 40 are stable on vyos-02 (24-48 hours):

  1. Continue with main runbook: VyOS Migration

  2. Phase 7+ for security hardening

  3. 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'
Table 1. ANSI Color Codes
Code Color Use For

\u001b[31m

Red

Errors, DISABLED, DOWN

\u001b[32m

Green

Success, ENABLED, UP

\u001b[33m

Yellow

Warnings, pending

\u001b[34m

Blue

Info, labels

\u001b[35m

Magenta

Highlights

\u001b[36m

Cyan

Secondary info

\u001b[1m

Bold

Emphasis

\u001b[0m

Reset

End color (required!)

Common Patterns Summary

Pattern Use Case Example

.field // "default"

Null safety

.message // "ok"

if .x then A else B end

Conditional

Status UP/DOWN

select(.x == "val")

Filter single

Find opt2

map(select(…​))

Filter array

Find multiple interfaces

.x | IN("a","b")

Membership test

Match opt2/3/4

.x | test("regex")

Regex match

Match opt[234]

@tsv / @csv

Tabular output

Pipe to column

group_by(.field)

Aggregate

Count by status

sort_by(.field)

Order results

Alphabetical

[.[].x] | join(",")

Collect values

List interfaces