Skip to content

netbox-openbao — Proxbox credentials

The complete target architecture, migration matrix and setup runbook are in Audited Proxmox writes with RPC and OpenBao. Mandatory RPC writes and generic OpenBao-backed procedure variables are planned; the endpoint storage integration described here already exists.

Unavailable required credentials fail explicitly at secret-access boundaries. Password-reuse readiness and list views use one response-local metadata lookup restricted by the authenticated caller's reveal permission and the policy group gate. Missing, deleted, denied, or wrong-type references report SSH as unavailable without reading provider material or failing the entire response. SSH password reuse returns a safe 503 when stored material cannot be resolved; a token-only endpoint still returns 422 because an API token is not an SSH password. Scheduled monitoring records an affected endpoint's failure and continues evaluating other endpoints.

Endpoint edit forms preserve masked values from the original storage backend, using the submitted authentication selection after explicit clears. A complete new token pair can repair an endpoint that has no stored credentials. An existing unresolved counterpart reference still blocks preservation until the operator repairs, replaces, or explicitly clears that credential. The requesting actor is retained for authorized OpenBao reads and writes.

netbox-openbao is a separate NetBox plugin from the Proxbox suite. It is fully optional: netbox-proxbox installs and stores credentials without it. not a companion plugin in the same sense as netbox-pbs or netbox-ceph — you do not install it through proxbox-api sync jobs — but it is the supported way to store SSH login material for Proxmox guests that netbox-proxbox already models in NetBox.

Problem split

Concern Plugin Storage
Discover VMs, LXC, interfaces, IPs netbox-proxbox (+ proxbox-api) NetBox PostgreSQL
Store SSH passwords and keypairs netbox-openbao OpenBao KV v2 + NetBox metadata
Optional vault credential isolation netbox-openbao-broker AppRole off the NetBox host
Audited SSH / host procedures netbox-rpc (+ rpc-backend) Procedure catalog in NetBox

Proxbox answers what exists. Openbao answers how you log in — after the object exists. RPC answers what automated host operations are allowed — through a catalogued executor, not ad-hoc shell.

No secrets in the sync pipeline

Proxmox guest credentials — cloud-init passwords, hypervisor-stored root passwords, QEMU agent secrets — are not part of the proxbox discovery contract. A proxbox job failure must not rotate credentials; a credential reveal must not call the Proxmox API.

Architecture

Inventory lane (netbox-proxbox)

flowchart TB
    PVE["Proxmox VE"]
    API["proxbox-api"]
    PX["netbox-proxbox"]
    NB[("NetBox\nVirtualMachine · IP · Service ssh:22")]

    PVE -->|"read-only"| API
    API --> PX
    PX --> NB

Target secrets and automation stack

The RPC-to-OpenBao executor link below is the planned generic integration. Its presence in this diagram does not establish runtime support in an installed executor; verify the capability and version contract before enabling writes.

flowchart TB
    OP["Operator · nbx"]
    OB["netbox-openbao"]
    BR["netbox-openbao-broker\noptional"]
    BAO[("OpenBao KV v2")]
    RPC["netbox-rpc"]
    RBE["netbox-rpc-backend"]
    VM["VirtualMachine / host"]

    OP --> OB
    OB --> BR
    OB -.-> BAO
    BR --> BAO
    OP --> RPC
    RPC --> RBE
    RBE --> OB
    RBE --> VM

The inventory and secrets lanes meet on the same VirtualMachine (or Device) row. Guest assignment coordinates through NetBox objects and OpenBao assignments. Endpoint storage additionally uses Proxbox's optional integrations/openbao.py adapter, which imports OpenBao models and services at call time.

Endpoint and node credentials vs guest credentials

netbox-proxbox can also store Proxmox endpoint tokens and SSH secrets through netbox-openbao when ProxboxPluginSettings.credential_storage_backend = openbao. Automatic (the blank default) selects that backend only when netbox_openbao is enabled in PLUGINS; otherwise it selects legacy Fernet. Explicit selections remain authoritative and never downgrade when OpenBao becomes unavailable. That path secures how NetBox talks to Proxmox, not how operators SSH into a synced guest.

Endpoint browser-terminal SSH can be modeled as an endpoint-owned application service. ProxmoxEndpointService is a netbox-proxbox model whose parent is the ProxmoxEndpoint; it is not a core ipam.Service. Its one-to-one ServiceSSHCredential stores either Fernet ciphertext or opaque OpenBao UUID references. With OpenBao selected, newly entered material is created through the provider write path and assigned to the application service with purpose console and is_primary=True. Selecting an existing ssh-password or ssh-keypair credential creates or reuses that same primary assignment and demotes another primary for the service and purpose. Provider-backed reads fail closed and never use local ciphertext as a fallback. A reveal also requires the request actor to hold object-level reveal access to the exact credential and the exact enabled console assignment to still target the application service.

Service creation, reference changes, assignment changes, and material writes commit in one provider-owned transaction. A provider failure or stale reference rolls back the complete service chain. Supported service, credential, and parent endpoint deletions authorize and remove only their assignments; shared OpenBao credentials remain intact. Raw deletion is refused while provider-owned state remains.

netbox-proxbox registers netbox_proxbox.proxmoxendpointservice as an OpenBao assignment target only when netbox-openbao is enabled. Imports remain lazy, so installing the Python package without adding it to NetBox PLUGINS does not query provider models or migrations.

Migration 0105_reset_implicit_openbao_storage_default resets a global stored openbao value created by the historical default to Automatic only when none of the supported credential owners contains an OpenBao UUID reference. It does not inspect or change endpoint backend overrides, and it preserves the global selection when any reference exists. To opt out, open Settings, set Credential storage backend to Automatic or Legacy Fernet-encrypted local storage, and save. Automatic continues to follow the enabled companion set; Legacy always uses the plugin's configured Fernet key.

Proxbox refuses that switch while any credential that follows the plugin-wide setting still holds an OpenBao reference or assignment: Proxmox endpoints that inherit the setting, node SSH credentials, FastAPI/PBS/PDM/Firecracker tokens, and VM cloud-init secrets. Clear those credentials first so no OpenBao material is silently replaced by Fernet storage. Endpoints without OpenBao references can be created, edited, and deleted when netbox-openbao is absent, or installed but not listed in PLUGINS; an endpoint that still holds references cannot be deleted, individually or through bulk deletion, until netbox-openbao is restored and the references are cleared.

Storage changes through model save(), queryset update(), and bulk_update() lock the settings singleton before the final reference and assignment checks and retain that lock through persistence. OpenBao material transactions use the same common invariant without violating the provider boundary: enter the outer material transaction while the connection is in autocommit, then take the settings singleton as the first row lock inside the provider-owned durable transaction, before every owner, assignment, or credential row. Their downstream row order remains transaction-specific. This serializes the first provider reference against a concurrent global downgrade without introducing an inverse lock order or an application atomic block outside the provider. After acquiring that lock, every endpoint, node SSH, single-secret, and cloud-init material writer revalidates the effective backend from the locked settings and owner graph before any provider or assignment write. If a global downgrade committed first, the queued material write is refused without creating a credential, reference, or assignment. Queryset storage writes bind the lock, final state reads, and persistence to the write-router database; storage-changing conflict upserts are unsupported and must use save(), update(), or bulk_update() instead. These guards cover both default and base manager querysets. A literal bulk_update() is validated while holding the singleton lock, then a context-local internal permit allows only Django's generated Case/Cast persistence; caller-provided expressions remain refused. A blank endpoint override resolves against the effective global selection from the locked settings row on the same write database, so an explicitly OpenBao endpoint cannot inherit an explicit global legacy selection while references remain.

Per-node SSH credentials used by hardware discovery and the node terminal use the same effective backend selection as their ProxmoxEndpoint. With OpenBao selected, Proxbox writes ssh-password and ssh-keypair material through the provider-owned transaction and persists opaque UUID references on NodeSSHCredential. The authentication method selected on the node credential is assigned to the linked dcim.Device as the primary credential for the login purpose.

A node may receive OpenBao material before sync has linked it to a dcim.Device. Proxbox deliberately keeps that credential unassigned until the link exists. Saving the node after a link, relink, or unlink automatically creates, moves, or removes only the Proxbox-owned primary login assignment; unrelated assignments and the OpenBao credential material remain intact. Conflicting foreign primary login assignments fail closed instead of being replaced.

When a ProxmoxNode has no local NodeSSHCredential row at all, the by-node secrets endpoint (netbox_proxbox/api/device_openbao_ssh_resolver.py) falls back to resolving SSH login material from the node's linked dcim.Device through netbox-openbao directly: it looks up a single credentialed SSH ServiceEndpoint assigned to that device and reveals its Credential through the same audited reveal_credential_material path used everywhere else in this integration, gated by the same SSH-access check the local-credential path applies. The Credential lookup is additionally scoped through Credential.objects.restrict(request.user, "reveal") — the same object permission netbox-openbao's own reveal API requires — so a caller authorized only for the local NodeSSHCredential path cannot reveal an arbitrary device's OpenBao-stored material through this fallback; an unauthenticated caller or one without reveal_credential gets a 403. More than one credentialed SSH endpoint on the device is treated as a denial, not an ambiguity to guess through — the caller may supply a port to disambiguate. When netbox-openbao is not installed, or has no matching endpoint for the device, resolution returns nothing and the endpoint answers the same 404 it always has; there is no further legacy fallback, since one would require importing a non-public, environment-specific plugin, which scripts/check_public_boundary.py refuses.

⚠️ ServiceEndpoint is not yet in the pinned CI netbox-openbao revision. The immutable NetBox 4.7 OpenBao CI cell (ci/netbox-requirements/netbox-openbao-58677ef-py312.in, commit 58677efdd595c34cf0d597ca3efa1951b05ea25e) predates ServiceEndpoint — it merged to netbox-openbao's develop branch as commit e7f94ee, but has not reached a public netbox-openbao release, so the pin must not move to pick it up yet. device_openbao_ssh_resolver.py feature-detects instead of assuming the model exists: every apps.get_model("netbox_openbao", …) lookup goes through _get_openbao_model(), which catches LookupError (what apps.get_model() raises for a model an installed app doesn't define) and treats it as "not available" — the by-node secrets fallback degrades to its existing 404 rather than an uncaught 500. Once a netbox-openbao release ships ServiceEndpoint, move the CI pin (and openbao_ref in .github/workflows/django-tests.yml) forward so this path is exercised against the real model in CI, and add a real-Django case that confirms an actual ServiceEndpoint row also resolves end to end.

FastAPI backend tokens, PBS tokens, PDM tokens, and Firecracker agent tokens follow the same effective storage selection. With OpenBao selected, each owner stores one opaque credential UUID and clears its legacy Fernet ciphertext only after the provider write succeeds. All four use credential type api-token; FastAPI/PBS/PDM assignments use purpose api, and Firecracker host assignments use purpose agent. Firecracker discovery and REST writes therefore store a new token in the effective backend instead of assuming local encryption.

Changing the effective backend does not migrate material. Legacy-to-OpenBao occurs only when an operator explicitly supplies new material. OpenBao-to-legacy is refused while a UUID reference or owned assignment remains, preserving the state needed for an explicit cleanup or migration workflow.

The UI and REST mutation handlers enter the provider material transaction before NetBox opens its own atomic edit, delete, or bulk-operation block. The request actor is carried through that outer boundary for credential and assignment permission checks. Direct QuerySet.update(), bulk_update(), bulk_create(), delete(), and raw-delete bypasses are rejected whenever they could skip OpenBao reconciliation. Deleting a linked Device, ProxmoxNode, or ProxmoxEndpoint is likewise refused while node references or assignments remain. Operators must first use the supported node-credential cleanup path; signal handlers never attempt provider cleanup from inside Django's deletion collector transaction.

The supported ProxmoxEndpoint.delete() path removes endpoint assignments before Django emits pre_delete. Endpoints with OpenBao references retain the provider-owned cleanup transaction. An explicitly legacy endpoint with only generic assignments instead deletes those non-secret relation rows through the plain ORM in the current application transaction; this also works when NetBox's delete view has already opened an atomic block. The cleanup freshly resolves the request actor and verifies delete permission on every exact assignment before deleting any row; a denial preserves the endpoint and all assignments. No credential row or assignment targeting another owner is deleted. Therefore the signal also refuses an endpoint with assignment-only state: such state at pre_delete proves that a queryset, bulk, cascade, or other unsupported delete bypassed supported cleanup. The refusal leaves both the endpoint and assignments intact.

REST bulk deletion retains NetBox's native request validation and background-job handling. Provider cleanup starts only for the validated queryset. If NetBox reports any protected or otherwise unsuccessful object deletion, the outer provider transaction rolls back every assignment cleanup and the entire batch remains intact. On NetBox 4.7, missing, malformed, and duplicate IDs are rejected before provider cleanup begins. NetBox 4.5 and 4.6 silently skip missing IDs and collapse duplicates, so cleanup covers only the objects that are actually selected for deletion. On every version, assignments are removed only when their owning endpoint or application service is successfully deleted.

Confirmed UI deletes of endpoints, application services, and service SSH credentials follow the same rule. A protected, restricted, or aborted delete rolls back the provider cleanup and leaves the object and its assignments in place. On NetBox 4.5 and 4.6, the framework's single-object delete runs inside its own savepoint, so the refusal is reported normally instead of failing with HTTP 500. A refused delete also discards the request's queued change events, so webhooks, scripts, and event rules never see a deletion that did not commit. NetBox 4.7 behaves this way natively.

The node credential metadata response exposes openbao_assignment_ready, openbao_assignment_detail, and the secret-free openbao_assignment_lookup selectors (assigned_object_type, assigned_object_id, and purpose). Ansible-compatible consumers may use those selectors against netbox-openbao's assignment API. Hardware discovery continues to obtain material only from the authenticated, token-protected node credential endpoint; readiness and lookup responses never contain a live password, private key, or credential UUID.

OpenBao resolution is strict. A missing or stale UUID, unavailable provider, invalid material shape, or denied reveal produces a sanitized error and never downgrades to a populated legacy Fernet column. When Legacy encrypted is explicitly selected, node passwords and private keys continue to use the local Fernet ciphertext columns and configured plugin encryption key.

Changing an endpoint or the global default from OpenBao to legacy is refused while node UUID references or assignments remain. If netbox-openbao has been removed prematurely, the validation error instructs the operator to restore the provider and run explicit cleanup; raw import failures and secret values are not exposed.

Missing OpenBao references and invalid or unavailable material fail explicitly with the endpoint and credential field named. Existing stored backend choices are preserved during upgrade; the automatic default does not migrate secrets. See credential storage configuration before changing the enabled plugin set.

At startup, Proxbox registers its endpoint and companion endpoint model labels with netbox-openbao's assignment registry. Endpoint material writes select only the configured openbao_policy_slug on the default engine and persist the credential UUID, assignment, material version, audit witness, and final metadata projection through netbox-openbao's transaction contract. A missing policy, denied permission, stale reference, CAS conflict, or assignment conflict rolls back the database graph and compensates only the material version owned by the failed attempt. Deployments using an older netbox-openbao release retain their configured allowlist and receive a startup warning instead of an import failure. That compatibility is registration-only: endpoint material writes require netbox-openbao 0.1.0 or newer and material transaction contract version 1, and otherwise fail with an actionable validation error before persistence.

Credential Stored by Bound to
Proxmox API token / dedicated endpoint SSH proxbox + openbao integration ProxmoxEndpoint
Endpoint application-service SSH proxbox + openbao integration ProxmoxEndpointService primary console assignment + ServiceSSHCredential UUID reference
Proxmox node SSH login proxbox + openbao integration NodeSSHCredential + linked dcim.Device primary login assignment
FastAPI/PBS/PDM API token proxbox + openbao integration owning endpoint primary api assignment
Firecracker agent token proxbox + openbao integration FirecrackerHost primary agent assignment
VM cloud-init password / private key proxbox + openbao integration parent VirtualMachine login assignment; primary follows ssh_pwauth
Guest SSH login openbao quick-add VirtualMachine + optional ipam.Service

Do not conflate the two: rotating an endpoint token does not create a guest SSH credential, and quick-add on a VM does not replace endpoint API authentication. The node integration is also the public replacement for the retired private unrelated credential-provider fallback: Proxbox owns node metadata and netbox-openbao owns credential material and assignments, with no private-plugin lookup.

Cloud-init intent accepts password and private-key material only through write-only REST inputs when OpenBao is selected. The provider stores them as password and ssh-keypair credentials assigned to the parent NetBox VM for the login purpose. When ssh_pwauth=true, the password is primary; otherwise the keypair is primary when configured. The public sshkeys reflection and encrypted sshkeys_intent bundle never enter an OpenBao payload. Selected OpenBao reads fail closed and never treat credential_reference_id as secret material. Explicit legacy mode preserves that opaque external reference; cross-store migration remains owned by a separate feature in the canonical Gitea workflow.

Operator workflow

  1. Sync inventory — manual sync, scheduled job, or proxbox-api SSE workflow so the VM or container exists under the correct cluster and node.
  2. Add SSH access — on the VirtualMachine page, use netbox-openbao's quick-add to create Credential, assignments, and ssh service (when services are modeled) in one transaction.
  3. Reveal when needed — operators or automation with reveal_credential POST to /api/plugins/openbao/credentials/{id}/reveal/; material never appears on GET or in exports.
  4. Optional automation — use the installed RPC release's supported fixed host procedures. Generic execution-bound OpenBao resolution is part of the integration plan; do not assume every executor implements it.

Installation

Install the plugins in NetBox; proxbox must be present before sync can create VM rows:

pip install netbox-proxbox netbox-openbao netbox-rpc
PLUGINS = [
    "netbox_proxbox",
    "netbox_openbao",
    "netbox_rpc",
]

Deploy netbox-openbao-broker when broker mode should keep AppRole material off the NetBox host. Configure OpenBao engines and policy tiers per netbox-openbao installation.

Further reading