diff --git a/README.md b/README.md index 139e940..cf41101 100644 --- a/README.md +++ b/README.md @@ -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 Agent. The launcher exposes the vdagent channel for host/guest clipboard 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 -automatically. Run `05-Install-OpenSSH-Admin.bat` from the `Installers` folder on -the desktop to install it through Windows Update, which can be slow, or -`06-Install-OpenSSH-Zip-Admin.bat` to install it from the Win32-OpenSSH zip for -the host architecture, which the seed ISO carries (downloaded by -`vm-images/download-images.sh openssh-win`), as -administrator, then connect with `ssh guofang@localhost -p 2222`; it also installs -the host's configured SSH public key for passwordless administrator login. +Change the password after first login. The OpenSSH Server is installed during +the first logon from the Win32-OpenSSH zip for the host architecture, which the +seed ISO carries (downloaded by `vm-images/download-images.sh openssh-win`), so +Windows Update is not needed. Connect with `ssh guofang@localhost -p 2222`; the +seed ISO also carries the host's SSH public key (`$SSH_PUBLIC_KEY_FILE`, else the +first of `~/.ssh/id_ed25519.pub`, `id_ecdsa.pub`, `id_rsa.pub`) for passwordless +administrator login. The Windows VM hardware (8G memory, 8 CPUs, system disk, devices) is defined in `qemu/windows11.ini`, with the accelerators and machine type in `qemu/windows11--.ini`; the launcher renders the former to diff --git a/run-windows11.py b/run-windows11.py index 8e28e21..4fddd6d 100755 --- a/run-windows11.py +++ b/run-windows11.py @@ -304,6 +304,13 @@ if installing_from_iso: seed_sources = [seed_dir / "Autounattend.xml", seed_dir / "Configure-Windows.ps1", *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 # downloads it when missing. seed_sources.append(Path(capture( diff --git a/seediso/windows11/05-Install-OpenSSH-Admin.bat b/seediso/windows11/05-Install-OpenSSH-Admin.bat deleted file mode 100644 index c4c99ca..0000000 --- a/seediso/windows11/05-Install-OpenSSH-Admin.bat +++ /dev/null @@ -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 diff --git a/seediso/windows11/06-Install-OpenSSH-Zip-Admin.bat b/seediso/windows11/06-Install-OpenSSH-Zip-Admin.bat deleted file mode 100644 index 89ad403..0000000 --- a/seediso/windows11/06-Install-OpenSSH-Zip-Admin.bat +++ /dev/null @@ -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 diff --git a/seediso/windows11/Autounattend.xml b/seediso/windows11/Autounattend.xml index 5024bb2..7cff78e 100644 --- a/seediso/windows11/Autounattend.xml +++ b/seediso/windows11/Autounattend.xml @@ -162,7 +162,7 @@ 1 Configure Windows from the answer-media script - powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "& ('{0}:\Configure-Windows.ps1' -f (Get-Volume -FileSystemLabel CIDATA).DriveLetter) -InstallVirtIO -EnableAutomaticLogon -DisableAutoUpdate -CopyInstallers -NoPause" + powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "& ('{0}:\Configure-Windows.ps1' -f (Get-Volume -FileSystemLabel CIDATA).DriveLetter) -InstallVirtIO -EnableAutomaticLogon -DisableAutoUpdate -CopyInstallers -InstallOpenSSH -NoPause" false diff --git a/seediso/windows11/Configure-Windows.ps1 b/seediso/windows11/Configure-Windows.ps1 index de3b459..def2764 100644 --- a/seediso/windows11/Configure-Windows.ps1 +++ b/seediso/windows11/Configure-Windows.ps1 @@ -3,6 +3,7 @@ param( [switch]$EnableAutomaticLogon, [switch]$DisableAutoUpdate, [switch]$CopyInstallers, + [switch]$InstallOpenSSH, [switch]$NoPause ) @@ -151,11 +152,52 @@ function Copy-InstallerScripts { $target = Join-Path ([Environment]::GetFolderPath('Desktop')) 'Installers' New-Item -ItemType Directory -Path $target -Force | Out-Null Copy-Item -Path $scripts.FullName -Destination $target -Force +} - # 05-Install-OpenSSH-Admin.bat installs from this zip when it is present. - $zips = @(Get-ChildItem -Path $PSScriptRoot -Filter 'OpenSSH-*.zip' -File) - if ($zips.Count -gt 0) { - Copy-Item -Path $zips.FullName -Destination $target -Force +# Installs the OpenSSH Server from the Win32-OpenSSH zip on the answer media, +# so no Windows Update download is needed. +function Install-OpenSSHServer { + $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) { Copy-InstallerScripts } + if ($InstallOpenSSH) { + Install-OpenSSHServer + } Write-Host 'Done.' -ForegroundColor Green } catch { diff --git a/seediso/windows11/build-seed.sh b/seediso/windows11/build-seed.sh index 0419a22..1b37b1c 100755 --- a/seediso/windows11/build-seed.sh +++ b/seediso/windows11/build-seed.sh @@ -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" ]] || { 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 # copy for the target architecture. work_dir="$(mktemp -d)" @@ -29,6 +39,10 @@ seed_files=( "$script_dir/Configure-Windows.ps1" "$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). shopt -s nullglob seed_files+=("$script_dir"/[0-9][0-9]-*.bat)