Skip to content

fix(claude_k8s): make adapterConfig.env default-deny, not name-pattern (BLO-22546) - #1463

Open
allyblockcast[bot] wants to merge 1 commit into
masterfrom
sre/blo-22546-layer4-default-deny
Open

fix(claude_k8s): make adapterConfig.env default-deny, not name-pattern (BLO-22546)#1463
allyblockcast[bot] wants to merge 1 commit into
masterfrom
sre/blo-22546-layer4-default-deny

Conversation

@allyblockcast

@allyblockcast allyblockcast Bot commented Aug 22, 2026

Copy link
Copy Markdown

Issue: https://paperclip.blockcast.net/BLO/issues/BLO-22546

Thinking Path

  • Paperclip runs every agent's work in a Kubernetes Job, and the claude_k8s adapter templates the PodSpec for all of them
  • Anything an operator puts in adapterConfig.env is rendered into that PodSpec, so its classification decides whether a credential is readable by get pods or only by get secrets
  • feat(vendor): bring claude_k8s adapter in-tree and retire CLAUDE_K8S_REF (BLO-17980) #1092 shipped that classification as a name pattern (/TOKEN|SECRET|PASSWORD|KEY|CREDENTIAL|AUTH/i), which is exactly what AC 3 of BLO-22546 forbids — PENSTOCK_BOARD, GH_PAT, *_CRED and FOO_BAR all miss it and ship as plaintext
  • This makes the operator-env layer default-deny regardless of key name, and adds a fail-closed assertion so a future edit cannot quietly undo it
  • The benefit is that the layer which motivated the ticket stops depending on someone naming their variable well

Linked Issues or Issue Description

Refs BLO-22546 (AC 3), follows #1092 which vendored this adapter, parent BLO-22471.

What Changed

Three files, all inside vendor/paperclip-adapter-claude-k8s/.

1. adapterConfig.env is default-deny (job-manifest.ts). The conversion loop previously routed a value into the env Secret only when isSensitiveEnvName(name) matched. It now also routes when the key is operator-supplied, whatever it is called:

if ((isSensitiveEnvName(name) || operatorOwnedKeys.has(name)) && value) {

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. operatorOwnedKeys is not simply Object.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 is merged[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

&& value is load-bearing. An empty operator value must stay inline. sensitiveEnvData only receives non-empty values, so emitting a secretKeyRef for an empty one would point at a key the Secret never gets and the pod would fail at startup with CreateContainerConfigError. Pinned by a test.

DOCKER_HOST is excluded via ADAPTER_REASSERTED_ENV_NAMES. enableDocker pushes a literal DOCKER_HOST onto the main container after buildEnvVars has returned. Without the exclusion, an operator setting the same key would get both a secretKeyRef and 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 with enableDocker: true.

Verification

check result
vitest run (full adapter suite) 802/802 pass, 14 files
tsc --noEmit exit 0
npm run build exit 0
integrity hash regenerated 69286948…ae16d982…, recomputed value matches recorded
default-deny falsifier vs. old classifier fails, as required

The 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 secretKeyRef fails, 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 shared ctx; the control patch was a throwaway and is not in this branch.)

Integrity hash method validated before use — I recomputed it on pristine HEAD first and reproduced the recorded 69286948… 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 via envSecret.data.
  • user env config overrides inherited env — operator still beats inherited; AWS_REGION matches 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

  • Blast radius is real but currently near-zero. This templates every agent Job pod, so a wrong change here is a fleet-wide outage. Mitigating fact: I enumerated all 16 agents in the BLO company and none currently sets adapterConfig.env — so operatorOwnedKeys is 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.
  • It is a behaviour change for operator-set env, not a pure refactor. Values move from env[].value to envSecret. Anything reading a Job's PodSpec expecting a literal operator value would now see a secretKeyRef. I checked for build-time readers: nothing does .find() on the env array or reads .value off it; envVars is only appended to and assigned at the container. Runtime consumers are unaffected — a secretKeyRef is transparent to the process.
  • Does NOT close BLO-22546. opencode_k8s is still cloned from kkroo/paperclip-adapter-opencode-k8s at OPENCODE_K8S_REF=ed03316 and still ships PAPERCLIP_API_KEY and OPENCODE_CONFIG_JSON as 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_KEY is untouched here and remains secretKeyRef-wrapped in env while the same value is published in plaintext as the paperclip.io/isolation-key pod label. Out of scope for this PR; noted on the issue as one decision to make.
  • Self-review gate applies. This PR is authored by the allyblockcast[bot] App identity, so per CLAUDE.md it trips the self-review path and needs both an App COMMENTED review 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

…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>
@allyblockcast

allyblockcast Bot commented Aug 22, 2026

Copy link
Copy Markdown
Author

🔗 Paperclip issue: BLO-22471
🔗 Paperclip issue: BLO-22546

1 similar comment
@allyblockcast

allyblockcast Bot commented Aug 22, 2026

Copy link
Copy Markdown
Author

🔗 Paperclip issue: BLO-22471
🔗 Paperclip issue: BLO-22546

@allyblockcast

allyblockcast Bot commented Aug 22, 2026

Copy link
Copy Markdown
Author

Hey @allyblockcast[bot]! Before this PR can be reviewed, a few things need attention:

Missing or incomplete:

  • Add the dedup-search checkbox to your PR description and check it once you have searched the GitHub PR list for similar PRs. See the PR template at .github/PULL_REQUEST_TEMPLATE.md and CONTRIBUTING.md → "Before You Start: Search First".

Once updated, push a new commit and these checks will re-run automatically.

— commitperclip

@allyblockcast allyblockcast Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 from merged[key] === envConfig[key], so transformed operator values are treated as adapter-owned and remain literal. For example, an operator-provided ANTHROPIC_CUSTOM_HEADERS value 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 as value instead of a secretKeyRef. This is a credential disclosure through GET 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_HEADERS value without an existing x-penstock-session line, asserting no literal value and Secret-backed data.

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

  1. Fix the Critical issue before merge.
  2. Re-run the adapter tests and submit a fresh exact-head review after the fix.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants