netbox-openbao-ansible
Resolve netbox-openbao
credentials from NetBox at play time. The material stays in OpenBao; NetBox
authorizes and audits every read.
- name: Resolve the switch login
ansible.builtin.set_fact:
switch_login: "{{ lookup('emersonfelipesp.netbox_openbao.credential',
device=inventory_hostname, purpose='login') }}"
run_once: true
delegate_to: localhost
no_log: true
Read this first: no_log: true
Every task that consumes a lookup result needs no_log: true.
A lookup result lands in Ansible's output by default. Without no_log, the
secret ends up in the play recap, in AWX or Tower job output, and in whatever
aggregates those logs — which would undo the entire point of keeping it in
OpenBao. This is the single most likely way to get this wrong, and Ansible will
not warn you.
- name: Correct
ansible.builtin.debug:
msg: "Connecting as {{ switch_login.username }}"
no_log: true # <- not optional
Read this second: the reveal rate limit
netbox-openbao rate limits reveals per user, by default 30/hour.
A lookup written inline on a task that runs against 200 hosts will exhaust that partway through, and the failure looks like a plugin bug rather than a quota. The collection raises a specific, actionable error when it happens — but the fix is a playbook pattern, not a library trick:
# Resolve once for the whole play, then reuse.
- name: Resolve
ansible.builtin.set_fact:
creds: "{{ lookup('emersonfelipesp.netbox_openbao.credential',
credential_id=142) }}"
run_once: true
delegate_to: localhost
no_log: true
The collection caches reveals in process, which helps for lookups evaluated
in play-level vars, in a run_once task, or repeatedly within one fork.
Ansible forks workers for host execution, and each fork gets its own cache — so
the cache is not a substitute for run_once. That limit is stated here rather
than glossed, because relying on a cache that silently does not apply is how
you hit the rate limit anyway.
If a fleet-wide per-host reveal is genuinely what you need, raise
reveal_rate_limit in the plugin's PLUGINS_CONFIG deliberately, rather than
discovering the ceiling at run time.
Install
Install the released Python distribution from PyPI:
python -m pip install netbox-openbao-ansible==0.1.0
The wheel installs the collection below
ansible_collections.emersonfelipesp.netbox_openbao, so Ansible discovers the
same fully qualified lookup name as a Galaxy installation. The conventional
Galaxy source installation remains available:
ansible-galaxy collection install git+https://github.com/emersonfelipesp/netbox-openbao-ansible.git
For a pre-release verification from TestPyPI, install dependencies from PyPI and only this package from TestPyPI:
python -m pip install 'ansible-core>=2.15'
python -m pip install --no-deps --index-url https://test.pypi.org/simple/ \
netbox-openbao-ansible==0.1.0
See the release procedure for immutable publication and recovery rules.
Configure
Both from the environment. Never put a token in a playbook — a token in a playbook is a token in version control.
export NETBOX_URL=https://netbox.example.net
export NETBOX_TOKEN=nbt_xxxxxxxx.yyyyyyyy # or NETBOX_OPENBAO_TOKEN
NetBox 4.7 issues v2 tokens (nbt_<key>.<token>) and still accepts legacy v1
tokens. The collection detects which scheme a token needs from its own shape,
so there is nothing to configure.
Permissions
The token needs netbox_openbao.view_credential and
netbox_openbao.reveal_credential.
Explicitly not add_credential. That distinction is real and recent: NetBox
maps POST to add_<model> by default, which had made the dedicated reveal
permission decorative until the plugin overrode it. The collection's live tests
run with a token holding exactly view + reveal, which is what makes this claim
verified rather than asserted.
TLS
Verification is on by default. For an internal CA, point at your bundle:
export NETBOX_CA_BUNDLE=/etc/ssl/certs/internal-ca.pem
validate_certs: false exists because Ansible modules conventionally offer it,
but it warns loudly and should never appear in a production playbook — with it
off, both the API token and the revealed material are readable by anyone on the
path.
Selecting a credential
| Selector | Example |
|---|---|
| Device + purpose | device='core-sw-01', purpose='login' |
| Virtual machine | virtual_machine='vm-01', purpose='login' |
| Service | service='ssh' |
| Any other object type | assigned_object_type='netbox_proxbox.proxmoxendpoint', assigned_object_id=3 |
| By ID | credential_id=142 |
| By UUID | uuid='0d2f8f6e-…' |
| By name | lookup(..., 'core-sw-01 login') |
Devices, VMs, and services accept a name or a numeric ID.
Object types beyond those three
netbox-openbao's assignable-model allowlist is configurable, and an installed
plugin can extend it from its own code. So the three friendly selectors above
cannot be exhaustive without a release of this collection per integration, and
assigned_object_type plus assigned_object_id is the escape hatch:
- name: Resolve the Proxmox endpoint API token
ansible.builtin.set_fact:
proxmox_api: "{{ lookup('emersonfelipesp.netbox_openbao.credential',
assigned_object_type='netbox_proxbox.proxmoxendpoint',
assigned_object_id='3',
purpose='api') }}"
run_once: true
delegate_to: localhost
no_log: true
Both halves are required, and the id must be ASCII digits naming a positive
integer. This collection cannot know which field is the natural key for a model
it has never heard of, and guessing name would fail confusingly on the models
that do not have one — so name resolution stays limited to devices, VMs, and
services.
Quote the id, as the example does. The option is declared str rather than
int on purpose: Ansible coerces an option to its declared type before the
plugin sees it, and int coercion is lossy in exactly the ways that matter —
True becomes 1, 3.0 becomes 3, ' 3 ' becomes 3. A templated value
that arrived as a boolean or a float would then select a different object and
hand back its credential rather than failing. Declared str, those arrive intact
and are refused, along with whitespace-padded strings, non-ASCII digits, zero,
and negatives — all before any request is made.
The object must be assignable in the target NetBox. An assignment cannot exist for a content type that instance's allowlist rejects, and the symptom is indistinguishable from an object that simply has no credential — so the lookup's not-found message names the allowlist as a possible cause rather than leaving you to guess.
Resolution prefers the assignment marked primary — which is exactly what
netbox-openbao's single-primary-per-(object, purpose) constraint exists to
make unambiguous. If several match and none is primary, the lookup fails
rather than guessing, because silently taking the first would make a play's
behaviour depend on database ordering.
credential_type=, uuid=, and name= combine with an object selector and
narrow the result:
device='core-sw-01', purpose='login', credential_type='ssh-keypair'
If nothing satisfies the constraint the lookup fails and names the types that are present, rather than falling back to the primary. That matters because these filters were previously accepted and ignored on this path — a play written to demand a key was handed whichever credential happened to be primary, with no error.
What you get back
The secret payload, plus the credential's non-secret identity so a play does not need a second lookup to learn the username:
{
"password": "…",
"id": 142,
"uuid": "0d2f8f6e-…",
"name": "core-sw-01 login",
"username": "admin",
"credential_type": "password",
"kv_version": 3,
"ttl": 300
}
field='password' returns just that value instead.
Other options
reason='CHG-1234'— recorded in the credential's access log, and required when the credential's policy setsrequire_reason.version=2— read a superseded KV version. Leave unset unless that is deliberately what you want; by default you get the version currently in service, which is not necessarily the latest one written if a rotation is staged.
Why this is a separate collection
The lookup runs on the Ansible control node, which has no NetBox, no Django, and no database. Shipping it inside the plugin's wheel would put an Ansible dependency in a Django package, make every NetBox installation carry code it will never execute, and chain the lookup's releases to the plugin's — when the two have quite different compatibility surfaces. The REST API is the contract between them, and a separate package makes that boundary explicit.
Development
pip install ansible-core build pytest ruff twine
pytest tests/unit # no server needed
ruff check .
python -m build
python -m twine check dist/*
Against a live NetBox:
export NETBOX_OPENBAO_TEST_URL=http://127.0.0.1:8000
export NETBOX_OPENBAO_TEST_TOKEN=nbt_…
export ANSIBLE_COLLECTIONS_PATH=/path/to/collections
pytest tests
The live tests are worth running. The only defect they have caught that a stub
could not was in netbox-openbao itself — creating a credential failed against
a real OpenBao because the plugin sent an empty custom_metadata value, which
its own fake backend had happily accepted. Stubs agree with whatever you
believe; servers do not.
License
Apache-2.0.
Metadata
Release files for netbox-openbao-ansible 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| netbox_openbao_ansible-0.1.0.tar.gz | 23.6 kB | Without provenance | Details for netbox_openbao_ansible-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| netbox_openbao_ansible-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for netbox_openbao_ansible-0.1.0-py3-none-any.whl |
Total release size: 60.8 kB
Release files / netbox_openbao_ansible-0.1.0.tar.gz
| Download URL | netbox_openbao_ansible-0.1.0.tar.gz |
|---|---|
| Size | 23.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
5c4a65bdacf3c753fb321e4a2e2fd5f36db7d4e43649c456af92e9cff72066c8
|
|
BLAKE2b-256 checksum How to use checksums |
3a6c7920ba519dffc11c5001eac6e5e3fa13dcd417c6ee64a204381d234d1b68
|
| 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_ansible-0.1.0-py3-none-any.whl
| Download URL | netbox_openbao_ansible-0.1.0-py3-none-any.whl |
|---|---|
| Size | 37.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
8344e3ff030181e9f3ac2f884cbc7e2cda470bec285118f0889a8735b2b4400e
|
|
BLAKE2b-256 checksum How to use checksums |
68ededf4f8db3323eec4df71f76b1f274bb69c3b05da01109dd6fe55d177ac6c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Security for netbox-openbao-ansible 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.