<# .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: - 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 }