Install OpenSSH Server during Windows setup

Install it at first logon from the Win32-OpenSSH zip on the seed ISO via
Configure-Windows.ps1 -InstallOpenSSH, replacing the manual 05/06 batch
files. The administrator public key now comes from the host
(SSH_PUBLIC_KEY_FILE or ~/.ssh/id_*.pub) instead of being hardcoded.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
GuofangandClaude Sonnet 5.5 committed 2026-10-20 11:55:38 +08:00
1 parent 552a292894
commit 65a5780e02
7 files changed
+78 -128

No files matched your search

+7 -8
View File
@@ -174,14 +174,13 @@ VirtIO ISO with `pnputil` on arm64), without which the guest has no network on
arm64. The installer includes the VirtIO drivers, SPICE vdagent, and QEMU Guest arm64. The installer includes the VirtIO drivers, SPICE vdagent, and QEMU Guest
Agent. The launcher exposes the vdagent channel for host/guest clipboard Agent. The launcher exposes the vdagent channel for host/guest clipboard
integration and the QEMU Guest Agent at `.vm/windows11/qemu-guest-agent.sock`. integration and the QEMU Guest Agent at `.vm/windows11/qemu-guest-agent.sock`.
Change the password after first login. The OpenSSH Server is not installed Change the password after first login. The OpenSSH Server is installed during
automatically. Run `05-Install-OpenSSH-Admin.bat` from the `Installers` folder on the first logon from the Win32-OpenSSH zip for the host architecture, which the
the desktop to install it through Windows Update, which can be slow, or seed ISO carries (downloaded by `vm-images/download-images.sh openssh-win`), so
`06-Install-OpenSSH-Zip-Admin.bat` to install it from the Win32-OpenSSH zip for Windows Update is not needed. Connect with `ssh guofang@localhost -p 2222`; the
the host architecture, which the seed ISO carries (downloaded by seed ISO also carries the host's SSH public key (`$SSH_PUBLIC_KEY_FILE`, else the
`vm-images/download-images.sh openssh-win`), as first of `~/.ssh/id_ed25519.pub`, `id_ecdsa.pub`, `id_rsa.pub`) for passwordless
administrator, then connect with `ssh guofang@localhost -p 2222`; it also installs administrator login.
the host's configured SSH public key for passwordless administrator login.
The Windows VM hardware (8G memory, 8 CPUs, system disk, devices) The Windows VM hardware (8G memory, 8 CPUs, system disk, devices)
is defined in `qemu/windows11.ini`, with the accelerators and machine type in is defined in `qemu/windows11.ini`, with the accelerators and machine type in
`qemu/windows11-<os>-<arch>.ini`; the launcher renders the former to `qemu/windows11-<os>-<arch>.ini`; the launcher renders the former to
+7
View File
@@ -304,6 +304,13 @@ if installing_from_iso:
seed_sources = [seed_dir / "Autounattend.xml", seed_dir / "Configure-Windows.ps1", seed_sources = [seed_dir / "Autounattend.xml", seed_dir / "Configure-Windows.ps1",
*sorted(seed_dir.glob("[0-9][0-9]-*.bat"))] *sorted(seed_dir.glob("[0-9][0-9]-*.bat"))]
# build-seed.sh embeds the administrator SSH public key.
ssh_key = os.environ.get("SSH_PUBLIC_KEY_FILE") or next(
(str(k) for n in ("id_ed25519", "id_ecdsa", "id_rsa")
if (k := Path.home() / ".ssh" / f"{n}.pub").is_file()), None)
if ssh_key:
seed_sources.append(Path(ssh_key))
# The seed carries the OpenSSH zip for this architecture; build-seed.sh # The seed carries the OpenSSH zip for this architecture; build-seed.sh
# downloads it when missing. # downloads it when missing.
seed_sources.append(Path(capture( seed_sources.append(Path(capture(
@@ -1,50 +0,0 @@
@echo off
setlocal
title Install OpenSSH Server
fltmc >nul 2>&1
if errorlevel 1 (
echo Please right-click this file and choose "Run as administrator".
pause
exit /b 1
)
rem The capability is downloaded through Windows Update, which can be slow.
echo Installing the OpenSSH Server Windows capability...
dism.exe /Online /Add-Capability /CapabilityName:OpenSSH.Server~~~~0.0.1.0
set "RC=%ERRORLEVEL%"
if not "%RC%"=="0" if not "%RC%"=="3010" (
echo dism exited with code %RC%.
pause
exit /b %RC%
)
echo Starting the sshd service...
sc.exe config sshd start= auto
net.exe start sshd >nul 2>&1
sc.exe query sshd | find "RUNNING" >nul
if errorlevel 1 (
echo The sshd service is not running.
pause
exit /b 1
)
netsh.exe advfirewall firewall show rule name="OpenSSH-Server-In-TCP" >nul 2>&1
if errorlevel 1 (
netsh.exe advfirewall firewall add rule name="OpenSSH-Server-In-TCP" ^
dir=in action=allow protocol=TCP localport=22 profile=private,public
)
echo Installing the administrator authorized key...
set "KEYFILE=%ProgramData%\ssh\administrators_authorized_keys"
> "%KEYFILE%" echo ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCwsF2ZDSHDaXM9BdDq3Vxkp6T+DJEakKaVZQJUEHexS34mbrxYAxmGBUPCrddfE01dS31kGS4XHVBa+wGUYodY+nPwnrfD1Q4phKVSzXr65K1Y/sCcjSiANBPWpv2Xd7IMOfLlrdLtmf5QcKWroIqO2LaIDGECwzUUHrfprgeaF9N0FGcmrgbmm23JUk+f6guRQ4br88L4rWIWEBvL9bTdwErNkdfsYE2F/U8aIFpMcVy2eJIOgy8ZjZvkGdm4r730YkTDL1VmAjiHH3/m2R1EOvwLzFhyWKoa4ULk/IQW+8iNnG42QEcaWRM+hMwlVdnNhPJP6WzS+fAEbibD3zM0q2X/4ex+cXRNOCd2/0KhnD2+3p1BM5QeFTzDappkk8W2meQkDE3hvTjtIQCzygFO6LNWtEEa7BTvvlMqF3CZWh88J9XiP750SjESe3Ge5o9A0T2hcJa+KZ1LxVEmNt4nD6E/OD4EymoaLIJZ1fH5ksSFjHzJ+mJouSVjc/H/VSE= guofang@GUOFANG-OFFICE
icacls.exe "%KEYFILE%" /inheritance:r /grant "*S-1-5-32-544:F" /grant "*S-1-5-18:F" >nul
if errorlevel 1 (
echo icacls exited with code %ERRORLEVEL%.
pause
exit /b 1
)
echo Done. SSH is available on port 22 of this machine.
pause
exit /b 0
@@ -1,65 +0,0 @@
@echo off
setlocal
title Install OpenSSH Server from zip
fltmc >nul 2>&1
if errorlevel 1 (
echo Please right-click this file and choose "Run as administrator".
pause
exit /b 1
)
set "ZIP="
for %%Z in ("%~dp0OpenSSH-*.zip") do set "ZIP=%%~fZ"
if not defined ZIP (
echo No OpenSSH-*.zip was found next to this file.
pause
exit /b 1
)
echo Installing the OpenSSH Server from %ZIP%...
if not exist "%ProgramFiles%\OpenSSH\install-sshd.ps1" (
powershell.exe -NoProfile -ExecutionPolicy Bypass -Command ^
"$t = Join-Path $env:TEMP 'openssh'; Remove-Item $t -Recurse -Force -ErrorAction SilentlyContinue; Expand-Archive -LiteralPath $env:ZIP -DestinationPath $t; Remove-Item (Join-Path $env:ProgramFiles 'OpenSSH') -Recurse -Force -ErrorAction SilentlyContinue; Move-Item (Get-ChildItem $t -Directory | Select-Object -First 1).FullName (Join-Path $env:ProgramFiles 'OpenSSH'); Remove-Item $t -Recurse -Force"
if errorlevel 1 (
echo Extracting the zip failed.
pause
exit /b 1
)
)
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%ProgramFiles%\OpenSSH\install-sshd.ps1"
if errorlevel 1 (
echo install-sshd.ps1 failed.
pause
exit /b 1
)
echo Starting the sshd service...
sc.exe config sshd start= auto
net.exe start sshd >nul 2>&1
sc.exe query sshd | find "RUNNING" >nul
if errorlevel 1 (
echo The sshd service is not running.
pause
exit /b 1
)
netsh.exe advfirewall firewall show rule name="OpenSSH-Server-In-TCP" >nul 2>&1
if errorlevel 1 (
netsh.exe advfirewall firewall add rule name="OpenSSH-Server-In-TCP" ^
dir=in action=allow protocol=TCP localport=22 profile=private,public
)
echo Installing the administrator authorized key...
set "KEYFILE=%ProgramData%\ssh\administrators_authorized_keys"
> "%KEYFILE%" echo ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCwsF2ZDSHDaXM9BdDq3Vxkp6T+DJEakKaVZQJUEHexS34mbrxYAxmGBUPCrddfE01dS31kGS4XHVBa+wGUYodY+nPwnrfD1Q4phKVSzXr65K1Y/sCcjSiANBPWpv2Xd7IMOfLlrdLtmf5QcKWroIqO2LaIDGECwzUUHrfprgeaF9N0FGcmrgbmm23JUk+f6guRQ4br88L4rWIWEBvL9bTdwErNkdfsYE2F/U8aIFpMcVy2eJIOgy8ZjZvkGdm4r730YkTDL1VmAjiHH3/m2R1EOvwLzFhyWKoa4ULk/IQW+8iNnG42QEcaWRM+hMwlVdnNhPJP6WzS+fAEbibD3zM0q2X/4ex+cXRNOCd2/0KhnD2+3p1BM5QeFTzDappkk8W2meQkDE3hvTjtIQCzygFO6LNWtEEa7BTvvlMqF3CZWh88J9XiP750SjESe3Ge5o9A0T2hcJa+KZ1LxVEmNt4nD6E/OD4EymoaLIJZ1fH5ksSFjHzJ+mJouSVjc/H/VSE= guofang@GUOFANG-OFFICE
icacls.exe "%KEYFILE%" /inheritance:r /grant "*S-1-5-32-544:F" /grant "*S-1-5-18:F" >nul
if errorlevel 1 (
echo icacls exited with code %ERRORLEVEL%.
pause
exit /b 1
)
echo Done. SSH is available on port 22 of this machine.
pause
exit /b 0
+1 -1
View File
@@ -162,7 +162,7 @@
<SynchronousCommand wcm:action="add"> <SynchronousCommand wcm:action="add">
<Order>1</Order> <Order>1</Order>
<Description>Configure Windows from the answer-media script</Description> <Description>Configure Windows from the answer-media script</Description>
<CommandLine>powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "&amp; ('{0}:\Configure-Windows.ps1' -f (Get-Volume -FileSystemLabel CIDATA).DriveLetter) -InstallVirtIO -EnableAutomaticLogon -DisableAutoUpdate -CopyInstallers -NoPause"</CommandLine> <CommandLine>powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "&amp; ('{0}:\Configure-Windows.ps1' -f (Get-Volume -FileSystemLabel CIDATA).DriveLetter) -InstallVirtIO -EnableAutomaticLogon -DisableAutoUpdate -CopyInstallers -InstallOpenSSH -NoPause"</CommandLine>
<RequiresUserInput>false</RequiresUserInput> <RequiresUserInput>false</RequiresUserInput>
</SynchronousCommand> </SynchronousCommand>
</FirstLogonCommands> </FirstLogonCommands>
+49 -4
View File
@@ -3,6 +3,7 @@ param(
[switch]$EnableAutomaticLogon, [switch]$EnableAutomaticLogon,
[switch]$DisableAutoUpdate, [switch]$DisableAutoUpdate,
[switch]$CopyInstallers, [switch]$CopyInstallers,
[switch]$InstallOpenSSH,
[switch]$NoPause [switch]$NoPause
) )
@@ -151,11 +152,52 @@ function Copy-InstallerScripts {
$target = Join-Path ([Environment]::GetFolderPath('Desktop')) 'Installers' $target = Join-Path ([Environment]::GetFolderPath('Desktop')) 'Installers'
New-Item -ItemType Directory -Path $target -Force | Out-Null New-Item -ItemType Directory -Path $target -Force | Out-Null
Copy-Item -Path $scripts.FullName -Destination $target -Force Copy-Item -Path $scripts.FullName -Destination $target -Force
}
# 05-Install-OpenSSH-Admin.bat installs from this zip when it is present. # Installs the OpenSSH Server from the Win32-OpenSSH zip on the answer media,
$zips = @(Get-ChildItem -Path $PSScriptRoot -Filter 'OpenSSH-*.zip' -File) # so no Windows Update download is needed.
if ($zips.Count -gt 0) { function Install-OpenSSHServer {
Copy-Item -Path $zips.FullName -Destination $target -Force $zip = Get-ChildItem -Path $PSScriptRoot -Filter 'OpenSSH-*.zip' -File |
Select-Object -First 1
if (-not $zip) {
throw "No OpenSSH-*.zip was found next to $PSCommandPath."
}
$installDir = Join-Path $env:ProgramFiles 'OpenSSH'
if (-not (Test-Path -LiteralPath (Join-Path $installDir 'install-sshd.ps1'))) {
$temp = Join-Path $env:TEMP 'openssh'
Remove-Item $temp -Recurse -Force -ErrorAction SilentlyContinue
Expand-Archive -LiteralPath $zip.FullName -DestinationPath $temp
Remove-Item $installDir -Recurse -Force -ErrorAction SilentlyContinue
Move-Item (Get-ChildItem $temp -Directory | Select-Object -First 1).FullName $installDir
Remove-Item $temp -Recurse -Force
}
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File (Join-Path $installDir 'install-sshd.ps1') | Out-Host
if ($LASTEXITCODE -ne 0) {
throw "install-sshd.ps1 failed (exit code $LASTEXITCODE)."
}
Set-Service -Name sshd -StartupType Automatic
Start-Service -Name sshd
if (-not (Get-NetFirewallRule -Name 'OpenSSH-Server-In-TCP' -ErrorAction SilentlyContinue)) {
New-NetFirewallRule -Name 'OpenSSH-Server-In-TCP' -DisplayName 'OpenSSH Server (sshd)' `
-Direction Inbound -Action Allow -Protocol TCP -LocalPort 22 -Profile Private, Public | Out-Null
}
# build-seed.sh puts the host's public key on the media; without it, only
# password login works.
$key = Join-Path $PSScriptRoot 'authorized_keys'
if (-not (Test-Path -LiteralPath $key)) {
Write-Warning "No authorized_keys was found next to $PSCommandPath; skipping the administrator key."
return
}
$keyFile = Join-Path $env:ProgramData 'ssh\administrators_authorized_keys'
Copy-Item -LiteralPath $key -Destination $keyFile -Force
& icacls.exe $keyFile /inheritance:r /grant '*S-1-5-32-544:F' /grant '*S-1-5-18:F' | Out-Null
if ($LASTEXITCODE -ne 0) {
throw "icacls failed (exit code $LASTEXITCODE)."
} }
} }
@@ -173,6 +215,9 @@ try {
if ($CopyInstallers) { if ($CopyInstallers) {
Copy-InstallerScripts Copy-InstallerScripts
} }
if ($InstallOpenSSH) {
Install-OpenSSHServer
}
Write-Host 'Done.' -ForegroundColor Green Write-Host 'Done.' -ForegroundColor Green
} }
catch { catch {
+14
View File
@@ -17,6 +17,16 @@ openssh_zip="$(VM_IMAGE_ARCH="$answer_arch" "$image_helper" --print-path openssh
[[ -s "$openssh_zip" ]] || VM_IMAGE_ARCH="$answer_arch" "$image_helper" openssh-win [[ -s "$openssh_zip" ]] || VM_IMAGE_ARCH="$answer_arch" "$image_helper" openssh-win
[[ -s "$openssh_zip" ]] || { printf 'Error: %s was not downloaded.\n' "$openssh_zip" >&2; exit 1; } [[ -s "$openssh_zip" ]] || { printf 'Error: %s was not downloaded.\n' "$openssh_zip" >&2; exit 1; }
# The public key for administrator SSH login: SSH_PUBLIC_KEY_FILE, else the
# first default key of the invoking user. The guest skips it when absent.
ssh_key="${SSH_PUBLIC_KEY_FILE:-}"
if [[ -z "$ssh_key" ]]; then
for candidate in "$HOME"/.ssh/id_ed25519.pub "$HOME"/.ssh/id_ecdsa.pub "$HOME"/.ssh/id_rsa.pub; do
[[ -f "$candidate" ]] && { ssh_key="$candidate"; break; }
done
fi
[[ -z "$ssh_key" || -f "$ssh_key" ]] || { printf 'Error: %s does not exist.\n' "$ssh_key" >&2; exit 1; }
# The answer file's components name the processor architecture, so render a # The answer file's components name the processor architecture, so render a
# copy for the target architecture. # copy for the target architecture.
work_dir="$(mktemp -d)" work_dir="$(mktemp -d)"
@@ -29,6 +39,10 @@ seed_files=(
"$script_dir/Configure-Windows.ps1" "$script_dir/Configure-Windows.ps1"
"$openssh_zip" "$openssh_zip"
) )
if [[ -n "$ssh_key" ]]; then
cp -- "$ssh_key" "$work_dir/authorized_keys"
seed_files+=("$work_dir/authorized_keys")
fi
# Numbered installer scripts (10-Install-Git-Admin.bat, 20-..., in install order). # Numbered installer scripts (10-Install-Git-Admin.bat, 20-..., in install order).
shopt -s nullglob shopt -s nullglob
seed_files+=("$script_dir"/[0-9][0-9]-*.bat) seed_files+=("$script_dir"/[0-9][0-9]-*.bat)