Files
Scripts/sshinstall.ps1
edemilovandClaude Sonnet 5 337a209713 fix: final review fixes for sshinstall (elevation guard, Win8 drop, authorized_keys newline, style)
Final whole-branch review pass before merge:

1. sshinstall.ps1: #Requires -RunAsAdministrator doesn't enforce under
   `irm ... | iex` (piping into Invoke-Expression). Add an explicit
   WindowsPrincipal/IsInRole elevation check before anything else runs,
   so a non-elevated run fails fast instead of silently falling through
   Get-WindowsCapability's -ErrorAction SilentlyContinue into the
   unsupported-OS branch.

2. sshinstall.ps1: drop the Windows 8 portable Win32-OpenSSH fallback —
   it depends on Expand-Archive (PS 5.0+) and implicit TLS 1.2, neither
   present on stock Windows 8's PS 3.0/4.0, so the one path that existed
   for Windows 8 was guaranteed to fail on Windows 8. Replaced with a
   clear unsupported-OS failure. Windows support is now 10 (1809+)/11
   only; README table updated to match.

3. sshinstall.sh: guarantee a trailing newline before appending the
   admin key to authorized_keys, so a pre-existing file without one
   doesn't get its last line glued to the new key (which also defeats
   the grep -qF idempotency check on every future run). Added the
   equivalent guard to sshinstall.ps1's Add-Content path too — its
   WriteLine only guarantees a trailing newline after what it writes,
   not before, so a hand-edited or externally-written
   administrators_authorized_keys has the same exposure.

4. sshinstall.sh: pacman -Sy -> -S (drop the partial-upgrade footgun,
   matches apps.sh's install-vs-explicit-update convention); chown -R
   now sets both user and group explicitly (matches apps.sh/
   netdrivelink.sh convention).

5. sshinstall.ps1: rename the firewall rule this script creates to
   OpenSSH-Server-In-TCP-sshinstall — the previous name collided with
   the exact rule name Microsoft's own Add-WindowsCapability OpenSSH
   install uses, so a disabled Microsoft-created rule of that name
   would cause New-NetFirewallRule to throw under
   $ErrorActionPreference = "Stop" and kill the script before the key
   was deployed.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-09-17 15:27:30 +03:00

132 lines
7.2 KiB
PowerShell

# ═══════════════════════════════════════════════════════════════════
# sshinstall.ps1
# Windows equivalent of sshinstall.sh — installs/enables OpenSSH Server,
# deploys the shared admin key for passwordless login
# Run in PowerShell as Administrator:
# irm https://git.edemilov.ru/edemilov/scripts/raw/branch/master/sshinstall.ps1 | iex
# ═══════════════════════════════════════════════════════════════════
#Requires -RunAsAdministrator
$ErrorActionPreference = "Stop"
function Write-Header { param($t) Write-Host "`n── $t ──────────────────────────────" -ForegroundColor Cyan }
function Write-Info { param($t) Write-Host "[•] $t" -ForegroundColor Cyan }
function Write-Success { param($t) Write-Host "[✓] $t" -ForegroundColor Green }
function Write-Skip { param($t) Write-Host "[~] $t (already done)" -ForegroundColor Magenta }
function Write-Warn { param($t) Write-Host "[!] $t" -ForegroundColor Yellow }
# ── Require elevation ────────────────────────────────────────────
# #Requires -RunAsAdministrator above does not enforce under
# `irm ... | iex` (piping into Invoke-Expression is widely reported not to
# trigger the #Requires check), so this explicit check is the real guard.
$currentPrincipal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())
if (-not $currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
Write-Warn "Must be run as Administrator — re-run PowerShell as Administrator"
exit 1
}
$AdminKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH/hWD8YgjVzA/9V364W4w2HiEpuPjKMQ/Id3SRlyriM edemilov@laptop"
Write-Host ""
Write-Host " ╔══════════════════════════════════════════════╗" -ForegroundColor Cyan
Write-Host " ║ sshinstall.ps1 — SSH bootstrap ║" -ForegroundColor Cyan
Write-Host " ╚══════════════════════════════════════════════╝" -ForegroundColor Cyan
Write-Host ""
# ── Install OpenSSH Server ────────────────────────────────────────
Write-Header "SSH server"
$capability = Get-WindowsCapability -Online -Name "OpenSSH.Server~~~~0.0.1.0" -ErrorAction SilentlyContinue
$sshdPath = "$env:ProgramFiles\OpenSSH\sshd.exe"
if ($capability -and $capability.State -eq "Installed") {
Write-Skip "OpenSSH Server (Windows capability)"
} elseif ($capability) {
Write-Info "Installing OpenSSH Server via Windows capability..."
Add-WindowsCapability -Online -Name "OpenSSH.Server~~~~0.0.1.0" | Out-Null
Write-Success "OpenSSH Server capability installed"
} elseif (Test-Path $sshdPath) {
Write-Skip "OpenSSH Server (portable install found at $sshdPath)"
} else {
Write-Warn "OpenSSH Server capability is not available on this Windows version. This script supports Windows 10 (1809+) and Windows 11 only."
exit 1
}
# ── Enable + start service ────────────────────────────────────────
Write-Header "SSH service"
$svc = Get-Service -Name sshd -ErrorAction SilentlyContinue
if ($svc -and $svc.Status -eq "Running") {
Write-Skip "sshd service"
} else {
Write-Info "Enabling + starting sshd..."
Set-Service -Name sshd -StartupType Automatic
Start-Service sshd
Write-Success "sshd running"
}
# ── Firewall ───────────────────────────────────────────────────────
Write-Header "Firewall"
$rule = Get-NetFirewallRule -ErrorAction SilentlyContinue | Where-Object {
$_.Enabled -eq "True" -and $_.Direction -eq "Inbound" -and
(Get-NetFirewallPortFilter -AssociatedNetFirewallRule $_ -ErrorAction SilentlyContinue | Where-Object { $_.LocalPort -eq 22 })
}
if ($rule) {
Write-Skip "Inbound firewall rule for port 22"
} else {
Write-Info "Adding inbound firewall rule for port 22..."
New-NetFirewallRule -Name "OpenSSH-Server-In-TCP-sshinstall" -DisplayName "OpenSSH Server (sshinstall)" -Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 22 | Out-Null
Write-Success "Firewall rule added"
}
# ── Deploy admin key ───────────────────────────────────────────────
Write-Header "Admin key"
$adminSid = New-Object System.Security.Principal.SecurityIdentifier("S-1-5-32-544")
$isAdminAccount = ([Security.Principal.WindowsIdentity]::GetCurrent().Groups -contains $adminSid)
if ($isAdminAccount) {
$keyFile = "$env:ProgramData\ssh\administrators_authorized_keys"
} else {
$keyFile = "$env:USERPROFILE\.ssh\authorized_keys"
}
$keyDir = Split-Path $keyFile
if (-not (Test-Path $keyDir)) { New-Item -ItemType Directory -Path $keyDir -Force | Out-Null }
if (-not (Test-Path $keyFile)) { New-Item -ItemType File -Path $keyFile -Force | Out-Null }
# Add-Content resumes writing at the file's current end-of-file position and only
# guarantees a trailing newline after what IT writes — it does not fix up a pre-existing
# last line that lacks one (e.g. a hand-edited or externally-written file). Without this
# guard that can glue the new key onto the end of the last existing line, corrupting both,
# same as the bash >> case this fix wave addresses.
if ((Get-Item $keyFile).Length -gt 0) {
$existingRaw = Get-Content -Path $keyFile -Raw
if ($existingRaw.Length -gt 0 -and $existingRaw[-1] -ne "`n") {
Add-Content -Path $keyFile -Value ""
}
}
$existing = Get-Content $keyFile -ErrorAction SilentlyContinue
if ($existing -contains $AdminKey) {
Write-Skip "admin key in $keyFile"
} else {
Add-Content -Path $keyFile -Value $AdminKey
Write-Success "admin key added to $keyFile"
}
if ($isAdminAccount) {
$aclOutput = (icacls $keyFile) -join "`n"
if ($aclOutput -match 'BUILTIN\\Users|Everyone|Authenticated Users|BUILTIN\\Guests') {
icacls $keyFile /inheritance:r | Out-Null
icacls $keyFile /grant "SYSTEM:F" | Out-Null
icacls $keyFile /grant "Administrators:F" | Out-Null
Write-Success "Locked down ACLs on $keyFile (SYSTEM + Administrators only)"
} else {
Write-Skip "ACLs on $keyFile"
}
}
Write-Host ""
Write-Host " ╔══════════════════════════════════════════════╗" -ForegroundColor Green
Write-Host " ║ Done — SSH ready, key deployed ║" -ForegroundColor Green
Write-Host " ╚══════════════════════════════════════════════╝" -ForegroundColor Green
Write-Host ""