Repository navigation
security: per-sandbox secret binding for sandbox→gateway RPCs #1354
Description
Context
Surfaced during review of PR #1323 (agent feedback loop /wait endpoint).
The sandbox supervisor authenticates to the gateway with
`x-sandbox-secret`, validated against the server-wide
`ssh_handshake_secret` config value. The secret is a single string for
the whole gateway — not per-sandbox.
Several gRPC methods accept this auth scheme:
- `SubmitPolicyAnalysis` (SANDBOX_SECRET_METHODS, existing) — request
carries a `sandbox_name` field; handler persists chunks under that
name without verifying the caller "is" that sandbox. - `GetDraftPolicy` (DUAL_AUTH_METHODS, added in feat(policy): agent-driven policy management — the agent half #1323 so policy.local
/wait works under OIDC) — request carries a `name` field; handler
looks up the sandbox by name without verifying the caller's identity. - Other entries in SANDBOX_SECRET_METHODS follow the same pattern.
Exposure
Any process that holds the shared sandbox secret can act under any
sandbox name. Concrete impact:
- Read: enumerate another sandbox's draft policy chunks
(including rationale/proposed_rule, which can leak the agent's intent
and the developer's reviewer guidance). - Write: submit policy proposals to another sandbox's draft set, with
the receiving developer seeing them in their TUI inbox as if from
their own agent.
This is symmetric across the read and write paths — both already
exist; #1323 extends the read surface as a no-net-new-attack-class
expansion. Worth fixing in one motion rather than two.
Proposed direction
Bind the sandbox secret to a sandbox identity at handshake time so the
gateway can enforce "caller name matches request name" on every
sandbox-secret-authenticated method.
Two plausible shapes:
- Per-sandbox secret. Gateway issues each sandbox a unique secret
during handshake; secret carries (or is keyed on) the sandbox name.
Validation extracts the name from the secret rather than the
request. - Signed sandbox identity claim. Single shared secret, but a
short-lived signed token issued at handshake carries the sandbox
name. Sandbox includes the token alongside the secret; gateway
verifies the signature and compares to the request name.
(1) is simpler but requires per-sandbox secret storage and rotation.
(2) preserves the current single-secret deploy model but adds a
signing step.
Out of scope for this issue
- Bearer-token / OIDC authz (already enforced via scopes).
- Network policy on the sandbox→gateway channel.
Acceptance
- All SANDBOX_SECRET_METHODS and DUAL_AUTH_METHODS handlers that name
a sandbox in the request body verify the caller is that sandbox. - Cross-sandbox call is denied with a clear error.
- Migration path documented for existing deployments (existing
sandboxes can't speak the new scheme until they restart against an
upgraded gateway). - Regression tests cover at least: same-sandbox call succeeds,
cross-sandbox call fails with permission_denied.
References
- PR feat(policy): agent-driven policy management — the agent half #1323 review thread (John Myers): feat(policy): agent-driven policy management — the agent half #1323
- Current sandbox-secret validation:
`crates/openshell-server/src/auth/oidc.rs::validate_sandbox_secret`
Activity
refactor(server): introduce Authenticator trait and Principal enum
feat(server): gateway-minted sandbox JWTs and IssueSandboxToken RPC
feat(server)!: per-sandbox JWT identity over Bearer (wire break)
fix(sandbox): strip supervisor-only credentials from entrypoint env
fix(server): per-handler sandbox_id equality check (closes #1354)
refactor(server): introduce Authenticator trait and Principal enum
feat(server): gateway-minted sandbox JWTs and IssueSandboxToken RPC
feat(server)!: per-sandbox JWT identity over Bearer (wire break)
fix(sandbox): strip supervisor-only credentials from entrypoint env
fix(server): per-handler sandbox_id equality check (closes #1354)
refactor(server): introduce Authenticator trait and Principal enum
feat(server): gateway-minted sandbox JWTs and IssueSandboxToken RPC
24 remaining items
feat(server): gateway-minted sandbox JWTs and IssueSandboxToken RPC
feat(server)!: per-sandbox JWT identity over Bearer (wire break)
fix(sandbox): strip supervisor-only credentials from entrypoint env
fix(server): per-handler sandbox_id equality check (closes #1354)
refactor(server): introduce Authenticator trait and Principal enum
feat(server): gateway-minted sandbox JWTs and IssueSandboxToken RPC
feat(server)!: per-sandbox JWT identity over Bearer (wire break)
fix(sandbox): strip supervisor-only credentials from entrypoint env
fix(server): per-handler sandbox_id equality check (closes #1354)
refactor(server): introduce Authenticator trait and Principal enum
feat(server): gateway-minted sandbox JWTs and IssueSandboxToken RPC
feat(server)!: per-sandbox JWT identity over Bearer (wire break)
fix(sandbox): strip supervisor-only credentials from entrypoint env
fix(server): per-handler sandbox_id equality check (closes #1354)