Mastering Tailscale ACLs and Subnet Routing
Introduction
I recently decided to burn down my traditional VPN setup. OpenVPN was clunky, WireGuard was fast but hard to manage at scale, and I wanted something that felt more like βmagic.β
Enter Tailscale.
But simply installing Tailscale on my laptop wasnβt enough. I needed my entire home lab infrastructureβProxmox LXC containers, Docker swarms, and random smart devicesβto be accessible from anywhere, without installing the Tailscale client on every single toaster.
This is the guide I wish I had read. It covers Subnet Routing, ACLs (Access Control Lists), and the deep Linux routing table hacks needed to make it work with Proxmox containers.
The Problem: The βIsolated Islandβ
My home lab sits on 192.168.10.0/24. When Iβm at a coffee shop, Iβm on some random subnet. I wanted to type ssh 192.168.10.25 and have it just work, as if I was sitting on my couch.
Tailscaleβs Subnet Routers promise significantly this capability, but out of the box, they often hit a wall when dealing with complex setups like Proxmox LXC containers, which have their own networking quirks.
The Solution: A Programmable Network
1. The Subnet Router
First, I designated a node (in this case, an LXC container acting as my gateway, 10.42.0.1) to act as the bridge.
sudo tailscale up \
--advertise-routes=10.42.0.0/24,192.168.10.0/24 \
--advertise-tags=tag:gateway
This tells the Tailnet: βHey, if anyone wants to reach 192.168.10.x, send the packets to me.β
2. The ACLs (The βBouncerβ)
By default, Tailscale is βallow all.β Thatβs bad security practice. I locked it down using ACLs. The magic here uses Tags and Auto-Approvers.
{
"tagOwners": {
"tag:gateway": ["[email protected]"],
"tag:server": ["[email protected]"]
},
"acls": [
// Allow me to access everything
{ "action": "accept", "src": ["[email protected]"], "dst": ["*:*"] },
// Allow gateway to route traffic
{ "action": "accept", "src": ["tag:gateway"], "dst": ["*:*"] }
],
"autoApprovers": {
"routes": {
"192.168.10.0/24": ["tag:gateway"],
"10.42.0.0/24": ["tag:gateway"]
}
}
}
The autoApprovers section is critical. It prevents the annoying manual approval step in the Tailscale admin console every time the router restarts.
3. The βDeep Magicβ: Linux Routing Tables
This is where most people get stuck. If youβre running LXC containers (which share the host kernel), they often ignore the subnet router because the return path is confused. The packet comes in via Tailscale, but tries to leave via the default gateway, which drops it.
We need Source Based Routing.
I created a custom routing table script (/etc/network/if-up.d/tailscale-routes):
#!/bin/bash
# Wait for Tailscale interface
sleep 5
# Add routes to a specific table (52)
ip route add 192.168.10.0/24 dev vmbr0 src 192.168.10.10 table 52 2>/dev/null || true
# Rule: If traffic comes from Tailscale, use Table 52
ip rule add iif tailscale0 lookup 52 prio 100 2>/dev/null || true
This ensures that traffic entering via Tailscale returns via Tailscale, preventing asymmetric routing drops.
Conclusion
With this setup, my network is no longer a collection of isolated servers. Itβs a flat, global mesh. I can access my NAS (192.168.10.10) from a hotel room in Tokyo purely by its internal IP. No port forwarding, no firewall holes.
It feels like the future.