# Cascades — Voice VLAN (VLAN 30) Cutover Runbook + Recon

- **Created:** 2026-06-16 (Howard-Home / claude-main)
- **Status:** APPROVED TO EXECUTE — Richard confirmed 2026-06-17. **pfSense PART A BUILT + VERIFIED 2026-06-17** (VLAN 30 iface `igc1.30`/opt241 @ `10.0.30.1/24`; DHCP `10.0.30.100-.250`, DNS `8.8.8.8/1.1.1.1`; 4 isolation rules enforced, verified via `pfctl -sr` to match the Guest VLAN exactly — any-proto quick blocks to 192.168.0.0/22 + 10.0.0.0/8 + 172.16.0.0/12, then pass any). **Remaining:** Part B (UniFi VOICE network + voice PPSK), then the live device moves in a maintenance window.
- **PART B BUILT 2026-06-17 (build-only):** UniFi VOICE network created (VLAN Only, VLAN 30, Cascades site). Voice PPSK key DEFINED on the CSCNet SSID -> VOICE network (configured in UniFi only; NOT yet entered on any handset, so no device is on VLAN 30 — pure build). Key vaulted at `clients/cascades-tucson/wifi-voice-ppsk.sops.yaml` (do NOT paste it here). Remaining = the live device moves (USW-16-PoE ports 1-8 + 16 -> VOICE; re-point 22 Poly phones to the voice key) in a maintenance window with Richard.
- **Gotcha caught 2026-06-17:** first GUI build set the rule Protocol to TCP — that leaves UDP (SIP/RTP/DNS) un-blocked to internal (leaks via the floating `pass inet all`). Isolation rules MUST be Protocol=Any. Fixed via the pfSense PHP config API.

## Vendor confirmation (Richard Turner, 2026-06-17) — materially simplifies the plan
Richard replied "we are good to start." Two confirmations change the runbook:
1. **Remote desktop gets its IP from DHCP** (not static, as recon had assumed). Verified on pfSense 2026-06-17: no static mapping for `e4:e7:49:52:3a:06` in ISC `dhcpd.conf`, but an active lease exists (`client-hostname "Vertical-Remote"`). => **Desktop cutover is zero-touch**: flip port 16 -> VOICE and it re-DHCPs onto `10.0.30.x`. No NIC change, no Vertical coordination needed for the desktop itself.
2. **Vertical reaches the desktop via LogMeIn, NOT the pfSense OpenVPN.** => **Part A step 6 (OpenVPN Client-Specific-Override + his cert CN) is DROPPED.** LogMeIn is an outbound agent, so the desktop only needs internet egress on VOICE — already granted by firewall rule (d). This is also better for HIPAA: a LogMeIn session lands on the isolated voice VLAN with no path to PHI / main LAN / VLAN 20.

Also resolved: **DHCP backend = ISC dhcpd** (Kea config present but dormant), confirmed live 2026-06-17 — reservations placed out-of-pool (below .100) are safe.
- **Driver:** Vertical (VoIP vendor, Richard Turner <RTurner@vertical.com>) cannot reach the phones from the remote-management desktop, and phone IPs drift. Root cause: when the network was segmented into VLANs, the Vertical remote desktop and the wired phones were left on the original LAN while the wireless phones landed on VLAN 20 — so the desktop has no path to the wireless phones (main-LAN -> VLAN 20 is blocked at pfSense).

## Goal
Consolidate ALL voice gear (Poly WiFi phones + AudioCodes wired phones + Vertical-Remote desktop) onto a dedicated, isolated voice network. Voice reaches the internet; blocked from main LAN / VLAN 20 / PHI. Vertical's pfSense OpenVPN scoped to the voice subnet only.

```
VOICE network:   VLAN 30
Subnet/gateway:  10.0.30.0/24   gw 10.0.30.1   (pfSense igc1.30)
DHCP pool:       10.0.30.100 - 10.0.30.250
Reservations:    NONE (2026-06-17 — Howard: desktop needs no static IP to connect; it's
                 a normal DHCP client like the phones; LogMeIn is name/agent-based)
Desktop:         dynamic lease from the pool (Vertical-Remote, e4:e7:49:52:3a:06)
```

## Systems
pfSense `192.168.0.1` does ALL routing + DHCP. UniFi (UOS controller `172.16.3.29`, Cascades site `685f39068e65331c46ef6dd2`) is L2 only here — every UniFi network is `purpose: vlan-only` (no subnets in UniFi). So building VLAN 30 touches BOTH systems.

---

## Confirmed architecture (UOS controller, 2026-06-16)

| Device class | Count | Attach | Currently lands on |
|---|---|---|---|
| Poly phones | 22 active (~29 historical) | **WiFi**, SSID **CSCNet**, APs building-wide | VLAN 20 "Internal" (`10.0.20.x`) |
| AudioCodes phones | 8 | **Wired**, USW-16-PoE **ports 1-8** | "Default" / main LAN (`192.168.2/3.x`) |
| Vertical-Remote desktop | 1 | **Wired**, USW-16-PoE **port 16** | "Default" / main LAN (`192.168.2.180`, static) |

**CSCNet is a shared PPSK SSID** (`wlanconf 685f39078e65331c46ef7ee5`, `private_preshared_keys_enabled:true`, base networkconf = Default, `vlan_enabled:false`). ~230 per-key->network mappings: most keys map to per-room resident VLANs (101-631), a few to Default, and one phone key maps to "Internal"/VLAN 20 (`networkconf 69405ba36db796548c947130`). Historical CSCNet clients: 1,190 (residents' IoT/TVs/phones/laptops + staff + the phones). **=> Do NOT repoint the CSCNet SSID itself** — that would move every resident/staff device. Move the phones at the PPSK level instead.

Networks of interest:
- Default (main LAN): `685f39078e65331c46ef8ac4`, `192.168.0.0/22`
- Internal (VLAN 20): `69405ba36db796548c947130`, `10.0.20.0/24`
- Guest (VLAN 50): `10.0.50.0/24`
- OpenVPN Server: `192.168.8.1/24` (purpose remote-user-vpn) — Vertical comes in here.

---

## PBX recon (CS-SERVER via GuruRMM, 2026-06-16)

Probed from CS-SERVER (`192.168.2.254`, same LAN segment) — read-only.

| Target | TCP open | SIP UDP 5060 | Conclusion |
|---|---|---|---|
| `192.168.2.180` (desktop) | 3389 (RDP) only | no reply | **Not a PBX** — RDP management/jump box |
| `192.168.2.228` (CS-QB, labeled "VoIP server") | 445 (SMB) only | no reply | **Not a live SIP PBX** — behaves like an SMB box despite the label |

**Implication:** no on-prem SIP PBX detected -> phones almost certainly register to a **cloud/hosted PBX** (Vertical). If confirmed, the voice VLAN only needs internet egress and the on-prem PBX pinhole (Part A step 5b) is **NOT needed**. Caveat: external port view only — a non-standard port / known-peer-only / host-firewalled PBX can't be 100% excluded, so Richard's confirm is the authority.

---

## PART A — pfSense (`https://192.168.0.1`)

1. **VLAN interface:** Interfaces -> VLANs -> Add: Parent `igc1`, Tag `30`, Desc `VOICE`.
2. **Assign + IP:** Interfaces -> Assignments -> add `igc1.30` -> Enable, Static `10.0.30.1/24`.
3. **DHCP:** Services -> DHCP Server -> VOICE: enable, range `10.0.30.100-.250`, **DNS `8.8.8.8, 1.1.1.1`** (PUBLIC resolvers — must NOT be `10.0.30.1`; the isolation rules below block the firewall IP, and this matches how the Guest VLAN already resolves DNS).
4. **Reservation: SKIP (2026-06-17).** Desktop needs no static IP to connect (LogMeIn is name/agent-based) — it takes a normal pool lease like the phones, and reaches all phones on-subnet regardless. No static mappings needed for anything.
5. **Firewall (VOICE tab) — CLONE THE GUEST VLAN (VLAN 50 / igc1.50), verified 2026-06-17 as the only properly-isolated template.** Four interface rules, top-to-bottom (GUI interface rules are `quick` -> first match wins, so blocks bite before the internet pass). **No RFC1918 alias** — use literal CIDRs, matching the Guest convention:
   - (1) BLOCK: VOICE subnets -> `192.168.0.0/22`  (Block VOICE to LAN)
   - (2) BLOCK: VOICE subnets -> `10.0.0.0/8`       (Block VOICE to private 10.x — includes own subnet, but intra-subnet is L2-switched so desktop<->phones still work)
   - (3) BLOCK: VOICE subnets -> `172.16.0.0/12`    (Block VOICE to ACG mgmt)
   - (4) PASS:  VOICE subnets -> `any`               (internet egress — cloud PBX + LogMeIn + public DNS)
   - **No DNS/NTP-to-firewall rule** (would be blocked by rule 2 and isn't needed — DNS is public via DHCP). **No on-prem PBX pinhole** (cloud PBX). **DO NOT add VOICE to the `All_Networks` interface group** (Guest isn't in it; isolation depends on staying out).
   - Corrects the earlier draft (DNS-to-firewall + RFC1918 alias) and the earlier wrong idea of cloning VLAN 20 — VLAN 20 is NOT isolated (only rule = opt238net->lan; everything else rides a floating `pass inet all`).
6. ~~**OpenVPN — reach desktop on VOICE, scoped to voice only**~~ — **NOT NEEDED (2026-06-17).** Vertical uses **LogMeIn** (outbound agent), not the pfSense OpenVPN, to reach the desktop. The desktop's internet egress on VOICE (rule (d)) is all LogMeIn requires. No CSO, no cert CN, no OpenVPN firewall rules for Vertical. (Howard's own OpenVPN access is unaffected.)

## PART B — UniFi (UOS controller)

7. **Network:** Settings -> Networks -> Add: `VOICE`, purpose `VLAN Only`, VLAN `30`.
8. **Wired ports (USW-16-PoE):** set Native Network = VOICE (untagged) on **ports 1-8** (AudioCodes) and **port 16** (desktop). **Then bounce each moved port** (PoE Power-Cycle for the AudioCodes; disable/enable for the desktop) — see the CRITICAL note in the Cutover sequence. No NIC change needed (desktop is DHCP).
9. **Wireless Poly (PPSK):** Settings -> Profiles -> Private Pre-Shared Keys (CSCNet) -> **add a new key -> Network VOICE** (vault the key). Re-point each Poly phone's WiFi to the voice key (by hand / Vertical provisioning). Also fixes the 2 currently mis-keyed phones (one on VLAN 422, one on Default). [Alt zero-touch: remap the existing phone key VLAN 20 -> VOICE, ONLY if that key is confirmed phone-exclusive — ~70 non-phone devices also showed on VLAN 20, so default to the dedicated key.]
   - Confirm inter-switch / AP uplinks + the pfSense trunk carry VLAN 30 (default "All" port profile auto-includes it).

---

## Cutover sequence (avoid stranding anything)

> **CRITICAL — re-VLANing a wired port does NOT force a new IP.** Changing a switch port's
> native VLAN leaves the device's NIC link UP, so the OS keeps its OLD DHCP lease and never
> sends a fresh DISCOVER on the new VLAN (its unicast renewal to the old DHCP server is then
> blocked by the VOICE isolation rules, so it just holds the stale IP until lease expiry).
> A UniFi **client block/unblock does NOT fix this** — that's a MAC filter, not a link bounce.
> **After every wired port move you MUST bounce the link to force re-DHCP** (proven on the
> Vertical desktop 2026-06-17: stuck on `192.168.2.180` until port 16 was bounced -> pulled
> `10.0.30.201`). Methods:
> - **AudioCodes (PoE-powered):** UniFi -> the switch -> port -> **Power Cycle** (cycles PoE,
>   reboots the phone, forces fresh DHCP). Cleanest for ports 1-8.
> - **Non-PoE device (the desktop):** toggle the port admin state (disable -> re-enable), or
>   on the device `ipconfig /release && /renew`, or reboot it. (Scripted bounce = PUT
>   `rest/device/<id>` `port_overrides` with port's `forward:disabled`, then restore — needs
>   the `X-CSRF-Token` from login header `x-updated-csrf-token`.)

1. Build everything with no live impact: pfSense VLAN/DHCP/firewall, UniFi network, create the voice PPSK. **[DONE 2026-06-17]**
2. **AudioCodes:** flip USW-16-PoE ports 1-8 -> VOICE, **then Power Cycle each of ports 1-8** so the phones re-DHCP onto `10.0.30.x` + re-register (brief blip).
3. **Poly:** re-key to voice PPSK -> they re-associate (a WiFi re-auth IS a fresh DHCP, no separate bounce needed) and roam onto VOICE.
4. **Desktop (DHCP, LogMeIn):** flip port 16 -> VOICE, **then bounce port 16** (disable/re-enable) so it re-DHCPs to a `10.0.30.x` lease. LogMeIn re-homes over internet egress automatically (no NIC change, no static IP, no Vertical action needed). Brief blip only.
5. **Verify each move on pfSense** (`/var/dhcpd/var/db/dhcpd.leases` + `arp -an | grep igc1.30`) — a device still on its old IP means the bounce didn't take; bounce again.
6. Confirm with Richard: LogMeIn reconnects to the desktop, and from the desktop he can reach the phones on `10.0.30.x`.

## Validation
- VOICE DHCP leases show phones AND the desktop on `10.0.30.x` (all dynamic).
- From desktop: reach several phones (Poly + AudioCodes).
- Isolation negative test: from VOICE, CANNOT reach CS-SERVER `192.168.2.254` or `10.0.20.x`.
- Phones registered / dial tone on a sample handset.
- Richard: LogMeIn -> desktop -> reach a phone's web UI on `10.0.30.x`.

## Rollback
Revert the UniFi port native VLAN (1-8, 16) + the PPSK key to their prior networks, **then bounce the ports** so devices re-DHCP back onto the old segments. pfSense VOICE iface/DHCP/rules can stay inert or be removed. Desktop is DHCP (no NIC revert needed) — it just re-leases on the old VLAN after the bounce.

---

## Open items
- [RESOLVED 2026-06-17] Desktop is **DHCP** (verified on pfSense) -> zero-touch cutover.
- [RESOLVED 2026-06-17] Remote access is **LogMeIn**, not OpenVPN -> step 6 dropped; only internet egress needed.
- [RESOLVED 2026-06-17] DHCP backend = **ISC dhcpd** (Kea dormant) -> out-of-pool reservations safe.
- [OPEN] Confirm phones register to **cloud/hosted PBX** (recon says yes) -> if so, Part A step 5b pinhole stays skipped. Low risk: if a phone fails to register after cutover, add the pinhole.
- [OPEN] **Schedule the maintenance window** for the live port flips (AudioCodes ports 1-8 = brief blip; port 16 desktop = brief blip).
- [OPEN] **Poly re-key method**: 22 WiFi phones must be pointed at the new voice PPSK — by hand per phone, or via Vertical provisioning. Richard offered to help during the transition; bulk provisioning by Vertical is the clean path.

## Appendix — device inventory (MACs)
**AudioCodes (wired, USW-16-PoE):**
```
port1 00:90:8f:da:98:05   port5 00:90:8f:e1:3d:90
port2 00:90:8f:e2:40:5e   port6 00:90:8f:e1:3d:5e
port3 00:90:8f:e2:d2:a4   port7 00:90:8f:e1:3d:a9
port4 00:90:8f:e1:3d:de   port8 00:90:8f:e1:3e:17
```
**Poly (wireless, CSCNet -> voice PPSK):**
```
48:25:67:d0:af:10  48:25:67:64:8a:88  48:25:67:64:95:6b
48:25:67:d0:b4:26  48:25:67:64:93:34  48:25:67:64:8e:ae
48:25:67:64:81:8e  48:25:67:64:93:25  48:25:67:64:92:6b
48:25:67:d0:ae:3e  48:25:67:64:95:62  48:25:67:64:93:d3
48:25:67:d0:b8:ac  48:25:67:64:94:84  48:25:67:64:94:ba
48:25:67:64:8f:14  48:25:67:64:95:74  48:25:67:64:8f:0b
48:25:67:d0:b1:83  48:25:67:64:92:89  48:25:67:64:8f:1d
48:25:67:a3:f8:3b
(22 total — source: Richard's 2026-06-16 scan list)
```
**Desktop:** `e4:e7:49:52:3a:06` (Vertical-Remote) -> reserve `10.0.30.10`.
