🔒 Proxmox · SSH

SSH Public Key Authentication

Password-free access across your lab — client to Proxmox host, client to guest VMs, and Proxmox to guest VMs.

ssh-keygen ssh-copy-id authorized_keys Proxmox VE Ubuntu

Overview

This guide covers three connection paths:

  • Client → Proxmox host — so you can manage the hypervisor from your Mac without a password
  • Client → Guest VMs — direct access from your Mac to any VM, either via cloud-init injection at provisioning time or manually after the fact
  • Proxmox → Guest VMs — useful for automation, scripts, or backups running on the Proxmox host that need to SSH into guests

Prerequisites

  • A client machine (Mac or Linux) with SSH installed
  • SSH access to the Proxmox host (password-based initially, to bootstrap)
  • At least one guest VM running and reachable on the network

1 Generate a Key Pair

Run this on your client machine (Mac). Skip if you already have a key at ~/.ssh/id_rsa or ~/.ssh/id_ed25519.

# Check for an existing key pair first
ls ~/.ssh/id_*.pub

# Generate a new Ed25519 key (preferred — smaller and faster than RSA)
ssh-keygen -t ed25519 -C "{your_email_or_label}"

# Or RSA 4096 if you need broader compatibility
ssh-keygen -t rsa -b 4096 -C "{your_email_or_label}"

Accept the default path when prompted. Set a passphrase or leave it empty. On macOS, the key is stored in the Keychain and you won't be prompted after the first use.

# Print your public key — this is what you copy to remote hosts
cat ~/.ssh/id_ed25519.pub

The output looks like: ssh-ed25519 AAAA... your_label. This is the string you'll distribute. The private key (id_ed25519, no .pub) never leaves your machine.

2 Client → Proxmox Host

Push your public key to the Proxmox host using ssh-copy-id. This appends your key to /root/.ssh/authorized_keys on the host.

# Copy your public key to the Proxmox root account
ssh-copy-id root@{PROXMOX_IP}

You'll be prompted for the root password once. After that, test key-based login:

# Verify — should connect without a password prompt
ssh root@{PROXMOX_IP}

Manual Method (if ssh-copy-id isn't available)

# Append your public key over SSH in one line
cat ~/.ssh/id_ed25519.pub | ssh root@{PROXMOX_IP} "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"

3 Disable Password Auth (Optional Hardening)

Run this on the Proxmox host after confirming key-based login works:

# Harden SSH — disable password and root password auth
sed -i 's/^#\?PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config
sed -i 's/^#\?PermitRootLogin.*/PermitRootLogin prohibit-password/' /etc/ssh/sshd_config

# Reload SSH daemon to apply changes
systemctl reload sshd

# Verify the settings took effect
grep -E 'PasswordAuthentication|PermitRootLogin' /etc/ssh/sshd_config

PermitRootLogin prohibit-password keeps root login available via key but rejects password attempts. This is appropriate for a home lab where you need root access but want to close the password attack surface.

4 Client → Guest VMs

Two paths depending on whether the VM already exists.

New VM — Inject via Cloud-Init (preferred)

When provisioning with cloud-init, the public key is baked in before first boot. The VM is SSH-ready the moment it comes up, with no post-boot steps.

# Write your public key to a temp file on the Proxmox host
echo "{YOUR_PUBKEY}" > /tmp/vm-key.pub

# Inject the key during cloud-init configuration (run on Proxmox host)
qm set {VMID} --sshkeys /tmp/vm-key.pub

# Clean up
rm /tmp/vm-key.pub

See Ubuntu Cloud-Init VM deployment guide for the full provisioning workflow, including how this fits into Step 6.

Existing VM — Push Key Manually

For a VM that's already running, use ssh-copy-id from the client the same way as with the Proxmox host:

# Copy your client public key to a running guest VM
ssh-copy-id {username}@{VM_IP}

# Verify
ssh {username}@{VM_IP}

Hardening Guest VMs

Same sshd_config changes apply to guest VMs. Run the same sed commands from the previous step inside the VM once key access is confirmed.

5 Proxmox → Guest VMs

If you want Proxmox itself to SSH into guest VMs — for backup scripts, health checks, or automated tasks — you need a separate key pair on the Proxmox host and its public key in each guest's authorized_keys.

Generate a Key Pair on Proxmox

# Run on the Proxmox host as root
ssh-keygen -t ed25519 -C "proxmox-host" -f ~/.ssh/id_ed25519 -N ""

# Print the Proxmox host's public key
cat ~/.ssh/id_ed25519.pub

The -N "" sets an empty passphrase, which is appropriate for automated/non-interactive use on a host you already control.

Push the Proxmox Key to a Guest VM

The guest needs to be running and reachable. If you already have client → VM access set up, you can relay through the client. Or use ssh-copy-id directly from Proxmox if the guest still allows password login:

# Push Proxmox host key to a guest VM (run on Proxmox host)
ssh-copy-id {username}@{VM_IP}

# Test the connection from Proxmox to the guest
ssh {username}@{VM_IP} "hostname && uptime"

Relay via Client (if guest has no password auth)

If the guest already has password auth disabled, use your client to append the Proxmox public key:

# From your client Mac — fetch Proxmox's pubkey and push it to the guest
PROXMOX_PUBKEY=$(ssh root@{PROXMOX_IP} "cat ~/.ssh/id_ed25519.pub")
ssh {username}@{VM_IP} "echo '$PROXMOX_PUBKEY' >> ~/.ssh/authorized_keys"

6 SSH Config Aliases

Putting hosts in ~/.ssh/config means you never type an IP again. ssh proxmox, ssh ollama — that's it.

Client ~/.ssh/config

# Add to ~/.ssh/config on your client Mac

Host proxmox
    HostName {PROXMOX_IP}
    User root
    IdentityFile ~/.ssh/id_ed25519

Host ollama
    HostName {VM_IP}
    User {username}
    IdentityFile ~/.ssh/id_ed25519
# Test your aliases
ssh proxmox "pveversion"
ssh ollama "hostname"

Proxmox ~/.ssh/config (for Proxmox → VM access)

# Add to ~/.ssh/config on the Proxmox host

Host ollama
    HostName {VM_IP}
    User {username}
    IdentityFile ~/.ssh/id_ed25519

After this, scripts running on Proxmox can call ssh ollama "command" without any IP or credential management.

Troubleshooting

Permission Denied (publickey)

Almost always a file permissions issue. SSH is strict — if authorized_keys or the .ssh directory is world-writable, the key will be silently rejected.

# Fix permissions on the remote host
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys
chown -R $USER:$USER ~/.ssh

Wrong Key Being Offered

# Run with verbose output to see which key is being tried
ssh -v {username}@{HOST_IP}

# Force a specific identity file
ssh -i ~/.ssh/id_ed25519 {username}@{HOST_IP}

Key Not in authorized_keys

# Check what keys are authorized on the remote host
cat ~/.ssh/authorized_keys

# Compare against your local public key
cat ~/.ssh/id_ed25519.pub

SELinux / AppArmor Context (rare on Ubuntu)

On some hardened systems, SELinux can block authorized_keys reads even with correct file permissions. Check /var/log/auth.log on the remote host for detailed rejection reasons:

# Check SSH auth log on the remote host for rejection details
sudo tail -50 /var/log/auth.log | grep sshd

Post-Quantum KEX & Legacy Hosts

OpenSSH 8.5 introduced sntrup761x25519-sha512@openssh.com as its default key exchange algorithm — a hybrid that combines a classical elliptic-curve exchange with a post-quantum algorithm. OpenSSH 9.9 (shipped with macOS Sequoia and current Linux distributions) promoted mlkem768x25519-sha256 (ML-KEM, from NIST FIPS 203) to the top of the default preference list.

When your client is newer than the server, you may see warnings, algorithm negotiation failures, or silent fallback depending on how large the version gap is. Here's what's happening, what the risks actually are, and what to do about it.

What the Warnings Look Like

A full failure — when the server doesn't support any algorithm in the client's list:

Unable to negotiate with {HOST_IP} port 22: no matching key exchange method found.
Their offer: diffie-hellman-group14-sha1,diffie-hellman-group-exchange-sha256

A negotiation warning with verbose output (ssh -v):

debug1: kex: server->client cipher: aes128-ctr MAC: hmac-sha1 compression: none
debug1: expecting SSH2_MSG_KEX_ECDH_REPLY
# Client fell back from mlkem768x25519 to curve25519 silently

Silent fallback is the common case when the server is only slightly behind — the connection works but you're not getting the algorithm you thought you were. You'd only notice this with ssh -v or when auditing.

Check What Your Server Actually Supports

# Query the server's supported KEX algorithms without connecting
ssh -vv {username}@{HOST_IP} 2>&1 | grep -i "kex\|key exchange"

# Or check directly on the server
sshd -T | grep kexalgorithms

How to Suppress the Warnings

Per-host in ~/.ssh/config — the right approach when targeting a specific legacy machine:

# Target only the specific host that needs the fallback
Host old-server
    HostName {HOST_IP}
    User {username}
    # Remove post-quantum algorithms from the offer for this host only
    KexAlgorithms -mlkem768x25519-sha256,-sntrup761x25519-sha512@openssh.com

Per-connection, one-off override:

# Force classical KEX for a single connection
ssh -o KexAlgorithms=curve25519-sha256,ecdh-sha2-nistp521 {username}@{HOST_IP}

Global suppression in ~/.ssh/config — applies to all hosts, which is almost never the right answer:

# Global override — affects every connection. Think carefully before using this.
Host *
    KexAlgorithms -mlkem768x25519-sha256,-sntrup761x25519-sha512@openssh.com

Why You Should Think Carefully Before Suppressing

The risks of suppressing post-quantum KEX are real and worth laying out clearly:

  • Long-lived secrets become vulnerable. SSH sessions often carry credentials, private keys, and configuration data. If the session is recorded and later decrypted, everything in it is exposed — not just the connection itself but every secret transmitted during it.
  • The fallback is permanent for that host. Once you add a KexAlgorithms override, it stays. You're committing to weaker cryptography every time you connect to that host until someone removes the config line and upgrades the server.
  • The warning is useful signal. It's telling you the server is running outdated software. Suppressing the warning doesn't fix that — it just removes the reminder. The underlying gap remains.
  • Global suppression spreads the risk everywhere. A Host * override disables post-quantum protection for every host you connect to, including future hosts that do support it. You've downgraded your entire SSH posture, not just the one legacy server.
  • Lab habits become production habits. If you routinely silence crypto warnings in your home lab, that reflex carries over. Warnings exist to be acted on, not routed around.

The Right Fix: Update the Server

Post-quantum KEX support is available in OpenSSH 8.5+ (for sntrup761) and 9.9+ (for ML-KEM). Ubuntu 24.04 ships OpenSSH 9.6, which supports sntrup761x25519-sha512. Proxmox VE 8.x on Debian 12 ships OpenSSH 9.2. Neither of these should be generating KEX warnings against a current macOS client.

If you're seeing warnings, check the server version first:

# Check OpenSSH version on the server
ssh --version
sshd --version

# Update OpenSSH on Ubuntu/Debian
sudo apt update && sudo apt upgrade openssh-server

For genuinely legacy hosts that cannot be updated — old network equipment, embedded systems, inherited infrastructure — scoped per-host suppression is the pragmatic choice. Apply it to the minimum set of hosts that actually need it, document why, and review it periodically. Don't let a temporary workaround quietly become permanent policy.

Changelog

  • 2026-03-19 — Add post-quantum KEX section: warnings, risks, suppression methods, and why to update the server instead
  • 2026-03-19 — Initial guide: key generation, client-to-Proxmox, client-to-VM, Proxmox-to-VM, SSH config aliases, troubleshooting