Security¶
Security reference for the control plane: the threat model, enrollment and credential machinery, middleware guarantees, audit events and the operator checklist.

Threat model¶
| Actor | Can do | Must NOT be able to |
|---|---|---|
| Panel user (Client API) | Manage own instances. | See or touch others' instances (existence never leaked: 404, not 403). |
| Application API key | Read/write per its scope. | Exceed its ACL level; obtain it without a manual grant. |
| Hypervisor agent | Heartbeat, push metrics, claim and drive its own tasks. | Impersonate another hypervisor, touch foreign tasks (404), inject command strings, replay old requests. |
| Whoever holds an enrollment token | Enroll once, within the TTL. | Reuse the token, enroll after expiry/revocation, enumerate tokens (hashes only). |
| Proxmox API token (operator-scoped) | What PVE ACLs allow. | Exceed least privilege (operator duty). |
| Database reader | See rows. | Recover any secret: credential secrets are bcrypt, enrollment tokens are SHA-256, driver configs are encrypted. |
Out of scope for this stage: per-request HMAC signing and mTLS — the deliberate future hardening path.
No-arbitrary-shell guarantee¶
Agents receive exactly one thing: validated task payloads. A per-type validator whitelists keys; unknown keys are rejected at task creation, and no schema anywhere carries a command string, path or shell fragment. There is no "run this" field in the protocol, and the agent API exposes no endpoint that accepts one. Agent-reported capability strings are intersected with an allow-list before persisting.
Root-password policy¶
Regenerating the root password generates a fresh random password, returns it once in the response, and never stores it — no column exists for it. The operation is recorded for audit. Applying the password inside the guest is a cloud-init/agent concern; until then the panel generates and displays but does not inject.
What is never logged¶
- Raw enrollment tokens — only the SHA-256 hash is stored.
- Credential secrets — never stored (bcrypt only), never logged.
- Proxmox token secrets — stored encrypted; API errors are re-thrown with the
secret replaced by
[redacted]. - Extension exception messages never contain internals.
Audit events¶
| Event | When |
|---|---|
cloud:agent.enrolled |
Successful enrollment. |
cloud:agent.enrollment_token_reused |
Burned token presented again. |
cloud:agent.authentication_failed |
Failed credential verification. |
cloud:agent.revoked |
Credential revoked. |
cloud:hypervisor.* |
Registration, health transitions, maintenance. |
cloud:instance.* |
Full control-plane audit via the activity bridge. |
Operator checklist: Proxmox tokens (least privilege)¶
- Create a dedicated PVE user (for example
pterocloud@pve) and API token. Prefer--privsep 0so the role is the single source of truth. - Grant exactly the privileges the driver needs — see Proxmox permissions.
- Scope to
/nodes/{node}and the storage path — never grant/unless required. - Use
pinned_fingerprintor a correct CA; never disable TLS verification. - Rotate by registering a new token, updating the connection, then deleting the old token in PVE.
- Keep the VMID start above every manually created VMID so panel-allocated VMIDs never collide with hand-made VMs.
Future hardening path¶
- HMAC request signing — sign method+path+timestamp+body with the credential secret, making the timestamp mandatory and killing replay without trusting TLS alone. Header names and the timestamp window are already in place.
- mTLS — per-hypervisor client certificates as an alternative to bearer secrets.
- Nonce store — deliberately skipped now; HMAC + timestamp makes replay windows small enough that a nonce table adds little.
Related¶
- License security
- Troubleshooting: security
PC-SEC-###/PC-AGT-###in the error reference