Selective internet access for IoT devices with RouterOS address lists
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:
- Forward-chain rules that accept their DNS queries to internal resolvers (before the IoT isolation drop)
- 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:
- Add a static DHCP lease entry in
network.yaml(hostname, IP, MAC, VLAN, roles) - Add
firewall_address_lists: [{list: "IoT_Internet_Allowed"}]to that entry - Run the DHCP leases playbook to apply the static lease
- 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:
- The device appears in the RouterOS address list:
/ip firewall address-list print where list=IoT_Internet_Allowed - 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: removein 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
Fixing the VLAN 30 IoT DNS Isolation Leak to Pi-hole
Pi-hole logs showed IoT devices on the isolated VLAN hitting internal DNS anyway. The cause was RouterOS DHCP plus firewall rule order, not a single mis-ticked box.
Troubleshooting RouterOS Local Gateway IP Unreachability
SSH to the router worked from other VLANs but not from the same VLAN as the gateway. What I ruled out, what still does not have a clean root cause, and the workaround that kept management sane.
Auditing static DHCP leases in RouterOS: ten mismatches and four missing devices
What happens when your DHCP config drifts from your network state file. How I found fourteen lease issues in RouterOS and fixed them with Ansible and a device-by-device review.
Ready to Transform Your Career?
Let's work together to unlock your potential and achieve your professional goals.