#Requires -RunAsAdministrator <# .SYNOPSIS Nerdy Neighbor - fast, clean Win32-OpenSSH install, LAN-locked, with our key. .DESCRIPTION Installs OpenSSH Server on Windows: - Downloads the latest OpenSSH-Win64.zip straight from the PowerShell/Win32-OpenSSH GitHub release (always the newest version), falling back to the nerdindustries R2 mirror only if GitHub is unreachable. - Cleanly removes any prior sshd/ssh-agent service first. - Extracts to C:\Program Files\OpenSSH, runs install-sshd.ps1, sets the service to Automatic and starts it, and makes PowerShell the default remote shell. - Authorizes our admin key in administrators_authorized_keys with the correct locked-down ACL (SYSTEM + Administrators only, no inheritance). - Firewall: inbound TCP 22 restricted to LocalSubnet (LAN only) on any network profile; strips broad OpenSSH rules so nothing else exposes it. .NOTES Run with: irm openssh.nerdyneighbor.net | iex (elevated PowerShell) Remove with: irm openssh-uninstall.nerdyneighbor.net | iex Windows 7: needs WMF 5.1, and PowerShell there defaults to TLS 1.0, so run [Net.ServicePointManager]::SecurityProtocol='Tls12'; irm openssh.nerdyneighbor.net | iex (firewall/IP steps use netsh/WMI automatically when the Win8+ cmdlets are missing) Connect from the LAN: ssh @ (any Administrators-group user; the admin key works for all of them. uses this box's default key, id_ed25519 / claude-debug) #> $ErrorActionPreference = 'Stop' $ProgressPreference = 'SilentlyContinue' [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 # --- Config ------------------------------------------------------------------ # Our admin key (goes in administrators_authorized_keys -> works for any account # in the Administrators group). $PubKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAQwebAP+RXnuDkk5VFYlQlvWpf6BZFZU6kX/HrQsOhE claude-debug" $InstallDir = "C:\Program Files\OpenSSH" $MirrorUrl = "https://bd.nerdindustries.net/OpenSSH-Win64.zip" # fast primary source $FirewallRuleName = "OpenSSH-Server-In-TCP-LAN" function Show-Step { param([string]$m) Write-Host "==> $m" -ForegroundColor Cyan } # Windows 7 has no NetSecurity/NetTCPIP modules (not even with WMF 5.1), so the # firewall and IP steps fall back to netsh / WMI there. $HasNetSecurity = [bool](Get-Command New-NetFirewallRule -ErrorAction SilentlyContinue) $RuleDisplay = "OpenSSH SSH Server (LAN only)" # Fast download: WebClient straight to disk (no IWR buffering/progress), BITS fallback. function Get-File { param([string]$Url, [string]$Out) try { (New-Object System.Net.WebClient).DownloadFile($Url, $Out) } catch { Import-Module BitsTransfer -ErrorAction SilentlyContinue Start-BitsTransfer -Source $Url -Destination $Out -ErrorAction Stop } } # Get OpenSSH-Win64.zip: latest GitHub release first (always newest), R2 mirror fallback. function Get-OpenSSHZip { param([string]$Dest) try { Show-Step "Fetching latest Win32-OpenSSH release from GitHub..." $rel = Invoke-RestMethod -Uri "https://api.github.com/repos/PowerShell/Win32-OpenSSH/releases/latest" -UseBasicParsing $asset = $rel.assets | Where-Object { $_.name -eq "OpenSSH-Win64.zip" } | Select-Object -First 1 if (-not $asset) { throw "OpenSSH-Win64.zip not found in latest release ($($rel.tag_name))" } Write-Host " Version: $($rel.tag_name)" Get-File $asset.browser_download_url $Dest if ((Get-Item $Dest).Length -gt 1MB) { return $rel.tag_name } throw "downloaded zip looks incomplete" } catch { Show-Step "GitHub unavailable ($($_.Exception.Message)) - falling back to nerdindustries mirror..." $cb = [DateTime]::UtcNow.Ticks Get-File "$MirrorUrl`?t=$cb" $Dest return "mirror" } } try { $principal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent()) if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { throw "This script must be run as Administrator." } if ($PSVersionTable.PSVersion.Major -lt 3) { throw "PowerShell $($PSVersionTable.PSVersion) is too old - install WMF 5.1 first (Windows 7: needs SP1 + .NET 4.5.2)." } # --- 1) Remove any prior OpenSSH services so the install is clean -------- foreach ($svc in 'sshd','ssh-agent') { $s = Get-Service $svc -ErrorAction SilentlyContinue if ($s) { Show-Step "Removing existing service: $svc" Stop-Service $svc -Force -ErrorAction SilentlyContinue & sc.exe delete $svc | Out-Null } } Start-Sleep -Seconds 1 # --- 2) Download + extract ---------------------------------------------- $zip = Join-Path $env:TEMP "OpenSSH-Win64.zip" $src = Get-OpenSSHZip $zip Write-Host " Source: $src" Show-Step "Extracting to $InstallDir..." $stage = Join-Path $env:TEMP "OpenSSH-stage-$([System.Guid]::NewGuid())" try { Expand-Archive -Path $zip -DestinationPath $stage -Force } catch { Add-Type -AssemblyName System.IO.Compression.FileSystem [System.IO.Compression.ZipFile]::ExtractToDirectory($zip, $stage) } $extracted = Get-ChildItem -Path $stage -Directory | Select-Object -First 1 if (-not $extracted) { throw "Unexpected archive layout - no directory inside zip." } if (Test-Path $InstallDir) { Remove-Item $InstallDir -Recurse -Force -ErrorAction SilentlyContinue } Move-Item -Path $extracted.FullName -Destination $InstallDir -Force # A move keeps %TEMP%'s ACL (SYSTEM, Administrators, current user only). sshd's # unprivileged per-connection helper then can't read libcrypto.dll and every # connection resets at key exchange. Reset to normal inherited Program Files ACL. & icacls.exe $InstallDir /reset /T /C /Q | Out-Null Remove-Item $stage -Recurse -Force -ErrorAction SilentlyContinue Remove-Item $zip -Force -ErrorAction SilentlyContinue # --- 3) Install + start the service ------------------------------------- Show-Step "Running install-sshd.ps1..." $installScript = Join-Path $InstallDir "install-sshd.ps1" if (-not (Test-Path $installScript)) { throw "install-sshd.ps1 not found in extracted archive." } & powershell.exe -NoProfile -ExecutionPolicy Bypass -File $installScript | Out-Null Show-Step "Setting sshd to Automatic and starting it..." Set-Service -Name sshd -StartupType Automatic Start-Service sshd # Default remote shell = PowerShell if (-not (Test-Path "HKLM:\SOFTWARE\OpenSSH")) { New-Item -Path "HKLM:\SOFTWARE\OpenSSH" -Force | Out-Null } New-ItemProperty -Path "HKLM:\SOFTWARE\OpenSSH" -Name DefaultShell ` -Value "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" -PropertyType String -Force | Out-Null # --- 4) Authorize our admin key ----------------------------------------- Show-Step "Installing authorized admin key..." $sshDir = "C:\ProgramData\ssh" if (-not (Test-Path $sshDir)) { New-Item -ItemType Directory -Path $sshDir -Force | Out-Null } $authKeys = Join-Path $sshDir "administrators_authorized_keys" # Match by the key body (col 2), so a changed comment doesn't duplicate it. $keyBody = ($PubKey -split '\s+')[1] $existing = if (Test-Path $authKeys) { Get-Content $authKeys -ErrorAction SilentlyContinue } else { @() } if (-not ($existing | Where-Object { $_ -match [regex]::Escape($keyBody) })) { Add-Content -Path $authKeys -Value $PubKey -Encoding ascii Write-Host " Key added" } else { Write-Host " Key already present, skipping" } # Required ACL: only SYSTEM + Administrators, no inheritance. & icacls.exe $authKeys /inheritance:r /grant "SYSTEM:F" /grant "BUILTIN\Administrators:F" | Out-Null # --- 5) Firewall: TCP 22 inbound, LAN (LocalSubnet) only ---------------- Show-Step "Configuring firewall: TCP 22 inbound, LocalSubnet only..." $oldRules = @("OpenSSH-Server-In-TCP","OpenSSH SSH Server","OpenSSH SSH Server (sshd)","sshd") if ($HasNetSecurity) { foreach ($dn in $oldRules) { Get-NetFirewallRule -DisplayName $dn -ErrorAction SilentlyContinue | Remove-NetFirewallRule -ErrorAction SilentlyContinue } Get-NetFirewallRule -Name $FirewallRuleName -ErrorAction SilentlyContinue | Remove-NetFirewallRule -ErrorAction SilentlyContinue New-NetFirewallRule ` -Name $FirewallRuleName ` -DisplayName $RuleDisplay ` -Description "Inbound TCP 22 restricted to LocalSubnet for sshd.exe" ` -Enabled True -Direction Inbound -Action Allow -Protocol TCP ` -LocalPort 22 -RemoteAddress LocalSubnet -Profile Any ` -Program (Join-Path $InstallDir "sshd.exe") | Out-Null } else { # netsh addresses rules by display name foreach ($dn in $oldRules + @($FirewallRuleName, $RuleDisplay)) { & netsh.exe advfirewall firewall delete rule name="$dn" | Out-Null } & netsh.exe advfirewall firewall add rule name="$RuleDisplay" dir=in action=allow protocol=TCP localport=22 ` remoteip=localsubnet profile=any program="$(Join-Path $InstallDir 'sshd.exe')" enable=yes | Out-Null if ($LASTEXITCODE -ne 0) { throw "netsh could not add the firewall rule." } } # --- 6) Verify + report ------------------------------------------------- Show-Step "Verifying..." $sshd = Get-Service sshd Write-Host " sshd: $($sshd.Status) (startup: $($sshd.StartType))" if ($HasNetSecurity) { $rule = Get-NetFirewallRule -Name $FirewallRuleName $port = $rule | Get-NetFirewallPortFilter $addr = $rule | Get-NetFirewallAddressFilter Write-Host " Firewall: enabled=$($rule.Enabled) profile=$($rule.Profile) port=$($port.LocalPort) from=$($addr.RemoteAddress -join ',')" } else { $rule = & netsh.exe advfirewall firewall show rule name="$RuleDisplay" | Select-String 'Enabled|Profiles|LocalPort|RemoteIP' Write-Host " Firewall: $(($rule | ForEach-Object { ($_.Line -replace '\s+', ' ').Trim() }) -join '; ')" } if (Get-Command Get-NetIPAddress -ErrorAction SilentlyContinue) { $ip = (Get-NetIPAddress -AddressFamily IPv4 -PrefixOrigin Dhcp,Manual -ErrorAction SilentlyContinue | Where-Object { $_.IPAddress -notlike "169.254.*" } | Select-Object -First 1 -ExpandProperty IPAddress) } else { $ip = Get-WmiObject Win32_NetworkAdapterConfiguration -Filter 'IPEnabled=True' | ForEach-Object { $_.IPAddress } | Where-Object { $_ -match '^\d+\.' -and $_ -notlike "169.254.*" } | Select-Object -First 1 } # Show the connect command for the actual logged-in user. Prefer the # interactive console user (correct even when the script is elevated as a # different admin), falling back to whoever is running it. The admin key # authorizes any Administrators-group account, so this is just the label. $loginUser = $env:USERNAME try { $csUser = (Get-CimInstance Win32_ComputerSystem -ErrorAction Stop).UserName if ($csUser) { $loginUser = ($csUser -split '\\')[-1] } } catch { } Write-Host "" Write-Host "OpenSSH installed and locked to LAN." -ForegroundColor Green Write-Host "" Write-Host " Connect from the LAN (any Administrators-group user):" Write-Host " ssh $loginUser@$($env:COMPUTERNAME)" if ($ip) { Write-Host " ssh $loginUser@$ip" } Write-Host "" } catch { Write-Host "" Write-Host "ERROR: $($_.Exception.Message)" -ForegroundColor Red if ($_.InvocationInfo.ScriptLineNumber) { Write-Host " at line $($_.InvocationInfo.ScriptLineNumber): $($_.InvocationInfo.Line.Trim())" -ForegroundColor DarkGray } return # not exit: under irm | iex, exit closes the tech's PowerShell window }