docs(wiki): add Tailscale client-management pattern + GuruRMM enroll script

One tailnet per client (never merge into ACG own tailnet), MSP holds Admin,
devices enrolled as tagged nodes via pre-auth keys pushed from GuruRMM.
Includes tailscale-client-enroll.ps1 (idempotent unattended Windows MSI
install + tagged auth-key join), a see-each-other tag ACL, the Windows
subnet-routing reality (userspace/netstack, not the old IP-forward hack),
and a runbook. Indexed under wiki Patterns.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-06 15:26:15 -07:00
parent fd30af6aba
commit 8d7e3805c7
3 changed files with 235 additions and 1 deletions

View File

@@ -0,0 +1,93 @@
<#
.SYNOPSIS
GuruRMM enrollment: install Tailscale on a Windows client machine and join the
CLIENT's tailnet unattended, using a pre-authorized tagged auth key.
.DESCRIPTION
Idempotent and safe to re-run. Designed to run as SYSTEM from GuruRMM with ZERO
end-user interaction:
1. Installs the Tailscale MSI silently in unattended mode (stays connected even
when no user is logged in - TS_UNATTENDEDMODE=always).
2. Authenticates with a pre-auth key. The device's tag + identity come from the
key, so there is no interactive IdP login for the (tech-inept) user to do.
For a "the machines just need to see each other" setup it advertises NO routes and
is NOT an exit node - each box is simply a node on the tailnet.
.PARAMETER AuthKey
Pre-auth key from the CLIENT's tailnet (Admin console -> Settings -> Keys ->
Generate auth key). Generate it as:
[x] Reusable - enroll multiple machines / tolerate re-runs
[x] Pre-approved - skip manual device approval
[x] Tags: tag:<client> - node inherits the tag; not tied to a user login
Expiry: ~90 days is fine for an onboarding window; rotate afterward.
Pass this into GuruRMM as a SECRET/masked parameter. NEVER hardcode it here.
.PARAMETER Hostname
Name the device shows as on the tailnet. Defaults to the Windows computer name.
.PARAMETER MsiUrl
Tailscale stable MSI. Defaults to the rolling "latest" stable. Pin a version if
you want deterministic installs (see https://pkgs.tailscale.com/stable/#windows).
.PARAMETER LoginServer
Custom coordination server. Leave blank for Tailscale's default (SaaS).
.NOTES
tailscale.exe lives at C:\Program Files\Tailscale\tailscale.exe after install.
Exit 0 = node is up on the tailnet. Exit 1 = failed (check the key + connectivity).
#>
param(
[Parameter(Mandatory = $true)][string]$AuthKey,
[string]$Hostname = $env:COMPUTERNAME,
[string]$MsiUrl = 'https://pkgs.tailscale.com/stable/tailscale-setup-latest-amd64.msi',
[string]$LoginServer = ''
)
$ErrorActionPreference = 'Stop'
$ts = "$env:ProgramFiles\Tailscale\tailscale.exe"
function Log($m) { Write-Output "[tailscale-enroll] $m" }
# 1. Install Tailscale if absent (silent, unattended, no GUI popup) ----------------
if (-not (Test-Path $ts)) {
Log "Tailscale not present - downloading MSI from $MsiUrl"
$msi = Join-Path $env:TEMP 'tailscale-setup.msi'
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
Invoke-WebRequest -Uri $MsiUrl -OutFile $msi -UseBasicParsing
Log "Installing MSI (TS_UNATTENDEDMODE=always, no GUI launch)"
$msiArgs = "/i `"$msi`" /quiet /norestart TS_UNATTENDEDMODE=always TS_NOLAUNCH=true"
$p = Start-Process msiexec.exe -ArgumentList $msiArgs -Wait -PassThru
if ($p.ExitCode -ne 0 -and $p.ExitCode -ne 3010) { throw "MSI install failed (msiexec exit $($p.ExitCode))" }
Remove-Item $msi -ErrorAction SilentlyContinue
Start-Sleep -Seconds 5
} else {
Log "Tailscale already installed: $(((& $ts version) -split "`n")[0])"
}
if (-not (Test-Path $ts)) { throw "tailscale.exe not found after install" }
# 2. Already connected? (idempotent - re-runs are a no-op) -------------------------
try { $cur = (& $ts status --json 2>$null | ConvertFrom-Json) } catch { $cur = $null }
if ($cur -and $cur.BackendState -eq 'Running' -and $cur.Self.TailscaleIPs) {
Log "Already connected as $($cur.Self.HostName) ($($cur.Self.TailscaleIPs -join ', ')) - nothing to do"
& $ts status
exit 0
}
# 3. Bring it up with the pre-auth key (tag + identity come FROM the key) ----------
# No --advertise-routes / --advertise-exit-node: peer-to-peer only.
$up = @('up', '--authkey', $AuthKey, '--hostname', $Hostname, '--accept-dns=true')
if ($LoginServer) { $up += "--login-server=$LoginServer" }
Log "Joining tailnet as '$Hostname'"
& $ts @up
Start-Sleep -Seconds 3
# 4. Verify + report the 100.x address for the RMM job output ----------------------
try { $st = (& $ts status --json 2>$null | ConvertFrom-Json) } catch { $st = $null }
if ($st -and $st.BackendState -eq 'Running' -and $st.Self.TailscaleIPs) {
Log "SUCCESS - $($st.Self.HostName) is on the tailnet at $($st.Self.TailscaleIPs -join ', ')"
& $ts status
exit 0
} else {
Log "FAILED - backend state: $(if($st){$st.BackendState}else{'unknown'})."
Log "Check the auth key (expired / not reusable / not pre-approved) and outbound connectivity to *.tailscale.com:443 + UDP 41641."
exit 1
}

View File

@@ -0,0 +1,141 @@
# Pattern: Tailscale for Client Networks (MSP-managed)
**Type:** pattern
**Applies to:** any ACG client that needs Tailscale (small offices especially)
**Companion script:** [`tailscale-client-enroll.ps1`](tailscale-client-enroll.ps1)
**Last updated:** 2026-06-06
---
## Decision (TL;DR)
**One tailnet PER client. Never merge a client into your own tailnet, and never share
one tailnet across multiple clients.** You hold an Admin/Owner seat on each client's
tailnet, enroll their devices as **tagged nodes via pre-auth keys pushed from GuruRMM**,
and bill on the client's own plan.
This gives clean isolation, clean offboarding, and zero reliance on a non-technical user
doing anything.
---
## Why per-client, not a shared tailnet
- **Isolation / liability.** Your tailnet carries ACG infra (the 100.x fleet, the vault
host, coord API, GuruRMM server). One ACL slip and a client box - or malware on it -
can reach your stuff or another client. Hard no.
- **Billing.** Client devices/users on your tailnet inflate and mix your bill. You want
their cost on their ledger (or cleanly bundled into their managed-services fee).
- **Offboarding.** Ending an engagement = delete the tailnet / revoke your admin: one
click. Untangling one client's nodes, ACLs, and keys out of a shared tailnet is
error-prone and risky.
- **IdP blast radius.** Their Microsoft/Google identity changes shouldn't touch a tailnet
that anyone else lives on.
---
## Ownership and roles
1. **Tie the tailnet to an identity that belongs to the client** - their Microsoft 365 or
Google Workspace (most ACG clients have one) as the login identity provider, or a
dedicated admin account you hold on their behalf.
2. **Add yourself as Owner/Admin.** Tailscale roles: Owner, Admin, IT admin, Network
admin, Auditor, Member. Admin is enough to manage devices/keys/ACLs.
3. **Billing on the client's subscription.** Two users fits the free tier *functionally*,
but for a commercial client the **Starter plan (~$6/user/mo)** is the correct,
supported footing - call it ~$12/mo, trivially bundled into their MSP fee.
---
## Device enrollment (built for tech-inept users)
The only step in Tailscale that is hard for a non-technical user is the initial IdP login.
**Eliminate it entirely** with tagged pre-auth keys - a tagged node is owned by a tag, not
a user, so there is nothing to sign into and nothing that breaks when a password changes.
1. In the client's admin console: **Settings -> Keys -> Generate auth key**
- [x] **Reusable** (enroll both machines / tolerate RMM re-runs)
- [x] **Pre-approved** (skip manual device approval)
- [x] **Tag:** `tag:<client>` (e.g. `tag:roberts`)
- Expiry ~90 days for the onboarding window; rotate after.
2. In GuruRMM, store the key as a **secret/masked parameter** and run
[`tailscale-client-enroll.ps1`](tailscale-client-enroll.ps1) on each machine.
It installs the MSI silently with `TS_UNATTENDEDMODE=always` (stays connected with no
user logged in) and authenticates with the key. Zero user interaction; survives reboots.
You already have the GuruRMM agent on their boxes, so this is the whole job - no site visit,
no walking a confused user through a login.
---
## "Just see each other" ACL
Tailscale's default ACL is allow-all; the moment you set an ACL it becomes default-deny.
This tag-scoped ACL lets the client's tagged machines reach each other and nothing else:
```json
{
"tagOwners": {
"tag:roberts": ["autogroup:admin"]
},
"acls": [
{ "action": "accept", "src": ["tag:roberts"], "dst": ["tag:roberts:*"] }
]
}
```
With **MagicDNS** on, the two boxes reach each other by name (e.g. `front-desk`,
`back-office`) with no IP juggling.
---
## Subnet routing on Windows - the real story
- **For two peers you do not need it.** Each machine is a node with its own 100.x address
and talks to the other directly. Skip subnet routing entirely.
- **If you ever need to expose a whole LAN behind one box:** modern Tailscale runs Windows
subnet routers in **userspace / netstack mode** - no kernel IP-forwarding, no
`IPEnableRouter` registry hack, and Windows automatically picks up the advertised routes.
So it is *not* "hard" the way it used to be. The only real caveat is **throughput**:
userspace routing is slower than Linux kernel-mode, so for a heavily used router drop in a
cheap Linux box/appliance instead. Summary: easier than its reputation, just not the fastest.
---
## Offboarding
Delete the client's tailnet, or revoke your admin seat and rotate the auth keys. Because the
client is isolated in their own tailnet, the blast radius is exactly one client.
---
## Multi-client management reality
There is **no native single pane of glass across tailnets** - each tailnet is its own admin
console, so per-client tailnets means a separate login per client. For a handful of small
clients that is fine. Record each client's **tailnet name + admin identity + key rotation
date** in their `wiki/clients/<client>.md`. (Re-check Tailscale's MSP/partner program
periodically for centralized multi-tenant tooling.)
---
## Runbook - Robert's office (2 machines, "see each other")
1. Stand up the tailnet on Robert's identity; add yourself as **Admin**.
2. Turn on **MagicDNS**; set the **ACL** above (`tag:roberts`).
3. Generate a **reusable + pre-approved** auth key tagged `tag:roberts`.
4. In GuruRMM, add the key as a secret var and run
[`tailscale-client-enroll.ps1`](tailscale-client-enroll.ps1) on both machines.
5. Confirm both show a 100.x IP in `tailscale status`; test reachability by MagicDNS name.
6. Record the tailnet name, your admin identity, and the key rotation date in Robert's
client wiki article.
---
## Sources
- [Subnet routers - Tailscale Docs](https://tailscale.com/docs/features/subnet-routers)
- [Kernel vs. netstack subnet routing & exit nodes - Tailscale Docs](https://tailscale.com/docs/reference/kernel-vs-userspace-routers)
- [Install Tailscale on Windows with MSI - Tailscale Docs](https://tailscale.com/docs/install/windows/msi)
- [Keep Tailscale running when I'm not logged in - Tailscale Docs](https://tailscale.com/docs/how-to/run-unattended)
- [Auth keys - Tailscale Docs](https://tailscale.com/kb/1085/auth-keys)