6 min read
Automating MikroTik Bridge VLAN Filtering & Proxmox Trunks with Terraform

If you have ever tried to configure VLANs on a MikroTik router, you know the pain. Transitioning from the legacy switch-chip VLAN methods to the modern Bridge VLAN Filtering often results in completely locking yourself out of the router.

When you add a virtualization host like Proxmox into the mix—which requires a trunk port passing multiple tagged VLANs—the complexity multiplies. Doing this via the WinBox GUI is a recipe for configuration drift and late-night network outages.

In this deep dive, I will show you how to tame MikroTik VLANs using Infrastructure as Code (Terraform). We will build a dynamic, scalable L2 network architecture that includes a Proxmox trunk port, dedicated management access, and hardware-offloaded access ports for edge devices (like Raspberry Pis).

View the complete MikroTik IaC source code on GitHub 🐙

The Architecture & Locals

Before writing any resources, we need to define our network’s topology. Hardcoding VLAN IDs across dozens of resources is a bad practice. Instead, we use Terraform locals to create a central source of truth.

locals {
  homelab_vlans = {
    "vlan20-srv"    = 20
    "vlan30-dmz"    = 30
    "vlan40-iot"    = 40
    "vlan100-admin" = 100
  }

  rpi_port_mapping = {
    "ether6" = 20 # RPi 4B #1 (Keepalived Node A)
    "ether7" = 20 # RPi 4B #2 (Keepalived Node B)
  }

  proxmox_port = "ether5"
}

This simple map dictates our entire Layer 2 strategy. If we ever need to add a “Guest” VLAN, we simply add it to the homelab_vlans map, and Terraform will automatically generate the interfaces, IP addresses, DHCP servers, and bridge matrix entries.

1. The Core Bridge & Ports

In modern RouterOS (v6.41+), the best practice is to use a single bridge for all ports and manage segmentation purely through VLAN filtering. This ensures maximum hardware offloading (if supported by your MikroTik switch chip).

resource "routeros_interface_bridge" "core_bridge" {
  name           = "bridge1"
  vlan_filtering = true
  comment        = "Core bridge managed by Terraform"
}

Assigning the Physical Ports

Next, we attach our physical ethernet ports to the bridge. This is where we define whether a port is an Access Port (untagged) or a Trunk Port (tagged).

The Proxmox Trunk: Our Proxmox server (ether5) needs to receive tagged traffic so the virtual machines can reside in different VLANs. We assign it a default pvid = 1 (acting as the native VLAN).

resource "routeros_interface_bridge_port" "proxmox_port" {
  bridge    = routeros_interface_bridge.core_bridge.name
  interface = local.proxmox_port
  pvid      = 1
  hw        = true
  comment   = "Proxmox Trunk (Tagged VLANs)"
}

The Edge Access Ports: For devices that don’t understand VLAN tags (like a standard PC or Raspberry Pi), we assign a specific pvid. The bridge will automatically strip the VLAN tag when sending traffic to these ports and add it when receiving.

resource "routeros_interface_bridge_port" "rpi_ports" {
  for_each  = local.rpi_port_mapping
  bridge    = routeros_interface_bridge.core_bridge.name
  interface = each.key
  pvid      = each.value
  hw        = true
  comment   = "Keepalived Node"
}

resource "routeros_interface_bridge_port" "mgmt_port" {
  bridge    = routeros_interface_bridge.core_bridge.name
  interface = "ether2"
  pvid      = 100
  hw        = true
  comment   = "Admin Workstation Access Port"
}

2. Creating the VLAN Interfaces

A bridge connects physical ports, but the router itself needs an IP address in each VLAN to act as the default gateway. We create virtual VLAN interfaces attached to the bridge1 interface.

Using Terraform’s for_each loop, we dynamically generate these based on our locals block:

resource "routeros_interface_vlan" "vlans" {
  for_each  = local.homelab_vlans
  interface = routeros_interface_bridge.core_bridge.name
  name      = each.key
  vlan_id   = each.value
}

# Assign Gateway IPs to the Router
resource "routeros_ip_address" "vlan_ips" {
  for_each  = local.homelab_vlans
  address   = "10.0.${each.value}.1/24"
  interface = routeros_interface_vlan.vlans[each.key].name
}

3. The Magic: Dynamic Bridge VLAN Matrix

This is the hardest part of MikroTik configuration, and where Terraform truly shines. We must explicitly tell the bridge which ports are allowed to carry which VLANs.

If we forget to add bridge1 to the tagged list, the router’s CPU won’t be able to process the traffic, and DHCP/Routing will fail entirely.

The Management Override

First, we explicitly define the Management VLAN (vlan100). We tag the CPU (core_bridge) and the Proxmox trunk.

resource "routeros_interface_bridge_vlan" "vlan100_mgmt" {
  bridge   = routeros_interface_bridge.core_bridge.name
  vlan_ids = [100]
  tagged = [
    routeros_interface_bridge.core_bridge.name,
    local.proxmox_port
  ]
  # Note: ether2 is dynamically added as untagged by its PVID
}

The Dynamic Matrix Loop

For the rest of the VLANs, we use an advanced Terraform loop. This block dynamically calculates the untagged ports by reading the local.rpi_port_mapping and checking if the required VLAN matches the port’s assigned PVID.

resource "routeros_interface_bridge_vlan" "vlan_matrix" {
  # Loop through all VLANs EXCEPT 100 (since we handled it above)
  for_each = { for k, v in local.homelab_vlans : k => v if v != 100 }

  bridge   = routeros_interface_bridge.core_bridge.name
  vlan_ids = [each.value]

  tagged = [
    routeros_interface_bridge.core_bridge.name,
    local.proxmox_port
  ]

  untagged = [
    for port, vlan in local.rpi_port_mapping : port if vlan == each.value
  ]
}

Why this is a Game Changer

Look at the untagged logic. If you decide to move a Raspberry Pi from the Server VLAN (20) to the DMZ VLAN (30), you don’t have to touch the bridge configuration, the matrix, or the interface settings.

You simply change the 20 to 30 in the local.rpi_port_mapping at the top of the file. Terraform will calculate the diff, modify the port’s PVID, and seamlessly update the Bridge VLAN table. This is true Infrastructure as Code.

Conclusion

By treating network infrastructure as code, we transform a fragile, click-heavy configuration into a robust, auditable state. Proxmox gets its tagged trunks, hardware edge devices get their native access ports, and the router maintains secure L2 isolation.

Want to take your automation further? If you are managing complex environments and need automated Zero-Trust setups for Azure or compliance-ready infrastructure, check out my Enterprise Terraform Blueprints. For custom engineering, connect with me on LinkedIn.