Skip to content
Networking

Selective internet access for IoT devices with RouterOS address lists

By Victor Da Luz
MikroTik RouterOS IoT VLAN firewall Ansible homelab

My IoT VLAN blocks outbound internet by default. That is the point - cameras, smart plugs, and blind motor controllers do not need to call home. But some devices legitimately do: a Govee leak sensor hub reports readings to its app, wall-mounted iPads running Home Assistant need iCloud and the App Store, and Garmin health monitors sync to their cloud.

“IoT blocked from internet” as a blanket policy is not quite right. What I actually want is “IoT blocked from internet unless I have explicitly approved it.”

RouterOS address lists give me exactly that.

The default policy

VLAN 30 sits behind a fw-drop-final rule in the forward chain. Every packet that does not match an earlier accept rule hits that drop. For most IoT devices, nothing matches - they get dropped at the WAN boundary.

The forward chain evaluates top to bottom and stops at the first match. That ordering is the whole mechanism.

The IoT_Internet_Allowed address list

I maintain an address list on the router called IoT_Internet_Allowed. Devices in that list get two things:

  1. Forward-chain rules that accept their DNS queries to internal resolvers (before the IoT isolation drop)
  2. A WAN chain rule that accepts their outbound traffic to the internet

The WAN rule in my Ansible playbook:

- comment: 'Allow IoT devices with internet access'
  action: accept
  out-interface-list: WAN
  src-address-list: IoT_Internet_Allowed

That rule fires before fw-drop-final, so devices in the list get through. Everything else does not.

Wiring it to the state file

I manage the address list contents through Ansible, not by hand in Winbox. The state file network.yaml is the source of truth.

Adding a device to the list means adding a firewall_address_lists entry to its state file record:

leak_sensor_bathroom:
  ip: '10.0.x.x'
  vlan: 'vlan30'
  hostname: 'leak-sensor.bathroom.internal'
  ansible_managed: true
  roles:
    - 'dhcp_type=sensor'
  firewall_address_lists:
    - list: 'IoT_Internet_Allowed'

Running the address-lists playbook reads that state and applies it:

ansible-playbook playbooks/infrastructure/routeros-firewall-address-lists.yml \
  --vault-password-file ~/.ansible-credentials

The playbook builds the full address list from state and applies it via the RouterOS API with handle_absent_entries: remove - meaning any device removed from state also disappears from the list on the next run. No stale entries to clean up manually.

Adding a new device

The workflow for any IoT device that needs internet access:

  1. Add a static DHCP lease entry in network.yaml (hostname, IP, MAC, VLAN, roles)
  2. Add firewall_address_lists: [{list: "IoT_Internet_Allowed"}] to that entry
  3. Run the DHCP leases playbook to apply the static lease
  4. Run the address-lists playbook to update the firewall list

The Govee hub I added recently followed exactly this path. It connects to Wi-Fi on VLAN 30 and syncs sensor readings to Govee’s cloud. Without internet access, it pairs fine locally but the app shows nothing. Adding it to IoT_Internet_Allowed and running the two playbooks was all it took.

The connection tracking gotcha

There is a non-obvious behavior worth knowing if you ever temporarily open the whole VLAN for troubleshooting.

RouterOS evaluates the forward chain top to bottom, but one of the early rules accepts any packet with connection-state=established,related,untracked. That rule runs before fw-drop-final. Once a TCP connection exists in the tracking table, every subsequent packet for that session matches the established rule - the drop never fires for it.

This matters because: if you enable a temporary “allow all VLAN 30 to WAN” rule for debugging, devices establish connections to their cloud backends. When you disable that rule, new connections stop - but existing ones do not. They are already in the tracking table. They keep flowing until the TCP established timeout expires, which is up to 24 hours by default in RouterOS.

The fix is to flush stale connections after disabling the temporary rule:

/ip firewall connection remove [find where src-address-list=VLAN30_IoT and srcnat=yes]

That kills all WAN-bound connections from VLAN 30. The srcnat=yes qualifier scopes it to NATted (outbound internet) connections only - internal connections to DNS, Home Assistant, and other LAN services are left alone. Devices in IoT_Internet_Allowed drop momentarily and reconnect through their permanent accept rule. Everything else stays blocked.

I have an Ansible playbook for this that handles both opening and closing the temporary window, and automatically flushes the connection table on close:

# Open temporarily (for debugging)
ansible-playbook playbooks/infrastructure/routeros-vlan30-internet-toggle.yml -e mode=enable

# Close and flush stale connections
ansible-playbook playbooks/infrastructure/routeros-vlan30-internet-toggle.yml -e mode=disable

Using the playbook instead of hand-editing Winbox means the flush happens together with the rule change. If you disable the rule by hand and forget to flush, those connections quietly live on.

Verification

After adding a device and running the playbooks, I check two things:

  1. The device appears in the RouterOS address list: /ip firewall address-list print where list=IoT_Internet_Allowed
  2. The device can actually reach the internet - for the Govee hub, opening the app and seeing live sensor readings confirmed it.

Lessons learned

  • The address-list pattern means adding or removing internet access for any IoT device is a one-field state file change and two playbook runs. That is the right level of abstraction.
  • Disabling a firewall rule does not kill existing connections. If something “still works” after you think you have blocked it, look at the connection table before concluding your rules are wrong.
  • handle_absent_entries: remove in the Ansible RouterOS API module is what keeps the address list clean automatically. Without it, removing a device from state leaves a ghost entry in the list.

The network runs a MikroTik RB4011 for routing and a MikroTik CRS326 for switching. The address-list technique works on any RouterOS setup. Same network as in my VLAN write-up.

Disclosure: This article contains affiliate links. If you purchase through these links, I may earn a commission at no extra cost to you.

Related reading

Ready to Transform Your Career?

Let's work together to unlock your potential and achieve your professional goals.