Repository navigation
fix(deploy): a provider's delete masks the secrets the deploy resolved (#2007) - #4862
Merged
Merged
Conversation
1 of 2 tasks
go-to-k
force-pushed
the
fix/2007-delete-context-masker
branch
2 times, most recently
from
October 13, 2026 06:58
9a5eea6 to
5eebe59
Compare
go-to-k
force-pushed
the
fix/2007-delete-context-masker
branch
from
October 13, 2026 08:41
bbc93ce to
594d838
Compare
#2007) DeleteContext now carries maskSecrets: engine replacement deletes, rollback replay deletes, cdkd destroy and template-dropped deletes pass a masker built from the bag each delete already runs under, so a provider can withhold a secret-derived name (the S3 Express "not empty" refusal no longer prints a command naming no bucket).
go-to-k
force-pushed
the
fix/2007-delete-context-masker
branch
from
October 13, 2026 11:55
93ce55f to
4f8c8fc
Compare
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.
Closes #2007
Problem
DeleteContextcarried nomaskSecrets, so a provider'sdelete()could not tell that a name or value it printed came from a secret:aws s3 rm s3://<name> --recursive, which the log's printing bag then turned intoaws s3 rm s3://*** --recursive: a command that names no bucket. That refusal is reached oncdkd destroy, on a rollback, and on a deploy that drops the bucket from its template (a replacement of a directory bucket is refused up front as stateful, and with--force-stateful-recreationthe bucket is emptied instead).Fix
DeleteContext extends SecretMaskingContext(src/provisioning/region-check.ts; type-only import, no runtime cycle).deploy-engine/{replacement,update-replace,update-in-place}.ts): each replacement delete (delete-first, create-first's delete of the old copy,--recreate-via-*, the update-not-supported fallback) passesmaskSecretsover the bag this deploy resolved plus the resource's derived-name registry (fix(deployment): a physical name derived from a secret is masked only inside the provider operation that derived it #3869, one union pass, so a provider asking "is this name secret-derived?" sees what the logger sink sees) and runs under that bag viawithPrintingSecrets, so the logger sink masks every provider's own delete lines, including the many providers that never read the context.createOpMasker(logger, secrets, deployBag)now masks the op's re-resolution (previous generation) and the failed deploy's bag (RollbackExecutorContext.logOnlyNeedlesFor, already plumbed) in ONE union pass (createUnionSecretMasker, so a needle overlap across the two bags leaves no tail). Every replay delete (completed-CREATE, failed-CREATE, reverse-replacement new-first / after-recreate, the delete of what a failed re-create made, re-adopt of a retained old copy) passes that op mask asmaskSecrets, and runs under both bags as printing bags (withReplayDeletePrinting). The replay's own failure line and event mask the deploy bag too. Redaction is unchanged: only the op'ssecretspositions it.cdkd destroyand a template-removal delete (destroy-runner.ts,deploy-engine/delete.ts): both delete from the persisted record, which resolved nothing this run but can still be NAMED from a secret, so each passes a masker over the printing bag that delete already runs under (the record's secret-derived name spellings, fix(deployment): a physical name derived from a secret is masked only inside the provider operation that derived it #3869). Every cdkd delete site now passes one; the custom resource's no-masker refusal arm (the critic's countedEXEMPTrow) is reached only by a direct caller, since the context stays optional.DELETE_PATH_UNMASKEDretired fordeletePathMasker, composing the NoEcho re-resolution's masker, the wider bag, first and then the caller's; the critic'sEXEMPTrow now covers only the no-masker arm, the two-arm refusal is kept for that reason, recorded on delete() has no secret masker, and the delete bag CAN carry plaintext on the in-process rollback path #2007; the bare parsecatchkeeps discardingerror.message, now documented as load-bearing for that arm); Cloud Control delete's id scrub (now sees a plaintext leaf as evidence); IAM role / user / group delete sinks (unmaskedDeleteSinks()retired) and the replacementupdate()forwards its masker into the inner delete of the old role; IAM managed policy (its internal 6thmaskparameter folded intoDeleteContext.maskSecrets); DynamoDB global table delete waits (replica-gone, table-gone, both index waits, the protection-flip ACTIVE wait); Glue database / table / connection / crawler delete lines and failure wraps; the RDS DB proxy target group deregister and already-gone lines; the SNS subscription replacement now forwards its masker into its inner delete, which masks its lines and failure wrap (the ARN embeds the topic name). Internal deletes forward the operation's masker too: ACM certificate replacement, and Cloud Control's cleanup of a failed create's remnant.docs/provider-rules.md's three sentences sayingDeleteContextcarries no masker are corrected.Verification
tests/unit/deployment/rollback-delete-masker-2007.test.ts(new) covers every replay delete arm, the union pass and by-reference reads;noecho-delete-reresolve-engine-4682.test.tsgains every engine replacement delete plus the template-removal delete's registry-only masker;destroy-secret-name-needles-3869.test.tspins destroy's masker; provider cases in the S3 Express, custom resource, Cloud Control, IAM, global table, Glue, SNS, ACM and RDS DB proxy suites.maskSecretsthreading, every printing-bag wrap, the deploy-bag arm ofcreateOpMaskerin both loops, and each provider consumer went red; restored byte-exact after each.Real-AWS arm
tests/integration/s3-directory-bucketPhase 5: a directory bucket named from a Secrets Manager secret (<secret>--<az>--x-s3, the only resource underS3DIR_SECRET_NAME) gets an object out of band and is then deleted through three sites in turn: (5a)cdkd destroy --verbose, (5b)cdkd rollback --verboseof a--no-rollbackdeploy whoseRollbackBreakerfailed after the bucket's create, (5c) a--verbosedeploy that drops the bucket (S3DIR_DROP=true). Each refusal must say the name is secret-derived and print NOaws s3 rm(FAIL otherwise; on main each printsaws s3 rm s3://*** --recursive), and no log may name the secret. Each sub-phase then empties the bucket and finishes its delete; the secret goes through a 0600file://, one trap cleans bucket, secret and state, and every object version under the stack prefix (state, journal,deployments/) is swept:allon the success path and whenever the trap's destroy succeeded,noncurrentwhen it failed (the state a recovery needs is kept, as the #2225 fence requires).The first Phase 5 (a replacement of the bucket) could not reach the refusal and is gone:
AWS::S3Express::DirectoryBucketis a stateful type, so its replacement is refused before any create, and--force-stateful-recreationempties it.Why no in-process rollback arm in
secret-derived-immutable-names: its rollback delete lines are already masked on main by the in-process replay's printing bag (replayPrintingBag, #3869), so an assertion there would pass with or without this PR. The in-process side is pinned by unit tests (rollback-delete-masker-2007.test.ts, including a rotated-secret case).Not in this PR
Known limit (display only): a replay delete's
maskSecretsis the op mask (the op's and the failed deploy's bags plus the derived-name spellings). A name known ONLY to a printing bag a caller bound around the replay (cdkd destroy's journaled-orphan batch, for a name the journal cannot re-derive, e.g. onegenerateResourceNameWithFallbackrewrote) is not in it, so a provider's "is this name secret-derived?" decision can be narrower than the logger sink. Nothing prints in plaintext: the sink still masks the line. Composing the sink into the masker was tried and withdrawn: it can UNDER-mask when an outer needle is a substring of a derived spelling (the sink cuts it first). The correct fix is one union pass over every bound bag and the op bags under the derived-name masks, which needs aresource-secrets-scopeAPI change.The UPDATE revert's provider masker (
rollback-executor.tsandreplay-revert.ts) still masks with the op's bag only. It is not a printing-only channel: providers'update()DECIDE with it (unchangedBehindSecretReferenceand the immutable-name guards treat a value the masker changes as secret-derived), so adding the failed deploy's bag would change refuse/patch decisions on a rollback revert, not just what prints. Its lines are already masked by the in-process replay's printing bag.