OPNsense: VLANs on a trunk from a Catalyst, with Kea DHCP

In short. Allow the VLAN on the trunk, create the VLAN device on the parent interface on both firewalls, assign and enable it, give fw01 .252, fw02 .253 and a CARP VIP at .254 with VHID 1. Put the tenant's rules in the fixed order: block management, block other tenants, block operations, allow the rest. Then a Kea subnet with a pool of .200 to .249. Nothing here syncs the interface for you.

New VLAN shows no traffic on OPNsense

The trunk is up, the switch says the VLAN is allowed, and OPNsense sees nothing. Nine times out of ten one of three things is missing: the VLAN is not actually in the trunk's allowed list, the VLAN device was created but never assigned and enabled, or it was done on one node and the CARP VIP is being advertised by a node that has no interface for it. This walk-through adds tenant 3 at site 1, which on this estate is VLAN 103 and 10.1.103.0/24.

What you need

Steps

  1. Allow the VLAN on the trunks on the switch. On a Catalyst that has a VLAN database, create it first, then add it to each trunk's allowed list without replacing the list.
vlan 103
 name T3-SITE1
interface range GigabitEthernet1/0/47 - 48
 switchport trunk allowed vlan add 103

Use add. A bare switchport trunk allowed vlan 103 replaces the list and takes every other VLAN off the trunk, which you will discover from your phone.

  1. Create the VLAN device on fw01. Interfaces > Devices > VLAN, add: parent igc1, tag 103, description T3. Save.

  2. Assign and enable it. Interfaces > Assignments shows the new device in the drop-down; add it. Open the new interface, tick Enable, set the description to T3, IPv4 configuration type Static, address 10.1.103.252/24. No gateway on an internal interface. Save and apply.

  3. Repeat steps 2 and 3 on fw02 with 10.1.103.253/24. The HA sync does not create devices or assignments, and the API cannot either. Both nodes need the interface with the same description, because rules refer to interfaces by their assignment, and a synced rule that names an interface the backup does not have is skipped.

  4. Add the CARP gateway on fw01. Firewall > Virtual IPs > Settings, add: mode CARP, interface T3, address 10.1.103.254/24, VHID 1, advertising base 1, skew 0, and a password per your standard. Save and apply. Virtual IPs do sync, so fw02 receives the row; open it on fw02 and make sure its skew is higher than the master's (the sync normally takes care of this, but look), then check Interfaces > Virtual IPs > Status on both nodes shows MASTER on one and BACKUP on the other.

  5. Add the tenant's firewall rules on the T3 interface, in this order from the top, and do not reorder them later:

1 block  from T3 net  to 10.1.1.0/24          # MGMT, hardware only
2 block  from T3 net  to 10.1.100.0/22        # every other tenant VLAN
3 block  from T3 net  to 10.1.10.0/23         # OPS-INT and OPS-EXT
4 pass   from T3 net  to any                  # internet and anything left

Use aliases for the three destination groups so the rules read the same on every tenant interface. The tenant's own .2 resolver and .10 ingress are inside T3 net, so they fall through to the pass rule.

  1. Configure Kea. Services > Kea DHCP > Kea DHCPv4, Settings tab: enable, and tick T3 in the interfaces list. Subnets tab: add 10.1.103.0/24, pool 10.1.103.200-10.1.103.249, router option 10.1.103.254, DNS server 10.1.103.2. Leave .11 to .199 out of the pool; that range is static addresses allocated from the source of record. Apply. Kea on 25.7 has its own high-availability setting; on a pair you either configure that so both nodes hand out leases from a shared view, or you run Kea on the master only and accept that a failover loses lease state until clients renew. Both are defensible. Pick one and write it down.
flowchart TB
  SW["Catalyst trunk<br/>allowed 1,2,5,10,11,101-110"] --> F1["fw01 igc1<br/>vlan 103: 10.1.103.252"]
  SW --> F2["fw02 igc1<br/>vlan 103: 10.1.103.253"]
  F1 --- V["CARP 10.1.103.254<br/>VHID 1"]
  F2 --- V
  V --> T["Tenant 3 hosts<br/>.2 dns, .10 ingress, .200-.249 pool"]

Verify it worked

On the switch, show interfaces trunk must list 103 under both "allowed" and "allowed and active" for each firewall port. On each firewall, ifconfig igc1_vlan103 shows the address and the carp line, and ifconfig | grep carp shows exactly one MASTER across the pair for VHID 1 on that interface. Put a laptop on an access port in VLAN 103: it should get a lease in the .200 range with gateway .254, be able to reach the internet, and get a timeout (not a refusal) to 10.1.1.1 and to another tenant's .10. Finally, enter CARP maintenance mode on the master and confirm the laptop's ping to .254 continues with at most a couple of lost packets.

Gotchas

FAQ

Why not let the HA sync create the interface? It cannot. Interface assignments are per node and the sync deliberately leaves them alone. The checklist for a new VLAN ends with "and now on fw02".

Why is the pool only .200 to .249? Provisioning. A host boots, gets a pool address, is registered in the source of record and rebooted onto its static address in .11 to .199. The pool is small on purpose so a VLAN full of unregistered hosts is noticed.

Does a tenant need its own DNS at .2? Not on day one. Point the Kea DNS option at the firewall's Unbound first; move it to the tenant container when that exists. The address .2 is reserved from the start so nothing else takes it.

Related