pve-agents
Security

Credentials

What is held where, what reaches a workspace, and the trade-off settled least comfortably.

WhatWhere it livesReaches a workspace as
Proxmox API token.envnever leaves the controller
GitHub App keyfile on disk, 0600a repo-scoped token, renewed hourly
Claude subscription token.env~/.config/agent-env, sourced by the runner

Both workspace credentials arrive on stdin, never as arguments, so neither appears in the process list. Git reads its credential from a store rather than a URL, because git repeats the remote it was using in its errors and those reach the timeline the UI renders.

ssh does not preserve argument boundaries — it joins the command and the remote shell re-splits it — so everything sent is quoted before it leaves.

Proxmox

Use a dedicated Proxmox user and privilege-separated API token. The controller holds it and nothing else does: it never reaches a workspace, and no agent can ask for it.

VM.Audit
VM.Clone
VM.Allocate
VM.PowerMgmt
VM.Config.Options
VM.Config.CPU
VM.Config.Memory
VM.Config.Network
Datastore.AllocateSpace
SDN.Use

Add VM.Config.Disk only if the controller changes disk configuration.

Scope permissions to the golden template (audit and clone), the pre-created disposable-workspaces pool, the clone storage, and the specific bridge or VNet. Do not grant broad administrator, console, permission-management, or host-modification privileges.

The template

Do not bake secrets into the LXC template.

SafeUnsafe
Controller SSH public keyProxmox API tokens
SSH user CA public keyGitHub access tokens or private deploy keys
SSH host CA public keyModel-provider API keys
Public TLS trust rootsClaude, Codex, or GitHub login state
Non-secret bootstrap configuration

LXC cloning does not provide QEMU-style cloud-init, and Proxmox does not expose a general REST pct exec or pct push. The initial bootstrap channel must therefore be prepared in the template or supplied through a separately controlled Proxmox-host mechanism.

Bake a dedicated controller public key for the non-root agent account. Keep its private key only on the hosted controller.

Git

GitHub App installation tokens, minted per repository and lasting an hour.

  1. Mint a token scoped to that repository alone.
  2. Store it with git's credential.helper store, written from stdin so it is never an argument and never appears in ps on the workspace.
  3. Clone over HTTPS from a credential-free URL and let git read the stored value itself.
  4. Replace the stored credential before the hour is up, for as long as the workspace lives.

The token is stored rather than used once and deleted, which is a deliberate departure from the obvious design. The agent pushes later under its own steam, and a workspace outlives an installation token several times over. A refresh pass replaces it on a living workspace without touching its repository.

Never put the token in the URL. git repeats the remote it was using in its error text, and that text reaches the workspace timeline the UI renders. Everything git prints is scrubbed of the token at the boundary too, because one of those paths will be missed eventually.

Avoid forwarding the user's general SSH agent into an autonomous workspace. It grants broader signing and repository access than the task usually needs.

Model provider

Provider authentication is the largest unavoidable trade-off here, and the one that was settled least comfortably.

What is used: a Claude subscription OAuth token, held in .env on the controller and written into each workspace as ~/.config/agent-env, which the runner sources explicitly. It arrives over stdin, never as an argument. An API key was available and was deliberately not chosen.

It is the fourth option on the list below, and it is chosen knowingly: this is a single-operator deployment, the token is reusable rather than task-scoped, and any workspace that gets it can spend against the subscription. The container being disposable is what bounds that, not the credential.

Better, in order, if this ever stops being a single-operator tool:

  1. Short-lived, task-scoped provider credential.
  2. Controller-side credential broker that exchanges a workspace identity for limited access.
  3. Narrowly scoped API key injected as a mode 0600 file.
  4. Reusable login state, which is what is in use.

Track which credential classes were injected, never their values. Remove credential files during destruction even though deleting the LXC also removes its filesystem.

Last updated on

On this page