Proxmox VE Node Hardening: SSH Lockdown and Non-Root Admin Access
Default Proxmox hands root SSH to anyone who can reach port 22. Three targeted layers close that gap: a non-root admin with sudo, root SSH disabled, and iptables restricting SSH to your bastion only.
A fresh Proxmox install authenticates with a single password over SSHA cryptographic protocol for secure remote login, command execution, and file transfer over an unsecured network. Read more → as root. One leaked credential and an attacker owns your entire hypervisorSoftware that creates and manages virtual machines by abstracting physical hardware resources and allocating them to isolated guest operating systems. Read more →: every VMA software-based emulation of a complete computer that runs its own operating system and applications, isolated from the host hardware. Read more →, every containerA lightweight, portable package that bundles an application with its dependencies and runs in an isolated process on the host OS, sharing the kernel. Read more →, every secret on the host. This guide applies three layers of hardening in order. By the end, the only path into your nodes is through a bastion serverA hardened server that acts as the single, controlled entry point for SSH access to an internal network. All connections to internal hosts must route through the Bastion, creating one audited choke point. Read more →, as a non-root user, with sudo required for privilege escalationAn attack where an adversary gains higher access permissions than originally granted, escalating from a normal user to administrator or root. Read more →. Root never touches the wire.
Concepts at a Glance
What You Will Build
By the end of this guide you will have:
- A non-root admin user with
sudoaccess on every Proxmox node - Root SSH login disabled across the cluster
- iptables rules restricting SSH to your bastion server and peer Proxmox nodes only
- PortA numbered endpoint on a device that identifies a specific application or service, allowing multiple network services to run on the same IP address. Read more → 8006 (web GUI) left open as a recovery failsafe
- Rules persisted across reboots via
iptables-persistent - An optional full-lockdown configuration restricting the web GUI to your management workstation
Prerequisites
Infrastructure:
- Proxmox VE cluster (single or multi-node)
- A bastion or jump server on your network with a known static IPA unique numerical label assigned to every device on a network, used to identify it and route traffic to the correct destination. Read more →
- Console access to each node (IPMI, iDRAC, or Proxmox web console): this is your last-resort recovery path
Access:
- Current root SSH access to every node in the cluster
- A workstation that can reach the Proxmox web GUI on port 8006
Required knowledge:
- Basic Linux command line (editing files, running commands as root)
- How to connect to a remote server over SSH
- Basic understanding of firewalls and port filtering
Part 1: Create the Admin User
Proxmox needs a non-root account that works for both GUI and SSH. PAM is the right realm. PVE-only accounts cannot log in over SSH; they have no Linux presence.
1a. Create the User in the GUI
Navigate to Datacenter > Permissions > Users > Add:
- User:
yourusername@pam - Realm: Linux PAM standard authentication
1b. Assign Cluster-Wide Admin Permissions
Navigate to Datacenter > Permissions > Add > User Permission:
- Path:
/ - User:
yourusername@pam - Role:
Administrator - Propagate: checked
This grants full admin rights across the entire cluster, not just the node you are configuring.
1c. Create the OS-Level Account on Every Node
The GUI step above registers the user with Proxmox. It does not create a Linux account. SSH to each node as root and run:
useradd -m yourusername
passwd yourusername
Repeat on every node in the cluster. PAM authenticates locally on whichever node you connect to, so the account must exist on each one.
If you skip this step and try to set the password from the GUI, you will get change password failed: user does not exist (500). The fix is to run useradd -m yourusername on the host and then set the password again.
1d. Install sudo and Set the Shell
Proxmox ships Debian minimal. sudo is not included by default.
apt install sudo
usermod -aG sudo yourusername
chsh -s /bin/bash yourusername
Log out and back in for group membership to take effect, then verify:
su - yourusername
sudo whoami
Expected output: root
If you see sudo: command not found, the package did not install. Run apt install sudo as root and retry.
Before you touch SSH config, open a second terminal and SSH into the node as your new user from a machine outside your current session. Confirm sudo works. Do not proceed to Part 2 until this is solid.
Part 2: Disable Root SSH
Only run this after confirming your non-root user can SSH in and sudo successfully. One locked-out node is recoverable via console. All nodes locked out simultaneously is a bad morning.
sudo sed -i 's/^PermitRootLogin yes/PermitRootLogin no/' /etc/ssh/sshd_config
sudo systemctl restart sshd
Verify from a separate terminal:
ssh root@your-node-ip
This should now be denied. If root SSH still connects, the sed pattern did not match. Inspect the file directly:
grep -i permitrootlogin /etc/ssh/sshd_config
The line may be commented out, absent, or have a different value. Edit it manually if needed, then restart sshd.
Repeat on every node, one at a time. Never make this change on all nodes in the same session.
Part 3: Restrict SSH with iptables
Lock port 22 to your bastion and peer Proxmox nodes. Everything else gets dropped.
Why Peer Nodes Need SSH Access
When you open a shell for Node B through Node A’s web GUI, Node A opens an SSH connection to Node B on your behalf. Block inter-node SSH and you break:
- GUI shell sessions to remote nodes
- Live VM and container migration
- Cluster synchronization
Peer nodes are not optional in the allowlist.
Apply the Rules
On each node, run the following with your actual IP addresses:
# SSH from bastion only
sudo iptables -A INPUT -p tcp --dport 22 -s BASTION_IP -j ACCEPT
# SSH from peer Proxmox node(s)
sudo iptables -A INPUT -p tcp --dport 22 -s PEER_NODE_IP -j ACCEPT
# Drop all other SSH
sudo iptables -A INPUT -p tcp --dport 22 -j DROP
# Web GUI accessible from anywhere (recovery failsafe)
sudo iptables -A INPUT -p tcp --dport 8006 -j ACCEPT
Make Rules Persistent
sudo apt install iptables-persistent
sudo netfilter-persistent save
During installation it will prompt to save current IPv4 and IPv6 rules. Accept both.
Verify the Rule Chain
sudo iptables -L INPUT -n --line-numbers
You should see exactly four rules in this order:
num target prot opt source destination
1 ACCEPT tcp -- BASTION_IP 0.0.0.0/0 tcp dpt:22
2 ACCEPT tcp -- PEER_NODE_IP 0.0.0.0/0 tcp dpt:22
3 DROP tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:22
4 ACCEPT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:8006
iptables evaluates top-down and stops at the first match. If your DROP rule lands before an ACCEPT, the ACCEPT will never fire. That source gets silently dropped. If rules are misordered or duplicated, flush the chain and start clean: sudo iptables -F INPUT. Then re-apply rules in the correct order and save.
Part 4: Validate the Full Chain
Run these checks before closing your root session.
From your workstation (not the bastion):
ssh yourusername@node-ipshould be refused; your workstation IP is not in the allowlist
From the bastion server:
ssh yourusername@node-ipshould connect successfully
From the Proxmox web GUI on Node A:
- Opening a shell tab for Node B should work; inter-node SSH is whitelisted
From your workstation:
https://node-ip:8006should load the Proxmox GUI; port 8006 is still open
If anything fails, run the following on the target node to see the actual source IP of incoming connections:
sudo ss -tnp | grep :22
Compare that IP against your iptables allowlist. Source IPs are the common failure point; NATA technique that translates private IP addresses to a public IP address so multiple devices on a local network can share a single internet connection. Read more →, proxies, and VPNs can change them.
Part 5: Lock Down the Web GUI
The rules above intentionally leave port 8006 open to all sources. This is a deliberate failsafe. If you misconfigure SSH and lose access, the web GUI lets you open a console session directly from your browser without SSH.
Once you are confident the bastion workflow is solid, restrict port 8006 the same way you restricted port 22:
# Remove the open 8006 rule
sudo iptables -D INPUT -p tcp --dport 8006 -j ACCEPT
# Insert a source-restricted 8006 rule at position 4
sudo iptables -I INPUT 4 -p tcp --dport 8006 -s YOUR_WORKSTATION_IP -j ACCEPT
# Drop all other 8006 traffic
sudo iptables -A INPUT -p tcp --dport 8006 -j DROP
# Save
sudo netfilter-persistent save
The final hardened ruleset looks like this:
num target prot opt source destination
1 ACCEPT tcp -- BASTION_IP 0.0.0.0/0 tcp dpt:22
2 ACCEPT tcp -- PEER_NODE_IP 0.0.0.0/0 tcp dpt:22
3 DROP tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:22
4 ACCEPT tcp -- WORKSTATION_IP 0.0.0.0/0 tcp dpt:8006
5 DROP tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:8006
Two services. Two access points. Everything else dropped.
Your workstation must have a static IP or DHCPA protocol that automatically assigns IP addresses and network settings to devices when they connect to a network. Read more → reservation before you restrict port 8006. If its IP changes, you lose GUI access. Make sure you still have IPMI, iDRAC, or physical console access before removing your last browser path into the box.
Common Pitfalls
This is why console access is listed as a prerequisite. If you lose SSH, connect via IPMI, iDRAC, or the Proxmox web console and flush the INPUT chain: sudo iptables -F INPUT. That removes all custom rules and restores full access. Then re-apply carefully.
Rules exist only in memory until saved. Always run sudo netfilter-persistent save after any rule change. Without it, a reboot returns you to an open SSH port.
If you are also running Proxmox’s built-in datacenter or node-level firewall, you now have two rulesets evaluating the same traffic. Check its status with pve-firewall status. Running both can produce unpredictable results. Pick one approach and stick with it.
New users default to /bin/sh on Debian. If your shell looks wrong, fix it: sudo chsh -s /bin/bash yourusername. Log out and back in for the change to take effect.
Summary
| Layer | Before this guide | After Part 1-4 | After Part 5 (full lockdown) |
|---|---|---|---|
| SSH user | root | Non-root with sudo | Non-root with sudo |
| Root SSH | Enabled | Disabled | Disabled |
| SSH sources | Any | Bastion + peer nodes | Bastion + peer nodes |
| Web GUI access | Open | Open (failsafe) | Workstation IP only |
| Rule persistence | None | iptables-persistent | iptables-persistent |
Get comfortable with the bastion workflow before running Part 5. The failsafe is there for a reason. Tighten the GUI only after you have tested SSH from the bastion across multiple sessions. Always keep console access as your last resort; never remove the last path into the box.