ProxKey distributes SSH public keys across Proxmox LXC containers and QEMU VMs via the Proxmox API — no prior SSH access required. Create a container, click deploy, you're in.
Proxmox API for discovery, pct exec / qm guest exec
for injection. Idempotent, auditable, and zero-bootstrap by design.
Keys go in through Proxmox's pct exec and qm guest exec, not via SSH. Brand-new containers with no SSH access yet are first-class.
Every LXC and QEMU VM on the node appears in the host grid. Running hosts surface their IP addresses automatically via the Proxmox API.
The inject script grep -qFs before appending. Deploying the same key twice is a no-op. Revoke removes only matching lines.
Stats cards, host breakdown by type and status, key-coverage donut, and per-deployment metrics — updates as you deploy.
A Proxmox hookscript that POSTs to /api/hooks/lxc-started auto-injects every global key the moment a container boots.
See exactly which keys live on any host — pulled live from the guest's authorized_keys via the same exec channel.
Find hosts by name, VMID, or IP. Filter by status or type. Sort by any column. One-click ssh root@<ip> copy from the card.
Every deploy, revoke, and key CRUD event lands in the audit table with timestamp, host, key, and outcome.
Runs on the PVE node? pct exec direct. Runs anywhere else? SSH to the node first, then pct exec. Same wire, one extra hop.
One .env file, one docker compose up. Or build
from source if you want to hack on it.
Copy the example env file and fill in your Proxmox host and API token.
git clone https://github.com/harshithsunku/proxkey.git
cd proxkey
cp .env.example .env
# edit .env: PROXMOX_HOST, PROXMOX_TOKEN_ID, PROXMOX_TOKEN_SECRET, PROXMOX_NODE
On the PVE node, give ProxKey just enough to discover hosts and exec inside them.
pveum user add proxkey@pve
pveum aclmod / -user proxkey@pve -role PVEAuditor
pveum aclmod /vms -user proxkey@pve -role PVEAdmin
pveum user token add proxkey@pve proxkey --privsep 0
# copy the secret → PROXMOX_TOKEN_SECRET in .env
Backend + nginx-fronted frontend come up together. Open http://localhost:8080.
docker compose up -d
# UI: http://localhost:8080
# API: http://localhost:8000 (Swagger at /docs)
One-liner that POSTs the VMID to ProxKey whenever a container moves into the running state.
cat > /var/lib/vz/snippets/proxkey-hook.sh <<'EOF'
#!/bin/bash
VMID="$1"
PHASE="$2"
[ "$PHASE" = "post-start" ] || exit 0
curl -fsS -X POST "http://proxkey-host:8000/api/hooks/lxc-started" \
-H "Content-Type: application/json" \
-d "{\"vmid\": ${VMID}}" || true
EOF
chmod +x /var/lib/vz/snippets/proxkey-hook.sh
Per-container, or as part of your provisioning template.
pct set <vmid> --hookscript local:snippets/proxkey-hook.sh
Any container that boots with the hookscript attached now gets every global key written before the first login window opens.
cd backend
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
cp ../.env.example .env
uvicorn app.main:app --reload # http://localhost:8000
Vite proxies /api to the backend automatically.
cd frontend
npm install
npm run dev # http://localhost:5173
Backend autoreloads on save. The Vite dev server hot-reloads the UI. /api/docs on the backend has live Swagger.
PVEAdmin on /vms; QEMU VMs need
qemu-guest-agent running for IP discovery and
qm guest exec. If ProxKey runs off the PVE node, set
PROXMOX_SSH_PASSWORD so it can SSH in to call pct exec.
The browser hits the FastAPI backend. The backend talks to the
Proxmox API via proxmoxer to discover hosts and call
pct exec / qm guest exec. The injection
script is base64-encoded before being piped through the exec
channel so shell quoting can't mangle it. The script is idempotent:
grep -qF before echo >>.
Every deploy and revoke is recorded in SQLite — deployment state, audit log, and the key library all live in a single mounted volume.
Read the architecture →If a feature needs prior SSH access to the guest, it doesn't belong here. The Proxmox API is the only injection channel.
Deploy twice, revoke twice, re-fire a hookscript — none of it should corrupt state or produce duplicates.
Anything crossing SSH or pct exec is base64-framed. Never concatenate user input into a shell command.
MIT-licensed. No proprietary names, IPs, credentials, or company-specific anything in code, docs, or history.