Idempotent scripts for Proxmox VE post-installation configuration. Brought to you by the hoopy froods at HyperSec.
- System Optimisation - Kernel tuning, nested virtualisation, IOMMU/VFIO passthrough, SSD TRIM
- Network Tuning - Tier-based TCP/UDP buffers (1/10/25/40/100/200 GbE), BBR congestion control, NIC offloading
- Power Management - CPU governors, PCIe ASPM, thermal monitoring, power profiles
- ZFS Tuning - RAM-aware ARC sizing, autotrim, dataset settings
- Repository Config - No-subscription repos, enterprise repo disable
- Conservative Updates - N-1 minor version pinning, UI customisation, APT persistence hooks
- Internal NAT - Host-side VM networking with any IPv4 CIDR
- Remote Access - AWS SSM and NetBird agents for emergency console access
- Drive Error Recovery - SCT ERC and kernel timeouts so ZFS sees errors, not stalls
- Artefact Cleanup - Removes superseded config left by earlier runs
Make a Proxmox VE host on the community (no-subscription) repositories behave as close to an enterprise deployment as it can: current on fixes, never first onto a new minor release, tuned for its hardware, and honest about what it is (the subscription nags are replaced with a statement of the actual update policy, not just hidden).
| Target 1 | Target 2 | |
|---|---|---|
| Shape | Single node | Multi-node cluster |
| Who | Solo dev, small team, home brick server | Production private cloud |
| Scripts run | All of them | All of them, on every node |
The scripts are the same on both, deliberately. Work that starts on someone's home Proxmox box should move to a production cluster without a rebuild, so nothing here is conditional on being "the small one".
Only two things differ, and both are detected rather than configured:
- Update policy is coordinated cluster-wide. A cluster requires matching
versions across nodes, so the resolved series are written to
/etc/pve(the replicated cluster filesystem) and every node reproduces the same decision instead of resolving independently and drifting apart. A standalone node has no cluster filesystem and simply resolves locally. - Bridge netfilter is only asserted where
pve-firewallis enabled.
# Download and extract
wget https://github.com/hyperi-io/proxmox/archive/refs/heads/main.zip
unzip main.zip && cd proxmox-main/postinstall
chmod +x *.sh *.py
# Run in order
sudo ./proxmox-cleanup.sh status # What did an earlier run leave behind?
sudo ./proxmox-repo.sh # Configure repositories
sudo ./proxmox-optimize.sh # Core system optimisation
sudo ./proxmox-zfs.sh # ZFS tuning (if applicable)
sudo ./proxmox-disk-errors.sh apply # Drive error recovery (if ZFS)
sudo ./proxmox-power-management.sh # Power management (optional)
sudo ./proxmox-network.sh 10gbe # Network tuning (optional)
sudo ./proxmox-update-policy.sh enable # Conservative updates (optional)Run proxmox-cleanup.sh status first on any host that has run an earlier
version of these scripts -- it reports superseded config that is still active
and changes nothing until you run apply.
After running, update GRUB and reboot if prompted:
sudo update-grub && sudo rebootConfigures Proxmox VE repositories for community (no-subscription) use.
What it does:
- Creates no-subscription repository configuration
- Disables enterprise repositories
- Updates package lists
Note: UI customisations (warning suppression) are handled by proxmox-update-policy.sh.
| Property | Value |
|---|---|
| Idempotent | Yes |
| Reboot | No |
| Backup | None (safe operations) |
Conservative update policy with n-0.1 minor version pinning. Keeps you one minor version behind bleeding edge while allowing patch updates.
What it does:
- Pins Proxmox packages to one minor version behind latest
- Applies UI patches to replace "not recommended for production" warnings
- Creates APT hook for persistence across package updates
- Supports daily cron job for automatic policy refresh
- Never downgrades below installed version
Policy behaviour:
- MAJOR: Same as latest available
- MINOR: max(installed, n-0.1) - never downgrades
- PATCH: Latest within target minor
Example: If repo has 9.2.3, policy pins to 9.1.* (gets 9.1.x patches, skips 9.2.x)
Commands:
sudo ./proxmox-update-policy.sh enable # Pin + suppress warnings
sudo ./proxmox-update-policy.sh enable --no-ui # Pin only, keep warnings
sudo ./proxmox-update-policy.sh ui-only # Suppress warnings, pin nothing
sudo ./proxmox-update-policy.sh ui-disable # Restore warnings, keep pinning
sudo ./proxmox-update-policy.sh disable # Remove both
sudo ./proxmox-update-policy.sh status # Show policy and versions
sudo ./proxmox-update-policy.sh update # Refresh pinning
sudo ./proxmox-update-policy.sh cron-enable # Install daily cron
sudo ./proxmox-update-policy.sh cron-disable # Remove cronPinning and the UI customisation are independent. A host can hold packages back, suppress the subscription warnings, both, or neither:
| Want | Command |
|---|---|
| Both | enable |
| Pin only, keep the warnings | enable --no-ui |
| Warnings gone, track latest | ui-only |
| Neither | disable |
ui-only is the right choice for a host that should stay current -- a lab or
a personal box -- but without Proxmox telling you off about it on every login.
UI customisations:
When enabled, the web interface reports the update policy that is actually in force instead of a generic warning:
- Suppresses the "no valid subscription" modal shown after login
- Replaces the no-subscription repository warning with "Conservative update policy active", and only when that warning was the only thing wrong -- a genuine problem still surfaces
- Persists across package updates via an APT hook
This relabels repository status. It does not represent the host as holding a subscription and does not change what the host is entitled to.
How it is implemented, and why it matters:
The customisation is a single injected JavaScript file using ExtJS class
overrides -- the framework's own extension mechanism. No Proxmox source file
is modified. The only edit is one <script> tag added to index.html.tpl.
Earlier versions text-patched proxmoxlib.js in seven places. That approach
broke on every PVE release that restructured the file (on PVE 9.2.5 only two of
the seven still matched), and because the patches carried placeholder line
numbers, patch(1) fuzzy-matched short repeated lines and could edit an
unintended one. The override survives package updates untouched, and if
upstream renames what it hooks the original warning simply reappears rather
than the UI breaking.
Deleting /usr/share/pve-manager/js/conservative-policy.js restores stock
behaviour completely.
Compatibility: Tested on PVE 9.x only. PVE 8.x may work.
| Property | Value |
|---|---|
| Idempotent | Yes |
| Reboot | No |
| Backup | /root/backup/proxmox-config/ |
Core system configuration for Proxmox VE hosts.
What it does:
- Backs up current settings
- Installs monitoring tools (htop, iotop, smartmontools)
- Configures kernel parameters (sysctl)
- Enables nested virtualisation (Intel VT-x / AMD-V)
- Configures IOMMU for device passthrough
- Enables SSD TRIM
- Creates management scripts
Kernel parameters applied:
vm.swappiness=10
vm.vfs_cache_pressure=50
net.core.netdev_max_backlog=8192
net.ipv4.tcp_fin_timeout=30
fs.file-max=2097152
net.bridge.bridge-nf-call-iptables=1
Created commands: proxmox-status
| Property | Value |
|---|---|
| Idempotent | Yes |
| Reboot | Yes (for IOMMU/nested virt) |
| Backup | /root/backup/proxmox-config/ |
Network configuration based on interface speed tier.
Usage:
sudo ./proxmox-network.sh 1gbe # 1 Gigabit (conservative)
sudo ./proxmox-network.sh 10gbe # 10 Gigabit (recommended)
sudo ./proxmox-network.sh 25gbe # 25 Gigabit
sudo ./proxmox-network.sh 40gbe # 40 Gigabit
sudo ./proxmox-network.sh 100gbe # 100 Gigabit
sudo ./proxmox-network.sh 200gbe # 200 GigabitWhat it does:
- Detects or accepts network speed tier
- Configures TCP/UDP buffer sizes
- Configures queue depths and backlogs
- Enables BBR congestion control for 10GbE+
- Configures NIC ring buffers and hardware offloading
- Supports jumbo frames (--jumbo flag)
Tier optimisations:
| Tier | TCP Buffer Max | Backlog | Congestion | Ring Buffer |
|---|---|---|---|---|
| 1 GbE | 8 MB | 5K | CUBIC | 512 |
| 10 GbE | 32 MB | 30K | BBR | 2048 |
| 25 GbE | 64 MB | 50K | BBR | 4096 |
| 40 GbE | 128 MB | 100K | BBR | 8192 |
| 100 GbE | 256 MB | 250K | BBR | 8192 |
| 200 GbE | 512 MB | 500K | BBR | 8192 |
Created commands: network-status, network-test
| Property | Value |
|---|---|
| Idempotent | Yes |
| Reboot | No |
| Backup | /root/backup/proxmox-config/ |
Power management and thermal control.
What it does:
- Configures CPU frequency governor (schedutil)
- Applies vendor-specific settings (Intel/AMD)
- Enables PCIe ASPM (powersave mode)
- Configures SATA link power management
- Enables network power management (WoL, EEE)
- Configures USB selective suspend
- Enables PCI runtime power management
- Updates kernel boot parameters
Kernel parameters (Intel):
intel_idle.max_cstate=6
intel_pstate=passive
pcie_aspm=powersave
Kernel parameters (AMD):
processor.max_cstate=6
amd_pstate=passive
pcie_aspm=powersave
Created commands: power-status, thermal-check, performance-mode, balanced-mode, powersave-mode
Systemd service: proxmox-power.service (auto-applies on boot)
| Property | Value |
|---|---|
| Idempotent | Yes |
| Reboot | Yes (for kernel parameters) |
| Backup | /root/backup/proxmox-config/ |
Safe ZFS optimisation for Proxmox storage.
What it does:
- Calculates ARC size based on total RAM
- Applies runtime ARC limits
- Creates persistent ZFS module configuration
- Enables autotrim on all pools
- Optimises VM storage datasets (atime, xattr)
- Generates status and tuning scripts
ARC sizing:
| Total RAM | ARC Min | ARC Max | VM Reserve |
|---|---|---|---|
| 16 GB | 1 GB | 2 GB | 14+ GB |
| 32 GB | 1 GB | 3 GB | 29+ GB |
| 64 GB | 2 GB | 4 GB | 60+ GB |
| 128 GB | 2 GB | 6 GB | 122+ GB |
| 256+ GB | 3 GB | 8 GB | 248+ GB |
Safety settings preserved: sync=standard, compression (Proxmox-managed), primarycache=all
zvol taskq sizing: zvol_threads scales with CPU count and zvol_num_taskqs
partitions zvols across independent taskqs. A single shared taskq is a
host-wide single point of failure: if one device wedges, its blocked threads
starve guests on completely unrelated, healthy pools.
Created commands: zfs-status, zfs-tune-guide
| Property | Value |
|---|---|
| Idempotent | Yes |
| Reboot | Recommended |
| Backup | None (safe operations) |
Bounds how long a drive may retry a bad sector, so it returns an error instead of stalling.
Why this matters more than it sounds: ZFS can only act on an error, never on a stall. A drive that retries without bound never triggers the repair-from-redundancy path, so a bad block is never healed and every I/O queued behind it waits. Redundancy you cannot reach is not redundancy.
What it does:
- Enables SCT ERC (7s) on drives that support it -- most do, and most ship with it Disabled
- Falls back to the kernel command timeout (10s, vs the 30s default) for drives with no SCT support
- Reapplies SCT ERC on every boot via systemd, because it does not survive a power cycle
- Matches the udev rule on drive serial, not
sdX, which is not stable
Only applied to pools that HAVE redundancy. On a single disk, telling the drive to give up early tells it to abandon data nothing else holds. The script detects layout and skips non-redundant pools.
sudo ./proxmox-disk-errors.sh apply # Configure
sudo ./proxmox-disk-errors.sh status # Per-drive report
sudo ./proxmox-disk-errors.sh remove # Revert| Property | Value |
|---|---|
| Idempotent | Yes |
| Reboot | No |
| Backup | N/A |
Finds and removes artefacts left by earlier versions of these scripts.
None of the other scripts clean up their predecessors. Because they write into drop-in directories that apply everything they contain, a superseded generation does not become inert -- it stays active and fights the current one.
What it looks for:
- APT hooks from earlier generations still patching the same files
- sysctl drop-ins superseded by
98-proxmox-optimize.conf - sysctl files that are raw
sysctl -adumps rather than configs - sysctl files setting read-only kernel statistics, which error on every boot
- More than one network tier file, all applying at once
sudo ./proxmox-cleanup.sh status # Report only, changes nothing (default)
sudo ./proxmox-cleanup.sh apply # Remove, backing each up first| Property | Value |
|---|---|
| Idempotent | Yes |
| Reboot | No |
| Backup | /root/backup/proxmox-config/removed-<timestamp>/ |
Host-side internal VM network with NAT outbound.
Usage:
sudo ./proxmox-internal-nat.sh apply --lan-cidr 10.42.0.0/16
sudo ./proxmox-internal-nat.sh remove
sudo ./proxmox-internal-nat.sh status --lan-cidr 10.42.0.0/16
sudo ./proxmox-internal-nat.sh health --lan-cidr 10.42.0.0/16Commands:
apply- Create internal bridge, enable forwarding, add NAT rulesremove- Remove all configuration and rulesstatus- Show CONFIG intent vs LIVE runtime statehealth- Deeper dataplane checks (routes + nft rules/counters)
Options:
--lan-cidr <CIDR>- Network CIDR (required for apply/status/health)--lan-gw <IP>- Gateway IP (default: first usable host)--wan-bridge <name>- WAN bridge name (default: vmbr0)--lan-bridge <name>- LAN bridge name (default: vmbr1)--reload- Reload networking after changes
Safety:
- Does NOT reload networking unless
--reloadis passed - Uses isolated nftables tables (won't conflict with pve-firewall)
- All files backed up before modification
| Property | Value |
|---|---|
| Idempotent | Yes |
| Reboot | No |
| Backup | Inline (marked sections) |
AWS Systems Manager agent for emergency console access via AWS SSM Session Manager.
Usage:
# Install with auto-created IAM role and activation
sudo ./proxmox-ssm.py install --region ap-southeast-2
# Install with existing activation
sudo ./proxmox-ssm.py install --activation-code XXXX --activation-id YYYY --region ap-southeast-2
# Check status
sudo ./proxmox-ssm.py status
# Uninstall
sudo ./proxmox-ssm.py uninstallWhat it does:
- Installs AWS SSM agent from official .deb package
- Creates IAM role with SSM trust policy (if not exists)
- Creates hybrid activation for on-premises registration
- Registers host as AWS managed instance
- Enables Session Manager access via AWS console/CLI
Region auto-detection (in order):
--regionflagAWS_REGIONenvironment variableAWS_DEFAULT_REGIONenvironment variableaws configure get region- Error if none found
Requirements:
- AWS CLI installed with appropriate IAM permissions
- Internet connectivity to AWS SSM endpoints
- For Session Manager: Advanced-instances tier enabled
Connect after install:
aws ssm start-session --target mi-XXXXXXXXX --region ap-southeast-2| Property | Value |
|---|---|
| Idempotent | Yes |
| Reboot | No |
| Backup | /root/backup/proxmox-config/ |
NetBird WireGuard mesh agent for emergency access. BSD-3-Clause licensed open source.
Usage:
# Install and connect to NetBird cloud
sudo ./proxmox-netbird.py install --setup-key nb-setup-XXXXXXXX
# Install and connect to self-hosted control plane
sudo ./proxmox-netbird.py install --setup-key XXXX --management-url https://netbird.example.com:443
# Check status
sudo ./proxmox-netbird.py status
# Uninstall
sudo ./proxmox-netbird.py uninstallWhat it does:
- Adds NetBird APT repository
- Installs netbird package
- Connects to management server with setup key
- Creates WireGuard tunnel (wt0 interface)
Getting a setup key:
- Go to https://app.netbird.io (or your self-hosted console)
- Navigate to Setup Keys
- Create a new key (reusable or one-time)
- Use with
--setup-key
Supports:
- NetBird cloud (api.netbird.io) - default
- Self-hosted control plane (
--management-url)
| Property | Value |
|---|---|
| Idempotent | Yes |
| Reboot | No |
| Backup | /root/backup/proxmox-config/ |
/etc/sysctl.d/98-proxmox-optimize.conf # Kernel parameters (base)
/etc/sysctl.d/99-proxmox-network-<tier>.conf # Kernel parameters (network tier)
/etc/modprobe.d/kvm-nested.conf # Nested virtualisation
/etc/modprobe.d/zfs.conf # ZFS ARC + zvol taskq
/etc/modules # VFIO modules
/etc/default/grub # Boot parameters
/etc/default/cpufrequtils # CPU governor
/etc/systemd/system/proxmox-power.service # Power service
/etc/systemd/system/zfs-disk-erc.service # SCT ERC (reapplied each boot)
/etc/udev/rules.d/60-zfs-disk-timeouts.rules # Kernel timeout, no-SCT drives
/etc/apt/sources.list.d/debian-official.sources # Debian repos
/etc/apt/sources.list.d/proxmox.sources # Proxmox no-subscription repo
/etc/apt/preferences.d/proxmox-conservative # Update policy pinning
/etc/apt/apt.conf.d/99proxmoxpolicy # Update policy APT hook
/etc/pve/proxmox-conservative-series # Cluster-agreed pin series
/usr/local/bin/proxmox-policy-hook.sh # Update policy hook script
/usr/share/pve-manager/js/conservative-policy.js # UI policy flag
The sysctl files are numbered so the network tier file deliberately overrides
the base one where they overlap. Only ONE tier file should ever exist -- see
proxmox-cleanup.sh if more than one is present.
All backups are stored in /root/backup/proxmox-config/ with timestamped filenames:
/root/backup/proxmox-config/
- sysctl-backup-YYYYMMDD_HHMMSS.conf # System
- grub.backup.YYYYMMDD_HHMMSS # System
- network-sysctl-YYYYMMDD_HHMMSS.conf # Network
- power-grub-YYYYMMDD_HHMMSS # Power
- power-cpufrequtils-YYYYMMDD_HHMMSS # Power
- proxmoxlib.js.original # Update policy
- pvemanagerlib.js.original # Update policy
- index.html.tpl.original # Update policy
After installation, these commands are available:
# System
proxmox-status # Overall system status
# Network
network-status # Network configuration
network-test # Performance testing guide
# Power
power-status # Power configuration
thermal-check # CPU temperature check
performance-mode # Switch to performance
balanced-mode # Switch to balanced
powersave-mode # Switch to powersave
# ZFS
zfs-status # ZFS status overview
zfs-tune-guide # Tuning recommendations
# Update Policy
proxmox-update-policy.sh status # Policy status
proxmox-update-policy.sh enable # Enable policy
proxmox-update-policy.sh disable # Disable policy
proxmox-update-policy.sh update # Refresh pinning
# Remote Access
proxmox-ssm.py status # SSM agent status
proxmox-netbird.py status # NetBird statusmkfs.ext4 issues a full-device TRIM by default. Against a thin zvol that
discard is enormous, and ZFS has to digest all of it before anything else
proceeds -- formatting a 2 TB thin zvol produced a 769-second transaction group
during the incident that prompted proxmox-disk-errors.sh, blocking every
guest on the host.
Rules:
-
Always pass
-E nodiscardwhen runningmkfs.ext4on a zvol.mkfs.ext4 -E nodiscard /dev/zvol/vmdata/vm-100-disk-1
-
Reclaim deliberately, not continuously.
proxmox-optimize.shenables the weeklyfstrim.timer, which is correct for ordinary filesystems but will repeat a large discard against mostly-empty thin zvols. For bulk data volumes, addX-fstrim.notrimto the fstab options and run a singlefstrim <mountpoint>by hand while the host is idle. Batched discard is far cheaper than synchronous per-delete discard. -
Avoid
fstrim -aon guests holding bulk thin volumes: it ignores theX-fstrim.notrimexclusion.
The test suite runs on any machine with bash and apt. No Proxmox, no root, no network:
./tests/test-update-policy.sh # the n-0.1 rule, version arithmetic
./tests/test-disk-errors.sh # pool redundancy + SCT ERC detection
./tests/test-cleanup.sh # superseded-artefact classification
./tests/test-apt-pinning.sh # apt behaviour against a throwaway repoThe scripts under test are sourceable: sourcing defines their functions and returns without executing anything, so the decision logic can be driven directly.
test-apt-pinning.sh is the one that matters most. It builds a real apt
repository of throwaway packages at known versions, points an isolated apt root
at it, and asserts on apt-cache policy -- so "the pin actually holds" is
proven rather than assumed. The previous pinning implementation looked correct
and pinned almost nothing; only asserting against apt catches that.
Integration coverage that needs a real PVE install (UI patching, the APT hook, ZFS module parameters) is documented in tests/nested-pve.md.
All scripts are safe to run multiple times:
- Check current state before applying changes
- Skip already-configured settings
- Clear status messages (Already configured vs Newly configured)
- No data loss risk - all optimisations preserve data integrity
- Automatic backups - system configs backed up before changes
- Conservative defaults - reliability over performance
- Proxmox-aware - respects Proxmox's management of VMs and storage
- Error trapping (
set -e,trap) - Graceful degradation on non-critical failures
- Detailed error messages with line numbers
- Intel VT-x nested virtualisation
- Intel IOMMU (VT-d)
- Intel P-state driver
- Intel Turbo Boost control
- AMD-V nested virtualisation
- AMD IOMMU (AMD-Vi)
- AMD P-state driver (EPP mode)
- AMD Core Performance Boost
Tested on:
- Proxmox VE 9.x / Debian 13 (Trixie) only
- PVE 8.x may work but is untested
- Intel Xeon, Core i-series CPUs
- AMD EPYC, Ryzen CPUs
Requirements:
- x86_64 CPU with virtualisation extensions (Intel VT-x / AMD-V)
- IOMMU support (Intel VT-d / AMD-Vi) for device passthrough
- lm-sensors compatible CPU for thermal monitoring
sudo -i # Ensure root
chmod +x /path/to/script.sh
pveversion # Check Proxmox versiongrep GRUB_CMDLINE_LINUX_DEFAULT /etc/default/grub
update-grub && reboot
dmesg | grep -i iommu # Verify after rebootls /sys/devices/system/cpu/cpu0/cpufreq/
modprobe acpi-cpufreq # or amd-pstate / intel_pstate
systemctl status proxmox-power.servicezpool list # Verify ZFS installed
whoami # Check root
lsmod | grep zfs # Verify modules loadedapt-get install lm-sensors
sensors-detect --auto
sensorsThis project uses Semantic Versioning:
- MAJOR - Incompatible changes
- MINOR - Backwards-compatible additions
- PATCH - Backwards-compatible fixes
See CHANGELOG.md for history.
Contributions welcome. See CONTRIBUTING.md for details.
Quick checklist:
- Scripts remain idempotent
- Follow existing error handling patterns
- Test on Proxmox VE 9.x
- Update documentation
- Pass ShellCheck
Apache License 2.0 - see LICENSE.
Copyright 2025 HyperSec
Licensed under the Apache License, Version 2.0
Apache-2.0 covers this toolkit. It does not cover Proxmox VE.
Proxmox VE and proxmox-widget-toolkit are Copyright Proxmox Server Solutions
GmbH and licensed AGPLv3. Nothing here vendors, redistributes or relicenses
any part of them.
These scripts configure a Proxmox installation on the machine they are run on.
The UI customisation is an ExtJS override -- it calls public framework and
Proxmox.Utils entry points and adds its own class; it contains no Proxmox
source. The only change made to a Proxmox-owned file is a single <script> tag
in index.html.tpl, applied locally at install time and reversible with
proxmox-update-policy.sh disable.
These scripts modify system configuration. While designed to be safe and idempotent:
- Test in non-production first
- Review code before running on production
- Ensure you have backups
Use at your own risk.
- Issues: GitHub Issues
- Documentation: This README and inline script comments
- Proxmox VE Team
- Debian Project
- Community contributors