Skip to content

fix(deploy): keep a NoEcho-named resource's ARN in its attributes, outputs and exports (#4849) - #4850

Merged
go-to-k merged 10 commits into
mainfrom
fix/4849-noecho-producer-attr-copy
Oct 13, 2026
Merged

go-to-k merged 10 commits into
mainfrom
fix/4849-noecho-producer-attr-copy

Conversation

@go-to-k

@go-to-k go-to-k commented Oct 12, 2026 •

Copy link
Copy Markdown
Owner

Summary

A resource named from a NoEcho template parameter (an SNS topic with TopicName: !Ref Name) stored its own ARN attribute, and every output and export holding that ARN, as ***. The next deploy that re-resolved a reader of the attribute (an edited subscription or grant using Fn::GetAtt Topic.TopicArn) failed with Cannot resolve Topic.TopicArn ... redaction mask and rolled back; a consumer stack importing the exported ARN in a later run was refused the same way.

The #4810 / #4833 rule keeps a copy of a physical id in plaintext, but it covered only the ids a reader's bag recorded. This PR closes the three places that fell outside it:

  • scrubResourceRecord (src/deployment/secret-redaction/redact-state.ts): a record's attributes are redacted with the record's own physicalId seeded as a physical-name copy (withPhysicalNameCopies), so a match inside it (the TopicArn) is kept. An attribute equal to the value whole (TopicName) is no copy of the id and stays ***, unless the physical id IS the value (a name-identified resource), where it is stored as the id is.
  • absorbOutputsPassSecrets (src/deployment/deploy-engine/masking.ts): the outputs bag now carries physicalNameCopiesOf from each outputs pass, so an !Ref Topic output and its export (and the exports index, written from redactOutputs) keep the ARN. An output of the value itself stays ***.
  • NestedStackProvider.readChildOutputsAsAttributes: a child output holding a child resource's physical id is recorded in a new attributes-only side set (attributeIdCopiesOf) on the row's bag, which scrubResourceRecord seeds into the attributes walk alongside the own id. The row's properties (its Parameters) are untouched and keep their reference form.
  • Repair of state an earlier cdkd wrote (TopicArn: "***" already persisted): a Fn::GetAtt of an attribute the record holds as exactly *** re-reads the producer through the fix(deployment): an issue-1824-era stale attribute map is never healed, and the resolver "not enriched for this resource type" message is now false for the two fixed types #1852 heal (healMaskedOwnIdAttribute in intrinsic-resolver/getatt-heal.ts, called from resolveGetAtt's cached-attribute arm) and serves the read only when it EQUALS the record's physical id, or is an ARN ENDING in it whose prefix holds only partition, service, region, 12-digit account and at most one resource-type token (OWN_ID_ARN_PREFIX), AND is left unchanged by the producer's own redaction bag for this run with that id exempted (the bag must hold a maskable value): a function's or role's Arn (isMaskedOwnIdCopy). The shape bounds an earlier generation's secret that today's bag no longer records (security review round 2); a role ARN with a path, or anything else beside the id, keeps the old refusal. The persist merge (withHealedAttributes) replaces the mask under the same predicate, so the record is repaired. Never for an attribute the producer declared NoEcho (a malformed noEchoAttributeNames reads as declared), never for any other value (TopicName stays ***). The cost is one provider read, only for a masked attribute a reader asks for, memoized per record per deploy.

Behavior note for the dynamic-reference class: the own-id exemption applies to the substring arm as well, so an attribute holding the record's own physical id (secret-derived name) is now stored as the id is rather than rewritten into the reference. The physical id itself is already stored in plaintext beside it, so nothing new is disclosed; rollback-restore-secret-named-copies.test.ts is updated accordingly. Accepted residual (security review N1): a leaf EQUAL to a physical id is kept before the whole-value arm, so a different secret whose value happens to equal that id (a short handler-chosen id) is stored in plaintext rather than as its reference. No new characters reach state (the id is already there); this is the same residual #4810 accepted for Ref / Fn::GetAtt reads. The NoEcho class is still covered by the positional arm.

An attribute that only contains the name (a queue's Arn, whose physical id is its URL) is unchanged: it still stores the reference, or *** for a NoEcho value (documented as a known bound in docs/state-schema-internals.md).

Tests

  • tests/unit/deployment/noecho-producer-attr-copy-4849.test.ts (real DeployEngine / DiffCalculator / resolver, mocked provider): own ARN attribute kept and TopicName masked; a later deploy editing the Fn::GetAtt reader succeeds; output + export keep the ARN, the value's output stays ***; negative control (a literal embedding the value stays ***).
  • tests/unit/provisioning/nested-stack-provider-output-id-copy-4849.test.ts: the nested carry, plus a negative control (an output only embedding the value records nothing and stays ***).
  • noecho-producer-attr-copy-4849.test.ts also covers the repair (a record seeded with TopicArn: "***" is repaired by the reader edit, TopicName stays ***), a declared attribute that is not re-read into the record, and isMaskedOwnIdCopy directly (equality only, declared and malformed declarations refused).
  • nested-stack-provider-output-id-copy-4849.test.ts covers the attributes-only scope (the row's Parameters keep the reference), no bound bag, and a non-object child resources.
  • rollback-restore-secret-named-copies.test.ts: the "differs, therefore redact" attributes negative is kept with a Tag attribute holding the name outside the own id.
  • Mutation probes (one at a time, tree restored byte-exact): redact-state.ts own-id seed -> 2 cases red; masking.ts carry -> the outputs case red; nested carry removed -> the nested case red; nested carry into the whole bag -> the scope case red; attributes-only side set ignored by the scrub -> 2 nested cases red; resolveGetAtt serve arm off -> the repair case red; persist-merge predicate off -> the repair case red; declared check off -> the predicate case red; equality relaxed to any string -> the predicate case red; containment arm off -> the name-identified engine case and the predicate case red; redaction-stability check off -> the predicate case red; maskable-bag guard off -> the predicate case red; ARN-shape check off -> the predicate case red; end-of-value check off -> the predicate case red; the producer bag dropped at the resolver or at the merge -> the name-identified engine case red.

Integration

New fixture tests/integration/noecho-named-producer/ (kept out of noecho-parameter-masking, which PR #4848 holds, so the two PRs do not collide). It deploys a topic named from a per-run NoEcho value with an SQS subscription reading Fn::GetAtt Topic.TopicArn and an exported !Ref Topic output; asserts the ARN in state, outputs, export and exports index with *** at TopicName and the value's output; deploys a consumer importing the export in a separate process; then redeploys the producer with the subscription edited (raw delivery on) and asserts the deploy exits 0 with no redaction mask refusal, the topic's one live subscription is the one state records, delivers to the queue and has raw delivery on, and state keeps the ARN with TopicName as ***. (cdkd's SNS subscription provider replaces a subscription on any update, so its ARN is not asserted unchanged; that replacement is filed as #4853.) Then it scans every object version under both stack prefixes and the exports index versions this run wrote (with a floor of one); destroys and sweeps.

Integ results on 1d111f2 (coordinator): noecho-parameter-masking, nested-stack-secret, custom-resource-noecho-nested, cross-stack-secret-import, rollback-explicit-name-rewrite and the broad set passed; noecho-named-producer failed at Phase 3 on the "subscription ARN unchanged" premise only (the edited deploy itself succeeded), which this head fixes. Pre-fix arm (src at the merge base): red at Phase 1 (state holds *** at TopicArn, output and export). With CDKD_TEST_NOECHO_PRODUCER_ARM=1 the fixture warns through Phase 1, skips Phase 2, and is expected red at Phase 3 (the Cannot resolve Topic.TopicArn ... redaction mask refusal); the arm always exits non-zero.

Suggested /run-integ: noecho-named-producer, noecho-parameter-masking, nested-stack-secret, custom-resource-noecho-nested, cross-stack-secret-import, rollback-explicit-name-rewrite.

Closes #4849

…tputs and exports (#4849)

A resource named from a NoEcho parameter stored its own ARN attribute, and
every output and export holding that ARN, as ***, so the next deploy that
re-resolved a Fn::GetAtt reader of it failed. The persist walk now exempts a
copy of the record's own physical id among its attributes, the outputs bag
carries the physical-name copies its passes read, and a nested stack row
records a child physical id its outputs copy. An attribute or output equal
to the value itself stays masked.

Adds the noecho-named-producer integ fixture (edited reader on a second
deploy, a consumer importing the export in a later run).
@github-actions github-actions Bot added effort:medium One re-review round, or a run of an existing integ fixture severity:high Undone: a wrong result, data loss, a security surface, or something users hit in normal operation labels Oct 12, 2026
…scope the nested carry to attributes (#4849)

Review round 1 on #4850:
- A Fn::GetAtt of an attribute the record holds as *** re-reads the producer
  through the stale-attribute heal and serves the read only when it equals
  the record's physical id; the persist merge replaces the mask under the
  same predicate. Declared NoEcho attributes are never re-read.
- The nested-stack carry records child ids in an attributes-only side set,
  so the row's Parameters keep their reference form.
- Qualify the docs and changelog where the physical id is the value itself.
- Tests: the repair, the declared refusal, the predicate, the nested scope
  and guards, and the rollback attributes negative.
- Integ: structural no-replacement check, and the exports index versions are
  scanned too.
…one equal to it (#4849)

Review round 2 on #4850: a name-identified resource's ARN (a function, a role,
a table named from a NoEcho value) contains its physical id rather than
equalling it. The repair now also accepts a re-read value that carries the
id and is left unchanged by the producer's own redaction bag with that id
exempted. Integ nits: the success line names the subscription, and the
exports index scan reads only versions this run wrote.
Review round 3 on #4850: the producer's bag for this run cannot vouch for an
earlier generation's secret it no longer records, so a value carrying the id
is accepted only as an ARN ending in it, with nothing but partition, service,
region, account and one resource-type token before it. Integ: the index scan
window allows five minutes of clock skew and fails on zero index versions.
The SNS subscription provider replaces a subscription on any update (filed as
#4853), so the edited deploy's subscription ARN changes by design. Phase 3
now asserts the deploy exits 0 without the redaction-mask refusal, the
topic's one live subscription is the recorded one, delivers to the queue
and has raw delivery on. A CDKD_TEST_NOECHO_PRODUCER_ARM=1 pre-fix arm warns
through Phase 1 and skips Phase 2 so a binary without the fix fails at Phase 3.
…cer-attr-copy

# Conflicts:
#	docs/_generated/integ-coverage.json
#	docs/integ-coverage.md
@go-to-k
go-to-k merged commit 2951a24 into main Oct 13, 2026
14 checks passed
@go-to-k
go-to-k deleted the fix/4849-noecho-producer-attr-copy branch October 13, 2026 05:28
@go-to-k

go-to-k commented Oct 13, 2026

Copy link
Copy Markdown
Owner Author

Merged — thanks. Moving the arm into its own fixture and making Phase 3 reachable pre-fix paid off: the ARM=1 run went red exactly at the redaction-mask refusal, and the noecho/secret set held across both main merges (#4851, #4842).

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

Labels

effort:medium One re-review round, or a run of an existing integ fixture severity:high Undone: a wrong result, data loss, a security surface, or something users hit in normal operation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A resource named from a NoEcho parameter stores its own ARN attribute and outputs as the mask, so a later reader edit fails

1 participant