fix(claude-k8s): allowlist what agent pods inherit from the server pod (BLO-22514) - #1377
Merged
Merged
Conversation
…LO-22514) Agent Job pods were built by copying the paperclip server pod's entire environment. getSelfPodInfo() snapshotted every literal env value, every valueFrom (including secretKeyRef), every envFrom source and every mounted secret volume, and job-manifest.ts replayed all of it onto every agent Job with no allowlist, denylist or filter anywhere on the path. So each agent container held PAPERCLIP_AGENT_JWT_SECRET (mint an API key for any agent, any scope), DATABASE_URL (bypass the API and all of authorization.ts) and GITHUB_APP_PRIVATE_KEY (mint installation tokens org-wide). The readers are our own agents, so the live path is prompt injection: agents routinely ingest attacker-influenceable text, and a payload that induces one agent to read and post these turns a content bug into control-plane compromise. Filter at getSelfPodInfo() rather than at the four replay sites, so a future replay site cannot reintroduce the leak by forgetting to filter, with a fail-closed findServerOnlyEnvVarsInPodSpec backstop in buildJobManifest because SelfPodInfo is a plain object callers construct directly. secretKeyRef entries are filtered on the same basis as literals. An earlier analysis called them "fine" because they do not surface through GET Pod; they are not — the kubelet resolves them into the container environment either way. The keep-set is derived from the adapter's own by-name reads plus a sweep of agent-pod consumers, not pattern-matched, and both directions are tested: a filter that dropped everything would pass a deny-only suite while breaking every run in the fleet. PAPERCLIP_PUBLIC_URL is in it only because of that sweep — it is read by packages/mcp-server and appears nowhere in this adapter. Measured against the live server env: 54 vars in, 24 inherited, 30 dropped, 0 control-plane credentials remaining. Also corrects the claim in sensitive-env-guard.ts that it "mirrors the same protection" in this adapter. It did not: that guard is the /TOKEN|SECRET|.../i denylist its own POLICY note explains cannot hold the invariant. And records the decision to keep env-guard.ts fail-OPEN, since this change removes what it was standing in front of. Refs BLO-22506. Closes BLO-22514. Co-Authored-By: Claude <noreply@anthropic.com>
Author
The row was written before the PR existed and guessed #1381. Co-Authored-By: Claude <noreply@anthropic.com>
Author
|
@ally please review at head Focus, in priority order:
Not asking you to re-litigate: credential rotation is strictly post-fix and out of scope, and the |
This was referenced Aug 16, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Thinking Path
Linked Issues or Issue Description
Closes #keyword — an earlier revision of this body wroteCloses #22514, which GitHub would have resolved to an unrelated number)k8s-roread path; this issue was split out of it)items:selector, merged today — a narrower fix on the same file, not a substitute for this one)What Changed
src/server/inherit-allowlist.ts— the policy, with every entry justified inline:SERVER_ONLY_ENV_DENY,AGENT_ENV_ALLOWLIST,AGENT_ENV_ALLOWED_PREFIXES,AGENT_SECRET_VOLUME_ALLOWLIST,AGENT_ENV_FROM_ALLOWLIST.getSelfPodInfo()(k8s-client.ts), not at the four replay sites.job-manifest.tsreplays inherited state in four places (:599literal spread,:679secretKeyRef,:1053secret volumes,:1470envFrom). Filtering at the source means a fifth replay site added later cannot reintroduce the leak by forgetting to filter.secretKeyRefentries are filtered on the same basis as literals. BLO-22506's analysis called these "fine" because they do not surface throughGET Pod. They are not fine — the kubelet resolves them into the container environment, so the agent reads them either way. That distinction is the whole reason this issue is separate from BLO-22506.secretName(not volume name, which is a chart-local label). The three agent-facing mounts are kept —values.blockcast.yamldocuments that propagation as intentional and agents genuinely need them forgh/git/gbrain.envFromdenied by default. It injects whole objects under names a per-name allowlist never observes, so it cannot be reconciled with one. NoenvFromexists anywhere indeploy/helm/paperclip, so this is a no-op against the current deployment that closes the path pre-emptively.findServerOnlyEnvVarsInPodSpec()throwing inbuildJobManifest, mirroring the existingfindLiteralSensitiveEnvVarsInPodSpecidiom.SelfPodInfois a plain object that callers and tests construct directly, so the type system never guarantees its contents came through the filter.packages/plugins/sandbox-providers/kubernetes/src/sensitive-env-guard.ts, which asserted it "mirrors the same protection in the external claude_k8s adapter". Wrong twice: the adapter is no longer external, and what it has is the/TOKEN|SECRET|…/idenylist that file's own POLICY note explains cannot hold the invariant.env-guard.tsfail-open decision in the file (comment-only). See Risks.vendor_claude_k8sjob requires.Deriving the keep-set
Dropping a needed key breaks every agent run in the fleet, so the keep-set is derived from code rather than pattern-matched — and by-name reads alone are not sufficient. Only two names (
CLAUDE_CONFIG_DIR,PAPERCLIP_API_URL) are read offinheritedEnvin this adapter; the rest are consumed downstream by Claude Code, theghwrapper and the MCP bridges, so they never appear as a read here. A sweep of agent-pod consumers foundPAPERCLIP_PUBLIC_URL(read bypackages/mcp-server/src/paperclip-links.ts, absent from this adapter entirely) — a genuine gap an adapter-only derivation would have shipped.Two deliberate calls worth reviewer attention:
XDG_CONFIG_HOMEis excluded. The server's value is/runtime-config, an emptyDir the Job pod never mounts, and the adapter only sets it under isolation. It is load-bearing forgit config --globalresolution, so inheriting a non-existent path is worse than letting it fall back to$HOME.PAPERCLIP_MASTER_KEY_SEEDwas added to the deny-set though BLO-22514 does not name it. Found while enumerating the server env; it is the secret-encryption master seed and belongs in the same class as the JWT key.Retained conservatively and flagged in-code as tightening candidates rather than dropped blind:
PAPERCLIP_GBRAIN_*andPAPERCLIP_CODEX_*/PAPERCLIP_OPENCODE_*(no agent-pod reader found, but they are a URL and routing config rather than credentials), andNODE_OPTIONS(status-quo preservation; re-tuning a 6144 MiB heap ceiling for an 8 Gi Job pod is a separate change, not one to bundle into a security fix).Verification
Provenance gate, as CI computes it:
Exercised against the real server env, not just fixtures — all 54 vars from
values.blockcast.yaml+deployment-api.yamlpushed through the predicate:Dropped includes
PAPERCLIP_AGENT_JWT_SECRET,DATABASE_URL,GITHUB_APP_PRIVATE_KEY,GITHUB_APP_ID,GITHUB_APP_INSTALLATION_ID,GITHUB_WEBHOOK_SECRET,PAPERCLIP_DEX_OIDC_CLIENT_SECRET,PAPERCLIP_ALERTMANAGER_WEBHOOK_TOKEN,PAPERCLIP_MASTER_KEY_SEED. Kept includesPAPERCLIP_API_URL,PAPERCLIP_PUBLIC_URL,PATH,PAPERCLIP_GITHUB_TOKEN_FILE, and theANTHROPIC_*/OPENAI_*provider families. Secret volumes: the three agent-facing mounts kept,paperclip-db-credentialsdropped.Only one pre-existing test needed changing: a cache-keying fixture asserting on a synthetic
CLUSTER_TAG. It was retargeted to an allowlisted var so it still proves cache isolation — no test asserting real agent behaviour broke, which is itself evidence the keep-set is right.A green CI run does not close this issue. BLO-22514's verifying signal requires confirming against a freshly-scheduled agent pod after rollout — a passing unit test does not prove the running adapter image carries the filter. I will run the length-only probe post-deploy and post it on the issue.
Risks
ANTHROPIC_,OPENAI_,AWS_,GOOGLE_,VERTEX_,AZURE_OPENAI_,CLAUDE_CODE_) so Bedrock/Vertex deployments routing through names this repo never mentions keep working. Those namespaces are owned by external SDKs, so no Paperclip control-plane secret can fall inside one — and the deny-set is checked first regardless.XDG_CONFIG_HOMEand ~29 other server-only vars.XDG_CONFIG_HOMEis called out above as a fix rather than a regression.assigneeAdapterOverrides.adapterConfigis writable through the Paperclip API, so a config-extensible allowlist would let an agent re-admitDATABASE_URLfor itself — reintroducing the exact escalation this closes. Widening requires a code change and a review.env-guard.tsstays fail-open — decision recorded in-file rather than only on the issue. BLO-22514 asked because it fails open on any error and did not fire against a liveprintenv "$V". Both true. It stays open because (a) it runs in the agent's own runtime and failing closed wedges every run on any parse error, (b) it cannot be made complete anyway —X=env; $Xandcat /proc/$$/environresolve at runtime and no static pass over command text can classify them, and (c) this PR removes what it was standing in front of: a successful dump now discloses agent-scoped provider credentials the agent already uses, not the control plane. Paying an availability cost for it is a worse trade after this change than before.Model Used
claude-opus-4-5, 1M context), extended thinking, agentic tool use via Claude Code / Paperclipclaude_k8sadapter.Checklist
Fixes: #/Closes #/Refs #OR (b) described the issue in-PR following the relevant issue templatesensitive-env-guard.tsheader)