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:
- 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. - 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| Item | Value |
|---|---|
| Home LAN | 192.168.10.0/24 |
| Home subnet router (HA add-on) | 192.168.20.80 — untagged, owned by the admin user |
| NPM | 192.168.10.6:443 |
| Jellyfin backend | 192.168.10.7:8081 |
| Immich backend | 192.168.10.6:2283 |
| Remote LAN | 192.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/24via192.168.1.16 - Home gateway:
192.168.1.0/24via192.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
sshsection. Eachsrclists 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 useacls. 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:
- Machines → remote HA node → … → Edit ACL tags → add
tag:remote-site→ Save - Check … → Edit route settings on both nodes to confirm the subnets are still approved.
Leave the home node untagged. It stays inside
autogroup:memberand 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: falseSave, 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/24back 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/10via192.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:
| Tab | Setting |
|---|---|
| Details | Name Block-Remote-Site. Satisfy Any off, Pass Auth off. |
| Authorization | No users |
| Access | In this order: 1. deny 192.168.1.0/242. 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-Siteto 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:
| Type | Hostname | IP |
|---|---|---|
| A | jellyfin.example.com | 192.168.10.6 |
| A | photos.example.com | 192.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/302From 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.comShould 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.comShould 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.1From home (should work):
ping 192.168.1.167. Troubleshooting
| Symptom | Check / fix |
|---|---|
nslookup returns a public IP | DNS override missing on the remote side |
| Resolves correctly but port 443 fails | Tunnel, grant, or return path |
tracert stops at the remote gateway | Static route missing, or the grant is wrong |
tracert reaches the home router, then nothing | Return path broken after SNAT was turned off. Check the home static route and gateway firewall. |
| Works with SNAT on, fails with it off | Home route for 192.168.1.0/24 missing, or a firewall zone rule dropping it |
| Port 443 opens but the page fails | NPM: wrong access list or certificate |
| Remote site can reach every NPM site | SNAT still on, or access list missing on that host |
| Remote site gets 403 on allowed services | Access list applied to them by mistake |
| Remote site can reach backend ports directly | Allow-all grant still in the policy |
| Home can't reach the remote site | Home node was tagged by mistake |
| Policy file won't save | Tag used but not in tagOwners, or a stray bracket |
8. Rollback
- Restore the allow-all grant in the Tailscale policy, or remove the remote-site grant.
- Remove
tag:remote-sitefrom the remote node. - Set
snat_subnet_routes: trueon the home HA add-on and restart it. - Remove
Block-Remote-Sitefrom the NPM proxy hosts, then delete the access list.