Hands-on Labs · Hands-on lab

Tailscale + Ansible Across Four Sites

Build and validate a Tailscale subnet-routed Ansible management fabric across four Internet-distributed sites for Linux and Windows learners.

Document class: Hands-on Labs guided implementation lab Applicability: A four-site subnet-routing lab with Tailscale, Ansible, Linux routers, Linux targets, and Windows management endpoints.

Lab overview

At the end of the lab, the Site B Ansible controller and an approved Site B home PC should be able to:

  • reach the four site prefixes through Tailscale subnet routers;
  • SSH to Linux subnet routers and Linux managed nodes;
  • manage Windows hosts through WinRM/PSRP;
  • test approved HTTPS, RDP, SMB, and application ports without exposing them to the public Internet;
  • run one Ansible validation play across all four sites;
  • demonstrate the difference between route advertisement, route approval, Tailscale policy, and host firewall policy; and
  • optionally test exact-prefix subnet-router failover.

This lab uses one router per site for the minimum path. Site A, C, and D therefore have an intentional router single point of failure in the base lab. The enterprise design recommends a second router at every site where the recovery objective requires it.

Target architecture

| Site | LAN prefix | Lab subnet router | Ansible-relevant systems | |-|---|---|---| | A | 192.168.0.0/24 | tsr-a-01 / 192.168.0.2 Linux VM | a-ad-01 .10, a-ad-02 .11, a-pc-01 to a-pc-10 .101 to .110 | | B | 192.168.10.0/24 | tsr-b-01 / 192.168.10.2 Linux | ansible-b-01 .20, 5 Windows servers .21 to .25, 5 Linux servers .41 to .45, 10 VMs .61 to .70 | | C | 192.168.30.0/24 | tsr-c-01 / 192.168.30.2 Linux workstation | c-node-01 .101 representative Linux workstation; other mobile/PC nodes are optional Tailscale clients | | D | 192.168.40.0/24 | tsr-d-01 / 192.168.40.2 Linux workstation | d-node-01 .101 representative Linux workstation; other mobile/PC nodes are optional Tailscale clients |

The example uses .2 for the site router. Reserve the address in DHCP/IPAM or replace it with the actual fixed address. Never use the same prefix at two sites.

Traffic paths

flowchart LR HOME[Site B home PC<br/>or Ansible control node] B[Site B subnet router<br/>192.168.10.2] TS((Tailscale tailnet<br/>WireGuard overlay)) A[Site A subnet router<br/>192.168.0.2] C[Site C subnet router<br/>192.168.30.2] D[Site D subnet router<br/>192.168.40.2] ALAN[Site A LAN<br/>AD + PCs] CLAN[Site C LAN<br/>Linux + devices] DLAN[Site D LAN<br/>Linux + devices] HOME --- B B --- TS TS --- A TS --- C TS --- D A --- ALAN C --- CLAN D --- DLAN

The Ansible controller should be a direct Tailscale node. A home PC should also run Tailscale if it needs to reach the remote prefixes directly. A non-Tailscale client on the Site B LAN needs static routes on the Site B gateway and corresponding return-path design.

Lab execution flow

flowchart LR PREP[Prepare policy and scoped keys] --> ROUTE[Enroll routers and advertise exact /24 routes] ROUTE --> APPROVE[Approve routes and accept them on controller] APPROVE --> TEST[Validate SSH, WinRM, and approved services] TEST --> HA[Optionally test exact-prefix failover] HA --> CLEAN[Cleanup: revoke keys, routes, and temporary firewall rules]

The execution flow makes cleanup a deliberate boundary: no temporary enrollment key, route advertisement, firewall exception, or lab account should remain after the evidence is captured.

Prerequisites

Required

  • An organization-owned Tailscale tailnet and an administrator who can create tags, auth keys, Grants, and route approvals.
  • One Linux system with Internet access and LAN reachability at each site. Site A’s Linux VM and the Site C/D Linux workstations satisfy the lab minimum.
  • One separate Linux Ansible control node at Site B. It must not be the only Site B subnet router for the enterprise pattern.
  • At least one reachable Linux target and one reachable Windows target for validation. The full inventory can be expanded after the representative path works.
  • Local administrator access to Windows hosts and sudo access to Linux hosts.
  • A protected place for auth keys, SSH private keys, WinRM credentials, and Ansible Vault passwords.

Safety rules

  • Do not paste a real Tailscale auth key, password, private key, or certificate into this repository.
  • Do not advertise a production prefix from a lab node.
  • Do not enable a default route or exit node for this lab.
  • Do not expose TCP 22, 3389, 445, 5985, or 5986 on a public interface.
  • The WinRM HTTP path below is a lab-only shortcut. Production Windows management must use WinRM HTTPS or an approved domain/Kerberos design.
  • Run the connectivity tests from a permitted source and an intentionally denied source so the access policy is proven rather than assumed.

Lab modules

Complete the following modules in order. Each module has a checkpoint or validation command before the next scope is opened.

Prepare the Tailscale policy

Create these tags and keep tag ownership limited to the network operations group:

TagPurpose
tag:subnet-router-aSite A route advertisers
tag:subnet-router-bSite B route advertisers
tag:subnet-router-cSite C route advertisers
tag:subnet-router-dSite D route advertisers
tag:ansible-controlSite B Ansible controller
tag:linux-managedDirectly enrolled Linux managed nodes

Start with a policy equivalent to the example in the enterprise design. For a lab, the minimum logical requirements are:

  1. auto-approve only the four exact site routes for the matching router tag, or approve them manually in the admin console;
  2. permit tag:ansible-control to the site prefixes on ICMP, TCP 22, TCP 443, and TCP 5986;
  3. permit lab-only WinRM TCP 5985 if the HTTP path is used;
  4. permit human operators to reach the subnet routers on TCP 22 and TCP 443; and
  5. add Tailscale SSH rules only for directly enrolled Linux nodes and approved local users.

Tailscale route approval and access policy are separate. A route can appear in the client routing table and still be denied by the policy, or a policy can allow an address without injecting a route.

Create scoped enrollment keys

Create separate pre-approved, tagged auth keys for the router roles and a separate key for the Ansible controller. Use placeholders in the commands below:

# Examples only; obtain keys from the Tailscale admin console or approved secret manager.
export TS_AUTHKEY_ROUTER_A='tskey-auth-REDACTED-A'
export TS_AUTHKEY_ROUTER_B='tskey-auth-REDACTED-B'
export TS_AUTHKEY_ROUTER_C='tskey-auth-REDACTED-C'
export TS_AUTHKEY_ROUTER_D='tskey-auth-REDACTED-D'
export TS_AUTHKEY_ANSIBLE='tskey-auth-REDACTED-ANSIBLE'

Use a separate key per site so that revoking Site C enrollment does not affect Site A or the control node. Store the real values in the shell environment, secret manager, or CI secret store only for the duration of enrollment.

Silent installation for multiple direct nodes

Use the following one-line commands from the approved node-management system for direct Tailscale clients. Run the Linux command with root privileges or Ansible become; run the Windows command from an elevated PowerShell or deployment context. These commands install the client only. Keep enrollment separate so each node receives its intended auth key, hostname, tag, and route. A host reached only through a subnet router does not need a direct Tailscale client.

Linux:

curl -fsSL https://tailscale.com/install.sh | sh && sudo systemctl enable --now tailscaled

Windows (64-bit x86 MSI example; use the MSI matching the node architecture):

msiexec.exe /i C:\Installers\tailscale-setup-amd64.msi /qn TS_NOLAUNCH=1 TS_UNATTENDEDMODE=always /L*v C:\Windows\Temp\tailscale-install.log

Prepare each Linux subnet router

Run the following on the Linux router at each site. Use the distribution’s package repository in production; the install script is convenient for a disposable lab.

# Install Tailscale on a supported Linux distribution.
curl -fsSL https://tailscale.com/install.sh | sh
sudo systemctl enable --now tailscaled

# Enable IPv4 forwarding for subnet routing.
echo 'net.ipv4.ip_forward = 1' | sudo tee /etc/sysctl.d/99-tailscale-router.conf
sudo sysctl -p /etc/sysctl.d/99-tailscale-router.conf

# Confirm the router has a LAN route and an Internet path before continuing.
ip -4 addr
ip route

Enable IPv6 forwarding as well if IPv6 is part of the real site design. Do not enable it merely because the lab does not use IPv6.

Enroll and advertise the site route

Run the site-specific command on each router:

# Site A
sudo tailscale up --auth-key="$TS_AUTHKEY_ROUTER_A" --hostname=tsr-a-01 --advertise-tags=tag:subnet-router-a
sudo tailscale set --advertise-routes=192.168.0.0/24

# Site B
sudo tailscale up --auth-key="$TS_AUTHKEY_ROUTER_B" --hostname=tsr-b-01 --advertise-tags=tag:subnet-router-b
sudo tailscale set --advertise-routes=192.168.10.0/24

# Site C
sudo tailscale up --auth-key="$TS_AUTHKEY_ROUTER_C" --hostname=tsr-c-01 --advertise-tags=tag:subnet-router-c
sudo tailscale set --advertise-routes=192.168.30.0/24

# Site D
sudo tailscale up --auth-key="$TS_AUTHKEY_ROUTER_D" --hostname=tsr-d-01 --advertise-tags=tag:subnet-router-d
sudo tailscale set --advertise-routes=192.168.40.0/24

Replace the auth-key value with the key for the specific site. Use tailscale status, tailscale ip -4, and tailscale status --json to confirm the node is enrolled and advertising the intended route.

Router firewall forwarding

The subnet router must allow forwarding from tailscale0 to the LAN interface and allow return traffic. The exact implementation depends on nftables, iptables, firewalld, or UFW. The following is a lab example; replace eth0 with the real LAN interface and persist the rules using the distribution’s firewall manager.

LAN_IF=eth0
sudo iptables -A FORWARD -i tailscale0 -o "$LAN_IF" -m conntrack --ctstate NEW,ESTABLISHED,RELATED -j ACCEPT
sudo iptables -A FORWARD -i "$LAN_IF" -o tailscale0 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT

If the host uses firewalld, ensure forwarding and the required masquerade or no-SNAT behavior are represented in firewalld rather than adding unmanaged iptables rules. Do not use a broad ACCEPT policy on all interfaces.

Choose the lab routing mode

For the first lab run, keep the default subnet-route SNAT. It minimizes changes to the four existing gateways and lets a directly enrolled Site B controller reach hosts behind the remote routers.

For the enterprise-style extension, disable SNAT only after static return routes are ready:

sudo tailscale set --snat-subnet-routes=false

The no-SNAT mode preserves the originating address but requires each LAN gateway to return remote prefixes through the local subnet router. The extension is documented in Section 14.

Approve and inspect the routes

In the Tailscale admin console:

  1. open Machines;
  2. filter for devices advertising subnets;
  3. inspect the exact advertised route on each router;
  4. approve only 192.168.0.0/24, 192.168.10.0/24, 192.168.30.0/24, and 192.168.40.0/24; and
  5. confirm the route is assigned to the intended router and tag.

If using autoApprovers, remember that changing the policy does not retroactively approve an already pending route. Re-advertise the route or approve it manually.

Enroll the Site B Ansible controller

On ansible-b-01:

curl -fsSL https://tailscale.com/install.sh | sh
sudo systemctl enable --now tailscaled
sudo tailscale up --auth-key="$TS_AUTHKEY_ANSIBLE" --hostname=ansible-b-01 --advertise-tags=tag:ansible-control

# Linux clients do not automatically accept subnet routes in every setup.
sudo tailscale set --accept-routes

tailscale ip -4
ip route

Confirm that the controller has routes for all four prefixes. The controller does not need to advertise Site B’s route unless it is also a subnet router; keep those roles separate in the enterprise design.

Enroll the home PC

Install the Tailscale client on the Site B home PC, authenticate it with the approved user identity, and verify that the client receives the approved subnet routes. Windows clients normally accept subnet routes automatically, but confirm the route and policy in the client status and with PowerShell:

Test-NetConnection 192.168.0.10 -Port 22
Test-NetConnection 192.168.30.2 -Port 22
Test-NetConnection 192.168.40.2 -Port 22

If the home PC is not directly enrolled, it cannot use the tailnet overlay by itself. Add static routes on the Site B gateway for the remote prefixes via the Site B subnet router, and design the return path before enabling broad home-LAN access.

Validate raw connectivity before Ansible

Run from ansible-b-01:

# Check the directly enrolled routers.
tailscale ping tsr-a-01
tailscale ping tsr-b-01
tailscale ping tsr-c-01
tailscale ping tsr-d-01

# Check routed LAN addresses. Use only hosts that actually exist.
ping -c 3 192.168.0.10
ping -c 3 192.168.10.41
ping -c 3 192.168.30.2
ping -c 3 192.168.40.2

# Check required management ports.
nc -vz -w 5 192.168.0.10 22
nc -vz -w 5 192.168.0.10 5986
nc -vz -w 5 192.168.30.2 22

Ping may be disabled by a host firewall. A failed ping is not conclusive; use the service-port test and the host firewall logs. A failed TCP test can indicate an unapproved route, a denied Grant, a missing return route, a local firewall rule, or a service that is not listening.

Bootstrap Linux SSH

On each Linux target, create a dedicated automation account and install the controller’s public key. Run the bootstrap with the existing approved administrative method, not through the new path until the path is validated.

sudo useradd --create-home --shell /bin/bash ansible
sudo install -d -m 0700 -o ansible -g ansible /home/ansible/.ssh
sudo install -m 0600 -o ansible -g ansible /tmp/ansible-b-01.pub /home/ansible/.ssh/authorized_keys

# Prefer a narrow sudoers policy per role. This broad lab rule is not a production standard.
echo 'ansible ALL=(ALL) NOPASSWD: ALL' | sudo tee /etc/sudoers.d/ansible-lab
sudo chmod 0440 /etc/sudoers.d/ansible-lab

From the controller:

ssh -i ~/.ssh/ansible_ed25519 -o IdentitiesOnly=yes ansible@192.168.0.2 hostname
ssh -i ~/.ssh/ansible_ed25519 -o IdentitiesOnly=yes ansible@192.168.30.2 hostname
ssh -i ~/.ssh/ansible_ed25519 -o IdentitiesOnly=yes ansible@192.168.40.2 hostname

For directly enrolled Linux routers, Tailscale SSH can be enabled for operator access with sudo tailscale set --ssh. It is optional and must be permitted in the tailnet policy. It is not the mechanism used to reach arbitrary Linux hosts behind a subnet router.

Bootstrap Windows management

Ansible supports Windows through WinRM/PSRP or SSH. This lab uses WinRM because it is common for Windows Server, AD servers, and Windows workstations. Configure Windows hosts before adding them to the inventory.

Lab-only WinRM HTTP path

Run as an elevated PowerShell session on the Windows test host. Set $allowedRouterIp to the LAN address of the subnet router at that site when the default Tailscale SNAT path is used.

Enable-PSRemoting -Force

# NTLM over HTTP uses WinRM message encryption in the lab profile.
Set-Item -Path WSMan:\localhost\Service\Auth\Basic -Value $false
Set-Item -Path WSMan:\localhost\Service\AllowUnencrypted -Value $false

$allowedRouterIp = '192.168.0.2'  # Use 192.168.10.2, .30.2, or .40.2 at other sites.
Disable-NetFirewallRule -DisplayGroup 'Windows Remote Management'
New-NetFirewallRule -DisplayName 'Ansible WinRM lab from Tailscale router' -Direction Inbound -Action Allow -Protocol TCP -LocalPort 5985 -RemoteAddress $allowedRouterIp -Profile Any

Use this only on an isolated lab or where the Tailscale path and host firewall have been explicitly approved. Do not set AllowUnencrypted to $true, and do not enable Basic authentication over HTTP.

Enterprise WinRM HTTPS path

For production, create a WinRM HTTPS listener with a certificate trusted by the Ansible controller, restrict the Windows firewall to the approved management sources, and use TCP 5986. In a domain environment, Kerberos is preferred when the DNS, SPN, and delegation design is ready. For local accounts, use NTLM or certificate authentication over trusted HTTPS.

Follow the current Ansible Windows Remote Management guide for listener creation, authentication, encryption, and CA trust. A self-signed certificate with ansible_winrm_server_cert_validation: ignore is a lab exception only and must not become the production configuration.

Install Ansible on the controller

Use a dedicated Python virtual environment on ansible-b-01:

sudo apt-get update
sudo apt-get install -y python3-venv python3-pip openssh-client
python3 -m venv ~/venvs/ansible-tailscale
source ~/venvs/ansible-tailscale/bin/activate
python -m pip install --upgrade pip
python -m pip install ansible-core pywinrm
ansible-galaxy collection install ansible.windows

Install pypsrp instead of or in addition to pywinrm when the inventory uses the PSRP connection plugin. Pin the versions in the real project and test upgrades in a staging tailnet.

Build the Ansible inventory

Create inventory/hosts.yml. This is a representative full-site skeleton; add the remaining mobile and PC nodes only when they expose an approved management protocol.

all:
  children:
    site_a:
      children:
        site_a_linux: {}
        site_a_windows: {}
    site_b:
      children:
        site_b_linux: {}
        site_b_windows: {}
    site_c:
      children:
        site_c_linux: {}
    site_d:
      children:
        site_d_linux: {}

    site_a_linux:
      hosts:
        tsr-a-01:
          ansible_host: 192.168.0.2
    site_a_windows:
      hosts:
        a-ad-01:
          ansible_host: 192.168.0.10
        a-ad-02:
          ansible_host: 192.168.0.11
        a-pc-01:
          ansible_host: 192.168.0.101
        a-pc-02:
          ansible_host: 192.168.0.102
        a-pc-03:
          ansible_host: 192.168.0.103
        a-pc-04:
          ansible_host: 192.168.0.104
        a-pc-05:
          ansible_host: 192.168.0.105
        a-pc-06:
          ansible_host: 192.168.0.106
        a-pc-07:
          ansible_host: 192.168.0.107
        a-pc-08:
          ansible_host: 192.168.0.108
        a-pc-09:
          ansible_host: 192.168.0.109
        a-pc-10:
          ansible_host: 192.168.0.110

    site_b_linux:
      hosts:
        ansible-b-01:
          ansible_host: 192.168.10.20
        tsr-b-01:
          ansible_host: 192.168.10.2
        b-linux-01:
          ansible_host: 192.168.10.41
        b-linux-02:
          ansible_host: 192.168.10.42
        b-linux-03:
          ansible_host: 192.168.10.43
        b-linux-04:
          ansible_host: 192.168.10.44
        b-linux-05:
          ansible_host: 192.168.10.45
        b-vm-01:
          ansible_host: 192.168.10.61
        b-vm-02:
          ansible_host: 192.168.10.62
        b-vm-03:
          ansible_host: 192.168.10.63
        b-vm-04:
          ansible_host: 192.168.10.64
        b-vm-05:
          ansible_host: 192.168.10.65
        b-vm-06:
          ansible_host: 192.168.10.66
        b-vm-07:
          ansible_host: 192.168.10.67
        b-vm-08:
          ansible_host: 192.168.10.68
        b-vm-09:
          ansible_host: 192.168.10.69
        b-vm-10:
          ansible_host: 192.168.10.70
    site_b_windows:
      hosts:
        b-win-srv-01:
          ansible_host: 192.168.10.21
        b-win-srv-02:
          ansible_host: 192.168.10.22
        b-win-srv-03:
          ansible_host: 192.168.10.23
        b-win-srv-04:
          ansible_host: 192.168.10.24
        b-win-srv-05:
          ansible_host: 192.168.10.25

    site_c_linux:
      hosts:
        tsr-c-01:
          ansible_host: 192.168.30.2
    site_d_linux:
      hosts:
        tsr-d-01:
          ansible_host: 192.168.40.2

    linux:
      children:
        site_a_linux: {}
        site_b_linux: {}
        site_c_linux: {}
        site_d_linux: {}
      vars:
        ansible_connection: ssh
        ansible_user: ansible
        ansible_become: true
        ansible_python_interpreter: /usr/bin/python3
        ansible_ssh_private_key_file: ~/.ssh/ansible_ed25519

    windows:
      children:
        site_a_windows: {}
        site_b_windows: {}
      vars:
        ansible_connection: winrm
        ansible_user: ansible_admin
        ansible_password: "{{ vault_windows_password }}"
        ansible_port: 5985
        ansible_winrm_transport: ntlm
        ansible_winrm_message_encryption: always

For the enterprise profile, change Windows to TCP 5986, set ansible_winrm_server_cert_validation: validate, and configure ansible_winrm_ca_trust_path when a private CA is used. Keep the Windows password in an Ansible Vault file, not in the inventory.

The inventory layout follows Ansible’s supported pattern of stable aliases, ansible_host connection addresses, and separate group variables. Use ansible-inventory --graph to verify the group hierarchy before connecting.

Optional no-SNAT site-to-site extension

Complete the base SNAT lab first. Then, if the site gateways are under your control, add static routes before disabling SNAT.

Gateway route plan

Each route points remote site prefixes to the local subnet router. Add routes for the Tailscale CGNAT range as well if remote LAN hosts must return traffic to directly enrolled tailnet clients without SNAT.

GatewayStatic routes via local router
Site A gateway192.168.10.0/24, 192.168.30.0/24, 192.168.40.0/24, and optionally 100.64.0.0/10 via 192.168.0.2
Site B gateway192.168.0.0/24, 192.168.30.0/24, 192.168.40.0/24, and optionally 100.64.0.0/10 via 192.168.10.2
Site C gateway192.168.0.0/24, 192.168.10.0/24, 192.168.40.0/24, and optionally 100.64.0.0/10 via 192.168.30.2
Site D gateway192.168.0.0/24, 192.168.10.0/24, 192.168.30.0/24, and optionally 100.64.0.0/10 via 192.168.40.2

The exact CLI is vendor-specific. Confirm that the gateway installs the route in the forwarding table and that the host firewall allows the source and destination ranges.

Disable SNAT and test

# Run on each subnet router only after the gateway routes are active.
sudo tailscale set --snat-subnet-routes=false

# On the controller, keep route acceptance enabled.
sudo tailscale set --accept-routes

ip route get 192.168.0.10
traceroute -n 192.168.0.10
nc -vz -w 5 192.168.0.10 22

Confirm the target’s logs show the intended source address. If a connection works with SNAT but fails without it, the first suspects are the return route and the target firewall.

Run Ansible validation

Create validate_fabric.yml:

---
- name: Validate Linux management paths
  hosts: linux
  gather_facts: false
  tasks:
    - name: Verify SSH and sudo path
      ansible.builtin.ping:

    - name: Collect Linux hostname
      ansible.builtin.command: hostname --fqdn
      register: linux_hostname
      changed_when: false

    - name: Show Linux site target
      ansible.builtin.debug:
        msg: "{{ inventory_hostname }} -> {{ linux_hostname.stdout }}"

- name: Validate Windows management paths
  hosts: windows
  gather_facts: false
  tasks:
    - name: Verify WinRM path
      ansible.windows.win_ping:

    - name: Collect Windows hostname
      ansible.windows.win_shell: '$env:COMPUTERNAME'
      register: windows_hostname
      changed_when: false

    - name: Show Windows site target
      ansible.builtin.debug:
        msg: "{{ inventory_hostname }} -> {{ windows_hostname.stdout | trim }}"

Run the inventory and validation in increasing scope:

source ~/venvs/ansible-tailscale/bin/activate
ansible-inventory -i inventory/hosts.yml --graph
ansible-inventory -i inventory/hosts.yml --list > /tmp/tailscale-ansible-inventory.json

ansible-playbook -i inventory/hosts.yml validate_fabric.yml --limit tsr-a-01,tsr-c-01,tsr-d-01
ansible-playbook -i inventory/hosts.yml validate_fabric.yml --limit a-ad-01,b-win-srv-01
ansible-playbook -i inventory/hosts.yml validate_fabric.yml

The first run limits blast radius. Do not move to the full inventory until the representative Linux and Windows nodes pass.

Test other approved services

From ansible-b-01, use a separate service test for each destination class. Do not infer that SSH success means SMB, RDP, or an application port is permitted.

# Linux/SSH
nc -vz -w 5 192.168.0.2 22
nc -vz -w 5 192.168.30.2 22

# Windows WinRM HTTPS production path
nc -vz -w 5 192.168.0.10 5986

# HTTPS application endpoint, if explicitly approved
curl --fail --connect-timeout 5 https://192.168.30.101/health

# Optional file-management path; use only for named servers
nc -vz -w 5 192.168.10.21 445

For RDP and Windows-side checks from the home PC:

Test-NetConnection 192.168.0.10 -Port 3389
Test-NetConnection 192.168.10.21 -Port 445
Test-NetConnection 192.168.30.101 -Port 443

Add or remove ports in the Tailscale Grants and local firewalls as separate changes with an owner and review date.

Optional exact-prefix failover lab

To test Tailscale subnet-router failover, deploy a second Linux router on the same LAN and advertise the exact same prefix with the same site tag:

# Example for a second Site B router.
sudo tailscale up --auth-key="$TS_AUTHKEY_ROUTER_B" --hostname=tsr-b-02 --advertise-tags=tag:subnet-router-b
sudo tailscale set --advertise-routes=192.168.10.0/24

Approve the second advertisement if auto-approval is not enabled. Start a continuous permitted service test, stop Tailscale on tsr-b-01, and verify that the exact /24 route becomes available through tsr-b-02. Allow approximately 15 seconds for the documented failover behavior and record the observed result.

Do not use a /16 on one router and a /24 on another as an HA test. Those are not equivalent failover candidates. For HA routers advertising the same local prefix, do not enable accept-routes on the standby unless the design specifically requires it, because the standby can select the remote copy of its own local route.

Troubleshooting guide

SymptomLikely causeCheck
Route is not visible on controllerController does not accept routes, route is not approved, or auto-approval did not applysudo tailscale set --accept-routes, admin console route state, tailscale status --json
Router appears online but LAN host is unreachableIP forwarding, router firewall, host firewall, or return routesysctl net.ipv4.ip_forward, forwarding rules, ip route, host logs
Tailscale route exists but connection is deniedGrant or host firewall denies the protocol/portReview grants, source tag, destination CIDR, and local firewall logs
SSH to Linux times outsshd is not listening, TCP 22 is blocked, or return traffic is asymmetricss -lntp, nc -vz, firewall rules, route mode
WinRM fails on 5985Listener, NTLM, local account, firewall scope, or message-encryption mismatchwinrm enumerate winrm/config/listener, Ansible connection vars, Windows event logs
WinRM HTTPS fails on 5986Certificate name/trust, CA path, listener, or port mismatchCertificate SAN, ansible_winrm_server_cert_validation, CA trust, Test-NetConnection
Tailscale SSH cannot reach a routed Linux hostDestination does not run Tailscale SSHUse standard SSH over the subnet route or enroll the destination directly
Only Tailscale-enabled clients workLAN gateway lacks routes for non-Tailscale clientsAdd gateway routes and return routes, or keep scope limited to enrolled clients
Failover does not occurRoutes differ, second route is pending, or policy does not permit the standbyCompare exact prefixes, approval state, tags, and policy
DNS name fails but IP worksMagicDNS does not resolve arbitrary routed LAN hostsUse internal DNS/search suffixes or stable IPs in inventory; do not weaken policy to solve DNS

Validation

The lab is complete when:

  • all four exact site routes are advertised, approved, and visible on the controller;
  • the controller reaches one Linux and one Windows representative where available at each site;
  • Linux validation passes through SSH and Windows validation passes through WinRM/PSRP;
  • the home PC can reach only approved remote services;
  • at least one denied source/port test is recorded;
  • the route mode, source NAT behavior, and return paths are documented;
  • the Ansible inventory is rendered successfully and the full validation play is idempotent;
  • the temporary WinRM HTTP exception is removed or explicitly documented as a lab-only state; and
  • if HA is tested, the second router advertises the exact same prefix and the observed failover time is recorded.

Cleanup

When the lab is finished:

  1. remove or disable the four lab route advertisements;
  2. delete or revoke the temporary enrollment keys;
  3. remove lab nodes from the tailnet and delete stale device records;
  4. remove lab firewall rules and temporary WinRM listeners;
  5. remove lab SSH accounts and keys from target systems;
  6. delete temporary Vault material and inventory exports; and
  7. retain the test results, policy version, route plan, and lessons learned separately from secrets.

References