Validate token_version matches the admin token kind; document live-mount repair
ci/woodpecker/pr/plan Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful

A live re-test showed the plugin's own NetBox calls (username resolution, cred
minting) still 403 after the KV admin_token was re-seeded bare, because the mount
uses ignore_changes=[token] and kept the old scheme-prefixed value. The plugin
builds its admin Authorization header from the token value's nbt_ prefix, not
from token_version, so a stale "Bearer nbt_..." value double-mangles into a
malformed header.

- Extend the netbox_secret_backend postcondition to also assert token_version
  matches the admin token kind (nbt_ v2 <-> token_version 2; bare v1 <-> 1), so
  a version/token mismatch fails at plan time with a clear message.
- Document that token_version does not affect the admin header (only minted
  token version), and give the exact command to push a corrected token into a
  running mount (vault write netbox/config token=<BARE>), warning against
  -replace which would drop the mount's roles.
This commit is contained in:
2026-08-11 21:56:45 +10:00
parent 28d0a6ed79
commit 743ad5ac33
2 changed files with 23 additions and 6 deletions
+10 -1
View File
@@ -22,7 +22,16 @@
#
# token_version 2 is the NetBox 4.6.5 default and requires API_TOKEN_PEPPERS to
# be configured on the NetBox server; set token_version: 1 here if the server
# has no peppers.
# has no peppers. token_version does NOT change how the plugin authenticates its
# own calls (that scheme comes from the admin_token value's nbt_ prefix); it only
# sets the version of the per-user tokens the engine mints. It must still MATCH
# the admin_token kind: nbt_ v2 token -> token_version 2; bare v1 token -> 1.
#
# The mount uses ignore_changes=[token], so editing KV alone does NOT reach the
# live mount. To push a corrected/rotated admin token into a running mount:
# vault write netbox/config token=<BARE_TOKEN>
# (netbox_url/token_version are preserved on a partial update). Do NOT -replace
# the mount to force a re-read - that recreates it and drops all roles/config.
description: "NetBox ephemeral scoped API token engine"
netbox_url: "https://netbox.k8s.syd1.au.unkin.net"
token_version: 2
@@ -10,15 +10,23 @@ data "vault_kv_secret_v2" "config" {
name = "service/vault/${var.country}/${var.region}/secret_backend/${var.path}/config"
lifecycle {
# The plugin adds the Authorization scheme itself (Bearer for v2 nbt_ tokens,
# Token for v1), so admin_token must be the BARE token. A literal `Bearer `/
# `Token ` prefix in the seed yields a malformed header and 403s on mint/rotate.
# The plugin builds its own Authorization header from the token VALUE, not
# token_version: a value starting with the nbt_ prefix is sent as
# "Bearer <token>" (v2), otherwise "Token <token>" (v1). So admin_token must
# be the BARE token - a literal `Bearer `/`Token ` scheme prefix yields a
# malformed three-part header and 403s on the plugin's own NetBox calls.
#
# token_version does NOT change that header; it only selects the version of
# the per-user tokens the engine mints for roles. It must still MATCH the
# admin token's kind so the mount and its minted creds line up: an nbt_ v2
# admin token pairs with token_version=2, a bare v1 token with token_version=1.
postcondition {
condition = nonsensitive(
!startswith(self.data["admin_token"], "Bearer ") &&
!startswith(self.data["admin_token"], "Token ")
!startswith(self.data["admin_token"], "Token ") &&
startswith(self.data["admin_token"], "nbt_") == (var.token_version == 2)
)
error_message = "KV admin_token for netbox backend '${var.path}' must be a BARE NetBox token with no 'Bearer '/'Token ' scheme prefix (v2: nbt_<key>.<secret>; v1: the 40-char value)."
error_message = "KV admin_token for netbox backend '${var.path}' must be a BARE NetBox token with no 'Bearer '/'Token ' scheme prefix, AND its version must match token_version: a v2 token (nbt_<key>.<secret>) requires token_version=2; a v1 token (bare 40-char value) requires token_version=1."
}
}
}