Feat: placeholder-resolve AuthBridge plugin (OpenShell credential injection)#626
Feat: placeholder-resolve AuthBridge plugin (OpenShell credential injection)#626huang195 wants to merge 9 commits into
Conversation
Resolve OpenShell-style credential placeholders (openshell:resolve:env:<KEY>) in request headers to their real secret values on the wire, so the agent never holds the real credential. The placeholder rides in Authorization (Bearer) for the OpenShell anthropic provider, which is the header the forward proxy propagates to the upstream. Resolution is via a pluggable Resolver interface (the swap seam for a future gateway-gRPC source backed by OpenShell's GetSandboxProviderEnvironment). Default resolvers: inline mappings (testing), a mounted secret dir, or the process env. Fails closed on an unresolved/unknown placeholder; rejects CR/LF/NUL in resolved values (CWE-113). Purely additive: implements the existing pipeline.Plugin/Configurable contract with no shared-interface change, registered via a build-tagged cmd/authbridge-proxy/plugins_placeholderresolve.go (matching the per-plugin registration scheme so the plugin is droppable via -tags exclude_plugin_placeholderresolve). Verified by unit tests and a forward-proxy isolation run (resolve -> real value on the wire, unknown key -> 401 fail-closed, non-placeholder -> passthrough). Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Hai Huang <huang195@gmail.com>
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Add a gateway-backed Resolver source to the placeholder-resolve plugin (B2). It fetches the sandbox's resolved provider environment from the OpenShell gateway and serves placeholder lookups from an in-memory cache, so AuthBridge — running as a sidecar in the sandbox pod — injects the live credential using OpenShell's native provider mechanism (no mounted secrets). - authlib/openshell: generated gRPC stubs (buf + vendored protos) and a Client that replicates the supervisor's auth chain: read the projected SA token, IssueSandboxToken (Bearer SA token) -> gateway JWT, GetSandboxProviderEnvironment(sandbox_id) (Bearer JWT) -> resolved env. Re-mints the JWT on Unauthenticated. - authlib/plugins/placeholderresolve/gateway: a caching Resolver that primes and refreshes the env map in the background and serves Resolve from cache (lock-free); honors per-key credential expiry; fails closed until primed. - placeholderresolve: a `gateway` config block selects the gateway source (precedence over mappings/secret_dir/env); the plugin gains Init/Ready/Shutdown to drive the resolver's warm-up (mirrors token-exchange's lifecycle). Structural interfaces avoid an import cycle. Verified offline against an in-process mock gateway: client metadata flow + re-mint, and a plugin end-to-end (placeholder -> real value), all under -race. Live in-cluster auth is deferred to the sidecar-deployment phase (the gateway restricts these RPCs to the sandbox's own identity). Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Hai Huang <huang195@gmail.com>
ca8cbd8 to
d49da3a
Compare
Address PR kagenti#626 review feedback on the OpenShell gateway resolver. - Fail closed on plaintext gRPC: http:// (or a bare host with no mtls_cert_dir) to a non-loopback gateway is refused, since the SA token and minted JWT would otherwise travel as cleartext metadata. An explicit `insecure: true` opt-in (logged with a warning) overrides; loopback is always allowed. (openshell.Config.Insecure, plumbed through the plugin's gateway config block.) - Bound every gateway RPC with a 10s timeout so a connected-but- unresponsive gateway cannot block the resolver's background refresh goroutine indefinitely (stale cache -> fail-closed denials). - Keep authbridge-lite minimal: exclude placeholder-resolve from the lite build (it pulls in gRPC + the openshell client) via the lite build-args in build.yaml / ci.yaml / CLAUDE.md. - Document the plugin's config in docs/plugin-reference.md. - Tests: plaintext fail-closed / insecure / loopback dial paths, mtlsConfig + hostOnly + isLoopbackHost, and the resolver's expiry-derived nextDelay. Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Hai Huang <huang195@gmail.com>
…er-resolve Address review feedback on the placeholder-resolve plugin (PR kagenti#626). Secured: - Remove the `env` resolver (the source AND the silent default). It let the agent name any KEY and exfiltrate the proxy's own environment onto the upstream Authorization header. An unconfigured source now fails closed. - credinject.FileResolver path-contains the key (rejects separators / "..") instead of trusting the env-key grammar, so it is safe for arbitrary keys. Simplified: - Require an explicit `source` enum (gateway | secret_dir), replacing the implicit by-field-presence selection and its undocumented precedence. - Drop the `headers` field; hardcode Authorization (the only header any listener reconciles to the upstream wire — others were silently dropped). - Drop the inline `mappings` source from the production config (tests inject a resolver directly). Reusable: - New authlib/credinject package: Resolver / LifecycleResolver interfaces, FileResolver / MapResolver / DenyResolver, and SafeHeaderValue / SafeSetHeader (the CWE-113 header guard, previously unique to this plugin). placeholder- resolve consumes it; the OpenShell gateway resolver satisfies the interfaces structurally. A future host-keyed (e.g. DAM) credential injector can import credinject rather than reimplement these. Optimized: - Replace the per-request regexp with a prefix scan + a strings.Contains early-out; removes the only regexp dependency in the plugin. Tests and plugin-reference.md updated. Build, lite build (placeholder-resolve excluded), go test -race, go vet, and golangci-lint all green. Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Hai Huang <huang195@gmail.com>
Out of curiosity, what is |
OpenShell injects provider-env placeholders as openshell:resolve:env:v<rev>_<NAME>
(secrets.rs: format!("{PREFIX}v{revision}_{key}")), but GetSandboxProviderEnvironment
returns the environment keyed by the bare <NAME>. The gateway resolver did a
byte-for-byte map lookup with no revision handling, so every revision-keyed
placeholder missed and the plugin failed closed (401) — making source: gateway
unusable in practice (only source: secret_dir worked, by naming the mounted file
with the revision prefix).
Resolve now tries the literal key first (covers revision==0 / bare placeholders and
any real var literally shaped like a prefix), then strips a single leading
"v<digits>_" and retries the bare name. Expiry is checked against the resolved
(bare) key. Fail-closed semantics are preserved.
Add TestResolveStripsRevisionPrefix: revision-keyed → bare value, bare still works,
absent/non-numeric/empty-revision fail closed, literal precedence, expiry on the
stripped key, cold cache.
Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Hai Huang <huang195@gmail.com>
…ey/mtls_ca paths OpenShell splits the gateway TLS material — client cert/key in one secret (OPENSHELL_TLS_CERT/KEY) and the CA in another (OPENSHELL_TLS_CA) — but the gateway client loaded ca.crt/tls.crt/tls.key from a single mtls_cert_dir, forcing consumers to recombine the split material into one dir. Replace mtls_cert_dir with three independent file paths (mtls_cert/mtls_key/mtls_ca) mirroring OpenShell's three env vars, so a sidecar/webhook can point straight at the driver-injected /tls/client + /tls/ca mounts (PR unmerged — no back-compat shim). - openshell.Config: MTLSCert/MTLSKey/MTLSCA; mtlsConfig(cert,key,ca,serverName) - gateway.Config + plugin gatewayConfig: mtls_cert/mtls_key/mtls_ca (dropped the mtls_cert_dir default) - tests + plugin-reference.md updated Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Hai Huang <huang195@gmail.com>
|
It's a new mode not yet implemented in OpenShell and lives only in our fork: https://github.com/kagenti/OpenShell/tree/feat/authbridge-egress. It's not yet working end to end, but I'm close. |
The s (hideInactive) toggle evaluated each event against its own phase's invocations — request-phase for a request, response-phase for a response. A passthrough response carries no invocations of its own, so a skip-only request could be hidden while its invocation-less response stayed, orphaning the response (and symmetrically an active request could keep a hidden response). Gate suppression on the whole exchange: a request/response pair is hidden only when BOTH halves are inactive, using the partner pairing already computed in rebuildEventsTable. Unpaired rows fall back to per-event. Add TestRebuildEventsTable_HideInactivePerExchange. Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Hai Huang <huang195@gmail.com>
…dentity) The auto-follow re-set the cursor on every streamed event with index-based heuristics: it INFERRED follow-mode from 'cursor on the last row' (wasAtEnd) and restored the cursor by ROW INDEX. A rebuild that shifts indices — a CONNECT folding into its inner request, hide-inactive filtering, or a buffer trim — landing between key presses could move the cursor opposite to the arrow key (the 'down sometimes scrolls up' report). Track follow mode explicitly (set on session entry / End / a Down reaching the last row; cleared on Up / Home / scrolling away) and, when not following, restore the cursor by EVENT IDENTITY (timestamp+direction+phase) instead of row index, so folding/filtering/trimming can't scroll the view out from under the user. Tests: FollowPinsToNewest, PreservesCursorByIdentity. Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Hai Huang <huang195@gmail.com>
…art, enforce doc note - abctl: goBottom (G/End) now sets m.follow=true, so jumping to the tail after a scroll-up re-pins to new events (app.go documented End/G as follow-entering but goBottom didn't set it). - gateway resolver: Start uses CompareAndSwap instead of load-then-store, so the double-start guard is correct by construction (not just via single-threaded Init); the loser cancels its unused context. - plugin-reference.md: note that 'the placeholder is never forwarded' holds under the default enforce policy; on_error: observe shadows the deny and forwards the literal placeholder (no real credential leaks, but fail-closed no longer holds). Also reviewed, no change: gateway server-cert trust = system roots + configured CA (matches the tlsbridge/upstream.go convention); and the build.yaml 'full plugin set, includes parsers' comment is correct (the default authbridge image builds with empty GO_BUILD_TAGS — parsers + placeholder-resolve included; the excludes are on the separate authbridge-lite image). Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com> Signed-off-by: Hai Huang <huang195@gmail.com>
Summary
Adds an outbound
placeholder-resolveAuthBridge plugin that resolves OpenShell-stylecredential placeholders (
openshell:resolve:env:<KEY>) to their real secret values on theoutbound
Authorizationheader — so a sandboxed agent egressing through AuthBridge never holdsthe real credential.
This is the AuthBridge half of routing an OpenShell sandbox's egress through an AuthBridge
sidecar (OpenShell's
NetworkMode::External): the agent receives a placeholder from OpenShell'snative provider mechanism, and AuthBridge swaps it for the live credential in flight, fail-closed.
What's included
B1 — the plugin (
012603a)placeholder-resolveoutbound plugin with a pluggableResolverinterface; default sources areinline
mappings(testing), a mountedsecret_dir, or the process env.pipeline.Plugin/Configurablecontract with noshared-interface change; registered via a build-tagged
cmd/authbridge-proxy/plugins_placeholderresolve.go(droppable with-tags exclude_plugin_placeholderresolve).B2 — the gateway resolver (
ca8cbd8)authlib/openshell: generated gRPC stubs (buf) + aClientthat replicates the supervisor'sauth chain — projected SA token →
IssueSandboxToken→ gateway JWT →GetSandboxProviderEnvironment(sandbox_id)→ resolved env; re-mints the JWT onUnauthenticated.placeholderresolve/gateway: a cachingResolverthat primes + refreshes the env map in thebackground, serves
Resolvelock-free, honors per-key credential expiry, and fails closed untilprimed.
gatewayconfig block selects this source (precedence overmappings/secret_dir/env); theplugin gains
Init/Ready/Shutdownto drive warm-up (mirrors token-exchange's lifecycle).Testing
metadata flow + JWT re-mint and a placeholder→real-value end-to-end, under
-race.non-placeholder → passthrough.
feat/authbridge-egressbranch: claude-code in asandbox reached its LLM through the AuthBridge sidecar with the credential injected, while the
sandbox env held only the placeholder. The OpenShell-side enabler (
NetworkMode::External) liveson a separate, unmerged branch and is not required to build or test this PR.
Notes
outbound.plugins.authlib/openshell/genproto/.gateway restricts these RPCs to the sandbox's own identity).
Assisted-By: Claude Code