fix(claude_k8s): make adapterConfig.env default-deny, not name-pattern (BLO-22546) - #1463
Open
allyblockcast[bot] wants to merge 1 commit into
Open
fix(claude_k8s): make adapterConfig.env default-deny, not name-pattern (BLO-22546)#1463allyblockcast[bot] wants to merge 1 commit into
allyblockcast[bot] wants to merge 1 commit into
Conversation
…n (BLO-22546) AC 3 of BLO-22546 requires default-deny classification for operator-supplied env. What shipped in #1092 is SENSITIVE_ENV_NAME_RE, a name *pattern*, so an operator key the pattern misses -- PENSTOCK_BOARD, GH_PAT, *_CRED, ANTHROPIC_CUSTOM_HEADERS, or a plain FOO_BAR -- reached the PodSpec as a literal `value:`, readable by any principal with `get pods`. That is a broader grant than `get secrets`, which is the whole point of the Secret/PodSpec split. adapterConfig.env is a small, operator-set map that is overwhelmingly credential material, so at that layer classification now ignores the key name entirely. The broad pattern still covers the merged/inherited layers, per the 2026-08-22 CTO ruling on the issue -- this does not Secret-ify the ~70 PATH/cache/flag vars that arrive by inheritance. Scoped to keys whose operator value actually survived the merge, because Layer 4 is applied before the session-header/HOME/isolation/cache blocks and several of those overwrite unconditionally. Where the adapter won, the shipped value is the adapter's own non-secret path and correctly stays a literal. Adds a fail-closed findLiteralOperatorEnvVarsInPodSpec backstop beside the two existing ones, checked on the assembled spec so a container added later cannot escape it. That is what makes the default-deny non-regressable rather than a convention a future edit quietly undoes. Three pre-existing tests asserted the old contract (operator env ships as a literal .value) and now assert Secret-backed delivery. Operator precedence is unchanged in all three; only the delivery mechanism is. Verified: 802/802 tests, tsc --noEmit clean, build clean, integrity hash regenerated (69286948 -> ae16d982), and the new default-deny falsifier confirmed to FAIL against a name-pattern-only classifier. Does NOT close BLO-22546: opencode_k8s is still cloned from kkroo at OPENCODE_K8S_REF and remains unfixed. Co-Authored-By: Paperclip <noreply@paperclip.ing>
Author
1 similar comment
Author
Author
|
Hey @allyblockcast[bot]! Before this PR can be reviewed, a few things need attention: Missing or incomplete:
Once updated, push a new commit and these checks will re-run automatically. — commitperclip |
Author
There was a problem hiding this comment.
Ally — Consolidated PR Review
Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: 9fa22e6
Critical Issues (1)
- [native-codex]
vendor/paperclip-adapter-claude-k8s/src/server/job-manifest.ts:757-786— operator ownership is inferred frommerged[key] === envConfig[key], so transformed operator values are treated as adapter-owned and remain literal. For example, an operator-providedANTHROPIC_CUSTOM_HEADERSvalue is concatenated with the Penstock session header at lines 709-717; the resulting value still contains operator-controlled material, but the equality check fails and the loop emits it asvalueinstead of asecretKeyRef. This is a credential disclosure throughGET Pod, contrary to the default-deny contract.- Track operator provenance separately from final string equality, and route any final value containing operator-supplied material through the Secret. Add a regression test for a non-empty
ANTHROPIC_CUSTOM_HEADERSvalue without an existingx-penstock-sessionline, asserting no literal value and Secret-backed data.
- Track operator provenance separately from final string equality, and route any final value containing operator-supplied material through the Secret. Add a regression test for a non-empty
Important Issues (0)
Suggestions (0)
Strengths
- The non-pattern key and empty-value cases are covered, and the assembled-pod fail-closed guard is a useful defense-in-depth check.
- The change preserves operator precedence while addressing the name-pattern bypass for values that are directly forwarded.
Recommended Action
- Fix the Critical issue before merge.
- Re-run the adapter tests and submit a fresh exact-head review after the fix.
This was referenced Sep 1, 2026
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.
Issue: https://paperclip.blockcast.net/BLO/issues/BLO-22546
Thinking Path
Linked Issues or Issue Description
Refs BLO-22546 (AC 3), follows #1092 which vendored this adapter, parent BLO-22471.
EnvSecretmachinery this change re-uses; fix(adapter): land the stranded BLO-21812 ServiceAccount fix in the vendored tree (BLO-25403) #1409/security(adapter): stop propagating the @allyblockcast user-seat token into agent Job pods (BLO-24056) #1411 touch the same file but on the ServiceAccount and inherit-allowlist paths.What Changed
Three files, all inside
vendor/paperclip-adapter-claude-k8s/.1.
adapterConfig.envis default-deny (job-manifest.ts). The conversion loop previously routed a value into the env Secret only whenisSensitiveEnvName(name)matched. It now also routes when the key is operator-supplied, whatever it is called:The broad name pattern is retained for the merged/inherited layers, per the 2026-08-22 CTO ruling on the issue. This deliberately does not Secret-ify the ~70
PATH/cache/flag vars that arrive by inheritance.2. Scoped to values that actually ship.
operatorOwnedKeysis not simplyObject.keys(adapterConfig.env). Layer 4 is merged before the session-header,HOME/isolation and cache blocks, several of which overwrite unconditionally — so "the operator set this key" is not the same as "the operator's value is what ships". The predicate ismerged[k] === envConfig[k], so where the adapter won, the shipped value is the adapter's own non-secret path and correctly stays a literal.3. Fail-closed backstop. New
findLiteralOperatorEnvVarsInPodSpec, checked on the assembled spec beside the two existing guards, so a container added later is covered automatically. This is the part that makes the guarantee non-regressable rather than a convention.The two details most worth reviewing
&& valueis load-bearing. An empty operator value must stay inline.sensitiveEnvDataonly receives non-empty values, so emitting asecretKeyReffor an empty one would point at a key the Secret never gets and the pod would fail at startup withCreateContainerConfigError. Pinned by a test.DOCKER_HOSTis excluded viaADAPTER_REASSERTED_ENV_NAMES.enableDockerpushes a literalDOCKER_HOSTonto the main container afterbuildEnvVarshas returned. Without the exclusion, an operator setting the same key would get both asecretKeyRefand a literal for one name, and the new guard would fire on the adapter's own write. Pinned by a test that drives the real assembled manifest withenableDocker: true.Verification
vitest run(full adapter suite)tsc --noEmitnpm run build69286948…→ae16d982…, recomputed value matches recordedThe falsifier was verified as a falsifier. AC 3's verifying signal asks for a test that a name-pattern implementation fails. I reverted the predicate to name-pattern-only as a positive control and confirmed
routes a deliberately non-credential-looking operator key to a secretKeyReffails, then restored. A test that passes both before and after would not have demonstrated anything. (That control run showed additional collateral failures from tests that mutate a sharedctx; the control patch was a throwaway and is not in this branch.)Integrity hash method validated before use — I recomputed it on pristine
HEADfirst and reproduced the recorded69286948…exactly, so the new value comes from a method known to agree with CI rather than an assumed one. File count unchanged at 39.Three pre-existing tests were updated, deliberately
These asserted the old contract — that operator env ships as a literal
.value— which is precisely what AC 3 forbids. Operator precedence is unchanged in all three; only the delivery mechanism is:preserves explicit adapter cache env overrides— override still takes effect, now viaenvSecret.data.user env config overrides inherited env— operator still beats inherited;AWS_REGIONmatches no sensitive pattern and previously shipped readable.appends the session header … respects a manual override— instructive case. Sub-case 1 (adapter appends to the operator value) still asserts a literal and passes unchanged; sub-case 2 (operator value survives untouched) is now Secret-backed. Same variable, different classification, decided by whether the operator's value is what actually ships.Risks
adapterConfig.env— sooperatorOwnedKeysis empty and this is a behavioural no-op on today's fleet, engaging only when an operator adds a key. Other companies are not visible to me; a reviewer with cross-company visibility should sanity-check that.env[].valuetoenvSecret. Anything reading a Job's PodSpec expecting a literal operator value would now see asecretKeyRef. I checked for build-time readers: nothing does.find()on the env array or reads.valueoff it;envVarsis only appended to and assigned at the container. Runtime consumers are unaffected — asecretKeyRefis transparent to the process.opencode_k8sis still cloned fromkkroo/paperclip-adapter-opencode-k8satOPENCODE_K8S_REF=ed03316and still shipsPAPERCLIP_API_KEYandOPENCODE_CONFIG_JSONas literals. Fixing it requires either push to that personal repo or vendoring it as feat(vendor): bring claude_k8s adapter in-tree and retire CLAUDE_K8S_REF (BLO-17980) #1092 did — neither is available to me. Left on the issue for the CTO with a migrate-vs-vendor decision, since exactly one agent (Ally) uses that adapter.PAPERCLIP_K8S_ISOLATION_KEYis untouched here and remainssecretKeyRef-wrapped in env while the same value is published in plaintext as thepaperclip.io/isolation-keypod label. Out of scope for this PR; noted on the issue as one decision to make.allyblockcast[bot]App identity, so perCLAUDE.mdit trips the self-review path and needs both an AppCOMMENTEDreview and the separate singleton User/team approval on the exact head. I have not pushed anything further to this branch so the head SHA stays frozen.Model Used
claude-opus-5[1m] via Claude Code
🤖 Generated with Claude Code