This guide documents how to enforce mutual TLS (mTLS) for the ProxmoxVEx admin path. The preferred implementation is performed at the reverse proxy, because Flask does not natively handle client-certificate verification.
Scope
The “admin path” includes all routes under:
/admin//api/admin//settings/(full settings modal backend)- Any endpoint that requires the
adminrole
Certificate authority
- Create or reuse an internal CA for admin client certificates.
- Issue client certificates to each admin user.
- Distribute the CA certificate to the reverse proxy.
Option A: NGINX reverse proxy
server {
listen 443 ssl;
server_name proxmoxvex.example.com;
# Server certificate and key
ssl_certificate /etc/nginx/ssl/proxmoxvex.crt;
ssl_certificate_key /etc/nginx/ssl/proxmoxvex.key;
# Client certificate verification
ssl_client_certificate /etc/nginx/ssl/admin-ca.crt;
ssl_verify_client on;
ssl_verify_depth 2;
# Admin routes require a valid client certificate and an admin role header
location ~ ^/(admin|api/admin|settings)/ {
if ($ssl_client_verify != SUCCESS) {
return 403;
}
proxy_pass http://proxmoxvex_backend;
proxy_set_header X-Admin-Client-DN $ssl_client_s_dn;
proxy_set_header X-Admin-Client-Serial $ssl_client_serial;
proxy_set_header X-Admin-mTLS-Verified true;
}
# Non-admin routes do not require client certificates
location / {
proxy_pass http://proxmoxvex_backend;
}
}
Option B: Traefik reverse proxy
http:
routers:
proxmoxvex-admin:
rule: "Host(`proxmoxvex.example.com`) && PathPrefix(`/admin/`, `/api/admin/`, `/settings/`)"
service: proxmoxvex
tls:
certResolver: default
options: admin-mtls
middlewares:
- admin-mtls
middlewares:
admin-mtls:
passTLSClientCert:
pem: true
And the TLS option:
tls:
options:
admin-mtls:
clientAuth:
caFiles:
- /etc/traefik/certs/admin-ca.crt
clientAuthType: RequireAndVerifyClientCert
Application-side enforcement
If the reverse proxy sets X-Admin-mTLS-Verified: true and X-Admin-Client-DN, ProxmoxVEx/app.py can optionally require these headers for admin routes:
from flask import request, abort
if request.path.startswith("/admin/") or request.path.startswith("/api/admin/"):
if request.headers.get("X-Admin-mTLS-Verified") != "true":
abort(403)
Operational notes
- Keep client certificate validity short (e.g., 90 days) for high-risk admin access.
- Revoke compromised certificates by removing the serial from the CRL or proxy allow-list.
- Log client DN and serial in the audit log for every admin request.
- Do not expose the admin path on a public internet address without mTLS or a bastion.