Overcome CloudFormation Limitation — Manage SecureString SSM Parameters

Problem Statement

Have you ever tried creating a SecureString SSM parameter using CloudFormation, only to find out it’s not directly supported? In this blog post, we’ll explore a workaround to this limitation and make use of AWS Custom Resource functionality to achieve the desired outcome.

There is an open issue since Aug 2019 to address this functionality. This is what AWS has mentioned on the AWS::SSM::Parameter page.

In this blog we will discuss two important aspects of the end to end solution:

  1. Creating new SecureString parameters using a Custom Resource lambda function.
  2. Securely updating the values of these parameters.

Solution

As an alternative, we would have to make use of AWS Custom resource functionality.

Parameter Store

CloudFormation resource for SecureString parameter:

sampleStringParam:
  Type: AWS::SSM::Parameter
  Properties:
  Name: !Sub ‘/app/test/APP_ENV’
  Type: String
  Value: production

securePassword:
  Type: Custom::Lambda
  Properties:
  ServiceToken: !GetAtt Lambda.Arn
  Type: SecureString
  KeyId:
  Fn::GetAtt:
  – ‘SSMKMSKey’
  – ‘Arn’
  Value: ‘APP_PASSWORD’
  Name: !Sub ‘app/test/APP_PASSWORD’

secureAPIToken:
  Type: Custom::Lambda
  Properties:
  ServiceToken: !GetAtt Lambda.Arn
  Name: !Sub ‘/app/test/SECURE_TOKEN’
  Type: SecureString
  KeyId:
  Fn::GetAtt:
  – ‘SSMKMSKey’
  – ‘Arn’
  Value: ‘SECURE_TOKEN’

If you notice difference in the value of Type property between SecureString vs String parameters.

We employ “Custom:Lambda,” signalling that this is not a native AWS resource but a custom one we’re about to create.

Also, pay special attention to the “ServiceToken,” which accepts a Lambda ARN as its value. This Lambda can either be an existing one or created as part of our CloudFormation template.

Lambda Function

Now, let’s discuss the Lambda resource itself. This Lambda resource holds the logic to receive input JSON and dynamically create, update, or delete a SecureString parameter using the powerful Boto3 method, “ssm.put_parameter”.

Here is the lambda resource:


Lambda:
  Type: AWS::Lambda::Function
  Properties:
  Handler: index.handler
  Role: !GetAtt Role.Arn
  Runtime: python3.12
  Timeout: 60
  Code:
  ZipFile: |

  import json
  import boto3
  import urllib3

  http = urllib3.PoolManager()


  def send(event, context, response_status, response_data, physical_resource_id):
  response_url = event[‘ResponseURL’]

  response_body = {
  ‘Status’: response_status,
  ‘Reason’: f‘See the details in CloudWatch Log Stream: {context.log_stream_name}’,
  ‘PhysicalResourceId’: physical_resource_id,
  ‘StackId’: event[‘StackId’],
  ‘RequestId’: event[‘RequestId’],
  ‘LogicalResourceId’: event[‘LogicalResourceId’],
  ‘NoEcho’: False,
  ‘Data’: response_data
  }

  json_response_body = json.dumps(response_body)

  headers = {
  ‘content-type’: ,
  ‘content-length’: str(len(json_response_body))
  }

  try:
  response = http.request(‘PUT’,response_url, body=json_response_body, headers=headers)
  print(f“Status code: {response.status}”)
  except Exception as e:
  print(f“send(..) failed executing requests.put(..): {e}”)

  def handler(event, context):
  print(event)

  ssm = boto3.client(‘ssm’)
  props = event[‘ResourceProperties’]

  split_stack_arn = event[‘StackId’].split(‘:’)
  region = split_stack_arn[3]
  account_id = split_stack_arn[4]

  stack_name = split_stack_arn[5].split(“/”)[1]
  param_name = props.get(‘Name’, f‘cfn-{stack_name}-{event[“LogicalResourceId”]}’)
  param_arn = f‘arn:aws:ssm:{region}:{account_id}:parameter{param_name}’

  try:
  params = {
  ‘Name’: param_name,
  ‘Type’: props[‘Type’],
  ‘Value’: props[‘Value’],
  ‘Overwrite’: False
  }

  if ‘Description’ in props:
  params[‘Description’] = props[‘Description’]
  if ‘KeyId’ in props:
  params[‘KeyId’] = props[‘KeyId’]

  if event[‘RequestType’] == ‘Create’:
ssm.put_parameter(**params)
  send(event, context, ‘SUCCESS’, {‘Arn’: param_arn, ‘Name’: param_name}, param_arn)

  elif event[‘RequestType’] == ‘Update’:
  params[‘Overwrite’] = True
  ssm.put_parameter(**params)
  send(event, context, ‘SUCCESS’, {‘Arn’: param_arn, ‘Name’: param_name}, param_arn)

elif event[‘RequestType’] == ‘Delete’:
  ssm.delete_parameter(Name=param_name)
  send(event, context, ‘SUCCESS’, {‘Arn’: param_arn, ‘Name’: param_name}, param_arn)

  except Exception as err:
  print(err)
  send(event, context, ‘FAILED’, {‘Arn’: param_arn, ‘Name’: param_name}, param_arn)


CloudFormation Stack

Resources it creates:

Update SecureString Params

Option1 – AWS Console

Navigate to the AWS console to manually update the parameters might not be an ideal solution, especially when dealing with a large number of parameters.

Option2 – Use Script

Make use of below python script to update SecureString parameters. Ensure to add *.csv file in .gitignore

 

“””
Script for updating AWS Systems Manager Parameter Store SecureString parameters from a CSV file.

Usage:
python upload_securestring_parameter.py <environment_name>

e.g:
python upload_securestring_parameter.py test
“””
import csv
import subprocess
import sys

# AWS CLI command to update the SecureString parameter
def update_parameter(param_name, value):
“””
Update AWS Systems Manager Parameter Store SecureString parameter.

Args:
    parameter_name (str): The name of the parameter.
    secure_value (str): The secure value for the parameter.
    environment_name (str): The name of the environment.

Returns:
    None
“””
command = (
    f‘aws ssm put-parameter –name {param_name} ‘
    f‘–value {value} –type SecureString –overwrite ‘
    f‘–no-cli-pager’
)
try:
    print(f“Updating parameter: {param_name}”)
        subprocess.run(command, shell=True, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
except subprocess.CalledProcessError as exc:
    print(f“Error updating parameter {parameter_name}: {exc}”)
    sys.exit(1)

# Check if the correct number of command line arguments is provided
if len(sys.argv) != 2:
print(“Usage: python script.py <environment_name>”)
sys.exit(1)

# Extract the environment name from the command line arguments
environment_name = sys.argv[1]
file_name = f“{environment_name}-params.csv”

# Read CSV file and update parameters
with open(file_name, ‘r’, encoding=‘utf-8’) as csv_file:
csv_reader = csv.DictReader(csv_file)
for row in csv_reader:
    parameter_name = row[‘ParameterName’]
    secure_value = row[‘SecureValue’]

    # Call the function to update the SecureString parameter
        update_parameter(parameter_name, secure_value)

Sample test-params.csv file
ParameterName,SecureValue
/test/APP_PASSWORD,secure_password
/test/SECURE_TOKEN,secure_token

 

Conclusion

While CloudFormation may have its limitations, leveraging AWS Custom Resource functionality empowers us to overcome these obstacles.

By strategically employing Lambda functions and Boto3, we can seamlessly manage SecureString SSM parameters, providing a robust solution to a longstanding challenge.

In conclusion, this approach not only addresses the existing issue but also opens up possibilities for handling other scenarios where CloudFormation might fall short.

Find this code in my GitHub repo here.

Enjoyed this blog?

Share it with your network!

Move faster with confidence