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 baselinecdkd driftcompares against. For a resource provisioned through Cloud Control it also records the reported values inattributes. Importing Existing Resources describes this case. - A credential that a provider records in
attributesso thatFn::GetAttcan return it, such as theSecretAccessKeyof anAWS::IAM::AccessKey. - A
NoEchoparameter's value in a record written before schema version 11. It stays until the nextcdkd deploy, and it stays in every earlier object version of the record. - A custom resource's response
Data, unless the handler setsNoEcho: 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
ResourceARN in an IAM policy; - stack outputs, the exports index,
rollback-journal.json, and the queue'sorphansentry if a rollback keeps the queue; - the
physicalIdfield 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 driftreports 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--acceptand--revertleave it alone.cdkd rollbackreads a masked position back from AWS and sends that value. It refuses when it cannot read the position.cdkd importandcdkd scrubstore***at every position that today's template fills from aNoEchoparameter.cdkd exportexports 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
orphansentry; - 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
NoEchovalue a stack held before the upgrade. Earlier object versions ofstate.jsonstill 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
NoEchoparameter there, the property's template text or resolved inputs changed since the last deploy, the resource changed type, the template carries aTransform, 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 asNoEcho, as a record from an earlier cdkd does. Runcdkd deployorcdkd scrubonce 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.
Related
cdkd scrub: remove secrets from existing records and audit a stack- State backup and bucket security: who can read the bucket, and what earlier object versions keep
- State Management: where state lives and what a record contains