<# .SYNOPSIS Make a bare IsardVDI Windows desktop reachable and usable: install the OpenSSH server (so `isard ssh` works) and spice-guest-tools (so SPICE copy-paste works). .DESCRIPTION A freshly-provisioned Windows template on IsardVDI has neither an SSH server nor the SPICE guest agent. The symptoms are two: * `isard ssh ` fails with `connection refused` -- the bastion reaches the guest but nothing is listening on port 22. * `isard view ` shows the screen but the clipboard never syncs with the host -- there is no spice-vdagent to relay it. Both live inside the guest and cannot be fixed from the Mac or from the `isard` CLI. This script fixes them in one pass. It is meant to be run once, in an *Administrator* PowerShell, straight off the web: iex (irm https://get.optersoft.com/isard.ps1) That single line is short enough to type into a SPICE console that has no working clipboard yet -- which is the whole point, because fixing the clipboard is one of the things it does. The paren form rather than a pipe because `|` is unmapped on that console. It is idempotent: safe to run again, skips whatever is already in place. .NOTES Requires internet egress from the guest (Windows Update FoD, or GitHub + spice-space.org as a fallback). If `irm` fetched this file, plain HTTPS works; the OpenSSH install falls back to GitHub if Windows Update FoD is blocked. A reboot is needed for the clipboard to start working. #> [CmdletBinding()] param( # A public key to authorise for SSH, appended to the machine's # administrators_authorized_keys. Rarely needed -- the IsardVDI bastion logs # into the guest with a username/password, not your key (see -BastionUser). [string] $AuthorizedKey = $env:ISARD_SSH_KEY, # Create a local account for the IsardVDI bastion to log in as. The bastion # authenticates to the guest as `guest_properties.credentials.username` with # its password, and a fresh template has no such account -- so `isard ssh` # fails with the bastion's "unable to authenticate, attempted methods # [none password]". Set both, then connect with # isard ssh --user --password [string] $BastionUser = $env:ISARD_USER, [string] $BastionPassword = $env:ISARD_PASS, # Skip the spice-guest-tools install (SSH only). [switch] $NoSpice, # Skip the OpenSSH install (clipboard only). [switch] $NoSsh ) $ErrorActionPreference = 'Stop' # PowerShell 5.1 defaults to TLS 1.0, which spice-space.org and GitHub refuse. [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 # Run through `iex (irm ...)`, the param block does not always bind, so read the # same settings straight from the environment as a fallback. This is what makes # $env:ISARD_USER='isard'; $env:ISARD_PASS='...'; iex (irm .../isard.ps1) # work reliably. if (-not $AuthorizedKey) { $AuthorizedKey = $env:ISARD_SSH_KEY } if (-not $BastionUser) { $BastionUser = $env:ISARD_USER } if (-not $BastionPassword) { $BastionPassword = $env:ISARD_PASS } function Assert-Admin { $id = [Security.Principal.WindowsIdentity]::GetCurrent() $principal = [Security.Principal.WindowsPrincipal]::new($id) if (-not $principal.IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator)) { throw 'Run this in an Administrator PowerShell (right-click Start > Terminal (Admin)).' } } function Write-Step($msg) { Write-Host "==> $msg" -ForegroundColor Cyan } function Write-Ok($msg) { Write-Host " ok $msg" -ForegroundColor Green } function Write-Skip($msg) { Write-Host " -- $msg" -ForegroundColor DarkGray } function Write-Warn2($msg) { Write-Host " !! $msg" -ForegroundColor Yellow } # --- OpenSSH server --------------------------------------------------------- function Install-OpenSSHViaCapability { $cap = Get-WindowsCapability -Online -Name 'OpenSSH.Server*' -ErrorAction Stop | Select-Object -First 1 if (-not $cap) { return $false } if ($cap.State -eq 'Installed') { Write-Skip 'OpenSSH.Server capability already installed'; return $true } Write-Step "Installing OpenSSH.Server via Windows capability ($($cap.Name))" Add-WindowsCapability -Online -Name $cap.Name | Out-Null return $true } function Install-OpenSSHFromGitHub { # Fallback when Feature-on-Demand is blocked: the official Win32-OpenSSH # release, extracted to Program Files, with its own installer script. Write-Step 'Windows FoD unavailable -- installing OpenSSH from GitHub release' $api = 'https://api.github.com/repos/PowerShell/Win32-OpenSSH/releases/latest' $rel = Invoke-RestMethod -Uri $api -Headers @{ 'User-Agent' = 'isard-windows-ps1' } $asset = $rel.assets | Where-Object { $_.name -eq 'OpenSSH-Win64.zip' } | Select-Object -First 1 if (-not $asset) { throw 'Could not find OpenSSH-Win64.zip in the latest release.' } $zip = Join-Path $env:TEMP 'OpenSSH-Win64.zip' Invoke-WebRequest -Uri $asset.browser_download_url -OutFile $zip -UseBasicParsing $dest = Join-Path $env:ProgramFiles 'OpenSSH' if (Test-Path $dest) { Remove-Item $dest -Recurse -Force } Expand-Archive -Path $zip -DestinationPath $env:ProgramFiles -Force Rename-Item (Join-Path $env:ProgramFiles 'OpenSSH-Win64') $dest -ErrorAction SilentlyContinue & powershell -ExecutionPolicy Bypass -File (Join-Path $dest 'install-sshd.ps1') } function Enable-Sshd { if (-not (Get-Service sshd -ErrorAction SilentlyContinue)) { try { if (-not (Install-OpenSSHViaCapability)) { Install-OpenSSHFromGitHub } } catch { Write-Warn2 "capability install failed ($($_.Exception.Message)) -- trying GitHub" Install-OpenSSHFromGitHub } } else { Write-Skip 'sshd service already present' } Write-Step 'Starting sshd and setting it to start automatically' Set-Service -Name sshd -StartupType Automatic Start-Service sshd Write-Ok "sshd is $((Get-Service sshd).Status)" # The capability usually creates this rule; add it if the GitHub path or an # odd image did not. if (-not (Get-NetFirewallRule -Name 'OpenSSH-Server-In-TCP' -ErrorAction SilentlyContinue)) { Write-Step 'Opening the firewall for TCP 22' New-NetFirewallRule -Name 'OpenSSH-Server-In-TCP' -DisplayName 'OpenSSH Server (sshd)' ` -Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 22 | Out-Null } Write-Ok 'firewall allows TCP 22' # Make PowerShell the login shell so scripted `isard ssh -- ` runs # PowerShell, not cmd.exe. Prefer pwsh (7+) if it is installed. $shell = (Get-Command pwsh -ErrorAction SilentlyContinue).Source if (-not $shell) { $shell = "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe" } New-Item -Path 'HKLM:\SOFTWARE\OpenSSH' -Force | Out-Null New-ItemProperty -Path 'HKLM:\SOFTWARE\OpenSSH' -Name DefaultShell -Value $shell ` -PropertyType String -Force | Out-Null Write-Ok "default SSH shell: $shell" } function Add-AuthorizedKey($key) { if (-not $key) { return } Write-Step 'Authorising the supplied SSH key for administrators' # Windows sshd reads admin keys from this one file, not per-user, and is # strict about its ACL: owner + Administrators + SYSTEM only. $path = Join-Path $env:ProgramData 'ssh\administrators_authorized_keys' $existing = '' if (Test-Path $path) { $existing = Get-Content $path -Raw } if ($existing -notlike "*$key*") { Add-Content -Path $path -Value $key -Encoding utf8 } icacls $path /inheritance:r /grant 'Administrators:F' 'SYSTEM:F' | Out-Null Write-Ok 'key authorised' } function Set-BastionAccount($user, $password) { if (-not $user -or -not $password) { return } Write-Step "Creating the local account '$user' for the IsardVDI bastion" $secure = ConvertTo-SecureString $password -AsPlainText -Force if (Get-LocalUser -Name $user -ErrorAction SilentlyContinue) { Set-LocalUser -Name $user -Password $secure -PasswordNeverExpires $true } else { New-LocalUser -Name $user -Password $secure ` -PasswordNeverExpires -AccountNeverExpires | Out-Null } # Admin so the account is useful over SSH; password auth (what the bastion # uses) works for any local account regardless of group. Add-LocalGroupMember -Group Administrators -Member $user -ErrorAction SilentlyContinue Write-Ok "account '$user' ready (member of Administrators)" } # --- SPICE guest tools (clipboard) ------------------------------------------ function Test-SpiceInstalled { $keys = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*', 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*' foreach ($k in $keys) { if (Get-ItemProperty $k -ErrorAction SilentlyContinue | Where-Object { $_.DisplayName -like '*SPICE Guest Tools*' }) { return $true } } return [bool](Get-Service -Name 'spice-agent','vdservice' -ErrorAction SilentlyContinue) } function Install-SpiceGuestTools { if (Test-SpiceInstalled) { Write-Skip 'SPICE guest tools already installed'; return $false } Write-Step 'Installing spice-guest-tools (enables host <-> guest clipboard)' $url = 'https://www.spice-space.org/download/windows/spice-guest-tools/spice-guest-tools-latest.exe' $exe = Join-Path $env:TEMP 'spice-guest-tools.exe' Invoke-WebRequest -Uri $url -OutFile $exe -UseBasicParsing # NSIS installer: /S is silent. $p = Start-Process -FilePath $exe -ArgumentList '/S' -Wait -PassThru if ($p.ExitCode -ne 0) { Write-Warn2 "installer exit code $($p.ExitCode)" } Write-Ok 'spice-guest-tools installed (reboot to activate the clipboard)' return $true } # --- main ------------------------------------------------------------------- Assert-Admin Write-Host '' Write-Host 'IsardVDI Windows desktop setup' -ForegroundColor White Write-Host '------------------------------' -ForegroundColor White $needReboot = $false if (-not $NoSsh) { Enable-Sshd Add-AuthorizedKey $AuthorizedKey Set-BastionAccount $BastionUser $BastionPassword } if (-not $NoSpice) { if (Install-SpiceGuestTools) { $needReboot = $true } } Write-Host '' Write-Host 'Done.' -ForegroundColor White if ($needReboot) { Write-Warn2 'Reboot to turn on the clipboard: Restart-Computer' } if ($BastionUser -and $BastionPassword) { Write-Host "From the Mac: isard ssh --user $BastionUser --password " -ForegroundColor White } else { Write-Host 'From the Mac: isard ssh ' -ForegroundColor White Write-Host 'If it fails with "attempted methods [none password]", the bastion has no' -ForegroundColor DarkGray Write-Host 'guest account to log in as. Re-run with one set, then connect with --user/--password:' -ForegroundColor DarkGray Write-Host " `$env:ISARD_USER='isard'; `$env:ISARD_PASS='Isard-VDI-2026!'; iex (irm https://get.optersoft.com/isard.ps1)" -ForegroundColor DarkGray }