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>
94 lines
4.4 KiB
PowerShell
94 lines
4.4 KiB
PowerShell
<#
|
|
.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
|
|
}
|