Skip to content

Shrink OAuth-token blast radius for suspicious workspaces (SANDY_SUSPICIOUS: strip refresh token, access-token-only) — pre-broker slice of #121 #130

Description

@rappdw

The residual

Field observation, in a maintainer's own words:

The one thing worth knowing: your Anthropic OAuth access and refresh tokens are on disk here, readable by any process running as your user in this container. They can't reach Gmail from inside this VLAN, but pypi.org and github.com are allowlisted, so exfiltration through one of those and reuse elsewhere is the realistic residual risk. That file is the one credential actually worth protecting on this machine.

Exactly right. Sandy mounts the host .credentials.jsonaccess token and refresh token — ephemerally into every container (load_credentials() → tmpdir → bind mount, sandy ~L7853/8140). The refresh token is the crown jewel: an exfiltrated refresh token is permanent, renewable account access until manually revoked, and permissive-mode egress allowlists (pypi/github) are plausible exfil channels a poisoned dependency or injected agent could use.

The gap vs. what's already tracked

Neither ships a cheap, near-term, per-workspace way to shrink the token's blast radius for a workspace you actively distrust. This issue is that slice.

Proposed: SANDY_SUSPICIOUS — a hardened-credential posture, no broker required

An umbrella opt-in for "I don't trust this code" that composes existing + new tightening, all strengthening (so passive-safe to turn on from a workspace .sandy/config — a repo may make itself more suspicious, never less):

  1. Strip the refresh token (the core win). Before mounting .credentials.json, rewrite it to drop claudeAiOauth.refreshToken (and any other long-lived fields), mounting only the short-TTL access token. Sandy already parses claudeAiOauth.expiresAt (token_needs_refresh), so this is a small JSON rewrite in the existing tmpdir path — no host helper, no TLS termination, no Adopt nono's phantom-token credential-proxy pattern for Phase-1 broker-not-mount #121 broker.
    • Blast radius: permanent-and-renewable → the remaining access-token lifetime (hours). An exfiltrated copy expires at expiresAt and cannot be refreshed. This is the single highest-leverage, lowest-cost mitigation for the stated residual.
    • Trade-off (document it): in-session token refresh fails once the access token expires → the agent must /login again or the long session ends. Acceptable-to-desirable for a deliberately-suspicious session (you don't want a marathon run against untrusted code anyway). For a fresh access token at launch, sandy can refresh host-side first (it already can) and mount the newest one.
  2. Connectors off — force disableClaudeAiConnectors: true (claude.ai account connectors (Gmail, Drive, …) leak into every sandbox — suppress by default, opt in per-instance #129) regardless of SANDY_CLAUDE_CONNECTORS.
  3. Default to strict egress — narrow the exfil channel set (still not airtight — allowlisted hosts remain — which is why Codex support, Gemini strengthening, multi-agent mode #1 matters most).

Alternative / complementary credential modes (menu, pick for v1)

  • Disposable API key. If the user supplies a scoped/revocable ANTHROPIC_API_KEY for the suspicious workspace, prefer it and mount no OAuth creds at all — clean compartmentalization, instantly revocable without touching the primary account. (Sandy already supports API-key auth; this is just "prefer it + skip the OAuth mount under SANDY_SUSPICIOUS.")
  • Access-token-only is the default hardened mode when only OAuth is available (Codex support, Gemini strengthening, multi-agent mode #1 above).

Tier & surface

  • New key SANDY_SUSPICIOUS (bool) — passive-safe (strengthening). Also settable host/env.
  • Records resolved credential posture in sandy-session.json (mirrors the effort field precedent, and Adopt nono's phantom-token credential-proxy pattern for Phase-1 broker-not-mount #121 item 4) so a run's protection level is provable after the fact: e.g. "cred_mode": "access-token-only" | "api-key" | "full".
  • Consider making SANDY_SUSPICIOUS the documented composition point so future hardening (seccomp Unix-socket block, SANDY_RUNTIME=runsc, tool-audit on) can bundle under it.

Relationship to #121

This is the pre-broker, no-new-daemon delivery of #121's A1 "OAuth refresh→short-lived access token" idea, scoped to the suspicious-workspace use case and shippable independently. #121 remains the endgame (token never in-container). Ship this first; it captures most of the value at a fraction of the cost, and de-risks the #121 A1 design by proving the short-lived-token flow end-to-end.

Acceptance criteria

  • SANDY_SUSPICIOUS=1: mounted .credentials.json contains an access token but no refreshToken; agent can call the API; after expiresAt refresh fails (verified, not just asserted).
  • Connectors off under SANDY_SUSPICIOUS (ties to claude.ai account connectors (Gmail, Drive, …) leak into every sandbox — suppress by default, opt in per-instance #129).
  • If a scoped ANTHROPIC_API_KEY is present, OAuth creds are not mounted at all.
  • sandy-session.json records cred_mode.
  • Passive-safe from a workspace .sandy/config (strengthening → no approval prompt); --print-schema metadata row; README + CLAUDE.md; run-tests.sh structural assertion (strip logic + session-marker field).

Honest limits

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions