netbox-openbao
Keep device, VM, and service secret material in OpenBao, while NetBox owns the credential inventory and its relationships.
netbox-secrets stores secrets *in* NetBox.
netbox-vault-secrets reads secrets in the *browser*.
netbox-openbao keeps secrets in OpenBao and resolves them *server-side
over the API* — so your automation, not just your
operators, can use them.
That third line is the gap this plugin exists to close.
Status: early alpha. The data model, REST API, and security invariants are implemented and tested against NetBox 4.7 and OpenBao 2.6. See Roadmap for what is deliberately not here yet.
The idea
Split secret material from credential metadata. Material lives only in OpenBao. Everything non-secret lives in NetBox, fully searchable, filterable, and API-queryable.
For an SSH keypair:
| Goes to OpenBao | Stays in NetBox (plaintext, indexed) |
|---|---|
| private key | public key |
| passphrase | SHA256 fingerprint |
| password | username |
| API token | key type and bit length |
| certificate private key | serial, issuer, subject, not_before, not_after |
So you can answer "which certificates expire in the next 30 days?", "which devices trust fingerprint X?", or render a config template containing a public key — with zero OpenBao reads and no reveal permission. That is the headline feature, and it falls out of the split rather than being bolted on.
Why a Django plugin and not a sidecar service
This is a credentials plugin. A separate service resolving secrets would have to answer "is this NetBox user allowed to see this credential?" itself — and then you have two authorization implementations for your most sensitive data, which will drift. Every drift is a privilege-escalation bug.
As a Django plugin it reuses NetBox's tokens, object permissions, constraints, and changelog directly. Blocking I/O is handled with NetBox's own RQ job framework: a single reveal is synchronous and sub-100ms; bulk work is a background job.
Security properties
These are enforced structurally, not by convention, and each is covered by a
test in netbox_openbao/tests/test_security.py:
- No
Credentialmodel field can hold credential material. There is no column to leak, so the changelog, export templates, and the REST representation are safe by construction rather than by careful configuration. OpenBao service identities live separately as versioned ciphertext inEngineAuthMaterialand are excluded from those surfaces. secret_dataiswrite_only. DRF itself refuses to serialize it — into aGET, abrief=trueresponse, the browsable API, or an OpenAPI example.revealis a permission of its own, separate fromviewand constrainable in the standard ObjectPermission UI. A role can inventory every credential and read none.- Reveal responses are JSON-only and
no-store. The browsable renderer would template the secret into cacheable HTML; it is explicitly removed. - The UI reveal is POST-only, so a secret is never fetched by a bookmark, a prefetch, a link scanner, or a history replay.
- Authentication material is encrypted at rest. RoleIDs, SecretIDs,
tokens, Kubernetes login settings, and broker client keys are stored only as
versioned ciphertext derived from Django's
SECRET_KEY. REST and UI surfaces expose configured/missing status, never values or ciphertext. - Per-tier AppRoles, so a leaked SecretID reads only its own tier and a tier whose SecretID was never delivered to an instance is unreadable from it. This bounds blast radius; it does not re-authorize the NetBox user, and the documentation is careful about the difference.
- Backend exceptions carry no server text. An OpenBao 403 body can enumerate policy rules; it never reaches a log, a traceback, or a response.
- Every access is audited — who, when, from where, and whether it succeeded. Never the value.
- OpenBao administration is typed and permissioned. Cluster, auth/MFA, secret-engine, policy, identity, OIDC, and namespace operations use static reviewed contracts intersected with runtime capability discovery. The Web UI and REST API never become a generic privileged proxy.
Requirements
| NetBox | 4.7 (4.7.0–4.7.99; exact 4.7 beta2 evidence below) |
| Python | 3.12+ |
| PostgreSQL | 15+ with the ltree extension (a NetBox 4.7 requirement) |
| Redis | 6+ |
| OpenBao | 2.6.x, KV v2 mount |
| HashiCorp Vault | supported as an alternative backend — see below |
| Broker mode | optional; needs netbox-openbao-broker |
NetBox 4.7 is required: SSH credentials are tied to an Application Service
(ipam.Service), whose ports 4.7 stores as a port_mappings array, and the
seeded SSH service template relies on the same model. NetBox 4.6 and earlier
are not supported. The full suite currently passes on exact NetBox
v4.7.0-beta2. See docs/installation.md
for the history of the former 4.6 boundary.
Install
pip install netbox-openbao
# configuration.py
PLUGINS = ['netbox_openbao']
python manage.py migrate
systemctl restart netbox netbox-rq
Configure all runtime settings and service identities in OpenBao →
Configuration → Settings, through /api/plugins/openbao/, or with the
management command:
python manage.py openbao_configure engine \
--slug primary --name Primary --api-url https://bao.example.net:8200
python manage.py openbao_configure auth --engine primary --set role_id
python manage.py openbao_configure auth --engine primary --set secret_id
The prompts do not echo material or place it in shell history. SECRET_KEY is
the single root of trust and cannot live in the database beside the ciphertext
it protects. See the rotation procedure in
docs/configuration.md.
assignable_models is the allowlist of object types a credential may be
assigned to. An installed plugin can add its own models to it from
AppConfig.ready() — see
Assignable object types — so
integrating with this plugin does not require every deployment to restate the
same list in a settings file. assignable_models_deny subtracts from the
result, which is what keeps the final word with the operator.
Full detail in docs/installation.md and
docs/configuration.md, or on the documentation site:
https://emersonfelipesp.github.io/netbox-openbao/.
Release operators must use the immutable Gitea package and NMS proof-v3
deployment path. Target IDs, release-candidate and final publication triggers,
validation, and rollback are documented in
docs/release-deployment.md.
Using it
# Inventory — never returns material
curl -H "Authorization: Bearer $TOKEN" \
'https://netbox.example.net/api/plugins/openbao/credentials/?credential_type=x509-keypair'
# Everything expiring in the next 30 days — zero OpenBao reads
curl -H "Authorization: Bearer $TOKEN" \
'https://netbox.example.net/api/plugins/openbao/credentials/?expires_within_days=30'
# What does device 88 hold?
curl -H "Authorization: Bearer $TOKEN" \
'https://netbox.example.net/api/plugins/openbao/credentials/?assigned_object_type=dcim.device&assigned_object_id=88'
# Resolve material — requires netbox_openbao.reveal_credential
curl -H "Authorization: Bearer $TOKEN" \
'https://netbox.example.net/api/plugins/openbao/credentials/142/reveal/?reason=CHG-1234'
OpenBao or Vault
The plugin is named for OpenBao and that is its reference backend, but a
SecretEngine can be pointed at HashiCorp Vault instead by changing one
field. The two share the KV v2 and AppRole surfaces, and the plugin's whole
wire-protocol contract is run as one shared suite against both servers, so this
is verified rather than claimed.
Broker mode, and what it is honestly worth
A third backend, broker, points an engine at
netbox-openbao-broker
instead of at OpenBao. NetBox then holds a client certificate that lets it
ask, and the broker holds the AppRole that can actually read.
It is worth being precise about what that buys, because it is easy to oversell and an operator might relax controls elsewhere on the strength of it:
It does not make "NetBox compromise ≠ secret compromise" true. An attacker with code execution in NetBox can still ask the broker for material, and the broker will answer for anything NetBox is authorized to request.
What it does buy: stealing NetBox's database or configuration no longer yields credentials that read the vault directly, the broker's audit log sits outside NetBox's blast radius, and the AppRole's SecretID never exists on the NetBox host at all. That is a real improvement against offline compromise, backup theft, and configuration leakage — and it is not the stronger claim.
Optional throughout. The default deployment is unchanged, and nothing above the
SecretBackend abstraction knows which mode is in use. See
docs/installation.md.
OpenBao administration also has full broker parity. The plugin negotiates a versioned, digest-bound, instance-enabled contract before exposing cluster, authentication, secret-engine, policy/identity, lease/tool, or Raft snapshot operations. The broker accepts only reviewed operation identifiers and bounded typed inputs; it is not an arbitrary OpenBao proxy and never replaces NetBox's per-user authorization.
Data model
| Model | Role |
|---|---|
OpenBaoCluster |
One administrative API endpoint and namespace. Holds no auth material. |
SecretEngine |
One credential-storage mount, associated with a cluster. Holds no auth material. |
CredentialPolicy |
An authorization tier mapped onto a real OpenBao policy, with its own AppRole. |
Credential |
The inventory record: identity, public material, lifecycle. Never the secret. |
CredentialAssignment |
Many-to-many binding to Devices, VMs, and Application Services, with a purpose. For SSH it also holds the pinned host key. |
SSHPublicKey |
A user's public SSH key and computed fingerprint for an SSH Application Service. |
CredentialAccessLog |
Append-only correlation between a NetBox user and an OpenBao read. |
OpenBaoAdministrationLog |
Append-only metadata correlation for administrative probes and operations. |
The OpenBao path is UUID-derived and immutable
(<prefix>/credentials/<uuid>). A path derived from the object graph would
break the first time a credential is renamed or reassigned, and a broken path
is an orphaned secret nobody can find. Discovery instead comes from KV v2
custom_metadata, which carries the credential's NetBox identity, type,
policy, and assignments, so tooling outside NetBox can list the mount and
filter on them.
Roadmap
Implemented: the credential models, the backend abstraction with the OpenBao implementation, credential type schemas and extractors, the full REST API with the security invariants above, list/detail/edit UI, Device/VM/Service panels, the background jobs, and staged rotation (write, verify, promote — never break running access). The OpenBao administration plane now has cluster inventory, bounded runtime capability discovery, dedicated permissions, metadata-only audit, guarded bootstrap and seal operations, HA and Raft state, peer-removal safety, and bounded authenticated snapshot recovery. Its pinned OpenBao 2.6.2 parity manifest records cluster bootstrap, Raft storage, secrets-engine lifecycle, the classified API explorer, KV v1/v2, transit, database, SSH, TOTP, PKI, Kubernetes, policies, identity, OIDC, namespaces, leases, tools, and UI response-header administration as complete. Mounted operation execution covers runtime-advertised GET, LIST, POST, PUT, PATCH, and DELETE templates that satisfy the reviewed grammar, schema, authorization, and material-handling contract, including external plugin-style schemas. The checked-in final-operation fixture and runtime conformance endpoint fail on missing, duplicate, unclassified, or stale OpenBao 2.6.2 contracts. Other discovered operations remain non-executable until their capability family is implemented and reviewed.
Also shipped since: the Ansible lookup plugin and broker mode, whose earlier description here — "so a NetBox compromise is not a secret compromise" — claimed more than the design delivers and has been corrected above.
Deliberately not yet here:
- Cloud credential engines and the remaining typed dynamic-secret families
- OpenBao capabilities introduced after the pinned 2.6.2 UI and runtime contracts. A later minor release requires a new review and evidence fixture; runtime discovery alone never expands execution.
Host operations via netbox-rpc are implemented on SecretEngine: bind an
OpenBao host dcim.Device, then dispatch the seeded service.openbao.1.*
read/write procedures through POST …/engines/{id}/run-procedure/ (UI mirror:
the engine detail page). Each dispatch creates an audited OpenBaoProcedureRun
row linked to netbox_rpc.RPCExecution.
Giving a device SSH access
There is an Add SSH access button on every Device and VM page. One form
creates the ipam.Service, generates or accepts the keypair, writes the
private half to OpenBao, and assigns the credential — in a single transaction.
If it generates the key, the public half is shown once so it can go
straight into authorized_keys.
Coming from netbox-secrets?
python manage.py openbao_import_secrets --engine primary --policy imported --dry-run
The importer copies — it never deletes — infers each secret's type and proves
the inference by extraction, and is resumable. See
docs/migration-from-netbox-secrets.md.
Connections use NetBox's built-in Application Service (ipam.Service); the
plugin has no endpoint model of its own. The host is the single IP address
filled in on the service and the port is its single TCP port. A service with no
IP, several IPs, no TCP port or several TCP ports cannot be used to connect and
is reported with a clear error. The SSH host-key pin lives on the credential
assignment. The earlier NMS credential importer and its preflight command were
removed in 0.2.1.
Development
docker compose -f docker-compose.dev.yml up -d
See docs/development.md for running the suite against
real NetBox 4.7 and a live OpenBao dev server, and
CONTRIBUTING.md for what a change is expected to bring
with it.
The documentation site builds from the checkout with no package install and no
NetBox — mkdocstrings reads the source statically:
pip install '.[docs]'
mkdocs serve
License
Apache-2.0.
Metadata
Release files for netbox-openbao 0.2.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| netbox_openbao-0.2.2.tar.gz | 480.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| netbox_openbao-0.2.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 1.1 MB
Release files / netbox_openbao-0.2.2.tar.gz
| Download URL | netbox_openbao-0.2.2.tar.gz |
|---|---|
| Size | 480.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
5493206490a7313b453bf8470bf5b3fdc62e0557efa468f4e9506fa233f1fe73
|
|
BLAKE2b-256 checksum How to use checksums |
c4463990c8ff7a89b75abf443e8960aeda9d15edacf18cf8e248f0de8b8fe04f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Release files / netbox_openbao-0.2.2-py3-none-any.whl
| Download URL | netbox_openbao-0.2.2-py3-none-any.whl |
|---|---|
| Size | 581.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
b682ae7ced48dce2b588e1ba8c4f2ff4ba525857f1d2759f1c7ee54a9c7ad24b
|
|
BLAKE2b-256 checksum How to use checksums |
e0ae4a02482e0ceea44fcdd0a68c7a5d4969ca82b398ca8d9fb0a12e22a78d41
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|