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.253and a CARP VIP at.254with 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.200to.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
- A Catalyst (any IOS or IOS-XE release that does 802.1Q trunks) with a trunk port to each firewall.
- Two OPNsense 25.7 nodes in a CARP pair, with the trunk arriving on the same parent interface name on both, for example
igc1. - The tenant number. Everything else derives from it: VLAN
100+N, subnet10.<site>.<vlan>.0/24, gateway.254. - Services > Kea DHCP available on both nodes (it ships with 25.7; the ISC server is the legacy option).
Steps
- 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.
-
Create the VLAN device on fw01. Interfaces > Devices > VLAN, add: parent
igc1, tag 103, descriptionT3. Save. -
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, address10.1.103.252/24. No gateway on an internal interface. Save and apply. -
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. -
Add the CARP gateway on fw01. Firewall > Virtual IPs > Settings, add: mode CARP, interface
T3, address10.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. -
Add the tenant's firewall rules on the
T3interface, 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.
- Configure Kea. Services > Kea DHCP > Kea DHCPv4, Settings tab: enable, and tick
T3in the interfaces list. Subnets tab: add10.1.103.0/24, pool10.1.103.200-10.1.103.249, router option10.1.103.254, DNS server10.1.103.2. Leave.11to.199out 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
- VHID 1 on every VLAN. The old "VHID equals VLAN" scheme fails at VLAN 256 because CARP builds the virtual MAC from a one-byte VHID. The switch learns MACs per VLAN, so VHID 1 everywhere is fine.
- The parent interface must be assigned, or at least present and up, on both nodes. A VLAN device on an unassigned parent is created happily and carries nothing.
- Jumbo frames: VLAN 5 for storage runs at 9000; a tenant VLAN does not. Set the MTU on the VLAN device, not the parent, if a VLAN needs it.
- Rule order is the policy. OPNsense evaluates first match, so a pass rule above the blocks lets a tenant reach management. Put the blocks first and leave them there.
- DHCP later moved off the firewall and into the tenant's
.2container alongside PowerDNS, fed from the source of record, so reservations and DNS names come from one place. Kea on the firewall is the right place to start; just expect to move it.
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.