Proxmox VE ships with a self-signed certificate. The browser warning it produces is not just an annoyance — users clicking through “Your connection is not private” warnings train themselves to ignore certificate errors, which is dangerous. More practically: in environments with multiple admins, the self-signed certificate means you cannot distinguish a legitimate Proxmox node from a rogue one without manually verifying the fingerprint every time. This article covers replacing the default self-signed certificate with a Let’s Encrypt or internal CA certificate, and automating renewal.
How Proxmox VE uses TLS
Proxmox VE uses TLS in several places:
- Web UI and API (port 8006) — served by
pveproxy - VNC/SPICE console access — proxied through
pveproxy - Cluster communication — uses its own certificates managed by
pvecm - Corosync — uses its own authentication
This article focuses on the web UI and API certificate (port 8006). Cluster certificates are managed separately and should not be replaced manually.
Where Proxmox stores the web UI certificate
The certificate and key used by pveproxy live at:
/etc/pve/local/pve-ssl.pem— the certificate (PEM format)/etc/pve/local/pve-ssl.key— the private key (PEM format)
/etc/pve/ is a cluster filesystem (pmxcfs). Writes to it are replicated across all cluster nodes. However, each node has a local/ directory that is node-specific — so you must replace the certificate on each node separately.
Backing up the current certificate before replacing it:
cp /etc/pve/local/pve-ssl.pem /root/pve-ssl.pem.bak
cp /etc/pve/local/pve-ssl.key /root/pve-ssl.key.bak
Option A: Let’s Encrypt using the built-in ACME client
Proxmox VE 6.1+ has a built-in ACME/Let’s Encrypt client accessible from the web UI and command line. This is the easiest approach when the Proxmox node has a public DNS record.
Via the web UI
- Log in to the Proxmox web UI at
https://your-node:8006. - Select your node in the left panel.
- Go to Certificates.
- Under ACME, click Add ACME Account and register with Let’s Encrypt.
- Add a domain — enter the node’s FQDN (e.g.,
pve1.example.com). - Choose a validation method:
- Standalone — opens port 80 temporarily (the node must be publicly reachable)
- DNS plugin — uses your DNS provider’s API to create TXT records (works behind firewalls)
- Click Order Certificates Now.
The certificate is automatically renewed 30 days before expiry by the pveproxy renewal service.
Via the command line
Register an ACME account:
pvenode acme account register default your@email.com --directory https://acme-v02.api.letsencrypt.org/directory
Add a domain and request the certificate:
# Using HTTP-01 challenge (requires port 80 to be reachable)
pvenode config set --acmedomain0 pve1.example.com
# Request the certificate
pvenode acme cert order
Check the result:
pvenode acme cert info
Using DNS challenge (for nodes behind a firewall)
Add the DNS plugin for your provider first. Proxmox supports many DNS providers (Cloudflare, Route53, etc.):
# Cloudflare example
pvenode acme plugin add dns cloudflare --api cf \
--data "CF_Token=your-cloudflare-api-token"
pvenode config set --acmedomain0 pve1.example.com,plugin=cloudflare
pvenode acme cert order
Option B: Installing a certificate from an internal CA or external provider
If you cannot use Let’s Encrypt (air-gapped environment, internal domain), replace the certificate manually.
Step 1: Generate a certificate
If using an internal CA:
# On the CA host — not on the Proxmox node
openssl genrsa -out pve1.key 4096
openssl req -new -key pve1.key -out pve1.csr \
-subj "/CN=pve1.example.com/O=example/C=US" \
openssl x509 -req -in pve1.csr \
-CA /etc/pki/ca/ca.crt -CAkey /etc/pki/ca/ca.key \
-CAcreateserial -out pve1.crt -days 365 \
-extfile <(echo "subjectAltName=DNS:pve1.example.com,IP:192.168.1.10")
Step 2: Copy the certificate and key to the Proxmox node
scp pve1.crt root@pve1:/tmp/
scp pve1.key root@pve1:/tmp/
On the Proxmox node:
# Copy to pveproxy certificate locations
cp /tmp/pve1.crt /etc/pve/local/pve-ssl.pem
cp /tmp/pve1.key /etc/pve/local/pve-ssl.key
# Set permissions
chown root:www-data /etc/pve/local/pve-ssl.key
chmod 640 /etc/pve/local/pve-ssl.key
chmod 644 /etc/pve/local/pve-ssl.pem
Step 3: Restart pveproxy
systemctl restart pveproxy
Verify:
openssl s_client -connect pve1.example.com:8006 -servername pve1.example.com </dev/null 2>&1 \
| openssl x509 -noout -subject -dates
Distributing the CA certificate to browsers and clients
After installing a certificate signed by an internal CA, browsers will still show a warning unless the CA is trusted. Deploy the CA certificate:
Windows (Group Policy):
Create a GPO that deploys the CA to Trusted Root Certification Authorities on all machines.
macOS:
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain ca.crt
Ubuntu/Debian:
sudo cp ca.crt /usr/local/share/ca-certificates/internal-ca.crt
sudo update-ca-certificates
Firefox (which uses its own cert store):
Firefox does not use the OS certificate store by default. Go to Settings → Privacy & Security → Certificates → View Certificates → Authorities → Import and import the CA certificate.
Automating renewal for manually installed certificates
For manually installed certificates from an internal CA, create a renewal script run by cron or systemd timer:
nano /usr/local/bin/renew-proxmox-cert.sh
#!/bin/bash
# Renew Proxmox VE TLS certificate from internal CA ACME endpoint or CFSSL
# Adapt to your CA's renewal mechanism
CA_API="https://ca.internal/api/v1/issue"
DOMAIN="pve1.example.com"
CERT_DIR="/etc/pve/local"
# Example: use cfssl or step-ca to issue a new cert
# Substitute with your CA's CLI or API call
# cfssl gencert -profile=server -ca=... -ca-key=... server.json | cfssljson -bare new-cert
# After issuing:
# cp new-cert.pem ${CERT_DIR}/pve-ssl.pem
# cp new-cert-key.pem ${CERT_DIR}/pve-ssl.key
# chown root:www-data ${CERT_DIR}/pve-ssl.key
# chmod 640 ${CERT_DIR}/pve-ssl.key
# systemctl restart pveproxy
echo "Update this script with your CA's renewal command"
Verify the certificate is being served correctly
# Check certificate details
curl -k https://pve1.example.com:8006 -v 2>&1 | grep -A5 "Server certificate"
# Check expiry
echo | openssl s_client -connect pve1.example.com:8006 2>/dev/null | \
openssl x509 -noout -enddate
Multi-node clusters: replace on each node
In a Proxmox cluster, each node serves its own certificate. /etc/pve/local/ is node-specific, so you must repeat the certificate installation on each node separately. The Let’s Encrypt built-in client runs per-node.
If you manage certificates centrally, create a script that deploys to all nodes:
for node in pve1 pve2 pve3; do
echo "Deploying certificate to ${node}..."
scp /tmp/${node}.crt root@${node}:/etc/pve/local/pve-ssl.pem
scp /tmp/${node}.key root@${node}:/etc/pve/local/pve-ssl.key
ssh root@${node} "chmod 640 /etc/pve/local/pve-ssl.key && systemctl restart pveproxy"
done
Troubleshooting
| Problem | Cause | Fix |
|---|---|---|
| Browser still shows old self-signed cert after restart | Browser cached the old cert | Clear browser TLS cache or test with curl |
pveproxy: error loading certificate | Wrong file format or permissions | Check PEM format with openssl x509 -in pve-ssl.pem -text -noout |
pveproxy fails to start | Invalid private key | Verify key matches cert: openssl x509 -noout -modulus -in pve-ssl.pem | md5sum vs openssl rsa -noout -modulus -in pve-ssl.key | md5sum |
| ACME order fails with port 80 blocked | Firewall blocks HTTP challenge | Use DNS challenge plugin instead |
| Certificate renewed but old cert still served | pveproxy not restarted | Run systemctl restart pveproxy |
Summary
Proxmox VE’s web UI certificate is stored at /etc/pve/local/pve-ssl.pem and /etc/pve/local/pve-ssl.key. The easiest replacement method is the built-in ACME client (pvenode acme cert order) with either HTTP or DNS challenge. For internal CAs, copy the certificate and key manually and restart pveproxy. In a cluster, each node’s certificate is independent — deploy and renew separately on each node. Distribute the CA certificate to all administrator browsers to eliminate the security warning completely.