Testing AWS SSM SecureString parameter reads locally with Yulin
Application configuration in AWS Systems Manager Parameter Store often mixes plain values with
SecureString ones. A SecureString read comes back encrypted unless the request specifically asks
for decryption. Nothing fails at that point, so you just get the base64 encoded ciphertext.
Simulated SSM in Yulin encrypts a
SecureString value through simulated KMS when it is written, so a local test reads back what real
Parameter Store would return rather than the raw value it was given.
Here’s a SecureString parameter written and then read without asking for decryption:
import { GetParameterCommand, PutParameterCommand } from "@aws-sdk/client-ssm";
import { SimAws } from "@kensio/yulin";
const simAws = new SimAws();
const simSsm = simAws.ssm();
await simSsm.putParameter(
new PutParameterCommand({
Name: "/foobar/prod/db-password",
Type: "SecureString",
Value: "hunter2",
}),
);
const read = await simSsm.getParameter(
new GetParameterCommand({ Name: "/foobar/prod/db-password" }),
);
console.log(read.Parameter?.Type);
console.log(read.Parameter?.Value);
SecureString
WVVMSU5LTVMBS2Fybjphd3M6a21zOnVzLWVhc3QtMTo4ODg4ODg4ODg4ODg6a2V5...
The read succeeds and the parameter reports its Type, so the only sign that this isn’t the
password is the value itself. Adding WithDecryption gets the plaintext:
const decrypted = await simSsm.getParameter(
new GetParameterCommand({
Name: "/foobar/prod/db-password",
WithDecryption: true,
}),
);
console.log(decrypted.Parameter?.Value);
hunter2
That parameter does not name a specific key, so it’s encrypted under the aws/ssm AWS managed key,
which simulated KMS creates the first time something asks for it.
Nothing has to set up a key for a test, and the caller does not need KMS permission of its own.
Parameter Store supplies kms:ViaService and that key’s policy allows the account’s
principals to use it through Systems Manager.
Passing a KeyId encrypts under a customer-managed key instead, and a decrypting read then needs
kms:Decrypt on that key as well as ssm:GetParameter on the parameter.
The flag belongs to the request rather than to the parameter, so a read which collects a whole configuration prefix needs it too. Here’s a plain parameter written alongside the encrypted one, with each read via its parameter path:
import { GetParametersByPathCommand } from "@aws-sdk/client-ssm";
await simSsm.putParameter(
new PutParameterCommand({
Name: "/foobar/prod/db-host",
Type: "String",
Value: "db.internal",
}),
);
const listed = await simSsm.getParametersByPath(
new GetParametersByPathCommand({
Path: "/foobar/prod",
WithDecryption: true,
}),
);
console.log(listed.Parameters?.map((parameter) => parameter.Value));
[ "db.internal", "hunter2" ]
WithDecryption on a String or StringList parameter is ignored rather than refused, as real
Parameter Store ignores it. A listing which mixes the two can ask for decryption without
special-casing the plain ones. GetParameters behaves the same way.
Only the standard tier is simulated, which encrypts under the KMS key directly. The advanced tier’s
envelope encryption through the AWS Encryption SDK is not supported yet, so kms:GenerateDataKey
does not come into it.
That puts a forgotten WithDecryption in front of an ordinary test, rather than leaving it to
appear in a deployed handler as a connection failure some way from the parameter read.