feat: mediate vault login -method=oidc (custom inject header + per-command open_port) - #1476
Conversation
PR Review SummarySize
Affected crates
Blast radius — ContainedThis PR touches: source code Updated automatically on each push to this PR. |
Integration fixup: nolabs-ai#1469/nolabs-ai#1443 added CredentialRouteDef.upgrades; Signed-off-by: James Carnegie <me@kipz.org> nolabs-ai#1476's vault test literal predates it. Combined-branch only.
| if provider | ||
| .credential_format | ||
| .as_deref() | ||
| .is_some_and(|format| !format.contains("{}")) | ||
| { | ||
| return Err(NonoError::ProfileParse(format!( | ||
| "credential_providers.{name}.credential_format must contain the '{{}}' token placeholder" | ||
| ))); | ||
| } | ||
| if provider | ||
| .inject_header | ||
| .as_deref() | ||
| .is_some_and(|header| !is_valid_http_header_name(header)) | ||
| { | ||
| return Err(NonoError::ProfileParse(format!( | ||
| "credential_providers.{name}.inject_header must be a valid HTTP header name (RFC 7230 token)" | ||
| ))); | ||
| } |
There was a problem hiding this comment.
looks good @kipz , just one small extra worth getting in. credential_format's value template isn't CRLF-checked but the header name is, and it flows through the same raw-string path. It's operator-supplied/trusted, so it's a consistency/defense-in-depth nit, not a blocker
perhaps something like this on line 200 of your last commit (above the check for .inject_header):
if provider
.credential_format
.as_deref()
.is_some_and(|format| format.bytes().any(|b| matches!(b, b'\r' | b'\n' | b'\0')))
{
return Err(NonoError::ProfileParse(format!(
"credential_providers.{name}.credential_format must not contain control characters (CR, LF, NUL)"
)));
}There was a problem hiding this comment.
@lukehinds done in 2c84b73. I widened the guard while there: rejects all HTTP field-value control bytes except horizontal tab (b.is_ascii_control() && b != b'\t'), not just CR/LF/NUL. added tests plus a positive case that tab is allowed.
Add format (nolabs-ai#1489) and upgrades (nolabs-ai#1443) fields to vault-oidc (nolabs-ai#1476) test initializers that only one side of the merge reconciled. Signed-off-by: James Carnegie <me@kipz.org>
2c84b73 to
9b1690c
Compare
oauth_capture providers can set inject_header/credential_format for the
api_hosts redemption route (default stays Authorization/Bearer {}), and
request_nonce_fields is optional for capture-only token endpoints. This
lets credentials for non-Bearer APIs (raw token in a custom header) be
brokered.
Command network policies gain open_port/open_port_range, wired through
add_policy_network on macOS and Linux via the localhost-port-range path,
so a proxy-routed child command can bind a localhost OAuth callback
listener the way the top-level network policy already allows.
Signed-off-by: James Carnegie <me@kipz.org>
The format template flows into a raw header value on the redemption route, so a stray control byte enables header injection. Reject all HTTP field-value control characters (everything except horizontal tab), matching the guard already applied to the header name. Signed-off-by: James Carnegie <me@kipz.org>
9b1690c to
0e5b4d6
Compare
Linked Issue
Closes #1473
Summary
Enables mediating HashiCorp Vault's
vault login -method=oidcso the real Vault token never enters the sandbox — captured at the OIDC callback, replaced with a phantom, redeemed on egress to the Vault API. Three genericoauth_capture/policy gaps blocked the Vault CLI flow:inject_header/credential_formatonoauth_captureproviders — Vault authenticates withX-Vault-Token: <raw token>, notAuthorization: Bearer {}. Defaults unchanged; setX-Vault-Token/{}for Vault. Validated at parse time:credential_formatmust contain{};inject_headermust be a valid HTTP header token.request_nonce_fieldsnow optional — the Vault OIDC callback (/v1/auth/<mount>/oidc/callback) is capture-only: the token arrives in the response and is never re-sent in a request body. The field only marks where to swap a phantom by value in an outbound refresh body — it does not gate capture, andapi_hostsis mandatory, so omitting it is safe.open_port/open_port_rangeon per-command network policies — so the mediatedvaultcommand can bind itslocalhost:8250OIDC callback listener. Mirrors the top-levelnetwork.open_port, wired throughadd_policy_network(macOS + Linux) via the existinglocalhost_port_rangespath (enforceable on macOS, unliketcp_bind_ports).Agent Disclosure
This PR was authored by an AI agent (disclosed on #1473).
credential_provider.rs,proxy_runtime.rs,command_policy.rs,tool-sandbox/platform/{macos,linux}.rs,oauth_capture/{rewrite,endpoint}.rs, andCLAUDE.md.add_localhost_port_range->proxy_bind_port_ranges; theapi_hostsroute builder) rather than adding new machinery.unwrap/expectin new code; usesNonoError; adds no new filesystem paths.Test Plan
make ciclean (fmt, clippy -D warnings, tests) on a fresh rebase ontoupstream/main.inject_headeron the api_hosts route;open_portmerge + parse round-trip;open_port-> localhost bind ranges (macOS + Linux);credential_format/inject_headervalidation.vault login -method=oidc:auth.client_tokencaptured at the callback and replaced with a phantom, the sandboxedvaultonly ever holds the phantom, and it is redeemed to the real token on egress to the Vault API.sandboxed-oauth-logins.mdx(new fields + Vault callback-bind note) andtool-sandbox.mdx(open_portreference).Checklist
CHANGELOG.mdif neededAgent Compliance Check