Skip to content

Claude Code + Vertex AI Authentication Flow

How Claude Code authenticates to Google Vertex AI when running inside an OpenShell sandbox, from secret storage to API response.

The full chain

┌─────────────────────────────────────────────────────────────────────┐
│ GitHub Actions CI                                                   │
│                                                                     │
│  ┌──────────────┐    google-github-actions/auth@v2                  │
│  │ GitHub Secret │──────────────────────────────┐                   │
│  │ GCP_SA_KEY   │  (raw JSON content)           │                   │
│  └──────────────┘                               ▼                   │
│                                        ┌──────────────────┐         │
│                                        │ gha-creds-*.json │         │
│                                        │ (file on disk)   │         │
│                                        └────────┬─────────┘         │
│                                                 │                   │
│                          GOOGLE_APPLICATION_CREDENTIALS              │
│                          = /path/to/gha-creds-*.json                │
│                                                 │                   │
│  ┌──────────────────────────────────────────────┼──────────────┐    │
│  │ fullsend runner                              │              │    │
│  │                                              ▼              │    │
│  │  1. Load providers/gcp-vertex.yaml                          │    │
│  │  2. openshell provider create --name gcp-vertex ...         │    │
│  │  3. openshell sandbox create --provider gcp-vertex          │    │
│  │  4. SCP gha-creds-*.json → sandbox (credential_files)      │    │
│  └─────────────────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────────────────┘




┌─────────────────────────────────────────────────────────────────────┐
│ OpenShell Gateway (k3s cluster)                                     │
│                                                                     │
│  Provider store:                                                    │
│    gcp-vertex:                                                      │
│      CLAUDE_CODE_USE_VERTEX = "1"                                   │
│      ANTHROPIC_VERTEX_PROJECT_ID = "my-project"                     │
│      CLOUD_ML_REGION = "us-central1"                                │
│      GOOGLE_APPLICATION_CREDENTIALS = "/tmp/workspace/...json"      │
│                                                                     │
│  On sandbox creation: injects env vars into sandbox supervisor      │
└─────────────────────────────────────────────────────────────────────┘




┌─────────────────────────────────────────────────────────────────────┐
│ OpenShell Sandbox                                                   │
│                                                                     │
│  Environment (injected by supervisor via provider):                 │
│    CLAUDE_CODE_USE_VERTEX=1                                         │
│    ANTHROPIC_VERTEX_PROJECT_ID=my-project                           │
│    CLOUD_ML_REGION=us-central1                                      │
│    GOOGLE_APPLICATION_CREDENTIALS=/tmp/workspace/.gcp-creds.json    │
│                                                                     │
│  File (copied via SCP):                                             │
│    /tmp/workspace/.gcp-credentials.json  ← service account key      │
│                                                                     │
│  ┌──────────────────────────────────────────────────────────────┐   │
│  │ Claude Code (node process)                                   │   │
│  │                                                              │   │
│  │  1. Sees CLAUDE_CODE_USE_VERTEX=1 → activates Vertex mode    │   │
│  │  2. Reads ANTHROPIC_VERTEX_PROJECT_ID + CLOUD_ML_REGION      │   │
│  │  3. Constructs endpoint URL:                                 │   │
│  │     https://{region}-aiplatform.googleapis.com/v1/            │   │
│  │       projects/{project}/locations/{region}/                  │   │
│  │       publishers/anthropic/models/{model}:streamRawPredict   │   │
│  │                                                              │   │
│  │  ┌──────────────────────────────────────────────────────┐    │   │
│  │  │ google-auth-library                                  │    │   │
│  │  │                                                      │    │   │
│  │  │  1. Reads GOOGLE_APPLICATION_CREDENTIALS             │    │   │
│  │  │  2. Loads service account JSON (private key + email)  │    │   │
│  │  │  3. Creates JWT:                                     │    │   │
│  │  │     {                                                │    │   │
│  │  │       "iss": "sa@project.iam.gserviceaccount.com",   │    │   │
│  │  │       "scope": "cloud-platform",                     │    │   │
│  │  │       "aud": "https://oauth2.googleapis.com/token",  │    │   │
│  │  │       "exp": now + 3600                              │    │   │
│  │  │     }                                                │    │   │
│  │  │  4. Signs JWT with private key (RS256)               │    │   │
│  │  │  5. POST to oauth2.googleapis.com/token              │    │   │
│  │  │     → receives access_token (1-hour TTL)             │    │   │
│  │  └──────────────┬───────────────────────────────────────┘    │   │
│  │                 │                                            │   │
│  │                 ▼                                            │   │
│  │  6. Sends request to Vertex AI:                              │   │
│  │     POST https://us-central1-aiplatform.googleapis.com/...   │   │
│  │     Authorization: Bearer ya29.c.abc123...  (access token)   │   │
│  │     Content-Type: application/json                           │   │
│  │     Body: { "anthropic_version": "...", "messages": [...] }  │   │
│  │                                                              │   │
│  └──────────────────────────────┬───────────────────────────────┘   │
│                                 │                                   │
│  ┌──────────────────────────────┼───────────────────────────────┐   │
│  │ Sandbox Proxy                │                               │   │
│  │                              ▼                               │   │
│  │  Network policy check:                                       │   │
│  │    ✓ *.googleapis.com:443 — allowed                          │   │
│  │    ✗ anything else — denied                                  │   │
│  │                                                              │   │
│  │  Forwards request to upstream                                │   │
│  └──────────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────────┘




┌─────────────────────────────────────────────────────────────────────┐
│ Google Cloud                                                        │
│                                                                     │
│  oauth2.googleapis.com                                              │
│    ← JWT signed with SA private key                                 │
│    → access_token (1hr TTL)                                         │
│                                                                     │
│  us-central1-aiplatform.googleapis.com                              │
│    ← access_token + prompt                                          │
│    → Claude response (streamed)                                     │
└─────────────────────────────────────────────────────────────────────┘

What lives where

AssetLocationSensitivity
Service account JSON (private key)GitHub Secret → CI disk → sandbox fileHigh — can generate unlimited tokens
OAuth2 access tokenGenerated inside sandbox, sent in HTTP headerMedium — expires in 1 hour
Project ID, regionProvider env vars in sandboxLow — not secret
CLAUDE_CODE_USE_VERTEXProvider env var in sandboxNone — just a flag

Security boundary

The sandbox network policy restricts egress to *.googleapis.com:443 only. Even though the service account private key is inside the sandbox, the agent cannot exfiltrate it to any other destination. The proxy logs every allow/deny decision for audit.

Why OpenShell L7 egress policies can't fully isolate GCP credentials

OpenShell's provider placeholder model (openshell:resolve:env:*) works when credentials are strings placed in known HTTP locations (headers, query params, path segments) and auth is a single step. GCP Vertex AI breaks every assumption:

  1. Credential is a file, not a string. The service account JSON contains a private key, email, and project ID. Providers inject env vars, not files. GOOGLE_APPLICATION_CREDENTIALS points to a file path that must exist on disk.

  2. Auth is multi-step, not single-hop. google-auth-library (a Google npm package used internally by the Anthropic SDK's Vertex integration) reads the private key, signs a JWT locally, and POSTs to oauth2.googleapis.com to receive an access token. Claude Code and the Anthropic SDK never touch the private key directly — they just call getRequestHeaders() and get back a Bearer token. But google-auth-library needs the key file to do this work, and it runs inside the sandbox. The proxy sees two separate HTTPS connections (token exchange + API call) with no relationship between them.

  3. Cryptographic operation inside the sandbox. JWT signing (RS256) is performed by google-auth-library, not by Claude Code or the Anthropic SDK. But since it runs in the same process inside the sandbox, the private key must be present in the sandbox filesystem. The proxy can't sign JWTs on behalf of the library — there is no hook point.

  4. Dynamic URL construction. The Anthropic SDK builds {region}-aiplatform.googleapis.com from env vars before making any HTTP request. If CLOUD_ML_REGION were a placeholder, DNS resolution would fail. These values must be real strings in the process environment.

  5. Token refresh is automatic and internal. google-auth-library caches and refreshes the access token when it nears expiry. This requires the private key to remain available. A pre-generated token passed through a provider would expire after 1 hour with no way to refresh.

  6. No access token injection point. google-auth-library doesn't support GOOGLE_OAUTH_ACCESS_TOKEN or similar env var. You can't bypass the file-based auth without modifying Google's library.

What L7 policies CAN do

  • Restrict egress to *.googleapis.com:443 (prevents exfiltration)
  • Scope which binaries can make outbound connections
  • Log every allow/deny decision for audit

What L7 policies CANNOT do

  • Keep the private key out of the sandbox
  • Scope requests to specific Vertex AI models or projects at the proxy layer
  • Replace the REST server tier for GCP-authenticated services

Conclusion

OpenShell providers + L7 policies can fully replace the REST server tier for services with static API key auth (GitHub, OpenAI, Anthropic direct). For services with OAuth2/OIDC flows like GCP Vertex AI, the private key must be in the sandbox and the security boundary is the network policy, not credential isolation. Solving this would require OpenShell to act as an OAuth2 token broker — generating and refreshing tokens outside the sandbox on behalf of the agent.

Possible improvements

Short-term: If google-auth-library supported accepting a pre-generated access token via environment variable, the runner could generate the token on the host and pass only the short-lived token (1hr) through the provider — keeping the private key out of the sandbox entirely. This is not supported today.

Medium-term: Workload Identity Federation eliminates service account keys entirely by letting GitHub Actions authenticate to GCP using GitHub's OIDC identity. This removes the stored secret from GitHub but does not change the sandbox problem — the credential config file requires the GitHub OIDC token endpoint (ACTIONS_ID_TOKEN_REQUEST_URL) which is only available in CI, not inside the sandbox. Additionally, WIF couples the authentication to the CI platform. The GCP Workload Identity Pool must be configured with an OIDC provider for each platform (GitHub uses token.actions.githubusercontent.com, GitLab uses gitlab.com, Tekton uses the cluster's service account issuer). Moving to a different CI system or running locally requires reconfiguring the GCP trust relationship each time.

Long-term: OpenShell could add an OAuth2 provider type that handles token generation and refresh on the gateway side, injecting only short-lived access tokens into the sandbox via the existing placeholder system. This would require the gateway to manage service account keys and token lifecycles.