Skip to content

feat(dev): seal against the REAL KEK locally, closing the last parity gap - #861

Merged
choraria merged 2 commits into
mainfrom
lane/kms-parity
Jul 31, 2026
Merged

feat(dev): seal against the REAL KEK locally, closing the last parity gap#861
choraria merged 2 commits into
mainfrom
lane/kms-parity

Conversation

@choraria

Copy link
Copy Markdown
Contributor

KMS_MODE=local was the default, so every local stack sealed provider secrets and ingest tokens under a hermetic throwaway KEK. The envelope-encryption path — the compliance-critical one — was never exercised before a deploy.

The ledger said this one "has no alternative today: nobody should hold production AWS credentials to develop". The premise was wrong in exactly the way that page warns about: the credential already existed. IAM user webhook-co-claude-code is already least-privilege — kms:GenerateDataKey + kms:Decrypt, one key, nothing else.

Verified before changing anything

A DEK wrapped by the real KEK, unwrapped, and the two proven identical by encrypting with one and decrypting with the other. Then the engine booted with AWS bound, no KMS error.

Blast radius

What a machine holds is the ability to wrap and unwrap, not the key — the KEK never leaves AWS. It's still a production capability, so the credential pair is excluded from the vault for the same reason RESEND_API_KEY is. KMS_KEY_ARN and AWS_REGION do travel — an ARN and a region are identifiers, and withholding them is friction without security. Both halves of the pair are excluded together so nobody wonders which half was secret.

KMS_MODE survives as the opt-out, now external with blank as the correct value rather than local/"local" — the substitute is no longer the default.

⚠️ Honest limit

I could not trigger the AWS/local fence live — the provider is constructed lazily on endpoint creation, so booting never reaches it. Four existing tests cover it (including partial config); those are the evidence, not my probe.

Test plan

  • pnpm lint — 993 pass, 0 fail, exit 0
  • Real KMS round-trip passed; engine boots with AWS bound; dev-preflight green; 0 secrets in the committed example

🤖 Generated with Claude Code

… gap

`KMS_MODE=local` was the default, so every local stack sealed provider secrets
and ingest tokens under a hermetic throwaway KEK. The envelope-encryption path
— the compliance-critical one — was therefore never exercised before a deploy.
It ran in production and nowhere else.

The ledger said this one "has no alternative today: nobody should hold
production AWS credentials to develop". The premise was wrong, in exactly the
way that page warns about: THE CREDENTIAL ALREADY EXISTED. IAM user
`webhook-co-claude-code` is already least-privilege — kms:GenerateDataKey +
kms:Decrypt, on one key, nothing else. AGENTS.md says to look for the real
credential before inventing a substitute; it had been there the whole time,
recorded, while the page argued it could not exist.

Verified before changing anything: a DEK wrapped by the real KEK, unwrapped,
and the two proven to be the same key by encrypting with one and decrypting
with the other. Then the engine booted with AWS bound and no KMS error.

What a machine holds is the ability to wrap and unwrap, not the key — the KEK
never leaves AWS. It is still a production capability, so the credential PAIR
is excluded from the vault for the same reason RESEND_API_KEY is: narrow is not
the same as non-production, and a passphrase beside the ciphertext is not where
that belongs. KMS_KEY_ARN and AWS_REGION do travel, because an ARN and a region
name are identifiers and withholding them is friction without security. Both
halves of the pair are excluded together, so nobody has to wonder which half
was the secret one.

KMS_MODE survives as the opt-out for a contributor who cannot hold AWS
credentials, now `external` with BLANK as the correct value rather than `local`
with "local" — the substitute is no longer what you get by default. The engine
already refuses the flag when any AWS field is bound, so the two cannot be
half-applied; four existing tests cover that fence, including partial config.

⚠️ I could not trigger that fence live: the provider is constructed lazily on
endpoint creation, so booting the engine never reaches it. The tests are the
evidence, not my probe — saying so because "I ran it and saw nothing" is not
the same as "it fired".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BRmGUnxeYsQoG9c8BCZcae
@github-actions

github-actions Bot commented Jul 31, 2026

Copy link
Copy Markdown

🔴 AI review — VERDICT: BLOCK

Blocking issues found (advisory — review before merging; add the skip-ai-review label to silence).

Full review

Review (pr.diff)

Security-reviewer and qa-test-reviewer both reviewed this KMS local-parity flip (real AWS KMS as default; KMS_MODE=local as opt-out).

Blocking

1. Missing/weak test — shipped default not pinnedscripts/dev-secrets-manifest.mjs:189 (gap vs scripts/dev-preflight.test.mjs:207)

New preflight tests cover that the four AWS fields are parityRequired, that blank KMS_MODE demands them when present in .dev.vars, and that only local relaxes via isRelaxed. They do not pin that the engine manifest ships KMS_MODE as blank opt-out (scope: "external", no value: "local").

Regression that stays green: restore { name: "KMS_MODE", scope: "local", value: "local" }, regenerate examples → renderExample writes KMS_MODE=local (scripts/dev-secrets.mjs:111) → preflight relaxes AWS → runtime uses the hermetic KEK again. All three new preflight tests still pass.

Fix: Add a test that pins the default, e.g. engine KMS_MODE is blank by default (local is opt-out only):

  • Input: specsFor("engine") entry for KMS_MODE, and/or renderExample("engine")
  • Expected: scope === "external", no value (or not "local"); rendered line is KMS_MODE= (blank), not KMS_MODE=local

Security (no blockers)

No committed secret values, blank placeholders in apps/engine/dev.vars.example, AWS key pair correctly excluded from the vault with tests, no new credential logging, no injection/XSS/authz/SSRF/signature changes in this diff.

Non-blocking notes (not verdict-driving): IAM principal name in docs/local-parity.md:219; prefer a non-prod KEK for local rather than Decrypt on the production key.

VERDICT: BLOCK

Right, and the same gap the review caught on the last two PRs: the load-bearing
change here is that four AWS fields became `parityRequired` with
`relaxedBy: KMS_MODE=local`, and nothing pinned it. Drop the flag or break the
opt-out and local dev silently returns to sealing under a throwaway KEK with
the vault, example and kms-provider suites all still green.

The runtime fence does not cover this. It refuses `KMS_MODE=local` when AWS is
bound — a different failure, and one that only fires once a provider is
constructed, which is lazy. Nothing was checking that the fields are demanded
in the first place.

Three tests, mirroring the dmarc Resend and play Turnstile pins: each field is
required and relaxed by that one flag; a BLANK KMS_MODE demands all four; and
only "local" relaxes them.

That middle one matters most. Blank is the PARITY path here, which inverts the
usual convention — the substitute is the flag's presence, not its absence — so
"blank relaxes" is exactly the one-character change that would put everyone
back on the throwaway KEK without a single test going red. Mutation-verified:
dropping `parityRequired` fails, and making blank relax fails.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BRmGUnxeYsQoG9c8BCZcae
@choraria
choraria merged commit 318355f into main Jul 31, 2026
29 of 30 checks passed
@choraria
choraria deleted the lane/kms-parity branch July 31, 2026 23:02
choraria added a commit that referenced this pull request Jul 31, 2026
… name (#862)

I merged #861 while the review still said BLOCK, having assumed its verdict was
the previous round's. It was not: it had re-run and raised a different, correct
finding, and I did not read it before merging. Both fixes are here.

1) The shipped DEFAULT was not pinned, only the enforcement.

My tests covered that the four AWS fields are `parityRequired` and that only
`KMS_MODE=local` relaxes them. None of that notices the manifest going back to
`{ scope: "local", value: "local" }` — `pnpm dev:secrets` would then WRITE
`KMS_MODE=local` into every .dev.vars, preflight would relax all four, and the
engine would use the hermetic KEK again with all three tests still green. The
regression lives in what we SHIP, not in what we enforce, and I had only
guarded the second.

Now pinned on both: the spec's scope and value, and the rendered example line
(`KMS_MODE=`), because the rendered artifact is what actually reaches a
developer's machine.

⚠️ My first mutation check "passed" and I nearly took that as proof the test
was weak. It was the CHECK that was wrong: `s.index('name: "KMS_MODE",')`
matched a `relaxedBy: { name: "KMS_MODE", ... }` reference seven lines earlier,
so it mutated a different field's scope. Verifying the mutation actually
mutated — printing the resulting spec — is what caught it. A mutation that does
not mutate proves nothing in either direction.

2) The IAM principal name was in a public doc.

`no-secrets` covers account identifiers, not only secrets, and this repo is
public. The doc now says what the principal is scoped to without naming it; the
name lives in the credential store.


Claude-Session: https://claude.ai/code/session_01BRmGUnxeYsQoG9c8BCZcae

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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.

1 participant