# ===================================================================== # VuePoint Edge — remote-access beachhead (Stage 1) v4 # v4: standalone-OpenSSH fallback (capability can fail on WU-locked boxes), # stop-before-reinstall (re-run safety), shields-off + egress-block warning. # --------------------------------------------------------------------- # Run ONCE in an *Administrator* PowerShell. Connects this PC to VuePoint's # private network (Tailscale) and enables secure remote management (OpenSSH) # so VuePoint can finish setup remotely. Nothing exposed to the public net. # Idempotent — safe to re-run (skips Tailscale if already connected, so it # does NOT need a fresh key on a re-run). ~2 minutes. # # Usage (VuePoint provides the one-liner incl. the key): # $env:TS_KEY='tskey-...'; irm https://edge.vuepointsecure.com/edge-connect.ps1 | iex # ===================================================================== $ErrorActionPreference = 'Continue' function Info($m){ Write-Host "[vuepoint] $m" -ForegroundColor Cyan } function Warn($m){ Write-Host "[vuepoint] $m" -ForegroundColor Yellow } # 0) must be admin if (-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { Warn "Please run in an ADMINISTRATOR PowerShell (Start > 'powershell' > right-click > Run as administrator), then paste again."; return } # 1) Tailscale — install + connect (skip 'up' if already connected → no fresh key needed on re-run) $ts = "C:\Program Files\Tailscale\tailscale.exe" if (-not (Test-Path $ts)) { Info "Installing Tailscale..." $msi = Join-Path $env:TEMP 'tailscale-setup.msi' Invoke-WebRequest -Uri 'https://pkgs.tailscale.com/stable/tailscale-setup-latest-amd64.msi' -OutFile $msi -UseBasicParsing Start-Process msiexec.exe -ArgumentList "/i `"$msi`" /quiet /norestart" -Wait Start-Sleep -Seconds 5 } $connected = $false try { $connected = ((& $ts status --json | ConvertFrom-Json).BackendState -eq 'Running') } catch {} if ($connected) { Info "Tailscale already connected — skipping (no key needed)." } elseif ($env:TS_KEY) { Info "Joining VuePoint's private network..." & $ts up --authkey $env:TS_KEY --unattended --hostname ("vuepoint-" + $env:COMPUTERNAME) --accept-dns=false } else { Warn "Tailscale not connected and no TS_KEY provided — use the one-line command VuePoint gave you."; return } Start-Sleep -Seconds 2 # Always clear any inbound-blocking pref (a previously-configured node may have # "block incoming"/shields up, which lets ping through but drops SSH). Runs on both # the fresh-join and already-connected paths. & $ts set --shields-up=false 2>$null $tsip = (& $ts ip -4 2>$null | Select-Object -First 1) # Surface a Tailscale egress block (site router blocking Tailscale's UDP) instead of # failing silently later: if the control plane can't see us Online, the path is dead. try { $self = (& $ts status --json | ConvertFrom-Json).Self if ($self -and -not $self.Online) { Warn "Tailscale is NOT reaching the network (node offline at the control plane)." Warn "The site router/firewall is likely blocking Tailscale outbound — allow UDP 41641 + UDP 3478 and TCP 443, then re-run." } } catch {} # 2) OpenSSH server — install ROBUSTLY. The Windows Capability install can silently fail # or stay InstallPending on locked-down boxes (Windows Update disabled). Stop the service # first so a RE-RUN never corrupts a live install, then fall back to the standalone # Win32-OpenSSH build if the capability doesn't yield a usable sshd. Info "Enabling secure remote management (OpenSSH)..." Stop-Service sshd -Force -ErrorAction SilentlyContinue $cap = Get-WindowsCapability -Online -Name 'OpenSSH.Server*' -ErrorAction SilentlyContinue | Select-Object -First 1 if (-not (Get-Service sshd -ErrorAction SilentlyContinue) -and $cap.State -ne 'Installed') { Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0 -ErrorAction SilentlyContinue | Out-Null for ($i=0; $i -lt 15 -and -not (Get-Service sshd -ErrorAction SilentlyContinue); $i++) { Start-Sleep 2 } } if (-not (Get-Service sshd -ErrorAction SilentlyContinue)) { Warn "Windows OpenSSH capability unavailable (Windows Update likely disabled) — installing standalone build..." try { Stop-Service ssh-agent -Force -ErrorAction SilentlyContinue Get-ChildItem 'C:\Program Files' -Directory -Filter 'OpenSSH*' -ErrorAction SilentlyContinue | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue $rel = Invoke-RestMethod -Uri 'https://api.github.com/repos/PowerShell/Win32-OpenSSH/releases/latest' -UseBasicParsing $zipUrl = ($rel.assets | Where-Object { $_.name -like 'OpenSSH-Win64*.zip' } | Select-Object -First 1).browser_download_url $zip = Join-Path $env:TEMP 'openssh.zip' Invoke-WebRequest -Uri $zipUrl -OutFile $zip -UseBasicParsing Expand-Archive -Path $zip -DestinationPath 'C:\Program Files' -Force $d = (Get-ChildItem 'C:\Program Files' -Directory -Filter 'OpenSSH-Win64*' | Select-Object -First 1).FullName if ($d) { Rename-Item $d 'C:\Program Files\OpenSSH' -Force } & powershell -ExecutionPolicy Bypass -File 'C:\Program Files\OpenSSH\install-sshd.ps1' } catch { Warn "Standalone OpenSSH install failed: $($_.Exception.Message)" } } Set-Service -Name sshd -StartupType Automatic -ErrorAction SilentlyContinue Set-Service -Name ssh-agent -StartupType Automatic -ErrorAction SilentlyContinue for ($i=0; $i -lt 5; $i++) { Start-Service sshd -ErrorAction SilentlyContinue; if ((Get-Service sshd -ErrorAction SilentlyContinue).Status -eq 'Running') { break }; Start-Sleep 2 } # firewall: enable the capability's rule + our own (rebuilt to guarantee it's correct), all profiles Get-NetFirewallRule -Name 'OpenSSH-Server-In-TCP' -ErrorAction SilentlyContinue | Enable-NetFirewallRule -ErrorAction SilentlyContinue Remove-NetFirewallRule -Name 'vuepoint-sshd' -ErrorAction SilentlyContinue New-NetFirewallRule -Name 'vuepoint-sshd' -DisplayName 'OpenSSH Server (VuePoint)' -Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 22 -Profile Any | Out-Null # make the Tailscale adapter a Private network (belt-and-suspenders) Get-NetConnectionProfile | Where-Object { $_.InterfaceAlias -like 'Tailscale*' } | Set-NetConnectionProfile -NetworkCategory Private -ErrorAction SilentlyContinue # 3) Deterministic admin account 'vuepoint' — so VuePoint always logs in as vuepoint@ $user = 'vuepoint' try { if (-not (Get-LocalUser -Name $user -ErrorAction SilentlyContinue)) { $pw = ConvertTo-SecureString ([guid]::NewGuid().ToString() + 'Aa1!') -AsPlainText -Force New-LocalUser -Name $user -Password $pw -FullName 'VuePoint' -Description 'VuePoint remote management' -AccountNeverExpires -PasswordNeverExpires | Out-Null } Add-LocalGroupMember -Group 'Administrators' -Member $user -ErrorAction SilentlyContinue Info "Admin account '$user' ready." } catch { Warn "Could not create '$user' account: $($_.Exception.Message)" } # 4) Install VuePoint's management key — admins_authorized_keys (correct ACL, no BOM) $pub = 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDOOecLl4WySU6dq2+Fzae7V8oVStmPFzQp8vOnbIszZ vuepoint-platform' $sshDir = 'C:\ProgramData\ssh' if (-not (Test-Path $sshDir)) { New-Item -ItemType Directory -Path $sshDir -Force | Out-Null } $aak = Join-Path $sshDir 'administrators_authorized_keys' [System.IO.File]::WriteAllText($aak, $pub + "`n") icacls $aak /inheritance:r /grant 'SYSTEM:F' /grant 'BUILTIN\Administrators:F' | Out-Null New-ItemProperty -Path 'HKLM:\SOFTWARE\OpenSSH' -Name DefaultShell -Value 'C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe' -PropertyType String -Force -ErrorAction SilentlyContinue | Out-Null Restart-Service sshd -ErrorAction SilentlyContinue # 5) Keep the edge awake — an unattended edge must NEVER sleep/hibernate # (if it sleeps, every remote path drops at once and we lose it until a physical wake). Info "Disabling sleep / hibernate (unattended edge)..." powercfg /change standby-timeout-ac 0 2>$null powercfg /change standby-timeout-dc 0 2>$null powercfg /change hibernate-timeout-ac 0 2>$null powercfg /change hibernate-timeout-dc 0 2>$null powercfg /change monitor-timeout-ac 0 2>$null powercfg /change disk-timeout-ac 0 2>$null powercfg /hibernate off 2>$null # system unattended-sleep timeout -> never powercfg /setacvalueindex SCHEME_CURRENT 238C9FA8-0AAD-41ED-83F4-97BE242C8F20 7bc4a2f9-d8fc-4469-b07b-33eb785aaca0 0 2>$null # lid close -> do nothing (covers laptop/NUC form factors; harmless on desktops) powercfg /setacvalueindex SCHEME_CURRENT 4f971e89-eebd-4455-a8de-9e59040e7347 5ca83367-6e45-459f-a27b-476b1d01c936 0 2>$null powercfg /setdcvalueindex SCHEME_CURRENT 4f971e89-eebd-4455-a8de-9e59040e7347 5ca83367-6e45-459f-a27b-476b1d01c936 0 2>$null powercfg /SetActive SCHEME_CURRENT 2>$null # 6) report — verify sshd is actually LISTENING, not just "Running" $sshOk = (Get-Service sshd -ErrorAction SilentlyContinue).Status $listen = @(Get-NetTCPConnection -LocalPort 22 -State Listen -ErrorAction SilentlyContinue).Count Write-Host "" Info "===================== DONE =====================" Info (" Tailscale IP : " + $tsip) Info (" Name : vuepoint-" + $env:COMPUTERNAME) Info (" SSH service : " + $sshOk + " (listening=" + ($listen -gt 0) + ") login: vuepoint@" + $tsip) if ($sshOk -eq 'Running' -and $listen -gt 0) { Info "SSH is READY. VuePoint will finish the setup remotely — you can close this window." } else { Warn "SSH is NOT ready yet — please send this screen to VuePoint. The box is on Tailscale but not reachable on port 22." } Info "==============================================="