├── README.md ├── lambda-random-string-test.template └── lambda_function.py /README.md: -------------------------------------------------------------------------------- 1 | # cloudformation-random-string 2 | Generate a random string to use in your CloudFormation templates: which could then be used for example for an RDS master password. 3 | 4 | ## Usage 5 | 6 | 1. Create a new Lambda function with the code in lambda_function.py. No special permissions are required (unless you want to encrypt the string), 7 | so it can run with the basic execution role. 8 | 2. Run up the sample template. Pass in the ARN of the lambda function. 9 | 3. Check out the output of the stack. 10 | 11 | ## Parameters 12 | 13 | * Length (required) 14 | 15 | The length of string to generate. 16 | 17 | * Punctuation (optional, defaults false) 18 | 19 | Include the punctuation characters in the generated string 20 | 21 | * RDSCompatible (optional, defaults false) 22 | 23 | If using for an RDS master password, do not include the characters /,@," in the generated random string. 24 | These aren't allowed to be used in an RDS master password. 25 | 26 | * KeyId (optional) 27 | 28 | If specified, encrypt the random generated string with the KMS key identified by the KeyId parameter 29 | and return it in the 'EncryptedRandomString' attribute. Obviously means that the lambda function needs 30 | permission to encrypt with this key. 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /lambda-random-string-test.template: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion": "2010-09-09", 3 | "Description": "Demonstration of the Lambda random string custom resource", 4 | 5 | "Parameters": { 6 | "LambdaFunctionArn": { 7 | "Description": "ARN of the lambda function", 8 | "Type": "String" 9 | } 10 | }, 11 | 12 | "Resources": { 13 | "TestString": { 14 | "Type": "AWS::CloudFormation::CustomResource", 15 | "Properties": { 16 | "Length": 25, 17 | "Punctuation": true, 18 | "RDSCompatible": true, 19 | "ServiceToken": {"Ref": "LambdaFunctionArn"} 20 | } 21 | } 22 | }, 23 | 24 | "Outputs": { 25 | "TheRandomString": { 26 | "Value": {"Fn::GetAtt": ["TestString", "RandomString"]}, 27 | "Description": "The random string generated by Lambda" 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lambda_function.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | import random 3 | import string 4 | import uuid 5 | import httplib 6 | import urlparse 7 | import json 8 | import base64 9 | 10 | """ 11 | If included in a Cloudformation build as a CustomResource, generate a random string of length 12 | given by the 'length' parameter. 13 | By default the character set used is upper and lowercase ascii letters plus digits. 14 | If the 'punctuation' parameter is specified this also includes punctuation. 15 | If you specify a KMS key ID then it will be encrypted, too 16 | """ 17 | 18 | def send_response(request, response, status=None, reason=None): 19 | if status is not None: 20 | response['Status'] = status 21 | 22 | if reason is not None: 23 | response['Reason'] = reason 24 | 25 | if 'ResponseURL' in request and request['ResponseURL']: 26 | url = urlparse.urlparse(request['ResponseURL']) 27 | body = json.dumps(response) 28 | https = httplib.HTTPSConnection(url.hostname) 29 | https.request('PUT', url.path+'?'+url.query, body) 30 | 31 | return response 32 | 33 | 34 | def lambda_handler(event, context): 35 | 36 | response = { 37 | 'StackId': event['StackId'], 38 | 'RequestId': event['RequestId'], 39 | 'LogicalResourceId': event['LogicalResourceId'], 40 | 'Status': 'SUCCESS' 41 | } 42 | 43 | if 'PhysicalResourceId' in event: 44 | response['PhysicalResourceId'] = event['PhysicalResourceId'] 45 | else: 46 | response['PhysicalResourceId'] = str(uuid.uuid4()) 47 | 48 | if event['RequestType'] == 'Delete': 49 | return send_response(event, response) 50 | 51 | try: 52 | length = int(event['ResourceProperties']['Length']) 53 | except KeyError: 54 | return send_response( event, response, status='FAILED', reason='Must specify a length') 55 | except: 56 | return send_response( event, response, status='FAILED', reason='Length not an integer') 57 | try: 58 | punctuation = event['ResourceProperties']['Punctuation'] 59 | except KeyError: 60 | punctuation = False 61 | try: 62 | rds_compatible = event['ResourceProperties']['RDSCompatible'] 63 | except KeyError: 64 | rds_compatible = False 65 | valid_characters = string.ascii_letters+string.digits 66 | if punctuation not in [False,'false','False']: 67 | valid_characters = valid_characters + string.punctuation 68 | if rds_compatible not in [False,'false','False']: 69 | valid_characters = valid_characters.translate(None,'@/"') 70 | 71 | random_string = ''.join(random.choice(valid_characters) for i in range(length)) 72 | try: 73 | kmsKeyId = event['ResourceProperties']['KmsKeyId'] 74 | except KeyError: 75 | # don't want it encrypted 76 | response['Data'] = { 'RandomString': random_string } 77 | response['Reason'] = 'Successfully generated a random string' 78 | return send_response(event, response) 79 | 80 | kms = boto3.client('kms') 81 | try: 82 | encrypted = kms.encrypt(KeyId=kmsKeyId, Plaintext=random_string) 83 | except Exception as e: 84 | return send_response( event, response, status='FAILED', reason='Could not encrypt random string with KeyId {}: {}'.format(kmsKeyId,e)) 85 | 86 | response['Data'] = {'RandomString': random_string, 'EncryptedRandomString': base64.b64encode(encrypted['CiphertextBlob'])} 87 | response['Reason'] = 'Successfully created and encrypted random string' 88 | return send_response(event, response) 89 | 90 | --------------------------------------------------------------------------------