PSCloudInit.ps1 is a minimal PowerShell implementation of cloud-init functionality for Windows VMs running on Proxmox VE.
It provides basic support for the configuration parameters that Proxmox's cloud-init feature generates, focusing specifically on network configuration and SSH key management.
Important: This is not a full cloud-init implementation. It is designed to work specifically with Proxmox VE's cloud-init drive (NoCloud format) and only supports a minimal subset of cloud-init features.
This script enables Windows VMs created with new-vm-windows.sh to:
- Automatically configure static IPv4 and IPv6 addresses from Proxmox cloud-init settings
- Enable DHCP, DHCPv6, and IPv6 SLAAC for dynamic addressing
- Support multiple IP addresses (IPv4 and IPv6) on the same interface
- Set up DNS servers (IPv4 and IPv6) and search domains
- Install SSH public keys for administrator access
- Handle both on-link and off-link gateway configurations
- Run safely multiple times (idempotent)
The script is executed during Windows installation via SetupComplete.cmd, which runs automatically at the end of the Windows setup process (OOBE phase). The execution flow is:
new-vm-windows.shcreates an ISO with the script- The ISO is attached to the VM as drive
E:\ - Windows installation includes
SetupComplete.cmdinC:\Windows\Setup\Scripts\ - After installation completes, Windows automatically runs
SetupComplete.cmd SetupComplete.cmdexecutespowershell.exe -ExecutionPolicy Bypass -File E:\PSCloudInit.ps1
The script looks for a volume labeled cidata (Proxmox's NoCloud format):
$cidata = Get-Volume -FileSystemLabel "cidata"By default, it waits up to 5 seconds for the drive to appear.
The script reads three files from the root of the cloud-init drive:
- YAML format containing user configuration
- Used to extract:
- FQDN information (format:
fqdn: hostname.domain.com) - SSH authorized keys (under
ssh_authorized_keys:section)
- FQDN information (format:
- The domain suffix (everything after the first dot in FQDN) becomes the DNS search domain
Example:
#cloud-config
hostname: tst241
manage_etc_hosts: true
fqdn: tst241.example.com
password: asdf
ssh_authorized_keys:
- ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABQ...
- ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAA...
chpasswd:
expire: false
users:
- default
package_upgrade: true- YAML format containing instance metadata
- Key fields used:
instance_id: Instance identifierhostname: VM hostname
Example:
instance-id: tst241
hostname: tst241- YAML format (cloud-init network config version 1)
- Contains interface definitions with MAC addresses for reliable matching
- Includes static IP configuration, DHCP configuration, nameservers, and search domains
Example (IPv4 and IPv6):
version: 1
config:
- type: physical
name: eth0
mac_address: 'bc:24:11:8f:14:44'
subnets:
- type: dhcp # IPv4 DHCP
- type: dhcp6 # IPv6 DHCPv6
- type: physical
name: eth1
mac_address: 'bc:24:11:14:af:9a'
subnets:
- type: static
address: '192.168.10.241'
netmask: '255.255.255.0'
gateway: '192.168.10.1'
- type: static # IPv6 static address
address: '2001:db8::1'
netmask: '64'
- type: physical
name: eth2
mac_address: 'bc:24:11:c9:b8:fc'
subnets:
- type: ipv6_slaac # IPv6 auto-configuration
- type: physical
name: eth3
mac_address: 'bc:24:11:16:64:2b'
subnets:
- type: static
address: '1.1.1.1'
netmask: '255.240.0.0'
- type: nameserver
address:
- '192.168.10.1'
- '2001:4860:4860::8888' # IPv6 DNS (Google Public DNS)
search:
- 'example.com'When SSH public keys are found in user-data under ssh_authorized_keys::
- Creates
C:\ProgramData\ssh\directory if needed - Writes all public keys to
C:\ProgramData\ssh\administrators_authorized_keys - Sets proper ACL permissions (only SYSTEM and Administrators have access)
- Keys are written in OpenSSH format, one per line
Note: This assumes that the OpenSSH Server is -- or will be -- installed.
- Windows Server 2019 and 2022 include OpenSSH as an optional feature.
- Windows Server 2025 has OpenSSH installed by default.
The script uses MAC address-based matching for reliable interface identification:
- Each interface in
network-configincludes amac_addressfield - Windows network adapters are matched by comparing MAC addresses
- This approach is reliable even with dozens of interfaces and doesn't depend on adapter enumeration order
$adapter = $allAdapters | Where-Object { $_.MacAddress -eq $iface.MacAddress }Example matching process:
- Parse
network-configand extract interface withmac_address: 'bc:24:11:14:af:9a' - Find Windows adapter with matching MAC address
BC-24-11-14-AF-9A - Apply configuration to that specific adapter
This ensures configuration is always applied to the correct physical interface, regardless of:
- Adapter naming in Windows (Ethernet, Ethernet 2, etc.)
- Order in which adapters are detected by Windows
- Number of network interfaces in the VM
The script supports multiple subnet configurations per interface, including:
- static: Static IPv4 or IPv6 addresses with optional gateway
- dhcp: Dynamic IPv4 address assignment via DHCP
- dhcp6: Dynamic IPv6 address assignment via DHCPv6
- ipv6_slaac: IPv6 Stateless Address Autoconfiguration (SLAAC)
For each subnet configuration, the script:
- Detects if the address is IPv4 or IPv6 automatically
- Checks if the IP is already configured (idempotency)
- Converts netmask to CIDR prefix length (for IPv4)
- Configures the IP address only if not already present
- Checks for existing routes before adding new ones
The script handles two scenarios:
On-link Gateway (gateway is in the same subnet):
New-NetIPAddress -IPAddress $ip -PrefixLength $prefix -DefaultGateway $gatewayOff-link Gateway (gateway is NOT in the same subnet):
# Add IP without gateway
New-NetIPAddress -IPAddress $ip -PrefixLength $prefix
# Add host route to gateway
New-NetRoute -DestinationPrefix "$gateway/32" -NextHop "0.0.0.0"
# Add default route via gateway
New-NetRoute -DestinationPrefix "0.0.0.0/0" -NextHop $gatewayThis off-link gateway handling is crucial for VMs with public IP addresses where the gateway is outside the assigned subnet.
- DNS servers are configured per interface using
Set-DnsClientServerAddress - DNS search domain is applied to all interfaces that have network configuration
- The search domain is extracted from the FQDN in
user_data - This ensures single-interface VMs and multi-interface VMs both get proper DNS resolution
- The search domain is extracted from the FQDN in
The script uses a fail-fast approach with $ErrorActionPreference = "Stop". Any critical error during execution will cause the script to terminate immediately, ensuring that partial or incorrect configurations are not applied silently.
This means:
- Network configuration errors will halt execution
- SSH key installation errors will stop the script
- DNS configuration failures will prevent completion
The script is designed to be idempotent and can be safely run multiple times. It checks the current configuration before making changes:
- IP Addresses: Checks if the IP address is already configured before attempting to add it
- Routes: Verifies if routes exist before creating new ones (host routes and default routes)
- DNS: Compares current DNS server configuration with desired state
- DNS Search Domain: Only updates if different from current value
- DHCP: Checks if DHCP is already enabled before enabling it
This makes the script safe to use for both initial configuration and subsequent updates without causing errors or duplicate configurations.
All output is logged to C:\Windows\Panther\PSCloudInit.log using PowerShell's Start-Transcript and Stop-Transcript cmdlets.
Issue: The netmask-to-prefix conversion counts all '1' bits but doesn't validate that they are contiguous:
for ($b = 0; $b -lt 32; $b++) {
if (($maskInt -shr $b) -band 1) { $prefix++ }
}Impact: An invalid netmask like 255.0.255.0 would be incorrectly counted as /16 instead of being rejected.
Workaround: Only use valid netmasks (contiguous '1' bits from left to right).
Future Fix: Validate that netmask bits are contiguous before calculating prefix length.
Issue: The off-link gateway configuration adds a host route using 0.0.0.0 as next hop:
New-NetRoute -DestinationPrefix "$gateway/32" -NextHop "0.0.0.0"Impact: This works in most cases but may not be the most correct approach. Some network configurations might not accept this.
Workaround: The current implementation works for typical Proxmox hosting environments with public IPs.
Future Fix: Research and implement the most RFC-compliant method for configuring off-link gateways on Windows.
Issue: Only one default gateway is configured per interface.
Impact: Cannot configure multiple default routes or policy-based routing.
Workaround: Configure additional routes manually after VM creation.
Future Fix: Parse and configure multiple routes if provided in cloud-init data.
Issue: The script includes a basic YAML parser (ConvertFrom-SimpleYaml) that handles the simple key-value format used by Proxmox's user-data and meta-data files. However, it's not a full YAML parser.
Impact:
- Works well for Proxmox's simple YAML format
- The
network-configfile uses a more complex nested structure and is parsed with custom logic specific to the cloud-init network config v1 format - May not handle all possible YAML constructs
Workaround: The current implementation is specifically designed for Proxmox's cloud-init format and handles the structures that Proxmox generates.
Future Fix: Consider using a full-featured YAML parser module if more complex YAML processing is needed.
- Windows Server 2019+ or Windows 10/11 (with OpenSSH Server installed if using SSH keys)
- VM created with Proxmox cloud-init drive configured
- Script must be executed during or after Windows setup
The script supports the following parameters:
-
-Install: Installs the script as a Windows Scheduled Task that runs at system startup- Copies the script to
C:\Windows\Setup\Scripts\ - Creates a scheduled task named "PSCloudInit-Startup"
- Task runs at startup with SYSTEM privileges
- Copies the script to
-
-Verbose: Enable detailed diagnostic output for troubleshooting- Shows each decision branch taken during execution
- Displays parsing details and configuration validation
- Useful for debugging configuration issues
-
-WhatIf: Preview mode - shows what actions would be taken without making changes- Displays proposed network configurations
- Shows SSH keys that would be installed
- Safe for testing before actual execution
To install the script to run automatically at Windows startup:
# Must be run with Administrator privileges
powershell.exe -ExecutionPolicy Bypass -File E:\PSCloudInit.ps1 -InstallThis will:
- Copy the script to
C:\Windows\Setup\Scripts\PSCloudInit.ps1 - Create a scheduled task that runs at system startup
- Configure the task to run with SYSTEM privileges
- Run the configuration process if a
cidatavolume drive is found.
While designed for automated execution, the script can be run manually:
# Basic execution (must be run with Administrator privileges)
powershell.exe -ExecutionPolicy Bypass -File E:\PSCloudInit.ps1
# With verbose output for troubleshooting
powershell.exe -ExecutionPolicy Bypass -File E:\PSCloudInit.ps1 -Verbose
# Preview mode (no changes made)
powershell.exe -ExecutionPolicy Bypass -File E:\PSCloudInit.ps1 -WhatIfWhere E: is the drive letter of the mounted cloud-init drive.
Check the log file for execution details:
Get-Content C:\Windows\Panther\PSCloudInit.logAfter execution, verify the configuration:
# Check IP configuration
Get-NetIPAddress -AddressFamily IPv4
# Check routes
Get-NetRoute -AddressFamily IPv4
# Check DNS servers
Get-DnsClientServerAddress -AddressFamily IPv4
# Check DNS search domain
Get-DnsClient | Select-Object InterfaceAlias, ConnectionSpecificSuffix
# Check SSH authorized keys (if configured)
Get-Content C:\ProgramData\ssh\administrators_authorized_keysPotential improvements for future versions:
- Full cloud-init compatibility - Support more cloud-init features and modules
- User data script execution - Run custom scripts from user_data (runcmd, bootcmd modules)
- Multiple network routes - Support complex routing scenarios
- Validation and rollback - Verify configuration and rollback on failure
- Full YAML parser - Replace simple parser with a robust YAML library
- IPv6 gateway configuration - Add support for explicit IPv6 gateway configuration (currently relies on router advertisements)
- New Feature: CmdletBinding support for PowerShell best practices
- Added
-Verboseparameter for detailed diagnostic output - Added
-WhatIfparameter for preview mode (no changes made) - Write-Verbose used for debug information at each decision branch
- Write-Host used for user-facing action notifications
- Added
- New Feature: Installation as scheduled task
- Added
-Installparameter to set up automatic startup execution - Copies script to
C:\Windows\Setup\Scripts\ - Creates scheduled task "PSCloudInit-Startup" to run at startup
- Task runs with SYSTEM privileges
- Added
- Refactoring: Improved code organization
- All functions moved to dedicated section at file start
- Main script execution after function declarations
- No inline function definitions in main code
- Enhancement: Centralized error handling
- Single try/catch/finally block around main script
- Consistent error reporting and cleanup
- Proper transcript handling in all code paths
- Enhancement: Dedicated function for cloud-init drive detection
Wait-CloudInitDrivefunction with configurable timeout- Better logging and verbose output
- New Feature: Full IPv6 support
- Configure static IPv6 addresses
- Enable DHCPv6 for dynamic IPv6 addressing
- Support IPv6 SLAAC (Stateless Address Autoconfiguration)
- Automatic detection of IPv4 vs IPv6 addresses
- New Feature: Idempotency
- Safe to run multiple times without errors
- Checks existing IP addresses before adding
- Verifies routes exist before creating
- Compares DNS configuration before updating
- Smart DHCP enable/disable logic
- Enhancement: Multi-subnet support per interface
- Each interface can have multiple IPv4 and IPv6 addresses
- Mix of static, DHCP, and SLAAC on same interface
- Enhancement: Improved error handling and logging
- Breaking Change: Switched from ConfigDrive v2 to NoCloud format
- Changed drive label from
config-2tocidata - Updated file paths to root-level (
user-data,meta-data,network-config) - Implemented MAC address-based interface matching (resolves reliability issues)
- Added YAML parsing for network-config with full MAC address support
- Moved SSH keys parsing from meta-data to user-data (per NoCloud standard)
- Added support for global nameservers and search domains from network-config
- Enhanced logging and debugging output
- Can now reliably handle dozens of network interfaces
- ConfigDrive v2 format support
- Index-based interface matching
- Debian interfaces file format parsing
- JSON meta-data parsing