Site to Site Networking with Tailscale: Part 2 — Teaching Azure Some New Tricks
- Shannon
- 10 minutes ago
- 4 min read
In Part 1, I wrestled my Unifi Dream Machine Pro into submission and got it talking on Tailscale. That gave me a subnet router for my home LAN (192.168.1.0/24). Now it is time to bring Azure into the picture.
My goal was simple: connect workloads in Azure to my on-prem network without installing Tailscale on every VM. That would be a nightmare to maintain. The better option is to run a single subnet router VM in Azure, advertise my VNet into Tailscale, and let it handle routing.
Why subnet routers are the way to go
Yes, you can install Tailscale on every VM. That is like wiring a separate fiber line to each laptop instead of just installing Wi-Fi. Instead, advertise your entire vNet/subnet from one router VM. All your VMs get connectivity without knowing what Tailscale even is.
For me, that meant advertising my 10.5.0.0/16 vNet in East US, which holds my hub workloads.
Step 1: Build the router VM in Azure
First off, I decided to write this using AzCLI. I (of course) ran into some bumps and bruises along the way. This ultimately means I will publish the final code at the end of this blog series.
Here is the Az CLI script I used to spin up a lightweight Ubuntu VM in my East US hub VNet:
# Variables
RG="sbkEusHub"
LOCATION="eastus"
VNET="sbkEusHubVnet"
SUBNET="sbkEusHubSub1"
VM="sbk-eus-ts-router01"
NIC="${VM}-nic"
# Create NIC with IP forwarding enabled
az network nic create -g $RG -n $NIC \
--vnet-name $VNET --subnet $SUBNET \
--ip-forwarding true
# Create Ubuntu VM for router
az vm create -g $RG -n $VM \
--image UbuntuLTS \
--size Standard_B1s \
--admin-username azureuser \
--generate-ssh-keys \
--nics $NIC
# Open SSH inbound (for management only)
az vm open-port -g $RG -n $VM --port 22
Step 1.5: Public IPs and static NICs
For the build out, I created my VM with a public IP. The script above is VERY "quick and dirty" and nowhere near "production grade". In production, you do not need a public IP. Tailscale builds an outbound connection, so as long as the VM can reach the internet it does not need to be directly exposed. I realized the err of my way when I reviewed everything the AzCLI script created and decided to remove this public IP right away, as I still had my S2S VPN in place. Totally your call on when to remove, but I do recommend to remove the NIC (or write a better template/script - this will be a final parting gift for the last blog post in the series).
Remove the public IP after setup:
az network nic ip-config update \
-g sbkEusHub \
--nic-name sbk-eus-ts-router01-nic \
-n ipconfig1 \
--remove publicIpAddress
Make the NIC static:
az network nic ip-config update \
-g sbkEusHub \
--nic-name sbk-eus-ts-router01-nic \
-n ipconfig1 \
--private-ip-address <ROUTER_IP>
Step 2: Install and configure Tailscale
SSH into the VM and install Tailscale:
curl -fsSL https://tailscale.com/install.sh | sh
sudo systemctl enable --now tailscaled
Enable packet forwarding:
sudo tee /etc/sysctl.d/99-tailscale.conf <<EOF
net.ipv4.ip_forward=1
net.ipv6.conf.all.forwarding=1
EOF
sudo sysctl -p /etc/sysctl.d/99-tailscale.conf
Join your tailnet and advertise your VNet:
sudo tailscale up --reset \
--authkey tskey-auth-YOURKEYHERE \
--advertise-routes=10.5.0.0/16 \
--accept-routes \
--hostname sbk-eus-router01
Approve the route in the Tailscale admin console, then run tailscale status to confirm:
subnets: 10.5.0.0/16
Step 3: Why route tables are needed
At this point, the router VM can see both on-prem and Azure, but your other VMs in Azure have no idea where 192.168.1.0/24 lives. They will drop those packets unless you tell them otherwise.
That is where User Defined Routes (UDRs) come in. With a UDR, you can say: “Send anything for 192.168.1.0/24 to my router VM.”
Step 4: Build and apply the route table
Grab your router VM’s private IP:
az vm list-ip-addresses -g sbkEusHub -n sbk-eus-ts-router01 \
--query "[0].virtualMachine.network.privateIpAddresses[0]" -o tsv
Create the route table and route:
az network route-table create -g sbkEusHub -n rt-to-onprem
az network route-table route create -g sbkEusHub \
--route-table-name rt-to-onprem \
-n to-onprem-192-168-1-0-24 \
--address-prefix 192.168.1.0/24 \
--next-hop-type VirtualAppliance \
--next-hop-ip-address <ROUTER_IP>
Attach it to your workload subnets (not the GatewaySubnet - leave that alone):
az network vnet subnet update -g sbkEusHub \
--vnet-name sbkEusHubVnet -n sbkEusHubSub1 \
--route-table rt-to-onprem
az network vnet subnet update -g sbkEusHub \
--vnet-name sbkEusHubVnet -n sbkEusHubSub2 \
--route-table rt-to-onprem
Step 4.5: Training wheels with /32 routes
One lesson learned: do not flip on full /24 or full /16 routing right away. Domain controllers and other critical services can start throwing fits.
To test safely, I started with individual hosts as /32 routes:
sudo tailscale up --reset \
--authkey tskey-auth-YOURKEYHERE \
--advertise-routes=10.5.1.29/32,10.5.2.30/32 \
--accept-routes \
--hostname sbk-eus-router01
That let me validate traffic flow between specific machines. Once I knew routing was solid, I switched back to /24 and eventually /16 for the full subnet/vNet.
Final checklist before testing
Your UDM Pro will advertise your local network (mine was 192.168.1.0/24) into Tailscale and the route is approved in the admin console.
Azure router VM advertises your Azure network (mine eventually was 10.5.0.0/16) into Tailscale and the route is approved in the admin console.
Both routers have --accept-routes enabled.
Route tables in Azure send 192.168.1.0/24 traffic to the router VM.
Router NIC has IP forwarding on and a static IP set.
Firewall rules allow DNS and AD traffic across the connection.
Step 5: Validate end to end
From the Azure router: ping 192.168.1.1
From an Azure workload VM: ping 192.168.1.194 # on-prem DNS/DC
From an on-prem machine: ping 10.5.1.x # Azure workload
Seeing successful replies in all three directions was the green light.
Lessons learned
Subnet routers save you from installing Tailscale everywhere.
Route tables are non-negotiable if your VMs do not run Tailscale directly.
Starting with /32 training wheels avoids nasty routing surprises.
Approving routes in the admin console is easy to overlook but nothing works until you do it.
The cost of a small VM is far less than a new Azure VPN SKU.
Next up in Part 3: adding another Azure region (West US) into the tailnet for multi-region routing. I also will clean up all code + make it pretty and attempt to simulate a disaster recovery/failure scenario.
Comments