Simulated SSM Parameter Store
Yulin simulates AWS Systems Manager Parameter Store for tests and local development. You can create, read, update and delete parameters through the AWS SDK or a CloudFormation template. Parameters are stored in memory, each write creates a version, and simulated IAM authorizes every operation.
Other Systems Manager features, such as Run Command and Session Manager, are not simulated. Import
SSM-specific types from @kensio/yulin/ssm.
Writing and reading a parameter
Section titled “Writing and reading a parameter”PutParameter needs a Type when the parameter is new. GetParameter returns the current version.
/** * Writing a simulated parameter and reading it back. */
import { GetParameterCommand, PutParameterCommand } from "@aws-sdk/client-ssm";
import { SimAws } from "@kensio/yulin";
const simAws = new SimAws();const ssm = simAws.ssm();
await ssm.putParameter( new PutParameterCommand({ Name: "/myapp/prod/db-host", Type: "String", Value: "db.internal", }),);
const read = await ssm.getParameter( new GetParameterCommand({ Name: "/myapp/prod/db-host" }),);
console.log(read.Parameter?.Value); // "db.internal"console.log(read.Parameter?.Version); // 1A parameter written without a leading slash and one read with it are the same parameter, because both name the same ARN.
Parameter ARNs and IAM policies
Section titled “Parameter ARNs and IAM policies”A parameter ARN drops the leading slash from the name. /myapp/prod/db-host becomes
arn:aws:ssm:eu-west-2:111111111111:parameter/myapp/prod/db-host, with one slash after parameter
rather than two. A policy written with the doubled slash matches nothing.
/** * A simulated IAM policy allowing a Role to read one parameter. */
import { CreateRoleCommand, PutRolePolicyCommand } from "@aws-sdk/client-iam";import { GetParameterCommand, PutParameterCommand } from "@aws-sdk/client-ssm";
import { SimAws } from "@kensio/yulin";
const simAws = new SimAws();const accountId = simAws.defaultAccountId;const regionName = simAws.defaultRegionName;
const role = await simAws.iam().createRole( new CreateRoleCommand({ RoleName: "ConfigReader", AssumeRolePolicyDocument: JSON.stringify({ Version: "2012-10-17", Statement: { Effect: "Allow", Principal: { AWS: `arn:aws:iam::${accountId}:root` }, Action: "sts:AssumeRole", }, }), }),);
await simAws.iam().putRolePolicy( new PutRolePolicyCommand({ RoleName: "ConfigReader", PolicyName: "ReadDbHost", PolicyDocument: JSON.stringify({ Version: "2012-10-17", Statement: { Effect: "Allow", Action: "ssm:GetParameter", // One slash after `parameter`, not two, whatever the name looks like. Resource: `arn:aws:ssm:${regionName}:${accountId}:parameter/myapp/prod/db-host`, }, }), }),);
await simAws.ssm().putParameter( new PutParameterCommand({ Name: "/myapp/prod/db-host", Type: "String", Value: "db.internal", }),);
const read = await simAws .ssm() .getParameter(new GetParameterCommand({ Name: "/myapp/prod/db-host" }), { caller: { kind: "arn", arn: role.Role.Arn }, });
console.log(read.Parameter?.Value); // "db.internal"DescribeParameters is the exception. Real Parameter Store gives that action no resource-level
permissions. It authorizes against * here, and a policy naming individual parameter ARNs grants
nothing.
GetParametersByPath authorizes against the path rather than against each parameter it returns.
Access to a path is access to everything under it. A recursive listing of /myapp returns
/myapp/prod/db-host even where a policy explicitly denies that parameter.
Versions
Section titled “Versions”Every write makes a new version. PutParameter refuses a name that is already taken unless the
request sets Overwrite, and an earlier version stays readable by number.
/** * Overwriting a simulated parameter and reading an earlier version. */
import { GetParameterCommand, PutParameterCommand } from "@aws-sdk/client-ssm";
import { SimAws } from "@kensio/yulin";
const simAws = new SimAws();const ssm = simAws.ssm();
await ssm.putParameter( new PutParameterCommand({ Name: "/myapp/prod/db-host", Type: "String", Value: "db.internal", }),);
const overwritten = await ssm.putParameter( new PutParameterCommand({ Name: "/myapp/prod/db-host", Value: "db2.internal", Overwrite: true, }),);
console.log(overwritten.Version); // 2
const first = await ssm.getParameter( new GetParameterCommand({ Name: "/myapp/prod/db-host:1" }),);
console.log(first.Parameter?.Value); // "db.internal"console.log(first.Parameter?.Selector); // ":1"A parameter’s type cannot change. Overwriting a String parameter as a StringList fails with
HierarchyTypeMismatchException, as it does on real AWS. Delete the parameter and create a new one
instead.
Reading a hierarchy
Section titled “Reading a hierarchy”GetParametersByPath reads a whole level of the hierarchy. Without Recursive it returns only the
level immediately below the path.
/** * Reading a hierarchy of simulated parameters as application configuration. */
import { GetParametersByPathCommand, PutParameterCommand,} from "@aws-sdk/client-ssm";
import { SimAws } from "@kensio/yulin";
const simAws = new SimAws();const ssm = simAws.ssm();
await ssm.putParameter( new PutParameterCommand({ Name: "/myapp/prod/db-host", Type: "String", Value: "db.internal", }),);await ssm.putParameter( new PutParameterCommand({ Name: "/myapp/prod/db-port", Type: "String", Value: "5432", }),);await ssm.putParameter( new PutParameterCommand({ Name: "/myapp/test/db-host", Type: "String", Value: "db.test.internal", }),);
const listed = await ssm.getParametersByPath( new GetParametersByPathCommand({ Path: "/myapp/prod" }),);
console.log(listed.Parameters?.map((parameter) => parameter.Name));// [ "/myapp/prod/db-host", "/myapp/prod/db-port" ]A page holds ten parameters, as it does on real AWS. Follow NextToken to read the rest.
Reading several parameters at once
Section titled “Reading several parameters at once”GetParameters takes up to ten names. A name that resolves to nothing comes back in
InvalidParameters, and the request still succeeds. That is what makes a typo easy to miss.
/** * Reading several simulated parameters, including one name with a typo. */
import { GetParametersCommand, PutParameterCommand } from "@aws-sdk/client-ssm";
import { SimAws } from "@kensio/yulin";
const simAws = new SimAws();const ssm = simAws.ssm();
await ssm.putParameter( new PutParameterCommand({ Name: "/myapp/prod/db-host", Type: "String", Value: "db.internal", }),);
const read = await ssm.getParameters( new GetParametersCommand({ Names: ["/myapp/prod/db-host", "/myapp/prod/db-hostt"], }),);
console.log(read.Parameters?.map((parameter) => parameter.Name));// [ "/myapp/prod/db-host" ]console.log(read.InvalidParameters); // [ "/myapp/prod/db-hostt" ]GetParameters still authorizes each name. One name the caller may not read fails the whole
request.
String lists
Section titled “String lists”A StringList value comes back as one comma-separated string, on read as well as on write, never
as an array.
/** * Reading a simulated StringList parameter. */
import { GetParameterCommand, PutParameterCommand } from "@aws-sdk/client-ssm";
import { SimAws } from "@kensio/yulin";
const simAws = new SimAws();const ssm = simAws.ssm();
await ssm.putParameter( new PutParameterCommand({ Name: "/myapp/prod/allowed-origins", Type: "StringList", Value: "https://one.example,https://two.example", }),);
const read = await ssm.getParameter( new GetParameterCommand({ Name: "/myapp/prod/allowed-origins" }),);
const origins = read.Parameter?.Value?.split(",") ?? [];
console.log(origins.length); // 2Parameter names
Section titled “Parameter names”Parameter names use the same validation rules as AWS. A name:
- may contain letters, digits,
_,.,-and/ - must start with
/if it contains a hierarchy at all - may not have more than fifteen hierarchy levels
- may not start with
awsorssmin any case, which Parameter Store reserves - may not contain spaces between characters, though surrounding spaces are stripped
- may not make an ARN longer than 1011 characters, counting the ARN prefix for the account and region
A String or StringList value holds at most 4 KB, the standard tier limit. Larger configuration
documents must be split across parameters or stored elsewhere.
Deploying a parameter from CloudFormation
Section titled “Deploying a parameter from CloudFormation”Simulated CloudFormation creates a parameter from an AWS::SSM::Parameter resource, in the stack’s
account and region. The parameter is written through PutParameter. A template-created parameter is
the same thing an SDK caller would get, with the same name validation, the same ARN, and version 1.
Ref on the resource gives the parameter name, as it does on real AWS, and it can be handed straight
to GetParameter. Fn::GetAtt … Type and Fn::GetAtt … Value give those
properties.
/** * Deploying a parameter from a CloudFormation template and reading it back. */
import { GetParameterCommand } from "@aws-sdk/client-ssm";
import { SimAws } from "@kensio/yulin";
const simAws = new SimAws();
const stack = await simAws.cloudFormation().deployTemplate({ stackName: "config-stack", template: { Resources: { DbHost: { Type: "AWS::SSM::Parameter", Properties: { Name: "/myapp/prod/db-host", Type: "String", Value: "db.internal", Description: "Where the application database lives", }, }, }, Outputs: { DbHostParameter: { Value: { Ref: "DbHost" }, }, }, },});
await stack.waitForDeployComplete();
// Ref resolves to the parameter name, so it works as a GetParameter Name.const parameterName = stack.output("DbHostParameter");
const read = await simAws .ssm() .getParameter(new GetParameterCommand({ Name: parameterName }));
console.log(read.Parameter?.Value); // "db.internal"console.log(read.Parameter?.Version); // 1A parameter with no Name is named from the stack name, the logical ID and a tail derived from
both. A FeatureFlags in config-stack becomes config-stack-FeatureFlags- and twelve more
characters, where real CloudFormation ends the name in twelve random ones. The name carries no
leading slash, putting the parameter at the top of the hierarchy where real CloudFormation puts one
it names itself. Parameter Store counts the ARN prefix towards the 1011 characters a name may use,
and the CloudFormation docs
cover how the stack name and the logical ID share what is left.
An IAM policy granting access to a template-created parameter needs the ARN rather than the Ref.
Build it with Fn::Sub, remembering that the ARN drops the name’s leading slash:
Resource: !Sub "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/myapp/prod/db-host"CDK does this for you. ssm.StringParameter with grantRead(fn) synthesises a template that deploys
here without hand-editing.
Reading a parameter with a dynamic reference
Section titled “Reading a parameter with a dynamic reference”A template reads a parameter that already exists through a {{resolve:ssm:...}} dynamic reference.
The reference is replaced with the parameter’s value as the resource holding it is created.
{{resolve:ssm:name}} reads the current version, and {{resolve:ssm:name:3}} reads version 3. A
reference can sit inside a longer string, where only the reference itself is replaced.
/** * Reading an existing parameter from a template with a dynamic reference. */
import { GetParameterCommand, PutParameterCommand } from "@aws-sdk/client-ssm";
import { SimAws } from "@kensio/yulin";
const simAws = new SimAws();
await simAws.ssm().putParameter( new PutParameterCommand({ Name: "/myapp/prod/db-host", Type: "String", Value: "db.internal", }),);
const stack = await simAws.cloudFormation().deployTemplate({ stackName: "app-stack", template: { Resources: { DbUrl: { Type: "AWS::SSM::Parameter", Properties: { Name: "/myapp/prod/db-url", Type: "String", Value: "postgres://{{resolve:ssm:/myapp/prod/db-host}}:5432/app", }, }, }, },});
await stack.waitForDeployComplete();
const read = await simAws .ssm() .getParameter(new GetParameterCommand({ Name: "/myapp/prod/db-url" }));
console.log(read.Parameter?.Value); // "postgres://db.internal:5432/app"CDK emits one of these from ssm.StringParameter.valueForStringParameter when a version is given,
and from fromStringParameterAttributes with forceDynamicReference.
A reference inside Fn::Sub is read after the variables around it are substituted, so
!Sub "{{resolve:ssm:/myapp/${Environment}/db-host}}" looks up the name the substitution produced.
A StringList parameter resolves to the comma-separated string Parameter Store holds, which
Fn::Split then splits.
Real CloudFormation makes no dependency out of a dynamic reference, and neither does this. A
parameter another resource of the same stack creates is only there in time when the template says
DependsOn.
The parameter is read with GetParameter, as the caller deploying the stack. A policy denying that
read fails the resource holding the reference, the way it fails a real deployment. A stack deployed
without a caller reads as the account root.
A SecureString parameter is read through {{resolve:ssm-secure:...}}, which the next section
covers. {{resolve:secretsmanager:...}} reads a secret, and
simulated Secrets Manager
covers that one.
A reference the simulation cannot answer
Section titled “A reference the simulation cannot answer”When a reference names a parameter that does not exist, simulated CloudFormation substitutes
dummy-value-for-<name> and continues the deployment. This lets a test deploy the rest of a
template without setting up unrelated configuration.
The substitution is recorded on
stack.ignoredProperties,
naming the property that held the reference and why the value is a stand-in. A version the parameter
never had, a SecureString, and a body that is not a name and an optional integer version are all
recorded the same way.
Reading a SecureString with an ssm-secure reference
Section titled “Reading a SecureString with an ssm-secure reference”{{resolve:ssm-secure:name}} reads a SecureString parameter and resolves to its decrypted value.
{{resolve:ssm-secure:name:3}} reads version 3. The string scanning is the one plain ssm
references use, so a reference can sit inside a longer value and inside Fn::Sub.
Decryption goes to simulated KMS under the key the parameter was written with, as the caller
deploying the stack. A caller lacking kms:Decrypt on a customer managed key fails the resource with
the AccessDenied a decrypting GetParameter raises (see
A customer managed key needs its own permission).
A parameter under the aws/ssm managed key needs no KMS permission of the caller.
CDK writes one of these from SecretValue.ssmSecure, from
ssm.StringParameter.fromSecureStringParameterAttributes and from the deprecated
valueForSecureStringParameter.
Where a reference is accepted
Section titled “Where a reference is accepted”Real CloudFormation reads an ssm-secure reference in eleven resource properties and refuses it in
every other one. Simulated CloudFormation holds a template to the same list.
| Resource | Property |
|---|---|
AWS::DirectoryService::MicrosoftAD |
Password |
AWS::DirectoryService::SimpleAD |
Password |
AWS::ElastiCache::ReplicationGroup |
AuthToken |
AWS::IAM::User |
LoginProfile.Password |
AWS::KinesisFirehose::DeliveryStream |
RedshiftDestinationConfiguration.Password |
AWS::OpsWorks::App |
Source.Password |
AWS::OpsWorks::Stack |
CustomCookbooksSource.Password |
AWS::OpsWorks::Stack |
RdsDbInstances.DbPassword |
AWS::RDS::DBCluster |
MasterUserPassword |
AWS::RDS::DBInstance |
MasterUserPassword |
AWS::Redshift::Cluster |
MasterUserPassword |
A reference anywhere else fails the resource, naming the property that held it. A template breaking this rule is broken on real CloudFormation too.
AWS::IAM::User LoginProfile.Password is the pair a simulated resource holds today, and it is
where CDK writes SecretValue.ssmSecure. The other ten name resource types this simulation has yet
to reach.
/** * Reading a SecureString parameter into a resource property from a template. */
import { PutParameterCommand } from "@aws-sdk/client-ssm";
import { SimAws } from "@kensio/yulin";
const simAws = new SimAws();
await simAws.ssm().putParameter( new PutParameterCommand({ Name: "/myapp/prod/console-password", Type: "SecureString", Value: "hunter2", }),);
const stack = await simAws.cloudFormation().deployTemplate({ stackName: "console-stack", template: { Resources: { ConsoleUser: { Type: "AWS::IAM::User", Properties: { UserName: "ConsoleUser", LoginProfile: { Password: "{{resolve:ssm-secure:/myapp/prod/console-password}}", }, }, }, }, },});
await stack.waitForDeployComplete();
const user = simAws .iam() .users.values() .find((each) => each.userName === "ConsoleUser");
console.log(user?.loginProfile?.password); // "hunter2"A reference naming a String or a StringList parameter fails the resource, as real CloudFormation
fails it. Read a parameter stored in the clear with a plain {{resolve:ssm:...}} reference.
An ssm-secure reference the simulation cannot answer
Section titled “An ssm-secure reference the simulation cannot answer”The best-effort path is the one plain ssm references take. A reference naming a parameter that was
never created, or a version the parameter never had, resolves to dummy-value-for-<name> and the
stack carries on deploying. So does a body that is not a name and an optional integer version. Each
substitution is recorded on
stack.ignoredProperties.
Reading a parameter through a template Parameter
Section titled “Reading a parameter through a template Parameter”A template Parameters entry declared as AWS::SSM::Parameter::Value<String> is given a parameter
name, and Ref on it gives the value held under that name in the stack’s account and region. The
name comes from the value passed to CreateStack, and from the template Default when no value is
passed.
/** * Reading configuration into a template through a Parameter Store value type. */
import { PutParameterCommand } from "@aws-sdk/client-ssm";
import { SimAws } from "@kensio/yulin";
const simAws = new SimAws();
await simAws.ssm().putParameter( new PutParameterCommand({ Name: "/myapp/prod/uploads-bucket", Type: "String", Value: "myapp-prod-uploads", }),);
const stack = await simAws.cloudFormation().deployTemplate({ stackName: "app-stack", template: { Parameters: { UploadsBucketName: { Type: "AWS::SSM::Parameter::Value<String>", Default: "/myapp/prod/uploads-bucket", }, }, Resources: { UploadsBucket: { Type: "AWS::S3::Bucket", Properties: { BucketName: { Ref: "UploadsBucketName" } }, }, }, },});
await stack.waitForDeployComplete();
// The Bucket was created under the name the parameter holds.console.log(simAws.s3().getSimBucketByName("myapp-prod-uploads")?.bucketName);// "myapp-prod-uploads"CDK emits this Parameter from ssm.StringParameter.valueForStringParameter(scope, name) called
without a version, carrying the name as the Default.
AWS::SSM::Parameter::Value<List<String>> resolves to the stored comma-separated string split into
a list, which Fn::Select then reads by index. AWS::SSM::Parameter::Value<CommaDelimitedList>
resolves the same way. ssm.StringListParameter.fromListParameterAttributes without a version emits
the first of the two.
The Parameters section is read before any resource is created, as it is on real AWS. A name a
resource of the same stack goes on to create is never there in time, whatever the template says
about DependsOn.
The stored value goes unchecked against the inner type. A name held against something other than an
image ID resolves under AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>, where real CloudFormation
refuses the stack.
A name the simulation cannot answer
Section titled “A name the simulation cannot answer”The same best-effort answer a dynamic reference gets. A name Parameter Store has never held resolves
to dummy-value-for-<name>, and the stack carries on deploying. A SecureString resolves that way
too, since real CloudFormation refuses to read one into a template Parameter.
The substitution is recorded on
stack.ignoredProperties.
The logicalId names the template Parameter, the resourceType gives its declared type, and the
path is Parameters.<parameter name>.
Reading configuration in a Lambda handler
Section titled “Reading configuration in a Lambda handler”Function code that reads its configuration on cold start needs no special treatment. Any
@aws-sdk/client-ssm client the handler creates is intercepted and dispatched with the function’s
execution role as the caller. The role’s policy decides whether the read succeeds.
/** * A simulated Lambda handler reading its configuration from Parameter Store. */
import { CreateRoleCommand, PutRolePolicyCommand } from "@aws-sdk/client-iam";import { CreateFunctionCommand, InvokeCommand } from "@aws-sdk/client-lambda";import { PutParameterCommand } from "@aws-sdk/client-ssm";
import { SimAws } from "@kensio/yulin";import { makeLambdaCodeZip } from "@kensio/yulin/lambda";
const simAws = new SimAws();const accountId = simAws.defaultAccountId;const regionName = simAws.defaultRegionName;
await simAws.ssm().putParameter( new PutParameterCommand({ Name: "/myapp/prod/db-host", Type: "String", Value: "db.internal", }),);
const role = await simAws.iam().createRole( new CreateRoleCommand({ RoleName: "ConfigReaderRole", AssumeRolePolicyDocument: JSON.stringify({ Version: "2012-10-17", Statement: { Effect: "Allow", Principal: { Service: "lambda.amazonaws.com" }, Action: "sts:AssumeRole", }, }), }),);
await simAws.iam().putRolePolicy( new PutRolePolicyCommand({ RoleName: "ConfigReaderRole", PolicyName: "ReadConfig", PolicyDocument: JSON.stringify({ Version: "2012-10-17", Statement: { Effect: "Allow", Action: "ssm:GetParameter", Resource: `arn:aws:ssm:${regionName}:${accountId}:parameter/myapp/prod/*`, }, }), }),);
const handlerCode = [ 'const { SSMClient, GetParameterCommand } = require("@aws-sdk/client-ssm");', "exports.handler = async () => {", " const client = new SSMClient({});", ' const command = new GetParameterCommand({ Name: "/myapp/prod/db-host" });', " const out = await client.send(command);", " return out.Parameter.Value;", "};",].join("\n");
const zipFile = makeLambdaCodeZip({ "index.js": handlerCode });
await simAws.lambda().createFunction( new CreateFunctionCommand({ FunctionName: "config-reader", Role: role.Role.Arn, Handler: "index.handler", Code: { ZipFile: zipFile }, }),);
await simAws.backgroundTasksComplete();
const invoked = await simAws .lambda() .invoke(new InvokeCommand({ FunctionName: "config-reader" }));
console.log(Buffer.from(invoked.Payload ?? []).toString("utf8")); // "db.internal"Account and region scoping
Section titled “Account and region scoping”A parameter belongs to one account and region, as it does on real AWS. The same name in another scope is another parameter.
/** * The same simulated parameter name in two Account and Region scopes. */
import { GetParameterCommand, PutParameterCommand } from "@aws-sdk/client-ssm";
import { SimAws } from "@kensio/yulin";
const simAws = new SimAws();
await simAws .account("111111111111") .region("eu-west-2") .ssm() .putParameter( new PutParameterCommand({ Name: "/myapp/db-host", Type: "String", Value: "eu.db.internal", }), );
await simAws .account("222222222222") .region("us-east-1") .ssm() .putParameter( new PutParameterCommand({ Name: "/myapp/db-host", Type: "String", Value: "us.db.internal", }), );
const read = await simAws .account("111111111111") .region("eu-west-2") .ssm() .getParameter(new GetParameterCommand({ Name: "/myapp/db-host" }));
console.log(read.Parameter?.ARN);// "arn:aws:ssm:eu-west-2:111111111111:parameter/myapp/db-host"SecureString parameters
Section titled “SecureString parameters”A SecureString value is encrypted through simulated KMS, under the aws/ssm AWS managed key unless
the request names a key of its own. Simulated KMS creates that managed key the first time something
asks for it, with no setup needed.
A read returns the ciphertext unless it asks for decryption. This is the mistake that is easy to make
and hard to see. A handler that forgets WithDecryption parses a base64 blob as if it were a
password.
/** * Writing and reading a simulated SecureString parameter. */
import { GetParameterCommand, PutParameterCommand } from "@aws-sdk/client-ssm";
import { SimAws } from "@kensio/yulin";
const simAws = new SimAws();const ssm = simAws.ssm();
await ssm.putParameter( new PutParameterCommand({ Name: "/myapp/prod/db-password", Type: "SecureString", Value: "hunter2", }),);
const encrypted = await ssm.getParameter( new GetParameterCommand({ Name: "/myapp/prod/db-password" }),);
console.log(encrypted.Parameter?.Value); // a base64 ciphertext, not "hunter2"
const decrypted = await ssm.getParameter( new GetParameterCommand({ Name: "/myapp/prod/db-password", WithDecryption: true, }),);
console.log(decrypted.Parameter?.Value); // "hunter2"WithDecryption on a String or StringList parameter is ignored, as real Parameter Store ignores
it.
Pass KeyId to encrypt under a customer managed key instead. A KeyId naming a key that is absent,
disabled, or pending deletion fails with InvalidKeyId. That is how real Parameter Store reports
every KMS key problem.
Each value is bound to its own parameter’s ARN as the KMS encryption context, under the
PARAMETER_ARN key. A ciphertext lifted out of one parameter cannot be decrypted as another.
A customer managed key needs its own permission
Section titled “A customer managed key needs its own permission”Encrypting and decrypting go to simulated KMS as the caller, not as the service. Under a customer
managed key a write needs kms:Encrypt on the key on top of ssm:PutParameter on the parameter, and
a decrypting read needs kms:Decrypt on top of ssm:GetParameter. A role granted one and not the
other fails here, ahead of a deployment.
/** * A Role allowed to read a simulated SecureString but not to decrypt it. */
import { CreateRoleCommand, PutRolePolicyCommand } from "@aws-sdk/client-iam";import { CreateKeyCommand } from "@aws-sdk/client-kms";import { GetParameterCommand, PutParameterCommand } from "@aws-sdk/client-ssm";
import { SimAws } from "@kensio/yulin";
const simAws = new SimAws();const accountId = simAws.defaultAccountId;
const key = await simAws .kms() .createKey(new CreateKeyCommand({ Description: "Parameter key" }));
await simAws.ssm().putParameter( new PutParameterCommand({ Name: "/myapp/prod/db-password", Type: "SecureString", Value: "hunter2", KeyId: key.KeyMetadata?.Arn, }),);
const role = await simAws.iam().createRole( new CreateRoleCommand({ RoleName: "ConfigReader", AssumeRolePolicyDocument: JSON.stringify({ Version: "2012-10-17", Statement: { Effect: "Allow", Principal: { AWS: `arn:aws:iam::${accountId}:root` }, Action: "sts:AssumeRole", }, }), }),);
// The parameter is allowed, the key is not.await simAws.iam().putRolePolicy( new PutRolePolicyCommand({ RoleName: "ConfigReader", PolicyName: "ReadDbPassword", PolicyDocument: JSON.stringify({ Version: "2012-10-17", Statement: { Effect: "Allow", Action: "ssm:GetParameter", Resource: "*", }, }), }),);
const caller = { kind: "arn", arn: role.Role.Arn } as const;
try { await simAws.ssm().getParameter( new GetParameterCommand({ Name: "/myapp/prod/db-password", WithDecryption: true, }), { caller }, );} catch (error) { console.log((error as Error).name); // "AccessDenied"}The aws/ssm managed key allows the read itself
Section titled “The aws/ssm managed key allows the read itself”A parameter naming no key is encrypted under the aws/ssm AWS managed key. That key’s policy allows
the cryptographic actions to a wildcard principal under kms:ViaService and kms:CallerAccount
conditions, and Parameter Store satisfies both. A grant of that shape delegates nothing to IAM. A
role holding ssm:GetParameter and nothing on KMS reads the decrypted value, as it does in an
account, and withholding that access takes an explicit Deny.
GetParameters and GetParametersByPath decrypt through the same path and on the same permission.
A SecureString write under the managed key asks for no KMS permission either.
A kms:Decrypt grant scoped with a kms:ViaService condition (the shape a stack writes when the
key is chosen after synthesis) still works, and it is what a customer managed key wants.
The key stays out of reach of anything calling KMS directly. The same role handed the stored
ciphertext cannot decrypt it through Decrypt, because the policy’s kms:ViaService condition
matches only a request that arrived through Systems Manager. A caller in another account is refused
by the kms:CallerAccount condition.
An ECS task resolves a secrets entry through this path. An execution role holding
ssm:GetParameter alone reads a managed key SecureString into a container’s environment.
/** * Reading a simulated SecureString under the aws/ssm managed key. */
import { CreateRoleCommand, PutRolePolicyCommand } from "@aws-sdk/client-iam";import { GetParameterCommand, PutParameterCommand } from "@aws-sdk/client-ssm";
import { SimAws } from "@kensio/yulin";
const simAws = new SimAws();const accountId = simAws.defaultAccountId;
await simAws.ssm().putParameter( new PutParameterCommand({ Name: "/myapp/prod/db-password", Type: "SecureString", Value: "hunter2", }),);
const role = await simAws.iam().createRole( new CreateRoleCommand({ RoleName: "ConfigReader", AssumeRolePolicyDocument: JSON.stringify({ Version: "2012-10-17", Statement: { Effect: "Allow", Principal: { AWS: `arn:aws:iam::${accountId}:root` }, Action: "sts:AssumeRole", }, }), }),);
// The parameter, and nothing on KMS.await simAws.iam().putRolePolicy( new PutRolePolicyCommand({ RoleName: "ConfigReader", PolicyName: "ReadDbPassword", PolicyDocument: JSON.stringify({ Version: "2012-10-17", Statement: { Effect: "Allow", Action: "ssm:GetParameter", Resource: "*" }, }), }),);
const read = await simAws.ssm().getParameter( new GetParameterCommand({ Name: "/myapp/prod/db-password", WithDecryption: true, }), { caller: { kind: "arn", arn: role.Role.Arn } },);
console.log(read.Parameter?.Value); // "hunter2"DescribeParameters reports the key each SecureString is encrypted under as KeyId.
Supported operations
Section titled “Supported operations”Simulated Parameter Store supports:
PutParameterCommand, creating a parameter or overwriting oneGetParameterCommand, by name, byname:versionor byname:labelGetParametersCommand, reporting names it could not resolve inInvalidParametersGetParametersByPathCommand, with and withoutRecursiveDeleteParameterCommandandDeleteParametersCommandDescribeParametersCommandString,StringListandSecureStringparameter typesSecureStringvalues encrypted through simulated KMS, decrypted only withWithDecryption- The
AWS::SSM::ParameterCloudFormation resource, includingRefandFn::GetAtt {{resolve:ssm:...}}dynamic references in CloudFormation resource properties, by version or by current value, embedded in a longer string and insideFn::Sub{{resolve:ssm-secure:...}}dynamic references, decrypting theSecureStringthrough the KMS key it was written under, in the eleven resource properties CloudFormation reads one inStringListparameters read through a dynamic reference as the comma-separated stringFn::Splitthen splitsAWS::SSM::Parameter::Value<String>template parameters, resolvingRefto the stored valueAWS::SSM::Parameter::Value<List<String>>and<CommaDelimitedList>template parameters, resolvingRefto the stored string split into a list- Parameter name validation, including hierarchy depth and the reserved
awsandssmprefixes - Authorization of every operation by simulated IAM, against the real IAM action and ARN
- Calls made from inside a simulated Lambda handler, authorized as the function’s execution role
Limitations
Section titled “Limitations”- Only standard tier
SecureStringencryption is simulated, which encrypts under the KMS key directly. The advanced tier’s envelope encryption through the AWS Encryption SDK is left out, andkms:GenerateDataKeyis never needed. - Parameter labels are left out.
LabelParameterVersionis unimplemented, so a label can never be created, and aname:labelselector is refused with an error saying so. GetParameterHistoryis absent, though earlier versions stay readable by number.- The advanced tier is left out.
Tier: AdvancedandTier: Intelligent-Tieringare refused, and every parameter reportsTier: Standardwith the 4KB standard tier value limit. - Parameter policies (expiration and notification) are left out.
Policiesis refused, andDescribeParametersalways reports an emptyPolicieslist. - Tags are left out.
TagsonPutParameteris refused, andAddTagsToResource,RemoveTagsFromResourceandListTagsForResourceare absent. AnAWS::SSM::ParametercarryingTagsdeploys with the tags dropped and the property recorded. AllowedPatternis refused outright. Ignoring it would store a value it was meant to reject, without complaint.KeyIdon aStringorStringListparameter is refused, since nothing would encrypt a value stored in the clear.- Reading and writing a
SecureStringunder theaws/ssmmanaged key ask the caller for no KMS permission, which follows that key’s policy and the Parameter Store setup page. AWS’s SecureString encryption page documentskms:Encryptandkms:Decryptfor either kind of key, and the two disagree. A parameter under a customer managed key does need them. DataTypeother thantextis refused. Real Parameter Store validates anaws:ec2:imagevalue against EC2, which this simulation cannot do.- Filters are refused outright.
GetParametersByPathrefusesParameterFilters, andDescribeParametersrefusesFiltersandParameterFilters. Parameters are listed in name order. DescribeParametersrefusesShared. Parameters cannot be shared between simulated accounts, and there is no resource policy support, so cross-account access to a parameter cannot be granted.- Every version is kept. Real Parameter Store keeps the hundred most recent versions and deletes the
oldest as new ones are made, which can fail with
ParameterMaxVersionLimitExceeded. - There is no per-account parameter count limit, so
ParameterLimitExceedednever happens. - Deletion is immediate. Real Parameter Store asks for thirty seconds before a deleted name is reused, where here the name is free straight away.
AWS::SSM::ParametersupportsName,Type,Value,DescriptionandTier.AllowedPattern,DataTypeandPoliciesreachPutParameter, which refuses them for the reasons above.Tagsis the one difference from the command, and is recorded as an ignored property so a stack that tags every Resource in it still deploys.Type: SecureStringis refused, as real CloudFormation refuses it for this resource type. The plaintext value would sit in the template.- The other
AWS::SSM::*resource types (Document,Association,MaintenanceWindow,PatchBaseline,ResourceDataSyncand the rest) are reported as unsupported and skipped. - Every deployment of an
AWS::SSM::Parameteris a create. A name another stack already used is refused. A stack update that changesValuedeletes the parameter and creates it again, where real CloudFormation overwrites it in place, so the parameter’s version starts from 1 again. - A template parameter typed as
AWS::SSM::Parameter::Value<...>is read once, while theParameterssection is. Real CloudFormation does the same, so a name that only exists once the stack has deployed resolves to a stand-in value here and fails the stack there. - The value a template parameter resolves to goes unvalidated against the inner type. The
AWS::EC2::*andAWS::Route53::HostedZone::Idinner types name EC2 and Route53 resources, which this simulation holds none of. - A
{{resolve:ssm:...}}or{{resolve:ssm-secure:...}}reference naming a parameter, or a version, that simulated Parameter Store has never held resolves todummy-value-for-<name>and records the substitution. Real CloudFormation fails the stack. A template parameter naming one gets the same stand-in value. - An
ssm-securereference reads the stack’s own account and region. A parameter in another account is out of reach on real CloudFormation as well. - Public parameters under
/aws/service/...do not exist, and names under the reservedawsandssmprefixes are refused, as they are on real AWS. - The Parameters and Secrets Lambda extension HTTP endpoint is absent. Handler code has to use the SDK.
- Systems Manager is otherwise left out. Run Command, Session Manager, Patch Manager, State Manager, Automation, inventory and maintenance windows are all absent.
- SSM is not served as an HTTP API by
serveSimAws.
