Windows 11 VM Setup — Operator Guide (building the golden)
The QEMU Guest Agent is REQUIRED (not optional)
Install qemu-guest-agent in the golden before sysprep/export, and verify it
after: KubeVirt injects the Project's SSH public key through the agent and reads
guestOSInfo from it. A golden without a running agent still boots and serves
RDP, but Project key injection silently does nothing — this is exactly the
defect the 2026-07-31 gate run found in the previously published golden.
In the commands below, replace <backing-namespace> with the generated {organization}-{project} namespace for the operator Project used to build and validate the image.
Verify before publishing (must print AgentConnected ... True):
./hack/windows/automated/validate-clone.sh <backing-namespace>
# PHASE 1 proves Windows booted (RDP), PHASE 2 proves the agent is live.
Licensing model — BYOL (bring your own license)
Kube-DC ships the Windows golden image as an evaluation-based starter build: it is produced from the Microsoft Windows 11 Enterprise Evaluation ISO and is intended to let a Project user start working immediately (instant clone, drivers, guest-agent and SSH/RDP pre-configured).
The platform does not provide a Windows license. The customer is responsible for licensing every Windows VM they run:
- Activate with your own key inside the VM (
slmgr /ipk <product-key>thenslmgr /ato), or point the VM at your organisation's KMS (slmgr /skms kms.example.com+slmgr /ato). - Evaluation builds are time-limited by Microsoft (90 days). Assign a license before the evaluation window ends; an expired evaluation shuts down hourly.
- Operators: never publish a pre-activated or volume-licensed image as the shared golden — the shared image must remain the neutral evaluation base so each customer applies their own entitlement.
- The golden and installer media live in the anonymously readable
cdi-os-imagesbucket (CDI imports without credentials). That is an accepted, documented posture because the media is evaluation-grade and key-free — see "Windows media in the public image bucket" in OS-image operations. It is exactly why the rule above is absolute: publishing an activated or licensed image there would turn an accepted exposure into a redistribution problem.
This is the one-time operator task that produces the Windows 11 golden image for a
cluster. Once the golden is built and published to the cluster's S3 OS-image mirror
(s3.<your-domain>/cdi-os-images/private/windows/11/latest/windows11-x64-golden.qcow2), it
appears in the Console UI Operating System dropdown as Windows 11 Enterprise (Golden
Image), and end users create Windows VMs from the console — see
Creating a VM. Users do not run this guide.
The finished golden is ~75 GB. Each Windows VM clones it, so a user's project needs ~75–80 GB of free storage quota to launch one.
Overview
This guide provides two deployment methods:
- Golden Image Deployment (Recommended) - Deploy pre-configured VMs in 5-10 minutes
- Fresh Installation - Create custom Windows installations with full control
Prerequisites
- KubeVirt and CDI installed and running
- Multus CNI with OVN network configured
- StorageClass
local-pathavailable - Ingress controller for HTTP access to ISOs
Step 1: Create ISO Hosting Environment
1.1 Create Dedicated Namespace
# hack/windows/iso-namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
name: iso
1.2 Create Storage for ISOs
# hack/windows/iso-storage-pvc-iso-ns.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: iso-storage
namespace: iso
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 30Gi # Increased for Windows ISO + golden images
storageClassName: local-path
1.3 HTTP Server for ISOs
# hack/windows/nginx-iso-server-iso-ns.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-iso-server
namespace: iso
spec:
replicas: 1
selector:
matchLabels:
app: nginx-iso-server
template:
metadata:
labels:
app: nginx-iso-server
spec:
containers:
- name: nginx
image: nginx:1.25-alpine
ports:
- containerPort: 80
volumeMounts:
- name: iso-storage
mountPath: /usr/share/nginx/html
- name: nginx-config
mountPath: /etc/nginx/conf.d
volumes:
- name: iso-storage
persistentVolumeClaim:
claimName: iso-storage
- name: nginx-config
configMap:
name: nginx-iso-config
---
apiVersion: v1
kind: ConfigMap
metadata:
name: nginx-iso-config
namespace: iso
data:
default.conf: |
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
location / {
autoindex on;
autoindex_exact_size off;
autoindex_localtime on;
}
location ~* \.(iso|img|qcow2)$ {
add_header Content-Type application/octet-stream;
add_header Cache-Control "public, max-age=3600";
}
}
---
apiVersion: v1
kind: Service
metadata:
# NB: the Service is `iso-server` (NOT nginx-iso-server) — the Ingress and the
# export Job both address iso-server.iso.svc.cluster.local. Keep in sync with
# hack/windows/nginx-iso-server-iso-ns.yaml.
name: iso-server
namespace: iso
labels:
app: iso-server
spec:
selector:
app: iso-server
ports:
- port: 80
targetPort: 80
1.4 Ingress Configuration with TLS
# hack/windows/iso-ingress-iso-ns.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: iso-server-ingress
namespace: iso
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod-http
nginx.ingress.kubernetes.io/proxy-body-size: "0"
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-buffering: "off"
spec:
ingressClassName: nginx
tls:
- hosts:
- iso.example.com
secretName: iso-server-tls
rules:
- host: iso.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: iso-server
port:
number: 80
1.5 Deploy Infrastructure
# Deploy all infrastructure components
kubectl apply -f hack/windows/iso-namespace.yaml
kubectl apply -f hack/windows/iso-storage-pvc-iso-ns.yaml
kubectl apply -f hack/windows/nginx-iso-server-iso-ns.yaml
kubectl apply -f hack/windows/iso-ingress-iso-ns.yaml
# Verify deployment
kubectl get pods -n iso
kubectl get ingress -n iso
Step 2: Download and Upload Windows ISO
2.1 Download Windows 11 Enterprise ISO
- Visit: https://www.microsoft.com/en-us/evalcenter/download-windows-11-enterprise
- Select: ISO – Enterprise download 64-bit edition (90-day evaluation)
- Download the ISO file (approximately 5.4GB)
2.2 Download VirtIO Drivers
# Download latest VirtIO drivers
wget https://fedorapeople.org/groups/virt/virtio-win/direct-downloads/stable-virtio/virtio-win.iso
2.3 Upload Files to Cluster
# Create temporary upload pod
kubectl apply -f hack/windows/iso-upload-pod.yaml
# Wait for pod to be ready
kubectl wait --for=condition=Ready pod/iso-upload-pod -n iso --timeout=60s
# Upload Windows 11 ISO (replace with your actual filename)
kubectl cp ~/Win11_24H2_EnglishInternational_x64.iso iso/iso-upload-pod:/storage/win11-x64.iso
# Upload VirtIO drivers
kubectl cp ~/virtio-win.iso iso/iso-upload-pod:/storage/virtio-win.iso
# Upload OpenSSH installation script
kubectl cp hack/windows/install-openssh-windows.ps1 iso/iso-upload-pod:/storage/install-openssh-windows.ps1
# Clean up upload pod
kubectl delete pod iso-upload-pod -n iso --wait=true
# Verify files are accessible
curl -I https://iso.example.com/win11-x64.iso
curl -I https://iso.example.com/virtio-win.iso
Step 3: Fresh Windows Installation
3.1 Deploy Installation VM
Use the complete VM manifest that includes all required DataVolumes:
# Deploy Windows VM with installation ISOs
kubectl apply -f hack/windows/windows11-vm.yaml
# Monitor DataVolume download progress
kubectl get dv -n <backing-namespace>
# Check VM status
kubectl get vm,vmi -n <backing-namespace> | grep windows11
3.2 Windows Installation Process
# Access VM console via VNC
virtctl vnc windows11-vm -n <backing-namespace>
# Or use VNC proxy
virtctl vnc windows11-vm -n <backing-namespace> --proxy-only --port 5900
# Then connect VNC client to localhost:5900
Installation Steps:
-
Boot from Windows ISO: VM boots from Windows installer (bootOrder: 1)
-
Load VirtIO Drivers:
- When prompted for disk drivers, click "Load driver"
- Browse to VirtIO drivers CDROM
- Navigate to
/amd64/w11/folder - Install VirtIO SCSI controller drivers (for disk access)
- DO NOT install network drivers yet (to create local account)
-
Install Windows:
- Select the 60GB VirtIO disk for installation
- Complete Windows 11 setup with local account
-
Post-Installation:
- Install remaining VirtIO drivers from CDROM (network, balloon, RNG)
- Install QEMU Guest Agent from VirtIO drivers CDROM
- Run Windows Updates
3.3 Configure SSH and RDP
After Windows installation, configure SSH and RDP access:
# Method 1: Download and run script
Invoke-WebRequest -Uri "https://iso.example.com/install-openssh-windows.ps1" -OutFile "install-openssh-windows.ps1"
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
.\install-openssh-windows.ps1
# Method 2: Direct execution (bypass execution policy)
PowerShell -ExecutionPolicy Bypass -Command "Invoke-Expression (Invoke-WebRequest -Uri 'https://iso.example.com/install-openssh-windows.ps1').Content"
Script Features:
- ✅ Installs OpenSSH Server using Windows capabilities
- ✅ Configures SSH service for automatic startup
- ✅ Opens SSH port 22 in Windows Firewall (all network profiles)
- ✅ Enables Remote Desktop (port 3389)
- ✅ Enables ICMP ping (IPv4 and IPv6)
- ✅ Provides detailed verification and status reporting
Step 4: Create Golden Image
4.1 Prepare VM for Golden Image
# 1. Inside Windows VM, run Sysprep — REQUIRED, not optional.
# Microsoft requires generalization before an installation is cloned to other
# machines: without it every cloned VM inherits the same machine SID/identity
# and the image is unsupported (codex review HIGH).
# Navigate to: C:\Windows\System32\Sysprep\sysprep.exe
# Options: Generalize, Enter System Out-of-Box Experience (OOBE), Shutdown
# 2. Stop the source VM (CRITICAL for export)
kubectl patch vm windows11-vm -n <backing-namespace> --type merge -p '{"spec":{"runStrategy":"Halted"}}'
kubectl wait --for=delete vmi/windows11-vm -n <backing-namespace> --timeout=300s
4.2 Export to QCOW2 Golden Image
# hack/windows/export-golden-image.yaml
apiVersion: v1
kind: Pod
metadata:
name: export-golden-image
namespace: <backing-namespace>
spec:
restartPolicy: Never
containers:
- name: exporter
image: ubuntu:22.04
command: ["sh", "-c"]
args:
- |
set -e
apt-get update && apt-get install -y qemu-utils curl
cd /pvc
echo "=== Disk Information ==="
DISK_SIZE_BYTES=$(qemu-img info --output=json disk.img | grep '"virtual-size"' | cut -d: -f2 | tr -d ' ,')
DISK_SIZE_GB=$((DISK_SIZE_BYTES / 1024 / 1024 / 1024))
echo "Source disk: ${DISK_SIZE_GB}GB (${DISK_SIZE_BYTES} bytes)"
echo "=== Converting to compressed QCOW2 ==="
qemu-img convert -p -O qcow2 -c disk.img windows11-x64-golden.qcow2
echo "=== Final Golden Image ==="
ls -lh windows11-x64-golden.qcow2
qemu-img info windows11-x64-golden.qcow2
echo "=== Upload to ISO server ==="
# Upload to ISO server storage
curl -T windows11-x64-golden.qcow2 http://nginx-iso-server.iso.svc.cluster.local/windows11-x64-golden.qcow2 || echo "Upload failed, manual copy required"
echo "=== Export completed, sleeping for inspection ==="
sleep 3600
volumeMounts:
- name: source-disk
mountPath: /pvc
volumes:
- name: source-disk
persistentVolumeClaim:
claimName: windows11-disk
4.3 Export Process
# Export golden image
kubectl apply -f hack/windows/export-golden-image.yaml
kubectl wait --for=condition=Ready pod/export-golden-image -n <backing-namespace> --timeout=120s
# Monitor export progress
kubectl logs -n <backing-namespace> export-golden-image -f
# Manual copy to ISO server (if curl upload fails)
kubectl cp <backing-namespace>/export-golden-image:/pvc/windows11-x64-golden.qcow2 /tmp/
kubectl cp /tmp/windows11-x64-golden.qcow2 iso/iso-upload-pod:/storage/
# Verify golden image is available
curl -I https://iso.example.com/windows11-x64-golden.qcow2
# Clean up export pod
kubectl delete pod export-golden-image -n <backing-namespace> --wait=true
Step 5: Deploy from Golden Image
5.1 Golden Image Deployment (Recommended)
# Deploy VM from golden image
kubectl apply -f hack/windows/win11-x64.yaml
# Create SSH key secret for key injection
kubectl create secret generic authorized-keys-default \
--from-file=key1=~/.ssh/id_rsa.pub \
-n <backing-namespace>
# Monitor deployment
kubectl get vm,vmi,dv -n <backing-namespace> | grep win11-x64
# Get VM IP when ready
kubectl get vmi win11-x64 -n <backing-namespace> -o jsonpath='{.status.interfaces[0].ipAddress}'
# SSH to VM (once guest agent is ready)
ssh kube-dc@<vm-ip>
5.2 Golden Image Benefits
| Aspect | Golden Image | Fresh Install |
|---|---|---|
| Deployment Time | 5-10 minutes | 30+ minutes |
| Download Size | 21.3GB compressed | 5.4GB + drivers |
| Configuration | Pre-configured | Manual setup required |
| SSH/RDP | Ready immediately | Requires script execution |
| VirtIO Drivers | Pre-installed | Manual installation |
| Use Case | Production deployment | Custom configurations |
Step 6: Troubleshooting
6.1 Common Issues
DataVolume stuck in ImportScheduled:
# Check CDI importer pods
kubectl get pods -n cdi
kubectl logs -n cdi <importer-pod>
# Check storage provisioner
kubectl get pods -n local-path-storage
VM won't start - CPU resources:
# Check node resources
kubectl describe nodes | grep -A 10 "Allocated resources"
# Reduce VM CPU if needed
kubectl patch vm <vm-name> -n <namespace> --type merge -p '{"spec":{"template":{"spec":{"domain":{"cpu":{"cores":2}}}}}}'
SSH not working:
# Check guest agent connection
kubectl describe vmi <vm-name> -n <namespace> | grep -i agent
# Test network connectivity
kubectl run test-pod --image=nicolaka/netshoot --rm -it -- ping <vm-ip>
kubectl run test-pod --image=nicolaka/netshoot --rm -it -- nc -zv <vm-ip> 22
Storage issues with local-path:
# Check local-path provisioner
kubectl get pods -n local-path-storage
kubectl logs -n local-path-storage <provisioner-pod>
# Note: local-path doesn't support Block mode, use Filesystem mode
6.2 Verification Commands
# Check all Windows VMs
kubectl get vm -A | grep -i win
# Check DataVolume progress
kubectl get dv -n <namespace>
# Access VM console
virtctl vnc <vm-name> -n <namespace>
# Check VM resource usage
kubectl top pods -n <namespace> | grep virt-launcher
Required Manifests Summary
Infrastructure (Step 1):
hack/windows/iso-namespace.yaml- ISO namespacehack/windows/iso-storage-pvc-iso-ns.yaml- Storage for ISOshack/windows/nginx-iso-server-iso-ns.yaml- HTTP serverhack/windows/iso-ingress-iso-ns.yaml- Ingress configuration
Utilities:
hack/windows/iso-upload-pod.yaml- Upload files to clusterhack/windows/install-openssh-windows.ps1- SSH/RDP configuration script
VM Deployment:
hack/windows/windows11-vm.yaml- Fresh installation VMhack/windows/win11-x64.yaml- Golden image deployment
Golden Image Creation:
hack/windows/export-golden-image.yaml- Export VM to QCOW2hack/windows/windows11-enterprise-golden-datasource.yaml- DataSource for cloning
Optional:
hack/windows/windows11-from-datasource.yaml- Deploy from DataSource (same namespace)hack/windows/pvc-init-windows11-disk.yaml- Fix disk size issues if needed
Available Resources
Once deployed, the following resources are available:
- Windows 11 ISO:
https://iso.example.com/win11-x64.iso(5.4GB) - VirtIO Drivers:
https://iso.example.com/virtio-win.iso(700MB) - SSH Script:
https://iso.example.com/install-openssh-windows.ps1(5KB) - Golden Image:
https://iso.example.com/windows11-x64-golden.qcow2(21.3GB)
Security Considerations
- SSH Keys: Use KubeVirt accessCredentials for secure key injection
- Network Policies: Implement Kubernetes network policies for VM isolation
- Sysprep: Run before creating golden images to avoid SID conflicts
- Firewall: Script configures Windows Firewall appropriately for SSH, RDP, and ICMP
✅ Complete Windows 11 VM infrastructure with golden image support ready for production use!