Metadata-Version: 2.4
Name: netbox-openbao
Version: 0.1.0.post1
Summary: Keep NetBox device, VM, and service secret material in OpenBao while NetBox owns the credential inventory
Author-email: Emerson Felipe <emerson@netdevopsbr.com>
License-Expression: Apache-2.0
Project-URL: Homepage, https://github.com/emersonfelipesp/netbox-openbao
Project-URL: Documentation, https://emersonfelipesp.github.io/netbox-openbao/
Project-URL: Source, https://github.com/emersonfelipesp/netbox-openbao
Project-URL: Issues, https://github.com/emersonfelipesp/netbox-openbao/issues
Project-URL: Changelog, https://github.com/emersonfelipesp/netbox-openbao/blob/main/CHANGELOG.md
Keywords: netbox,netbox-plugin,openbao,vault,secrets,credentials
Classifier: Development Status :: 3 - Alpha
Classifier: Framework :: Django
Classifier: Framework :: Django :: 6.1
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: System Administrators
Classifier: Natural Language :: English
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Security
Classifier: Topic :: System :: Systems Administration
Requires-Python: >=3.12
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: hvac>=2.3.0
Requires-Dist: cryptography>=42.0.0
Requires-Dist: bcrypt>=4.0.0
Requires-Dist: jsonschema>=4.0.0
Requires-Dist: pydantic>=2.0
Requires-Dist: netbox-rpc>=0.1.8
Requires-Dist: rpgp-py<0.21,>=0.20.0
Provides-Extra: docs
Requires-Dist: mkdocs-material>=9.5; extra == "docs"
Requires-Dist: mkdocstrings[python]>=0.26; extra == "docs"
Requires-Dist: mkdocs-gen-files>=0.5; extra == "docs"
Requires-Dist: mkdocs-literate-nav>=0.6; extra == "docs"
Dynamic: license-file

# 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](#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_data` is `write_only`.** DRF itself refuses to serialize it — into a
  `GET`, a `brief=true` response, the browsable API, or an OpenAPI example.
- **`reveal` is a permission of its own**, separate from `view` and
  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`](https://github.com/emersonfelipesp/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`](docs/installation.md#netbox-46-and-47) for the exact
boundary.

## Install

```bash
pip install netbox-openbao
```

```python
# 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',
        ],
    },
}
```

```bash
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`:

```bash
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`:

```bash
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](docs/configuration.md#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`](docs/installation.md) and
[`docs/configuration.md`](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`](docs/release-deployment.md).

## Using it

```bash
# 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`](https://github.com/emersonfelipesp/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`](docs/installation.md#broker-mode).

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](docs/architecture/administration-plane.md)
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](https://github.com/emersonfelipesp/netbox-openbao-ansible)
and [broker mode](#broker-mode-and-what-it-is-honestly-worth), 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?

```bash
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`](docs/migration-from-netbox-secrets.md).

## Development

```bash
docker compose -f docker-compose.dev.yml up -d
```

See [`docs/development.md`](docs/development.md) for running the suite against
real NetBox 4.7 and a live OpenBao dev server, and
[`CONTRIBUTING.md`](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:

```bash
pip install '.[docs]'
mkdocs serve
```

## License

Apache-2.0.
