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]>
132 lines
7.2 KiB
PowerShell
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 ""
|