Skip to content
cybersecurity Intermediate 30-60 minutes · 10 min read

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.

Environment: Proxmox VE 8 (Debian 12)
iptables iptables-persistent openssh sudo
Proxmox VE Node Hardening: SSH Lockdown and Non-Root Admin Access

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

PVE Realm vs PAM Realm: Proxmox has two authentication realms. PVE stores credentials in Proxmox's internal database and works for GUI login only: no SSH, no shell. PAM authenticates against the Linux OS and supports SSH, shell, and GUI. PAM requires a matching local user account on each node.
Bastion Server: A hardened jump host that sits between your workstation and your internal machines. Instead of SSH-ing directly to every server, you connect to the bastion first. One hardened door means one thing to audit, monitor, and lock down. Full definition
iptables INPUT chain: The set of rules controlling what traffic is allowed to reach the local machine. Rules are evaluated top-down; the first match wins and processing stops. Order is not a preference; it determines what gets in.
PAM Authentication: Pluggable Authentication Modules, the Linux subsystem that handles login. When Proxmox uses PAM authentication, it delegates credential verification to the OS. The user must exist as a real Linux account on every node they log into.

What You Will Build

By the end of this guide you will have:

  • A non-root admin user with sudo access 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.

Missing Linux account:

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.

Test from an external machine before proceeding:

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 INPUT chain: top-down, first match winsRule 1ACCEPTtcp --dport 22 -s BASTION_IPBastion onlySSH: connectedno match, next ruleRule 2ACCEPTtcp --dport 22 -s PEER_NODE_IPPeer nodesSSH: connectedno match, next ruleRule 3DROPtcp --dport 22all other sourcesSSH: blockedno match, next ruleRule 4ACCEPTtcp --dport 8006all sourcesGUI: open (failsafe)Rule 4 is intentional: if SSH is misconfigured, the web GUI stays open for recovery.
iptables evaluates rules top-to-bottom and stops at the first match. The bastion and peer nodes match Rules 1 and 2 and get SSH. Everything else falls through to the DROP.
Rule order is not optional:

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-ip should be refused; your workstation IP is not in the allowlist

From the bastion server:

  • ssh yourusername@node-ip should 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:8006 should 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.

Static IP required before locking the GUI:

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

Locked out after iptables change:

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.

iptables rules lost after reboot:

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.

Proxmox built-in firewall conflict:

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.

Dropped into sh instead of bash:

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

LayerBefore this guideAfter Part 1-4After Part 5 (full lockdown)
SSH userrootNon-root with sudoNon-root with sudo
Root SSHEnabledDisabledDisabled
SSH sourcesAnyBastion + peer nodesBastion + peer nodes
Web GUI accessOpenOpen (failsafe)Workstation IP only
Rule persistenceNoneiptables-persistentiptables-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.

proxmox ssh iptables hardening homelab linux bastion security

Related Field Notes