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:
@@ -76,7 +76,7 @@ Run `/wiki-lint` to check for stale entries and broken backlinks.
|
|||||||
|
|
||||||
| Article | Summary | Last Compiled |
|
| Article | Summary | Last Compiled |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| *(none yet — patterns will be extracted during system/project compilation passes)* | | |
|
| [Tailscale client management](patterns/tailscale-client-management.md) | One tailnet per client (never merge into yours); you hold Admin; enroll devices as tagged nodes via pre-auth keys pushed from GuruRMM ([enroll script](patterns/tailscale-client-enroll.ps1)). Windows subnet-routing reality + "see each other" ACL. | 2026-06-06 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
93
wiki/patterns/tailscale-client-enroll.ps1
Normal file
93
wiki/patterns/tailscale-client-enroll.ps1
Normal 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
|
||||||
|
}
|
||||||
141
wiki/patterns/tailscale-client-management.md
Normal file
141
wiki/patterns/tailscale-client-management.md
Normal 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)
|
||||||
Reference in New Issue
Block a user