Skip to content

Add multi-user NetBox API-token pass-through to the MCP HTTP server - #151

Open
eaxi wants to merge 2 commits into
lance0:masterfrom
eaxi:pr-b-passthrough
Open

eaxi wants to merge 2 commits into
lance0:masterfrom
eaxi:pr-b-passthrough

Conversation

@eaxi

@eaxi eaxi commented Oct 6, 2026

Copy link
Copy Markdown

Problem

Every current serve mode uses a single NetBox credential. OIDC mode (Pattern 3) verifies the caller but still talks to NetBox as the service account — the docs call this "accountability, not per-user RBAC". Per-user writes exist (Pattern 2) but need an IdP plus a [serve.vault] mapping. So a shared nbox server cannot reflect NetBox's own permissions per user.

Motivation & discussion: #150

Change

Opt-in --netbox-token-passthrough / [serve].netbox_token_passthrough: each caller sends their own NetBox API token per request (X-NetBox-Token, or Authorization when that header isn't already used by OIDC/--http-token), and nbox forwards it verbatim. NetBox's object permissions and changelog then apply to the real caller — no IdP, no vault.

Security properties

  • Read cache partitioned per token fingerprint — NetBox filters reads by permission, so a shared cache would leak one user's view to another.
  • nbox_cache_clear only drops the caller's own partition (no cross-tenant cache-flush DoS).
  • Fail closed: no token → 401, never a fallback to the profile token.
  • Authorization is only read when it isn't already carrying a JWT/static bearer.
  • Tokens redacted in Debug/logs/errors; the audit log records auth=netbox-token plus a short SHA-256 fingerprint, which also keys the post-auth per-caller rate limit.
  • Requires --http (stdio has no per-request headers, so that combination is a usage error rather than a silent no-op); a routable bind is allowed, with TLS terminated in front, same as OIDC mode.
  • A configured [serve.vault] is ignored in this mode, with a startup warning.
  • With --allow-writes, writes run under the caller's token through the same plan/apply two-step and audit as before.

Notes for review

  • Adds a third MCP credential path, so ADR-0001 §7 ("two explicit modes") is amended by the included ADR-0003.
  • Implementation reuses the existing RequestContext::extensions handoff that write_caller_from_extensions() already uses for the OIDC identity; task-locals don't work here (the session manager runs its own worker task and the service factory has no request handle).
  • All 11 read tools were split into *_tool (rmcp adapter) + *_impl (logic). #[cfg(test)] shims keep the old method names, so all existing test call sites are unchanged — deliberate, as a regression check on the refactor.
  • The code calls this "Pattern 4", continuing the DESIGN §24 numbering; that label is descriptive, not contractual — happy to renumber.
  • Pre-existing: the coarse pre-auth rate-limit bucket keys on the peer IP (shared behind a reverse proxy) — same for OIDC today; now documented in docs/MCP.md rather than silently inherited.
  • Verified end-to-end against a real NetBox 4.5.10: two users, one process, each getting their own NetBox view and identity.

Checks

cargo fmt --all -- --check clean; cargo clippy --all-targets --all-features -D warnings clean; cargo build clean; cargo test --all-features 1404 passed / 0 failed (25 binaries). Rebased on current master (rmcp 3.3).

eaxi and others added 2 commits October 6, 2026 15:58
`nbox serve --http --netbox-token-passthrough` (or
`[serve].netbox_token_passthrough = true`) lets one process serve many
users: each caller presents their own NetBox API token per request and
nbox forwards it verbatim, so NetBox's object permissions and changelog
apply to the real caller — no IdP and no `[serve.vault]` mapping.

The token is read from `X-NetBox-Token`, or from `Authorization` when
that header isn't already used by OIDC or `--http-token` (so a host that
can only set a bearer works, without ever mistaking a JWT for a NetBox
token). Each request builds its own client from the caller's credential.

Isolation is a correctness requirement here, not an optimization, since
NetBox returns different results to different users:

- the read cache is partitioned per token fingerprint;
- `nbox_cache_clear` drops only the caller's own partition;
- `--allow-writes` runs writes under the caller's token.

Fail closed: a request without a caller token is rejected with 401 and
never downgraded to the server's profile token. Tokens are redacted in
Debug, logs, and errors; the audit log carries `auth=netbox-token` plus a
short SHA-256 fingerprint, which also keys the per-caller rate limit.
Pass-through requires the HTTP transport — stdio has no per-request
headers, so asking for it there is a usage error rather than a silent
no-op — and it allows a routable bind, with TLS terminated in front.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
ADR-0001 §7 documents MCP writes as running "in one of two explicit modes";
pass-through adds a third credential path, so the decision needs a record of
the same rank. Follows the ADR-0002 precedent: what problem the mode solves
(no per-user RBAC without an IdP and vault), the decision (caller NetBox API
token forwarded verbatim, fail closed, cache partitioned per fingerprint), and
the accepted trade-offs (TLS required in front, IP-keyed pre-auth rate limit,
tokens in process memory).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant