jmp-aasted (Windows jumphost)
jmp-aasted is a Windows VM on pve01 used as an RDP jumphost — a Windows desktop on the homelab LAN to remote into. It runs Windows Server 2025 by default, or Windows 11 (OS=win11); both are covered below. It is a standalone workgroup machine, not domain- or Entra-joined.
| Property | Value |
|---|---|
| vmid / node | 9002 / pve01 |
| IP (static, set in-guest) | 192.168.1.17/24, gw 192.168.1.1, DNS 192.168.1.60 |
| CPU / RAM / disk | 4 vCPU / 8 GiB / 80 GiB on local-lvm |
| OS | Windows Server 2025 (default) — or Windows 11 (OS=win11) |
| Firmware / machine | OVMF (UEFI) / q35, VirtIO SCSI + VirtIO NIC |
| Access | RDP (3389) with NLA, on the LAN |
This VM is only half infrastructure-as-code
The existing Debian VMs are fully declarative — one qm script plus cloud-init injects the user, IP and SSH keys, so a rebuild is qm destroy then re-run. Windows cannot be cloud-init'd. provision-jmp-aasted.sh builds the VM shell and attaches the media reproducibly, but the OS install, driver load, static IP, RDP enablement and updates are interactive in-guest steps (below). Treat the qm script as reproducible and the in-guest checklist as manual.
Two OS options — Windows Server (default) or Windows 11
Windows Server 2025 is the default: it needs no TPM 2.0 / Secure Boot (unlike Windows 11), so its qm spec is a plain sequence, and its evaluation is 180 days. Windows 11 is fully supported too — pass OS=win11 and provision-jmp-aasted.sh adds the vTPM Windows 11 requires; everything else is identical.
For Windows 11, use a Visual Studio Subscription rather than an eval: the recommended pick is Windows 11 Enterprise, version 25H2 (latest stable annual release; Pro is equally fine for RDP, and 24H2 is the conservative choice). Avoid 26H1 — it is an Insider/Dev preview, not a stable build. Download the ISO from the subscription's Downloads tab and a matching key from Product Keys. VS Subscription licences are dev/test use only — fine for a personal jumphost.
Prerequisites
- SSH access as root to
pve01(192.168.1.4) — the provision script is streamed there (bash -s), same as the other VMs. - A Windows ISO uploaded to the
localstore. For Server, download Windows Server 2025 from the Microsoft Evaluation Center (no key, 180 days). For Windows 11, download Windows 11 Enterprise/Pro 25H2 from your Visual Studio Subscription (see the note above) and grab a key from its Product Keys tab. Get it into the store with the host-side commands under Upload the Windows ISO below — not the browser upload, which stalls on multi-gigabyte files. Once there it shows aslocal:iso/<name>.iso. - The virtio-win driver ISO in the store (fetched once, below).
Upload the Windows ISO
The Proxmox web UI's ISO Images → Upload button pushes the file from your browser and routinely stalls or times out on a multi-gigabyte Windows ISO (the upload has no resume). Get the ISO into the local store from the command line instead — pick whichever path matches where the ISO lives.
You have a direct download URL (the Server 2025 eval, or the public Windows 11 ISO) — have pve01 pull it straight into the store, so nothing goes through your browser at all:
ssh root@192.168.1.4 'pvesh create /nodes/pve01/storage/local/download-url \
--content iso \
--filename windows-server-2025.iso \
--url "https://software-static.download.prss.microsoft.com/…/windows-server-2025.iso"'
This is the same "Download from URL" the UI offers, driven from the host — the fetch runs on pve01, not over your connection, so it doesn't stall. (Eval/CDN links can be redirect-based or time-limited; if the pull fails, fall back to the file-upload path below.)
You have the ISO as a local file (e.g. the Visual Studio Subscription download) — copy it to the store's ISO directory over SSH. Prefer rsync, which resumes if the link drops — the exact failure that makes the browser upload unreliable:
# Resumable — just re-run it if the connection breaks:
rsync -avP windows-11-25h2.iso root@192.168.1.4:/var/lib/vz/template/iso/
# Plain copy if rsync isn't available (not resumable):
scp windows-11-25h2.iso root@192.168.1.4:/var/lib/vz/template/iso/
Either way it then appears as local:iso/<name>.iso — the value you pass as WINISO when provisioning.
Fetch the virtio-win drivers (once)
Windows setup can't see a VirtIO SCSI disk without these drivers. Fetch the ISO into the store once; every Windows VM reuses it.
ssh root@192.168.1.4 'bash -s' < fetch-virtio-win.sh
#!/usr/bin/env bash
# Ensure the virtio-win driver ISO is in the Proxmox 'local' ISO store. Run first,
# as root on pve01, before provisioning any Windows VM. Downloads only if missing.
# Windows setup needs these drivers to see the VirtIO SCSI disk and VirtIO NIC.
set -euo pipefail
ISO=/var/lib/vz/template/iso/virtio-win.iso
# Official virtio-win project location; the 'stable' path redirects to the current build.
URL=https://fedorapeople.org/groups/virt/virtio-win/direct-downloads/stable-virtio/virtio-win.iso
if [ -f "$ISO" ]; then
echo "virtio-win ISO already present: $ISO"
exit 0
fi
echo "Downloading $URL"
# Download to a temp file in the same dir and move into place only on success, so
# an interrupted download never leaves a truncated ISO that a later run would treat
# as 'already present' and skip (a corrupt driver CD then fails Windows setup with
# no obvious cause).
tmp="$(mktemp "${ISO}.XXXXXX")"
if curl -fSL -o "$tmp" "$URL"; then
mv -f "$tmp" "$ISO"
echo "Fetched: $ISO"
else
rm -f "$tmp"
echo "Download failed -- nothing written to $ISO." >&2
exit 1
fi
Provision the VM
Build the VM shell and attach both ISOs. Idempotent — if VMID 9002 already exists and is jmp-aasted it exits without changes (and it refuses if 9002 is some other VM). Pass the exact Windows ISO name via WINISO; for Windows 11 add OS=win11 (which adds the vTPM). The assignments go inside the quotes so they are set on pve01 — placed before ssh they would apply to your local shell and never reach the remote script (the script then aborts with WINISO: … set WINISO=…):
# Windows Server 2025 (default):
ssh root@192.168.1.4 'WINISO=local:iso/windows-server-2025.iso bash -s' < provision-jmp-aasted.sh
# Windows 11:
ssh root@192.168.1.4 'OS=win11 WINISO=local:iso/windows-11-25h2.iso bash -s' < provision-jmp-aasted.sh
#!/usr/bin/env bash
# Create the jmp-aasted Windows jumphost VM on pve01. Run as root on pve01.
# Supports Windows Server 2025 (default) or Windows 11 (OS=win11, which adds the
# vTPM that Windows 11 requires). This builds the VM shell and attaches the
# installer media; the Windows install itself is INTERACTIVE (Windows can't be
# cloud-init'd like the Debian VMs) -- see jmp-aasted.md for the in-guest steps.
#
# The Windows ISO must already be uploaded to a store; name it via WINISO. Put
# the WINISO/OS assignments INSIDE the ssh quotes so they are set on pve01 --
# placed before `ssh` they apply to your local shell and never reach this script:
#
# # Windows Server 2025 (default):
# ssh root@192.168.1.4 'WINISO=local:iso/<windows-server-2025>.iso bash -s' < provision-jmp-aasted.sh
# # Windows 11 (adds a vTPM):
# ssh root@192.168.1.4 'OS=win11 WINISO=local:iso/<windows-11>.iso bash -s' < provision-jmp-aasted.sh
#
# Overridable via env: OS (server|win11), VMID (9002), NAME (jmp-aasted), IP
# (192.168.1.17), CIDR (24), GW (192.168.1.1), CORES (4), MEM (8192 MiB), DISK
# (80 GiB), BRIDGE (vmbr0), STORAGE (local-lvm), VIRTIO (local:iso/virtio-win.iso).
set -euo pipefail
if [ "$(id -u)" -ne 0 ]; then echo "Run as root on pve01." >&2; exit 1; fi
OS="${OS:-server}"
case "$OS" in server|win11) ;; *) echo "OS must be 'server' or 'win11' (got '$OS')." >&2; exit 1;; esac
VMID="${VMID:-9002}"
NAME="${NAME:-jmp-aasted}"
IP="${IP:-192.168.1.17}"; CIDR="${CIDR:-24}"; GW="${GW:-192.168.1.1}"
CORES="${CORES:-4}"; MEM="${MEM:-8192}"; DISK="${DISK:-80}"
BRIDGE="${BRIDGE:-vmbr0}"; STORAGE="${STORAGE:-local-lvm}"
WINISO="${WINISO:?set WINISO=local:iso/<windows-iso> -- upload the Windows Server or Windows 11 ISO to a store first}"
VIRTIO="${VIRTIO:-local:iso/virtio-win.iso}"
# Idempotent, but only for OUR jumphost: if VMID exists and is jmp-aasted, do
# nothing and succeed; if it is some OTHER VM, refuse rather than falsely report
# success or risk touching an unrelated guest.
if qm status "$VMID" >/dev/null 2>&1; then
if qm config "$VMID" 2>/dev/null | grep -qx "name: ${NAME}"; then
echo "VM $VMID ($NAME) already exists -- nothing to do."; exit 0
fi
echo "VMID $VMID already exists but is not '$NAME' -- refusing to touch it. Remove it or pick another VMID." >&2
exit 1
fi
# Fail early if the install media isn't actually present. Resolve each volume's
# real path with `pvesm path` so this honours the WINISO/VIRTIO overrides and
# works on any storage -- not just the default `local` store.
winfile="$(pvesm path "$WINISO" 2>/dev/null || true)"
[ -n "$winfile" ] && [ -f "$winfile" ] || { echo "Windows ISO not found: $WINISO -- upload it to a store, then set WINISO." >&2; exit 1; }
virtiofile="$(pvesm path "$VIRTIO" 2>/dev/null || true)"
[ -n "$virtiofile" ] && [ -f "$virtiofile" ] || { echo "virtio-win ISO not found: $VIRTIO -- run fetch-virtio-win.sh first." >&2; exit 1; }
# q35 + OVMF (UEFI). ostype win11 is Proxmox's id for BOTH Windows 11 and Windows
# Server 2022-2025. balloon 0 pins a fixed 8 GiB (predictable). pre-enrolled-keys
# loads Microsoft's Secure Boot keys on the EFI disk so a stock retail ISO boots.
# A vTPM is added below only for Windows 11 (Server does not require one).
qm create "$VMID" --name "$NAME" --ostype win11 --machine q35 --bios ovmf \
--cpu host --cores "$CORES" --memory "$MEM" --balloon 0 \
--scsihw virtio-scsi-single --net0 "virtio,bridge=$BRIDGE" \
--agent enabled=1 --onboot 1
# EFI disk on a persistent pool (never volatile, or the vars are lost on save).
qm set "$VMID" --efidisk0 "${STORAGE}:0,efitype=4m,pre-enrolled-keys=1"
# Windows 11 requires a TPM 2.0; Windows Server does not -- add a vTPM only for win11.
if [ "$OS" = win11 ]; then
qm set "$VMID" --tpmstate0 "${STORAGE}:0,version=v2.0"
fi
# System disk on VirtIO SCSI -- the installer needs the vioscsi driver (virtio CD) to see it.
qm set "$VMID" --scsi0 "${STORAGE}:${DISK},cache=writeback,discard=on,iothread=1,ssd=1"
# Two CD-ROMs: the Windows installer, and the virtio-win drivers.
qm set "$VMID" --ide0 "${WINISO},media=cdrom"
qm set "$VMID" --ide2 "${VIRTIO},media=cdrom"
# Boot the Windows ISO first; the disk takes over once Windows is installed.
qm set "$VMID" --boot "order=ide0;scsi0"
qm start "$VMID"
echo "VM $VMID ($NAME) created and started."
echo "Open the Proxmox console (noVNC) and run the Windows install (OS=$OS) -- see jmp-aasted.md."
echo "Target static IP (set inside Windows, post-install): ${IP}/${CIDR} gw ${GW}."
Install Windows (interactive)
Open the VM's console in Proxmox (pve01 → 9002 → Console, noVNC) and run setup:
- Boot the Windows ISO. For Server, choose the Desktop Experience edition (not Server Core) so there is a GUI; for Windows 11, use Pro or Enterprise (not Home — Home cannot host RDP).
- At "Where do you want to install?" the installer shows no disk (the VirtIO controller is unknown to Windows). Click Load driver → Browse, open the virtio-win CD, and select the folder for your OS —
vioscsi\w11\amd64for Windows 11,vioscsi\2k25\amd64for Server 2025 (use the nearest folder if absent, e.g.2k22). The 80 GiB disk then appears; select it and continue. - Set the local Administrator password when prompted and finish the install.
On the mid-install reboot, let it boot from disk
Windows setup reboots partway through, and the install ISO is still first in the boot order. If you see "Press any key to boot from CD/DVD…", do not press a key — let it time out so it boots from the disk and setup continues. (Pressing a key restarts setup from the beginning.) Detaching the media after install (below) removes the prompt for good.
If the NIC has no driver after first boot
Load NetKVM\w11\amd64 (Windows 11) or NetKVM\2k25\amd64 (Server) from the same virtio CD via Device Manager. Installing the guest tools (next) does this for you.
Post-install (in-guest checklist)
Not reproducible from the repo — do these once inside Windows:
- Guest tools + agent. From the virtio-win CD run
virtio-win-gt-x64.msi(all VirtIO drivers + balloon) and the QEMU guest agent (guest-agent\qemu-ga-x86_64.msi), or the combinedvirtio-win-guest-tools.exe. The guest agent is what lets Proxmox report the VM's IP and shut it down gracefully. - Static IP. Set the NIC to
192.168.1.17/24, gateway192.168.1.1, DNS192.168.1.60. -
Enable RDP with NLA. Server Manager → Local Server → Remote Desktop → Enable, keeping "Allow connections only from computers running Remote Desktop with Network Level Authentication." Equivalent PowerShell:
Set-ItemProperty 'HKLM:\System\CurrentControlSet\Control\Terminal Server' -Name fDenyTSConnections -Value 0 Set-ItemProperty 'HKLM:\System\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp' -Name UserAuthentication -Value 1 Enable-NetFirewallRule -DisplayGroup 'Remote Desktop' -
Rename + update. Set the computer name to
jmp-aasted, run Windows Update fully, reboot.
Reach it over the LAN — don't port-forward 3389 to the internet
RDP is a constant brute-force/exploit target. Keep NLA on, and reach the jumphost from the LAN (or over your existing VPN). Do not forward 3389 from the router to 192.168.1.17.
Sign in with a Microsoft account (optional)
By default you RDP in as the local Administrator. To use a personal Microsoft account instead, do the one-time setup below at the Proxmox console (noVNC), signed in as Administrator — two Windows 11 defaults otherwise block it.
Two Windows 11 blockers for Microsoft-account RDP
- Windows 11 defaults Microsoft accounts to Windows Hello sign-in only, which disables the password — and RDP needs the password. Turn that toggle off (step 2).
- A PIN is not the account password. Windows Hello (PIN/biometric) is device-local and cannot travel over RDP; authenticate with the actual Microsoft-account password.
- Add the account. Settings → Accounts → Other users → Add account → enter the Microsoft-account email and sign in. (The VM needs internet to validate it the first time.)
- Allow password sign-in. Settings → Accounts → Sign-in options → Additional settings → turn off "For improved security, only allow Windows Hello sign-in for Microsoft accounts on this device." Without this, RDP with the account password is refused.
-
Permit Remote Desktop. Make the account an administrator, or add it to Remote Desktop Users:
Add-LocalGroupMember -Group "Remote Desktop Users" -Member "MicrosoftAccount\you@example.com" -
Sign in once at the console with the account (email + password). NLA pre-authenticates the cached credential, so this first interactive logon must happen before RDP works — it provisions the profile and caches the credential.
Then RDP to 192.168.1.17 with:
- Username
MicrosoftAccount\you@example.com— theMicrosoftAccount\prefix is the reliable form under NLA (the bare email sometimes works too) - Password the Microsoft-account password — not the PIN
Work/school (Entra ID) accounts need an Entra join
This jumphost is a standalone workgroup machine, so the steps above are for a personal Microsoft account. A work/school Entra ID account cannot sign in over RDP unless the VM is first Entra-joined — a separate setup not covered here.
Verify
From pve01, the guest agent should report the static IP once the tools are installed:
ssh root@192.168.1.4 'qm agent 9002 network-get-interfaces' | grep 192.168.1.17
Agent installed in Windows but Proxmox says it's not running?
The agent talks to Proxmox over a VirtIO serial channel, so the QEMU-GA service running is not enough — the VirtIO Serial driver (vioser) must also be installed, or the service has nothing to talk over. Symptom: qm agent 9002 ping errors, and Device Manager shows a yellow-bang "PCI Simple Communications Controller" instead of "VirtIO Serial Driver". Fix by installing vioserial\w11\amd64 from the virtio CD, or just run virtio-win-guest-tools.exe (installs every driver and the agent) — the plain qemu-ga MSI installs only the service, not the serial driver, which is the usual cause. A guest reboot won't add the host-side channel; if agent: enabled=1 was set after the VM was already started, power-cycle it with qm stop 9002 && qm start 9002.
Then RDP to 192.168.1.17 from a LAN machine and log in as Administrator. A successful NLA login (credentials prompted before the session opens) confirms RDP + NLA are working.
Detach the install media
Once Windows is installed and updated, drop both CD-ROMs so they don't linger:
ssh root@192.168.1.4 'qm set 9002 --ide0 none --ide2 none'
Rebuild / remove
There is no cloud-init to re-seed, so a rebuild means re-running the interactive install. To tear it down: ssh root@192.168.1.4 'qm stop 9002 && qm destroy 9002' (this deletes the VM disk).