Proxmox provisioner backend
Run OpenClaw or a prepared Hermes image in an unprivileged Proxmox LXC. This execution target is experimental and requires an operator-run smoke test on real hardware before production use.Proxmox is an opt-in execution target for private infrastructure operators. Nora creates an unprivileged LXC through the Proxmox HTTPS API, then uses SSH with pinned host-key verification to bootstrap and operate the runtime through
pct.
Docker and Kubernetes remain the GA onboarding paths. Including proxmox in ENABLED_BACKENDS
exposes the target and its readiness state; missing or invalid security settings keep it unavailable
for deployment until the operator fixes the reported preflight issue.
Supported runtime paths
Hermes defaults to
/opt/hermes/.venv/bin/hermes. Override PROXMOX_HERMES_BIN when your prepared image installs the executable elsewhere. Its dashboard binds to loopback by default; the lifecycle and runtime API remain available to Nora without exposing the dashboard on the LXC network.
Secure configuration
Enable Proxmox alongside Docker while you validate it:PROXMOX_TOKEN_ID must use user@realm!tokenname API-token syntax. The API URL must use HTTPS and must not contain embedded credentials, query parameters, or a fragment. PROXMOX_NODE accepts letters, numbers, dots, underscores, and hyphens; PROXMOX_SSH_PORT must be an integer from 1 through 65535. Nora reads the configured CA and private-key files during catalog preflight, so an unreadable or empty file keeps the target unavailable instead of failing after a deployment is queued.
Get the SSH host-key fingerprint from a trusted network or console path, verify it out of band, and then copy the SHA256:... value:
PROXMOX_CA_CERT and PROXMOX_SSH_PRIVATE_KEY inline instead of file paths. File mounts are usually easier to rotate and keep out of process listings and shell history. Password-based SSH is supported as a fallback, but key authentication plus a pinned host fingerprint is preferred.
Required access
The API token must be authorized on the selected node, storage, and bridge to create, inspect, start, stop, restart, configure, and delete LXCs. The SSH account must be allowed to runpct non-interactively.
Root SSH is the supported direct path for stopped-LXC reconciliation. A non-root SSH account needs a separately installed, narrowly scoped helper configured through PROXMOX_OFFLINE_STAGE_COMMAND. The value must be one absolute executable path; Nora rejects missing helpers, generic sudo sh -c, arguments, traversal, and arbitrary shell prefixes during catalog preflight. PROXMOX_SUDO still applies to fixed pct lifecycle commands and to invoking the strict helper, but generic sudo access is not sufficient for safely editing a stopped root filesystem. If neither root SSH nor the strict helper is available, the target remains unavailable and start or restart cannot run stale credentials.
PROXMOX_PCT_COMMAND accepts only one command name or absolute executable path, without arguments.
For a non-root SSH account, leave PROXMOX_SUDO unset to use sudo -n, or set it to exactly
sudo -n or an absolute sudo path followed by -n. Nora rejects extra options, whitespace-delimited
commands, and shell metacharacters before opening SSH.
Non-root offline staging helper contract
Nora does not install this privileged helper. The operator must audit and install it on the Proxmox host as a root-owned executable that the SSH account cannot modify. Grant non-interactive sudo access to that one executable path only; do not grant a general shell or unrestrictedpct mount access.
Nora invokes the helper through the validated PROXMOX_SUDO prefix with exactly three arguments:
1 when Nora is applying an authoritative managed-state replacement and 0
for a scoped merge. Standard input is a tar archive containing only regular files:
env.patch: desiredNAME=BASE64_VALUEentries, one per line;env.keys: the exact environment names to remove before applyingenv.patch;env.scope: Nora-managed names that must remain in the managed-name registry;- OpenClaw:
build-auth.js,prestart.sh,prestart-reconcile.js,provider-bootstrap.sh,mcp-wrapper, anddropin.conf; - Hermes:
prestart.shanddropin.conf.
0600, 0700, 0755, or
0644 modes, unmount successfully, release the lock, remove all staging data, and return nonzero on
any ambiguity. It must never print archive contents or decoded credentials. Nora rechecks ownership,
lock state, and stopped status after the helper exits; a missing or nonzero confirmed exit blocks the
subsequent start.
Created containers are unprivileged and use DHCP on PROXMOX_BRIDGE. Ensure the bridge can provide an address and that the LXC can reach the package registries and providers required by the selected runtime.
Prepared Hermes template
Set the explicit template reference after building and validating it:- systemd and
base64; - an unprivileged
hermesuser and group; - an executable Hermes installation at
PROXMOX_HERMES_BINor onPATH; - network access required by the configured model and integrations.
hermes. The Hermes dashboard now always requires authentication: Nora injects a per-agent basic-auth credential into the runtime and its embed proxy signs in on the operator’s behalf, so the dashboard is reached only through Nora’s authenticated proxy. Setting PROXMOX_HERMES_ENABLE_INSECURE_DASHBOARD=true changes only the dashboard bind from 127.0.0.1 to 0.0.0.0 (it no longer disables authentication); leave it false unless you have a separate trusted network boundary.
Before publishing the template, instantiate a disposable LXC from it and validate the exact binary
path plus the unprivileged runtime account. Replace 9000 and the provider hostname with your test
VMID and provider:
PROXMOX_HERMES_BIN differs, substitute that exact path in both checks. Also perform one
credential-free TCP/TLS reachability check for every provider or integration host required by the
deployment. Do not publish the template until the LXC stops cleanly and all checks pass as the
hermes user where applicable. The protected smoke remains mandatory after uploading the resulting
archive to the configured vztmpl storage.
Credential staging and restart safety
Nora stores mutable runtime credentials only in mode-0600 managed state. The OpenClaw and Hermes systemd units use secret-free ExecStartPre hooks to rebuild Nora-owned auth, model, and environment state immediately before each launch. Generated service scripts do not contain provider keys.
When an LXC is stopped, Nora verifies ownership and stopped status, mounts the root filesystem with pct mount, stages a root-only archive over SSH stdin, preserves the unprivileged-container ownership mapping, unmounts, and confirms that no mounted lock remains. A mount, unmount, SSH, ownership, path-confinement, or completion ambiguity blocks the subsequent start. This is why provider rotation and deletion remain fail closed even for a stopped Proxmox runtime.
Verification
First verify that the control plane reports the target as enabled, configured, available, and experimental:PROXMOX_SMOKE_RUNTIME_FAMILIES=openclaw,hermes only after PROXMOX_HERMES_TEMPLATE is ready. The smoke verifies create, readiness, exec, live environment rotation, restart, logs, stopped-LXC offline environment replacement, start, and destroy. API operations fail if Proxmox omits the task id or final task exit status, and exec checks require a confirmed integer remote exit code. Set PROXMOX_SMOKE_KEEP_ON_FAILURE=true only when you intentionally want to preserve a failed LXC for diagnosis.
The wrapper forwards host-side PROXMOX_* and PROXMOX_SMOKE_* values into the one-off process in worker-provisioner. This means the command-prefix overrides above take effect even when the running container was created earlier, and credential values do not need to be appended to the control-plane .env file.
External real-hardware release gate
The repository includes the manually dispatched Proxmox Real Hardware workflow. It runs only on a self-hosted Linux x64 runner labelednora-proxmox, uses the protected proxmox-real-hardware GitHub Environment, and creates and destroys real LXCs. It is intentionally not a pull-request or GitHub-hosted CI job: the runner must have Docker Compose plus network access to the target Proxmox node.
Configure these GitHub Environment secrets:
PROXMOX_API_URL,PROXMOX_TOKEN_ID, andPROXMOX_TOKEN_SECRET;PROXMOX_SSH_HOST,PROXMOX_SSH_USER, andPROXMOX_SSH_HOST_FINGERPRINT;- either
PROXMOX_SSH_PRIVATE_KEY(plus optionalPROXMOX_SSH_PRIVATE_KEY_PASSPHRASE) orPROXMOX_SSH_PASSWORD; - optional
PROXMOX_CA_CERTwhen the API uses a private CA.
PROXMOX_NODE,PROXMOX_TEMPLATE,PROXMOX_ROOTFS_STORAGE, andPROXMOX_BRIDGE;- an exact
PROXMOX_OPENCLAW_PACKAGEregistry spec, such asopenclaw@2026.6.11; PROXMOX_HERMES_TEMPLATEwhen dispatching theopenclaw,hermesmatrix;PROXMOX_OFFLINE_STAGE_COMMANDwhenPROXMOX_SSH_USERis notroot;- optional SSH port, sudo/pct command, Node major, and Hermes binary overrides.
latest, other dist-tags, ranges such as ^
or ~, aliases, URLs, and Git references are rejected. The workflow forwards and records the exact
string; it does not resolve or rewrite it before the smoke. This keeps a later rerun from silently
qualifying different package contents under a floating selector.
The hardware runner’s SSH identity must also be able to resolve each selected template with
pvesm path and read it with sha256sum. Root SSH satisfies that requirement directly. For a
non-root identity, provide direct read access or narrowly scoped, non-interactive sudo rules for
those two read-only commands in addition to the lifecycle and offline-staging permissions described
above. Do not grant a general shell.
The workflow hard-codes verified TLS and pinned SSH transport, validates that the required external
values exist without printing secrets, starts an isolated Nora control-plane project, and then calls
the same destructive smoke script. It hashes every selected template before and after the smoke and
fails qualification if a reference changed contents during the run.
Every run emits a proxmox-qualification-<run-id>-<attempt> artifact containing a nonsecret JSON
record with:
- the candidate commit, workflow URL, run ID, and attempt;
- the PVE version when the API or host CLI exposes it;
- the node, root-filesystem storage, bridge, template references, and template SHA256 values;
- selected runtime families, the exact runtime package specs, SSH authentication/privilege mode, and
keep_on_failurevalue; - each smoke cell’s result and LXC cleanup evidence, plus the isolated Compose teardown result.

