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¶
- Sync inventory — manual sync, scheduled job, or proxbox-api SSE workflow so the VM or container exists under the correct cluster and node.
- Add SSH access — on the VirtualMachine page, use netbox-openbao's
quick-add to create
Credential, assignments, andsshservice (when services are modeled) in one transaction. - Reveal when needed — operators or automation with
reveal_credentialPOST to/api/plugins/openbao/credentials/{id}/reveal/; material never appears on GET or in exports. - 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.