Skip to content
cdkd

Secrets in state

cdkd keeps a secret value out of the state record when it can tell that the value is secret. In place of the value, the record holds either the reference that names the secret or the mask ***. AWS always receives the real value; only what cdkd writes down changes.

{
  "MasterUsername": "admin",
  "MasterUserPassword":
    "{{resolve:secretsmanager:prod/db:SecretString:password}}",
  "LicenseKey": "***"
}

This page covers what the record stores. To remove secrets from records that already hold them, use cdkd scrub. This page is part of State Management.

What cdkd stores for a secret

Where the value comes from What the record holds
A {{resolve:secretsmanager:...}} or {{resolve:ssm-secure:...}} reference The reference text
A template parameter declared NoEcho: true ***
A custom resource response declared NoEcho ***
The Fn::Base64 encoding of a secret reference, such as EC2 UserData ***

A reference can be resolved again on the next deploy, so storing it costs nothing. A mask cannot be turned back into the value, and the rest of this page describes what follows from that.

Values that are stored in the clear

Treat state.json as sensitive, and limit who can read the state bucket and its earlier object versions. cdkd cannot recognise every secret, and cdkd scrub detects none of the following:

  • Values that AWS reported, including ones your template never mentions. cdkd records what AWS reports in observedProperties, which is the baseline cdkd drift compares against. For a resource provisioned through Cloud Control it also records the reported values in attributes. Importing Existing Resources describes this case.
  • A credential that a provider records in attributes so that Fn::GetAtt can return it, such as the SecretAccessKey of an AWS::IAM::AccessKey.
  • A NoEcho parameter's value in a record written before schema version 11. It stays until the next cdkd deploy, and it stays in every earlier object version of the record.
  • A custom resource's response Data, unless the handler sets NoEcho: true.
  • A resource name built from a secret, described next.

A resource named after a secret keeps that name in state

Suppose a secret's value is used as a resource's name:

new sqs.Queue(this, 'Queue', {
  queueName: SecretValue.secretsManager('prod/tenant').unsafeUnwrap(),
});

The queue's name is now the secret's value. A name cannot be hidden, because cdkd needs it to find the queue again and every resource that refers to the queue receives it. The same applies to a NoEcho parameter used as a name. The value is stored in:

  • the queue's physicalId;
  • another resource's resolved copy of the name, or of an identifier that contains it, such as the Resource ARN in an IAM policy;
  • stack outputs, the exports index, rollback-journal.json, and the queue's orphans entry if a rollback keeps the queue;
  • the physicalId field of a deployment event.

The queue's own properties still hold the reference, or *** for a NoEcho parameter. When the name comes from a NoEcho parameter, the deploy warns.

cdkd masks the name in its logs and in cdkd diff output where it recognises that the value was read from a secret. Commands that print a stored record, such as cdkd state show and cdkd events, print the name as stored.

CloudFormation behaves the same way and advises against putting sensitive data in an identifier property. The fix is in your CDK code: do not use a secret as a name.

NoEcho parameters

Where a template parameter declared NoEcho: true supplies a value, cdkd stores *** in every part of the record that would hold the value:

Part of the record What is stored
properties, observedProperties *** at every position the parameter fills, whatever the type or length
attributes *** for an attribute declared NoEcho, and for one that echoes the value back, such as an SSM parameter's Value
outputs, the exports index *** for an output whose value reads the parameter
rollback-journal.json The same masks

A string that contains the value is stored as *** whole, so the text around the value is not in the record either.

A resource's physical ID is never masked. See A resource named after a secret.

How a deploy compares a masked value

The record only says ***, so a deploy cannot compare the new value with the stored one. cdkd resolves the parameter again on every deploy and then decides for each property that reads it. The decision depends on two things: whether the property can be updated in place or is create-only (changing it replaces the resource), and whether AWS reports the property's current value.

The property What the deploy does
Updatable, and AWS reports it Reads the resource; skips an unchanged value, updates a changed one
Updatable, and AWS does not report it Sends the value on every deploy, with one info line per resource
Create-only, and AWS was seen to report it exactly Reads the resource; a changed value replaces the resource
Create-only otherwise Never replaces; warns on every deploy while it cannot confirm the value
Create-only, and the read failed The resource fails with a message to re-run

An RDS MasterUserPassword is an example of the second row: AWS never returns a password.

A stack whose only difference is that its resources read a NoEcho parameter is reported as No changes.

Applying a new value that cdkd will not replace for. In the fourth row, cdkd keeps the resource because it cannot tell whether the value changed. To apply a new value, replace the resource yourself with --recreate-via-cc-api <logicalId> or --recreate-via-sdk-provider <logicalId>.

Approving a replacement. A replacement found by reading the resource goes through the usual guard for stateful resources (--force-stateful-recreation). The approval prompt before the deploy cannot see this replacement, because the prompt is built without reading AWS. So under --require-approval=destructive or any-change the deploy asks again when it reaches the replacement. --yes approves it.

What cdkd diff shows

cdkd diff cannot read AWS, so it compares the masks. It prints one note per stack saying how many unchanged resources read a NoEcho parameter. That note does not count as a change for --fail.

What other commands do with a masked value

  • cdkd drift reports a masked position in its own group and prints neither the stored nor the live value. The position does not affect the exit code, and --accept and --revert leave it alone.
  • cdkd rollback reads a masked position back from AWS and sends that value. It refuses when it cannot read the position.
  • cdkd import and cdkd scrub store *** at every position that today's template fills from a NoEcho parameter.
  • cdkd export exports the record. The exported template reads the parameter.

Edge cases

A delete needs a property that a NoEcho parameter fills. The record holds *** where the delete needs the value, so cdkd cannot address the resource. cdkd destroy, or a deploy that removes the resource, skips the delete, keeps the record and exits non-zero. Delete the resource by hand, then run cdkd state orphan to drop the record.

Another stack reads an output that a NoEcho parameter serves. This works only when both stacks deploy in one run, for example cdkd deploy --all. A separate run of the consuming stack reads *** and is refused.

A nested stack's parameter is filled from a NoEcho source. The child stack treats the parameter as NoEcho, whatever the child template declares.

An Export.Name holds a NoEcho value. The export is not published, and the deploy warns.

Values that still reach the record in plain text:

  • a value shorter than 4 characters, or a number, that lands somewhere the template does not name directly, such as inside a longer string read through an attribute;
  • the record of a resource the template no longer names, such as a resource being deleted or an orphans entry;
  • outputs saved after the deploy failed while resolving outputs.

The contributor page State schema internals has every case.

Upgrading from a record written before version 11

There is nothing to run. Schema version 11 is the version that introduced the masks. The first cdkd deploy after you upgrade compares each value with the plaintext the old record still holds. For an unchanged value it updates and replaces nothing, and it saves ***.

Warning

Rotate any NoEcho value a stack held before the upgrade. Earlier object versions of state.json still contain it, and cdkd does not purge them.

Only a cdkd deploy masks a record. A command that has no template, such as cdkd state refresh-observed or cdkd drift --accept, saves the record as version: 11 without masking anything. So the version number alone does not tell you that a record's values are masked.

Custom resources that read a NoEcho parameter

cdkd cannot read a custom resource back from AWS, so it cannot tell whether the value changed. It therefore calls the handler on every deploy.

Request What the handler receives
Update ResourceProperties holds the real value; OldResourceProperties holds *** at that position
Delete The real value when cdkd can resolve it again from the template; otherwise cdkd skips the delete

If your handler's Update is not idempotent, or it compares the old and new properties, pass it the name or ARN of a secret and have it read the value itself.

When the delete is sent

cdkd destroy run with the CDK app resolves the value again from the template and sends the delete. So does cdkd deploy --recreate-via-cc-api <logicalId>.

The handler receives the value the parameter has today. If the parameter changed since the last deploy, the handler gets the new value.

When the delete is skipped

cdkd skips the delete and keeps the record when it cannot resolve the value again. That happens when:

  • the command has no template. This covers cdkd state destroy, a deploy that removed the resource from the template, and a rollback;
  • the property read an attribute that a custom resource or a nested stack declared NoEcho;
  • the template changed in a way that makes the stored position unreliable. The template no longer reads a NoEcho parameter there, the property's template text or resolved inputs changed since the last deploy, the resource changed type, the template carries a Transform, or the template was synthesized for another region;
  • on cdkd destroy, the expression reads anything other than parameters, pseudo parameters and literals, or a parameter cannot be bound;
  • the record holds *** at a position it does not list as NoEcho, as a record from an earlier cdkd does. Run cdkd deploy or cdkd scrub once to record the positions, then destroy.

After a skipped delete, remove what the handler manages by hand and drop the record with cdkd state orphan.

Custom resource responses marked NoEcho

A custom resource handler can mark its response Data as sensitive by setting NoEcho: true on the response:

// in the handler
return {
  PhysicalResourceId: id,
  Data: { Token: mintedToken },
  NoEcho: true,
};

cdkd then stores every string in that Data as ***. The mask appears in three places: the custom resource's own attributes, the properties of every resource that read the value through Fn::GetAtt, and outputs. A longer string built around the value is stored as *** whole.

Resources that depend on the value are still created with the real one, as in CloudFormation. Without NoEcho: true, the record holds the handler's values in the clear.

A later deploy cannot recover the value

The handler generated the value, so there is no reference cdkd could resolve again. Once the mask is in the record, the only way to get the value back is to run the handler.

A later deploy must write the value, but the custom resource is unchanged. An unchanged custom resource's handler does not run, so cdkd has only the mask and refuses the write. Change one of the custom resource's properties so that its handler runs again in the same deploy.

The handler runs and returns the same value. cdkd cannot tell that the value is the same, so each resource that reads it takes one redundant update. A reader that is itself a custom resource has its own handler called again.

The value sits in a create-only property of a reader. cdkd reads the reader from AWS first. It replaces the reader only when AWS cannot confirm that the reader already holds the new value.

The reader is in another stack. It gets the value only when both stacks deploy in one cdkd deploy run. Redeploying the producing stack alone does not help, because the value is masked again on its way into that stack's record.

What other commands do with the mask

Command Behaviour
cdkd drift Masks the live value; --accept refuses to write it into the baseline
cdkd rollback Refuses to replay a record that holds the mask
cdkd export Blocks a resource whose properties hold the mask

The Fn::Base64 encoding of a secret reference

cdkd masks the Fn::Base64 encoding of a secret reference in the same way, because the encoding decodes straight back to the secret. The same commands refuse it in the same places.

No custom resource is involved, so forcing one to run changes nothing. A deploy that changes the resource sends the encoding again. If another resource needs the value, have that resource build it from the secret's own reference, so that it does not read the encoded copy from the first resource.

The full rules, including which longer strings are kept, are on the contributor page State schema internals.

Last updated: