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 model field can hold secret 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.
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.
- No auth material in the database. RoleIDs and SecretIDs come from the process environment, or a file it points at.
- 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.6 or 4.7 (4.6.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.6 and 4.7 are both supported. The releases differ in four capabilities
used here (notably ipam.Service port storage), and those differences are
isolated in netbox_openbao/compat.py. The full suite currently passes on exact
NetBox v4.7.0-beta2; the compatibility gate also retains NetBox 4.6.5 as the
backward-regression target. See
docs/installation.md for the exact
boundary.
Install
pip install netbox-openbao
# configuration.py
PLUGINS = ['netbox_openbao']
PLUGINS_CONFIG = {
'netbox_openbao': {
# Optional seed/fallback. Runtime settings are managed through the
# plugin API once its singleton settings row exists.
'assignable_models': [
'dcim.device',
'virtualization.virtualmachine',
'ipam.service',
],
},
}
python manage.py migrate
systemctl restart netbox netbox-rq
Then export the AppRole material for each engine. The prefix is derived from
the engine's slug — slug prod-core becomes NETBOX_BAO_PROD_CORE:
NETBOX_BAO_PRIMARY_ROLE_ID=...
NETBOX_BAO_PRIMARY_SECRET_ID=...
Either variable also accepts a _FILE form pointing at a mounted secret, which
keeps the value out of /proc/<pid>/environ:
NETBOX_BAO_PRIMARY_SECRET_ID_FILE=/run/secrets/bao-secret-id
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 Services, with a purpose. |
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.
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.1.0.post2
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.1.0.post2.tar.gz | 438.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| netbox_openbao-0.1.0.post2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 971.2 kB
Release files / netbox_openbao-0.1.0.post2.tar.gz
| Download URL | netbox_openbao-0.1.0.post2.tar.gz |
|---|---|
| Size | 438.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
03a45aac9f4160ae4a1da7ae60d981b152bb23e6bd2aa8c7222443105799b797
|
|
BLAKE2b-256 checksum How to use checksums |
4d856c660eafc5749d79d8ef115ecd6b208091bed82ee94f537dcda35c2b088b
|
| 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.1.0.post2-py3-none-any.whl
| Download URL | netbox_openbao-0.1.0.post2-py3-none-any.whl |
|---|---|
| Size | 533.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
d620e76e37f67b0c6cfe173fd04756be680552705d47d0dc38bd523f0d031ecc
|
|
BLAKE2b-256 checksum How to use checksums |
c22f0e52e9039ac8b90bbfc14f9989b2c0d754c6eb5c6e109693e260a89a3b10
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|