aws-cdk: (aws-ssm): stringListValue returns unsplit list as string instead of list of strings

What is the problem?

When using a StringListParameter imported from an existing string list parameter with StringListParameter.fromStringListParameterName, the resulting value from ipListParam.stringListValue ends up being a single string with the unsplit list in it, instead of the expected list of strings.

My use-case is using the StringListParameter to store a list of IP addresses to be used in a ResourcePolicy, like so:

const ipListParam = new StringListParameter(this, "ip-list", {
      stringListValue: ["x.x.x.1", "x.x.x.2"],
      parameterName: "ipList",
});
const apiResourcePolicy = new PolicyDocument({
      statements: [
        new PolicyStatement({
          effect: Effect.ALLOW,
          principals: [new AnyPrincipal()],
          resources: ["execute-api:/*/*/*"],
          actions: ["execute-api:Invoke"],
          conditions: {
            IpAddress: {
              "aws:SourceIp": ipListParam.stringListValue,
            },
          },
        }),
      ],
});

Reproduction Steps

I created a new Typescript CDK project using aws-cdk-lib=2.15.0, and deployed the following stack to create the parameter, REST API Gateway, and resource policy: Sample 1 (working):

import { aws_apigateway, RemovalPolicy, Stack, StackProps } from "aws-cdk-lib";
import { EndpointType } from "aws-cdk-lib/aws-apigateway";
import {
  AnyPrincipal,
  Effect,
  PolicyDocument,
  PolicyStatement,
} from "aws-cdk-lib/aws-iam";
import { StringListParameter } from "aws-cdk-lib/aws-ssm";
import { Construct } from "constructs";

export class StringlistTestStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    const ipListParam = new StringListParameter(this, "ip-list", {
      stringListValue: ["x.x.x.1", "x.x.x.2"],
      parameterName: "ipList",
    });
    ipListParam.applyRemovalPolicy(RemovalPolicy.RETAIN);
    const apiResourcePolicy = new PolicyDocument({
      statements: [
        new PolicyStatement({
          effect: Effect.ALLOW,
          principals: [new AnyPrincipal()],
          resources: ["execute-api:/*/*/*"],
          actions: ["execute-api:Invoke"],
          conditions: {
            IpAddress: {
              "aws:SourceIp": ipListParam.stringListValue,
            },
          },
        }),
      ],
    });

    const api = new aws_apigateway.RestApi(this, "pudim-api", {
      policy: apiResourcePolicy,
      endpointTypes: [EndpointType.REGIONAL],
    });
    const item = api.root.addResource("item");
    item.addMethod(
      "GET",
      new aws_apigateway.HttpIntegration("http://www.pudim.com.br")
    );
  }
}

Then, I removed ipList from the stack while retaining the parameter, and imported it by its name again. Sample 2 (not working):

import { aws_apigateway, RemovalPolicy, Stack, StackProps } from "aws-cdk-lib";
import { EndpointType } from "aws-cdk-lib/aws-apigateway";
import {
  AnyPrincipal,
  Effect,
  PolicyDocument,
  PolicyStatement,
} from "aws-cdk-lib/aws-iam";
import { StringListParameter } from "aws-cdk-lib/aws-ssm";
import { Construct } from "constructs";

export class StringlistTestStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    const ipListParam = StringListParameter.fromStringListParameterName(
      this,
      "ip-list",
      "ipList"
    );
    const apiResourcePolicy = new PolicyDocument({
      statements: [
        new PolicyStatement({
          effect: Effect.ALLOW,
          principals: [new AnyPrincipal()],
          resources: ["execute-api:/*/*/*"],
          actions: ["execute-api:Invoke"],
          conditions: {
            IpAddress: {
              "aws:SourceIp": ipListParam.stringListValue,
            },
          },
        }),
      ],
    });

    const api = new aws_apigateway.RestApi(this, "pudim-api", {
      policy: apiResourcePolicy,
      endpointTypes: [EndpointType.REGIONAL],
    });
    const item = api.root.addResource("item");
    item.addMethod(
      "GET",
      new aws_apigateway.HttpIntegration("http://www.pudim.com.br")
    );
  }
}

What did you expect to happen?

The API should be accessible for the IP addresses in the StringListParameter ipList.

I deployed and tested the endpoint resulting from Sample 1, and it worked as expected. The generated resource policy associated with the REST API Gateway was also correct (note the list in aws:SourceIp:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "AWS": "*"
            },
            "Action": "execute-api:Invoke",
            "Resource": "arn:aws:execute-api:eu-central-1:<account>:<api-id>/*/*/*",
            "Condition": {
                "IpAddress": {
                    "aws:SourceIp": [
                        "x.x.x.1",
                        "x.x.x.2"
                    ]
                }
            }
        }
    ]
}

What actually happened?

The API was not accessible, and the generated resource policy now contained a comma-separated string of IP addresses.

Generated resource policy:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "AWS": "*"
            },
            "Action": "execute-api:Invoke",
            "Resource": "arn:aws:execute-api:eu-central-1:<account>:<api-id>/*/*/*",
            "Condition": {
                "IpAddress": {
                    "aws:SourceIp": "x.x.x.1,x.x.x.2"
                }
            }
        }
    ]
}

I had the same problem with CDK 1.147.0.

CDK CLI Version

2.15.0 (build 151055e)

Framework Version

?

Node.js Version

v16.14.0

OS

macOS 12.2.1 (21D62)

Language

Typescript

Language Version

Typescript 3.9.7

Other information

When deploying, this is the change to the policy as presented by CDK:

IAM Statement Changes
┌───┬────────────────────┬────────┬────────────────────┬───────────┬──────────────────────────────────────────────────────────────┐
│   │ Resource           │ Effect │ Action             │ Principal │ Condition                                                    │
├───┼────────────────────┼────────┼────────────────────┼───────────┼──────────────────────────────────────────────────────────────┤
│ - │ execute-api:/*/*/* │ Allow  │ execute-api:Invoke │ AWS:*     │ "IpAddress": {                                               │
│   │                    │        │                    │           │   "aws:SourceIp": "{\"Fn::Split\":[\",\",\"${iplist29786949. │
│   │                    │        │                    │           │ Value}\"]}"                                                  │
│   │                    │        │                    │           │ }                                                            │
├───┼────────────────────┼────────┼────────────────────┼───────────┼──────────────────────────────────────────────────────────────┤
│ + │ execute-api:/*/*/* │ Allow  │ execute-api:Invoke │ AWS:*     │ "IpAddress": {                                               │
│   │                    │        │                    │           │   "aws:SourceIp": "{\"Fn::Split\":[\",\",\"{{resolve:ssm:ipL │
│   │                    │        │                    │           │ ist}}\"]}"                                                   │
│   │                    │        │                    │           │ }                                                            │
└───┴────────────────────┴────────┴────────────────────┴───────────┴──────────────────────────────────────────────────────────────┘

About this issue

  • Original URL
  • State: open
  • Created 2 years ago
  • Reactions: 10
  • Comments: 15 (7 by maintainers)

Most upvoted comments

@iwt-kschoenrock I was able to reproduce the issue, on a smaller example.

Here’s what I did:

  1. Wrote this CDK code:

        const ipListParam = new ssm.StringListParameter(this, "ip-list", {
            stringListValue: ["val1", "val2"],
            parameterName: "ipList",
        });
    

    Ran cdk deploy.

  2. Changed the code to:

        const ipListParam = new ssm.StringListParameter(this, "ip-list", {
            stringListValue: ["val1", "val2"],
            parameterName: "ipList",
        });
    
        const importedIpList = ssm.StringListParameter.fromStringListParameterName(
          this, 'ImportedIpList', "ipList");
    
        new s3.CfnBucket(this, 'CfnBucket', {
            corsConfiguration: {
                corsRules: [
                    {
                        allowedHeaders: importedIpList.stringListValue,
                        allowedMethods: ['GET'],
                        allowedOrigins: ['*'],
                    },
                ],
            },
        });
    

    Ran cdk deploy.

And here’s what I see in the AWS Console for S3, in the “Cross-origin resource sharing (CORS)” section:

[
    {
        "AllowedHeaders": [
            "val1,val2"
        ],
        "AllowedMethods": [
            "GET"
        ],
        "AllowedOrigins": [
            "*"
        ],
        "ExposeHeaders": []
    }
]

Clearly, it should be "AllowedHeaders": ["val1", "val2"] instead.

So it does look like there’s some issue with this, either in the CDK, or in the CloudFormation support.

The relevant part of the generated template can be seen here:

       "AllowedHeaders": {
        "Fn::Split": [
         ",",
         "{{resolve:ssm:ipList}}"
        ]
       },

We would expect the dynamic reference to be resolved, then have the split be applied. However, this is not what happens

If you test this with splitting by : instead of ,, this is what the allowed headers will resolve to in the S3 console:

        "AllowedHeaders": [
            "{{resolve",
            "ssm",
            "ipList}}"
        ],

This confirms that the split is occurring before the dynamic value is resolved. Since we rely on CloudFormation to keep these values secure, I’m not aware of any workarounds here. I’ve reported this to CloudFormation to see what they say P68236683

Hi guys,

I stumbled upon this issue in my own implementation and after a bit of research, I found a workaround:

const parameter = new cdk.CfnParameter(this,  'MyStringListParameter', { 
    type: 'AWS::SSM::Parameter::Value<List<String>>',
    default: "/path/to/my/parameter"
});

To use it as a list:

parameter.valueAsList

Thanks for responding @ShubhamJainSJ, I neglected on posting an update here once one became available. My apologies.

This is part of the response I got:

This is because CFN never stores the actual reference value. CFN only retrieve value during runtime (create/update stack or executechangeset). However, resolve Intrinsic Function happened before runtime (create/update stack or executechangeset).

They dove into a deeper explanation of CloudFormation architecture - Essentially, this is necessary to keep dynamic reference values secure.

As for a workaround…

I would say custom resource is the only way I can think about as a workaround at this stage 😦

Could get pretty sloppy.

Is there any progress on this issue? Still running into this on aws-cdk-lib@2.31.1 – this is very easy to reproduce. The split string is not correctly added to the template