* fix: verify checksums for downloaded ISOs/images, keep sshpass off argv ensure-base-iso.sh downloaded the PVE install ISO over plain HTTP with no checksum, caching it on the persistent /opt/pve-integration mount and booting it as the nested trust root the integration suite relies on. ensure-cloud-images.sh fetched the Ubuntu cloud image and OVA over HTTPS but never checked them either. prepare-test-environment.sh and diagnose-cluster.sh passed the nested root password to sshpass via -p, putting it in the process table. create-api-token.sh, unused anywhere in the repo, minted a privsep=0 root token and echoed the secret unmasked. - ensure-base-iso.sh now downloads from https://enterprise.proxmox.com/iso and verifies against its SHA256SUMS on every run, including a cache hit. download.proxmox.com's own TLS cert does not list download.proxmox.com in its SAN (confirmed with curl/openssl from this environment), so https to that name fails certificate validation; enterprise.proxmox.com serves the identical ISO tree over a valid cert. Verification happens before the downloaded file is moved to its canonical cache path. - ensure-cloud-images.sh verifies the cloud image and OVA against Ubuntu's published SHA256SUMS the same way, matching by upstream filename since the cloud image is cached locally under a different extension (.img upstream, .qcow2 cached — the bytes are already qcow2-formatted). - prepare-test-environment.sh and diagnose-cluster.sh now export SSHPASS and call sshpass -e, keeping the password out of argv/ps. This also fixes a latent bug: the old unquoted `sshpass -p ${ROOT_PASS}` word-split any password containing whitespace. - create-api-token.sh deleted; grep across the repo found no caller. Reviewers (codex:codex-rescue, correctness-reviewer, security-reviewer) all independently found the same blocking bug in the first pass: when a cached file failed verification and the subsequent redownload then failed, ensure-cloud-images.sh fell through to a "keep the stale copy" branch and returned that same known-bad file with exit 0 — verification could be bypassed by inducing one failed redownload. Fixed by deleting the file immediately on a failed verification, before the redownload is attempted, so the later "is there a safe stale copy" check can no longer find it. Added a test case (case 5) that reproduces this exact sequence and mutation-tested it against the unfixed code. The three reviews also flagged a real but separate bug already fixed in this same change: `trap ... RETURN` inside a function nested in another function is not scoped to that function in bash — it re-fires on the OUTER function's return, referencing an out-of-scope local. Both verify_checksum() helpers now clean up their temp file explicitly instead of via trap. Findings not acted on, judged out of scope for this fix: - SHA256SUMS-fetch failures are treated the same as a checksum mismatch (delete + fail) rather than left untouched — a transient network blip destroys a good multi-GB cached ISO. This is the safer failure direction (never silently trust unverified bytes) and was a deliberate trade-off, not a defect. - ensure-cloud-images.sh's 7-day cache window can span an upstream republish of noble/current, causing a legitimate re-verification churn (not a security issue, a cache-hit-rate one). Pre-existing cache design, unrelated to adding verification. - wait-for-pve.sh (curl -d with the password on argv) and prepare-test-environment.sh's own positional password argument (from run-integration.sh) carry the same password-on-argv pattern this issue targeted in create-api-token.sh, sshpass -p and diagnose-cluster.sh, but neither script nor run-integration.sh was named in the issue. Left untouched per scope; worth a follow-up issue. - GPG/detached-signature verification of the upstream SHA256SUMS was not added — the new checks defend against cache poisoning and transit corruption, not a compromised origin. Worth a follow-up issue. - The two new self-checks (ensure-base-iso.test.sh, ensure-cloud-images.test.sh) are not wired into .github/workflows/unit-tests.yml's shell-selfchecks job. That file is code-owned and out of scope for this change; needs an operator follow-up. Password rotation (the Testpass123! value from before it moved to a secret) is unaddressed here per the contract — flagged for the operator. Mutation-tested: broke the post-download checksum check in ensure-base-iso.sh, confirmed the affected test cases failed, restored it. Broke the sshpass -e change back to -p, confirmed the new assertions in prepare-test-environment.test.sh failed, restored it. Broke the fail-open fix in ensure-cloud-images.sh, confirmed case 5 failed, restored it. Closes #149 * fix: also verify the stale-by-age fallback copy in ensure-cloud-images.sh PR review on #166 (COMMENTED, non-blocking) found the sibling of the fail-open bug already fixed in this branch: when the cached cloud image is stale by *age* (>= 7 days) rather than failed verification, the redownload-failure fallback could hand back that file with exit 0 without ever re-verifying it in this run. A file that failed the earlier verification is already deleted by the time the fallback runs, but a stale-by-age file skips verification entirely on the way in. Fixed by verifying the stale-by-age file at the point of actual fallback use — after the redownload has failed, not proactively before it's attempted, so a copy the redownload was about to replace anyway isn't deleted along a path that would have succeeded. Added two test cases (6, 7): a still-verifying stale-by-age copy is used as a fallback; one that no longer verifies is not. Mutation-tested by reverting to the unfixed fallback and confirming case 7 fails, then restored. --------- Co-authored-by: goodolclint-claude[bot] <323206664+goodolclint-claude[bot]@users.noreply.github.com>
PSProxmoxVE
A production-grade C# binary PowerShell module for managing Proxmox VE environments.
Supported Proxmox VE Versions
| PVE Version | Status |
|---|---|
| 9.x (current, 9.1.6+) | Primary target — fully supported |
| 8.x | Supported |
| 7.x (7.0+) | Best-effort — core cmdlets work, newer features emit clear version errors |
| 6.x and older | Not supported — hard blocked by version checks |
Version Gating Policy
The module uses a two-tier version check for cmdlets that require newer PVE APIs:
- Hard block (introduced version): The API endpoint doesn't exist — the cmdlet emits a terminating error with a clear message like "This operation requires Proxmox VE 8.1 or later."
- Warning (default version): The feature exists but may not be enabled by default — a warning is emitted but the command proceeds, allowing users who manually enabled the feature to succeed.
Most cmdlets target endpoints available since PVE 4.2 and require no version check. Cmdlets that need newer APIs include SDN (introduced 6.2, default 8.0+), cloud-init management (7.2+), container interfaces (8.1+), VM disk import (8.1+), and pool management (8.1+ for update/delete).
Prerequisites
- PowerShell 5.1 (Windows PowerShell) or 7.2+
- OS: Windows, Linux, macOS
- Network: HTTPS access to Proxmox VE API (default port 8006)
.NET Compatibility
The module ships as a single netstandard2.0 assembly, which runs on both .NET Framework and .NET Core/.NET:
| PowerShell Version | Runtime | Status |
|---|---|---|
| 5.1 (Windows PowerShell) | .NET Framework 4.8 | Fully supported |
| 7.2, 7.4, 7.5 | .NET 8.0 / 10.0 | Fully supported |
Installation
# Install from the PowerShell Gallery
Install-Module -Name PSProxmoxVE -Scope CurrentUser
# Or install a prerelease version
Install-Module -Name PSProxmoxVE -Scope CurrentUser -AllowPrerelease
# Verify installation
Get-Module -ListAvailable PSProxmoxVE
Import-Module PSProxmoxVE
Quick Start
Connect to a Proxmox VE Server
# Using username/password (ticket-based authentication)
$cred = Get-Credential -UserName 'root@pam'
Connect-PveServer -Server 'pve.example.com' -Credential $cred -SkipCertificateCheck
# Using API token
Connect-PveServer -Server 'pve.example.com' -ApiToken 'root@pam!mytoken=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
# Verify connection
Test-PveConnection -Detailed
List and Manage VMs
# List all VMs
Get-PveVm
# List VMs on a specific node
Get-PveVm -Node 'pve1'
# Filter by status
Get-PveVm -Status 'running'
# Start/stop VMs
Get-PveVm -Name 'my-vm' | Start-PveVm -Wait
Get-PveVm -Name 'my-vm' | Stop-PveVm -Wait
# Clone a VM
Get-PveVm -VmId 100 | Copy-PveVm -NewVmId 200 -NewName 'my-clone' -Full -Wait
# Get VM configuration
Get-PveVm -VmId 100 | Get-PveVmConfig
Upload Files
# Upload a local ISO file to Proxmox storage
Send-PveFile -Node 'pve1' -Storage 'local' -Path './ubuntu-24.04-live-server-amd64.iso' -Wait
# Upload a disk image for VM import
Send-PveFile -Node 'pve1' -Storage 'local' -Path './disk.qcow2' -ContentType 'import' -Wait
Note:
Send-PveFileimplements a workaround for a long-standing Proxmox API multipart parsing bug (bugzilla 7389). Standard multipart HTTP libraries (including .NET'sMultipartFormDataContent) add sub-headers that Proxmox'spveproxymishandles, resulting in corrupt uploads. This cmdlet constructs the multipart body manually to ensure correct uploads where other tools may produce corrupt files.
Work with Snapshots
# List snapshots
Get-PveVm -VmId 100 | Get-PveSnapshot
# Create a snapshot
Get-PveVm -VmId 100 | New-PveSnapshot -Name 'before-upgrade' -Description 'Snapshot before OS upgrade' -Wait
# Rollback
Get-PveVm -VmId 100 | Get-PveSnapshot | Where-Object Name -eq 'before-upgrade' | Restore-PveSnapshot -Wait
Cloud-Init Configuration
# Get current cloud-init config
Get-PveVm -VmId 100 | Get-PveCloudInitConfig
# Set cloud-init config
Get-PveVm -VmId 100 | Set-PveCloudInitConfig -Hostname 'web01' -User 'admin' -SshKeys @('ssh-ed25519 AAAA...') -IpConfig 'ip=dhcp' -Wait
# Regenerate cloud-init image after changes
Get-PveVm -VmId 100 | Invoke-PveCloudInitRegenerate -Wait
Authentication Guide
Username/Password (Ticket-Based)
Ticket authentication uses Proxmox's built-in session system. The module POSTs to /api2/json/access/ticket and stores the returned ticket cookie and CSRF token.
- Format:
user@realm(e.g.,root@pam,admin@pve,user@mydomain) - Expiry: Tickets expire after 2 hours. The module detects expiry and prompts you to reconnect.
- Realms: Supports all Proxmox realms —
pam,pve, custom LDAP/AD realms. - When to use: Interactive sessions, ad-hoc management tasks.
$cred = Get-Credential -UserName 'admin@pve'
Connect-PveServer -Server 'pve.example.com' -Credential $cred
API Token
API tokens provide persistent, non-expiring authentication. They are the recommended approach for automation.
- Format:
USER@REALM!TOKENID=UUID(e.g.,root@pam!automation=12345678-abcd-efgh-ijkl-123456789012) - No expiry: Tokens remain valid until explicitly revoked.
- When to use: Automation, scripts, CI/CD pipelines.
Creating an API token in the PVE UI:
- Navigate to Datacenter → Permissions → API Tokens
- Click Add
- Select the user, enter a token ID, optionally uncheck "Privilege Separation"
- Copy the token value — it is shown only once
Connect-PveServer -Server 'pve.example.com' -ApiToken 'root@pam!automation=12345678-abcd-efgh-ijkl-123456789012'
Multi-Cluster Usage
Every cmdlet accepts an optional -Session parameter. This enables managing multiple Proxmox VE clusters simultaneously:
# Connect to two clusters
$prod = Connect-PveServer -Server 'pve-prod.example.com' -ApiToken $prodToken -PassThru
$dev = Connect-PveServer -Server 'pve-dev.example.com' -ApiToken $devToken -PassThru
# Query each cluster explicitly
$prodVms = Get-PveVm -Session $prod
$devVms = Get-PveVm -Session $dev
# The last Connect-PveServer call sets the default session
# So Get-PveVm without -Session uses $dev
Get-PveVm # Uses $dev session
SDN Management
Software-Defined Networking (SDN) features require Proxmox VE 8.0 or later.
# List SDN zones and VNets
Get-PveSdnZone
Get-PveSdnVnet
# Create a new zone
New-PveSdnZone -Zone 'myzone' -Type 'simple'
# Create a VNet
New-PveSdnVnet -Vnet 'myvnet' -Zone 'myzone' -Tag 100
If connected to a PVE server below version 8.0, SDN cmdlets will throw a clear error:
SDN management requires Proxmox VE 8.0 or later. Connected server is version 7.4.
Cmdlet Reference
Connection
| Cmdlet | Description |
|---|---|
Connect-PveServer |
Establish a session to a Proxmox VE server |
Disconnect-PveServer |
Close the active session |
Test-PveConnection |
Test if the current session is valid |
Nodes
| Cmdlet | Description |
|---|---|
Get-PveNode |
List cluster nodes |
Get-PveNodeStatus |
Get detailed node status |
Virtual Machines
| Cmdlet | Description |
|---|---|
Get-PveVm |
List VMs with optional filters |
New-PveVm |
Create a new VM |
Remove-PveVm |
Delete a VM |
Start-PveVm |
Start a VM |
Stop-PveVm |
Stop a VM (hard) |
Restart-PveVm |
Graceful restart (shutdown + start) |
Suspend-PveVm |
Suspend a VM |
Resume-PveVm |
Resume a suspended VM |
Reset-PveVm |
Hard reset a VM |
Copy-PveVm |
Clone a VM (full or linked) |
Move-PveVm |
Migrate a VM to another node |
Get-PveVmConfig |
Get VM configuration |
Set-PveVmConfig |
Modify VM configuration |
Resize-PveVmDisk |
Resize a VM disk |
Import-PveVmDisk |
Import a disk image (qcow2, raw, vmdk, OVA) into a VM |
Import-PveOva |
Import an OVA appliance as a new VM (parses OVF, uploads, creates VM, imports disks) |
Containers
| Cmdlet | Description |
|---|---|
Get-PveContainer |
List LXC containers |
New-PveContainer |
Create a new container |
Remove-PveContainer |
Delete a container |
Start-PveContainer |
Start a container |
Stop-PveContainer |
Stop a container |
Restart-PveContainer |
Restart a container |
Copy-PveContainer |
Clone a container |
Move-PveContainer |
Migrate a container to another node |
Get-PveContainerConfig |
Get container configuration |
Set-PveContainerConfig |
Modify container configuration |
Get-PveContainerSnapshot |
List container snapshots |
New-PveContainerSnapshot |
Create a container snapshot |
Remove-PveContainerSnapshot |
Delete a container snapshot |
Restore-PveContainerSnapshot |
Rollback to a container snapshot |
Storage
| Cmdlet | Description |
|---|---|
Get-PveStorage |
List storage pools |
Get-PveStorageContent |
List storage content (ISOs, images, etc.) |
Send-PveFile |
Upload a file (ISO, disk image, template) to storage |
Invoke-PveStorageDownload |
Download a URL to storage (server-side) |
New-PveStorage |
Create a storage pool |
Set-PveStorage |
Update a storage pool configuration |
Remove-PveStorage |
Remove a storage pool |
Snapshots
| Cmdlet | Description |
|---|---|
Get-PveSnapshot |
List VM snapshots |
New-PveSnapshot |
Create a snapshot |
Remove-PveSnapshot |
Delete a snapshot |
Restore-PveSnapshot |
Rollback to a snapshot |
Network
| Cmdlet | Description |
|---|---|
Get-PveNetwork |
List network interfaces |
New-PveNetwork |
Create a network interface |
Set-PveNetwork |
Modify a network interface |
Remove-PveNetwork |
Delete a network interface |
Invoke-PveNetworkApply |
Apply pending network changes |
SDN (PVE 8.0+)
| Cmdlet | Description |
|---|---|
Get-PveSdnZone |
List SDN zones |
New-PveSdnZone |
Create an SDN zone |
Remove-PveSdnZone |
Delete an SDN zone |
Get-PveSdnVnet |
List SDN VNets |
New-PveSdnVnet |
Create an SDN VNet |
Remove-PveSdnVnet |
Delete an SDN VNet |
Get-PveSdnSubnet |
List SDN subnets for a VNet |
New-PveSdnSubnet |
Create an SDN subnet |
Remove-PveSdnSubnet |
Delete an SDN subnet |
Users & Permissions
| Cmdlet | Description |
|---|---|
Get-PveUser |
List users |
New-PveUser |
Create a user |
Remove-PveUser |
Delete a user |
Set-PveUser |
Modify a user |
Get-PveRole |
List roles |
New-PveRole |
Create a role |
Remove-PveRole |
Delete a role |
Get-PvePermission |
List permissions |
Set-PvePermission |
Set a permission |
Get-PveApiToken |
List API tokens for a user |
New-PveApiToken |
Create an API token |
Remove-PveApiToken |
Delete an API token |
Templates
| Cmdlet | Description |
|---|---|
Get-PveTemplate |
List VM templates |
New-PveTemplate |
Convert a VM to a template |
Remove-PveTemplate |
Delete a template |
New-PveVmFromTemplate |
Create a VM from a template |
Cloud-Init
| Cmdlet | Description |
|---|---|
Get-PveCloudInitConfig |
Get cloud-init configuration |
Set-PveCloudInitConfig |
Set cloud-init configuration |
Invoke-PveCloudInitRegenerate |
Regenerate cloud-init image |
Tasks
| Cmdlet | Description |
|---|---|
Get-PveTask |
Get task status |
Get-PveTaskList |
List tasks on a node with optional filters |
Stop-PveTask |
Cancel a running task |
Wait-PveTask |
Wait for a task to complete |
Firewall
| Cmdlet | Description |
|---|---|
Get-PveFirewallRule |
List firewall rules (cluster/node/VM/container) |
New-PveFirewallRule |
Create a firewall rule |
Set-PveFirewallRule |
Update a firewall rule |
Remove-PveFirewallRule |
Delete a firewall rule |
Get-PveFirewallGroup |
List security groups or group rules |
New-PveFirewallGroup |
Create a security group |
Remove-PveFirewallGroup |
Delete a security group |
Get-PveFirewallAlias |
List firewall IP aliases |
New-PveFirewallAlias |
Create a firewall IP alias |
Set-PveFirewallAlias |
Update a firewall IP alias |
Remove-PveFirewallAlias |
Delete a firewall IP alias |
Get-PveFirewallIpSet |
List firewall IP sets |
New-PveFirewallIpSet |
Create a firewall IP set |
Remove-PveFirewallIpSet |
Delete a firewall IP set |
Get-PveFirewallIpSetEntry |
List entries in an IP set |
New-PveFirewallIpSetEntry |
Add an entry to an IP set |
Set-PveFirewallIpSetEntry |
Update an IP set entry |
Remove-PveFirewallIpSetEntry |
Remove an entry from an IP set |
Get-PveFirewallOptions |
Get firewall options |
Set-PveFirewallOptions |
Set firewall options |
Get-PveFirewallRef |
List firewall references (aliases, IP sets) |
Backup
| Cmdlet | Description |
|---|---|
New-PveBackup |
Create an ad-hoc backup (vzdump) |
Get-PveBackupJob |
List scheduled backup jobs |
New-PveBackupJob |
Create a scheduled backup job |
Set-PveBackupJob |
Update a scheduled backup job |
Remove-PveBackupJob |
Delete a scheduled backup job |
Get-PveBackupInfo |
Find VMs/containers not covered by backup jobs |
SDN — IPAM / DNS / Controllers (PVE 8.0+)
| Cmdlet | Description |
|---|---|
Get-PveSdnIpam |
List SDN IPAM plugins |
New-PveSdnIpam |
Create an SDN IPAM plugin |
Set-PveSdnIpam |
Update an SDN IPAM plugin |
Remove-PveSdnIpam |
Remove an SDN IPAM plugin |
Get-PveSdnDns |
List SDN DNS plugins |
New-PveSdnDns |
Create an SDN DNS plugin |
Set-PveSdnDns |
Update an SDN DNS plugin |
Remove-PveSdnDns |
Remove an SDN DNS plugin |
Get-PveSdnController |
List SDN controllers |
New-PveSdnController |
Create an SDN controller |
Set-PveSdnController |
Update an SDN controller |
Remove-PveSdnController |
Remove an SDN controller |
Invoke-PveSdnApply |
Apply pending SDN configuration changes |
Set-PveSdnZone |
Update an SDN zone |
Set-PveSdnVnet |
Update an SDN VNet |
Set-PveSdnSubnet |
Update an SDN subnet |
Cluster
| Cmdlet | Description |
|---|---|
Get-PveClusterResource |
List all resources (VMs, containers, nodes, storage) cluster-wide |
Pools
| Cmdlet | Description |
|---|---|
Get-PvePool |
List resource pools |
New-PvePool |
Create a resource pool |
Set-PvePool |
Update a resource pool |
Remove-PvePool |
Delete a resource pool |
VM Disk Operations
| Cmdlet | Description |
|---|---|
Move-PveVmDisk |
Move a VM disk to a different storage |
Remove-PveVmDisk |
Detach and optionally delete a VM disk |
Guest Agent Extensions
| Cmdlet | Description |
|---|---|
Get-PveVmGuestOsInfo |
Get guest OS information via QEMU agent |
Get-PveVmGuestFsInfo |
Get guest filesystem information |
Read-PveVmGuestFile |
Read a file from inside a guest VM |
Write-PveVmGuestFile |
Write a file inside a guest VM |
Set-PveVmGuestPassword |
Change a user password inside a guest VM |
Invoke-PveVmGuestFsTrim |
TRIM guest VM filesystems |
Additional Container Operations
| Cmdlet | Description |
|---|---|
Suspend-PveContainer |
Suspend (freeze) a container |
Resume-PveContainer |
Resume a suspended container |
Resize-PveContainerDisk |
Resize a container disk/volume |
New-PveContainerTemplate |
Convert a container to a template |
Move-PveContainerVolume |
Move a container volume to a different storage |
Get-PveContainerInterface |
Get container network interfaces |
Storage Content
| Cmdlet | Description |
|---|---|
Get-PveStorageStatus |
Get storage usage statistics |
Remove-PveStorageContent |
Delete a volume, backup, or ISO from storage |
Set-PveStorageContent |
Update volume notes/properties |
New-PveStorageDisk |
Allocate a new empty disk image |
Node Operations
| Cmdlet | Description |
|---|---|
Get-PveNodeConfig |
Get node configuration |
Set-PveNodeConfig |
Update node configuration |
Get-PveNodeDns |
Get node DNS configuration |
Set-PveNodeDns |
Update node DNS configuration |
Start-PveNodeVms |
Start all VMs on a node |
Stop-PveNodeVms |
Stop all VMs on a node |
Access — Groups & Domains
| Cmdlet | Description |
|---|---|
Get-PveGroup |
List user groups |
New-PveGroup |
Create a user group |
Set-PveGroup |
Update a user group |
Remove-PveGroup |
Delete a user group |
Get-PveDomain |
List authentication realms (PAM, LDAP, AD, OpenID) |
New-PveDomain |
Create an authentication realm |
Set-PveDomain |
Update an authentication realm |
Remove-PveDomain |
Delete an authentication realm |
Set-PvePassword |
Change a user's password |
Set-PveApiToken |
Update an API token |
Set-PveRole |
Update a role's privileges |
Known Limitations
- No automatic retries: Failed API calls are not retried. Implement your own retry logic if needed.
- Integration tests require live node: Integration tests require a dedicated Proxmox VE test node. See
tests/PSProxmoxVE.Tests/Integration/README.md. - No Ceph management: Ceph pool/OSD/monitor management is not included in v1.
- No PBS integration: Proxmox Backup Server operations are not included in v1.
- Task waiting:
Wait-PveTaskpolls with a minimum 1-second interval. For high-frequency monitoring, use the PVE web UI.
Contributing
- Clone the repository
- Open
PSProxmoxVE.slnin your IDE - Build:
dotnet build - Run unit tests — import the local build first, or an installed copy of the module shadows
it and the suite reports failures against correct code:
pwsh -Command "Import-Module ./src/PSProxmoxVE/bin/Debug/netstandard2.0/PSProxmoxVE.psd1 -Force; Invoke-Pester tests/PSProxmoxVE.Tests/ -ExcludeTagFilter Integration" - Run integration tests (provisions nested PVE, x86 only): see
CLAUDE.md, "Local dev environment"
Commit Convention
This project uses Conventional Commits:
feat:— new featurefix:— bug fixtest:— test additions or changesci:— CI/CD changesdocs:— documentation changesrefactor:— code refactoring