Skip to main content

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 sets require_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)

Source distribution for netbox-openbao-ansible 0.1.0
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)

Table of built distributions (wheels) for netbox-openbao-ansible 0.1.0
File Interpreter ABI Platform Actions
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.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page