My Notes My Notes

Tailscale site restrictions

Two sites are linked with the Tailscale add-on on a Home Assistant instance at each end, acting as subnet routers. Home can reach anything on the remote site. The remote site (e.g. family) can reach only two services, both through Nginx Proxy Manager (NPM): jellyfin.example.com and photos.example.com. No Tailscale is needed on the remote PCs.

0. How it's enforced & values

Tailscale filters on IP and port only. It can't see hostnames, so if several sites sit behind one reverse proxy, the restriction has two layers:

  1. Tailscale grants limit the remote site to NPM (192.168.10.6, ports 80 and 443) and nothing else on the home LAN, including the backend ports.
  2. NPM access lists block the remote subnet on every proxy host except the two allowed services.
Remote PC 192.168.1.x
   → Remote gateway (static route)
   → Remote HA (Tailscale add-on)  ═══ tailnet ═══  Home HA (Tailscale add-on, 192.168.20.80)
   → Home gateway
   → NPM 192.168.10.6:443 → Jellyfin 192.168.10.7:8081 / Immich 192.168.10.6:2283
ItemValue
Home LAN192.168.10.0/24
Home subnet router (HA add-on)192.168.20.80 — untagged, owned by the admin user
NPM192.168.10.6:443
Jellyfin backend192.168.10.7:8081
Immich backend192.168.10.6:2283
Remote LAN192.168.1.0/24
Remote subnet router (HA add-on)192.168.1.16 — tag:remote-site

Static routes already in place:

  • Remote gateway: 192.168.10.0/24 via 192.168.1.16
  • Home gateway: 192.168.1.0/24 via 192.168.20.80

1. Tailscale policy — tag and grants

Admin console → left sidebar → Access controls → JSON editor. The visual editor under Policies only edits one rule at a time.

Delete the default allow-all grant (src *, dst *, ip *), then make sure the file contains:

{
  "tagOwners": {
    "tag:remote-site": ["autogroup:admin"],
  },

  "grants": [
    // Home: full access, including the remote LAN
    {
      "src": ["autogroup:member", "192.168.10.0/24"],
      "dst": ["*"],
      "ip":  ["*"],
    },
    // Remote site: NPM only (80 for the HTTP→HTTPS redirect, 443 for the sites)
    {
      "src": ["tag:remote-site", "192.168.1.0/24"],
      "dst": ["192.168.10.6"],
      "ip":  ["tcp:80", "tcp:443"],
    },
  ],
}

Keep any existing ssh section. Each src lists both a tag/group and a CIDR, so the rules work whether the subnet router's SNAT is on or off.

Newer tailnets default to grants; older ones use acls. Either works, but a single allow-all rule left in either section overrides every restriction.


2. Tag the remote subnet router

The HA add-on can't set tags itself, so tag the node from the admin console:

  1. Machines → remote HA node → … → Edit ACL tags → add tag:remote-site → Save
  2. Check … → Edit route settings on both nodes to confirm the subnets are still approved.

Leave the home node untagged. It stays inside autogroup:member and keeps full access. Tagging disables key expiry on the remote node automatically.


3. Turn off SNAT on the home subnet router

With SNAT on, the home subnet router rewrites the source of forwarded traffic to its own IP. NPM would then see every remote request as coming from the router and couldn't tell remote users apart.

Home Assistant (home) → Settings → Add-ons → Tailscale → Configuration:

snat_subnet_routes: false

Save, then restart the add-on. Return traffic uses the static route on the home gateway (192.168.1.0/24 via 192.168.20.80).

Side effects of turning SNAT off:

  • Every home device must send replies for 192.168.1.0/24 back through the home gateway. Devices with a different gateway or their own static routes will break.
  • Host firewalls that only allow the local subnet (Windows Firewall "Local subnet", ufw, NAS rules) may drop remote traffic.
  • Future Tailscale clients such as a phone or laptop also arrive un-NATed. Add a static route on the home gateway first: 100.64.0.0/10 via 192.168.20.80.
  • Never reuse the remote subnet anywhere on the home side.

The upside is that NPM, Jellyfin and Immich logs show real client IPs.

Verify: load a site from a remote PC, then check the NPM access log for that proxy host. The client IP should be 192.168.1.x, not the router's IP.


4. NPM access list

NPM → Access Lists → Add Access List:

TabSetting
DetailsName Block-Remote-Site. Satisfy Any off, Pass Auth off.
AuthorizationNo users
AccessIn this order:
1. deny 192.168.1.0/24
2. allow all

Apply it to every proxy host except the Jellyfin and Photos hosts: Edit → Details → Access List → Block-Remote-Site → Save. Leave those two on Publicly Accessible.

Then set Settings → Default Site to 404 Page or No Response, so unknown hostnames pointed at the NPM IP don't land on anything.

When you add a new proxy host, apply Block-Remote-Site to it. New hosts are open to the remote site by default.


5. DNS on the remote site

Remote devices must resolve both hostnames to NPM's LAN IP, or traffic goes out to the internet instead of over the tunnel. Add local DNS records on the remote gateway:

TypeHostnameIP
Ajellyfin.example.com192.168.10.6
Aphotos.example.com192.168.10.6

6. Test

From a remote Windows PC (PowerShell):

nslookup jellyfin.example.com                  # → 192.168.10.6
Test-NetConnection 192.168.10.6 -Port 443      # → TcpTestSucceeded : True
tracert -d -h 6 192.168.10.6
curl.exe -kIv https://jellyfin.example.com     # → 200/302

From a remote Linux/Mac PC:

nslookup jellyfin.example.com
nc -vz -w 5 192.168.10.6 443
traceroute -n -m 6 192.168.10.6
curl -kIv https://jellyfin.example.com

Should return 403 from NPM (replace other.example.com with one of your other proxy hosts):

curl -kI --resolve other.example.com:443:192.168.10.6 https://other.example.com

Should time out (blocked by Tailscale):

curl -m 5 http://192.168.10.7:8081
curl -m 5 http://192.168.10.6:2283
ping 192.168.10.1

From home (should work):

ping 192.168.1.16

7. Troubleshooting

SymptomCheck / fix
nslookup returns a public IPDNS override missing on the remote side
Resolves correctly but port 443 failsTunnel, grant, or return path
tracert stops at the remote gatewayStatic route missing, or the grant is wrong
tracert reaches the home router, then nothingReturn path broken after SNAT was turned off. Check the home static route and gateway firewall.
Works with SNAT on, fails with it offHome route for 192.168.1.0/24 missing, or a firewall zone rule dropping it
Port 443 opens but the page failsNPM: wrong access list or certificate
Remote site can reach every NPM siteSNAT still on, or access list missing on that host
Remote site gets 403 on allowed servicesAccess list applied to them by mistake
Remote site can reach backend ports directlyAllow-all grant still in the policy
Home can't reach the remote siteHome node was tagged by mistake
Policy file won't saveTag used but not in tagOwners, or a stray bracket

8. Rollback

  1. Restore the allow-all grant in the Tailscale policy, or remove the remote-site grant.
  2. Remove tag:remote-site from the remote node.
  3. Set snat_subnet_routes: true on the home HA add-on and restart it.
  4. Remove Block-Remote-Site from the NPM proxy hosts, then delete the access list.