Build-Time Secrets

Build-Time Secrets #

Two builds need a live Proxmox API token:

BuildWhereNeeds
Fedora VM templates (Packer)deevnet-image-factory: make proxmox-fedora-pve1 / -pve2the node’s API token
The tenant fabric (Terraform)deevnet-tenant-fabric: make fabric-init / fabric-plan / fabric-applythe tenant hypervisor’s API token

The rule: a build secret is fetched per run into the build’s own process environment. It is never written to a file and never passed on a command line ( Security Controls, CHG-0026). Until CHG-0026, the image factory rendered the token into build/pve-env/<node>.env. Those files outlived every build, and one of them, under a node’s old name, was still being read by the fabric weeks later.


How it flows #

inventory vault (authoritative)          host_vars/<hypervisor>/vault.yml: vault_proxmox_token_*
        โ”‚  scripts/pve-creds  (reads the vault in-process, prints exports, writes nothing)
        โ–ผ
eval "$(โ€ฆ)" in the make recipe           TF_VAR_proxmox_* in that shell only โ†’ packer / terraform
  • Builds read the vault, never OpenBao. OpenBao is sealed outside work sessions ( ADR-0042), and a template build or a fabric plan must not need it open ( CHG-0037).
  • scripts/pve-creds lives in deevnet-image-factory, and both repositories’ Makefiles call it. It prints export lines on stdout and nothing else, and writes nothing.
  • It uses Ansible’s own libraries in-process to read the hypervisor’s host_vars, so templated values resolve as they do in a play. It needs the vault password: ANSIBLE_VAULT_PASSWORD_FILE, or a prompt.
  • OpenBao holds no build credential. It has no image-factory mount, AppRole or policy; the openbao role deletes them if they reappear.

Everyday use #

# Build a template (fetches the credentials itself)
make -C deevnet-image-factory proxmox-fedora-pve2

# Plan the fabric
make -C deevnet-tenant-fabric fabric-plan

# Put the credentials in your own shell, for a hand-run packer or terraform
eval "$(make -s -C deevnet-image-factory pve2-env)"

Rotating a Proxmox token #

  1. On the node: pveum user token add terraform-prov@pve <new-name> --privsep 1. Note the secret; Proxmox shows it once. A separated token holds only what its own ACL entries give, as well as its user’s, so it starts with nothing.
  2. Put it in host_vars/<node>/vault.yml (vault_proxmox_token_id, vault_proxmox_token_secret). Then make vault, commit and push (see Vault Operations).
  3. Check it, without printing the token:
    eval "$(make -s -C deevnet-image-factory pve2-env)"
    curl -sk -o /dev/null -w '%{http_code}\n' \
      -H "Authorization: PVEAPIToken=$TF_VAR_proxmox_token_id=$TF_VAR_proxmox_token_secret" \
      "$TF_VAR_proxmox_url/version"                       # 200
    
  4. Give it the old token’s roles: in host_vars/<node>/vars.yml, rename the token in proxmox_node_access (its users entry and every ACL’s tokens), and run ansible-playbook playbooks/site.yml --limit <node> --tags proxmox-access in ansible-collection-deevnet.builder. Check step 3 again.
  5. Then delete the old token on the node: pveum user token remove terraform-prov@pve <old-name>. Its ACL entries go with it.

The Deevnet API has its own Proxmox token (vault_deevnet_api_proxmox_token_*, in OpenBao’s deevnet-api/backends). Rotating the build token doesn’t touch it.

When it fails #

SymptomMeans
<node> has no proxmox_token_id / proxmox_token_secret in the inventoryThe token isn’t in host_vars/<node>/vault.yml, or the host isn’t in the inventory under that name
inventory not foundDEEVNET_INVENTORY doesn’t point at the inventory’s mobile directory
The fabric plan shows changes to a node you didn’t touchCheck TF_VAR_proxmox_node. It now comes from PVE_HOST each run (default dv02hyp002p02), never from a leftover file
Page last modified: November 12, 2026