# Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. # # Permission is hereby granted, free of charge, to any person obtaining a copy of this # software and associated documentation files (the "Software"), to deal in the Software # without restriction, including without limitation the rights to use, copy, modify, # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. --- AWSTemplateFormatVersion: '2010-09-09' Description: Demo environment for 2018 re:Invent Keeping Secrets chalk talk. # data-protection-main.yaml # # This CloudFormation template creates infrastructure for demonstrating # AWS Security Services. You must install the data-protection-vpc.yaml # stack before installing this stack. # # The template builds the following resources: # # - An S3 bucket for Logging # - A CloudTrail that logs to the S3 bucket # - An EC2 instance running the Amazon Linux 2 operating system # # - Some bash scripts on the bastion instance # mysql.oldway.sh - connect to the database with hard-coded passwords # mysql.newway.sh - connect to the database with secrets manager # # - A private MariadB RDS database Metadata: AWS::CloudFormation::Interface: ParameterGroups: - Label: default: 'Please supply values for parameters in this section' Parameters: - MariaDBName - MariaDBSecretName - BastionKeyPair - Label: default: 'Additional Parameters (verify default values)' Parameters: - MariaDBPort - BastionCidr - KmsCmkArn - SnsTopicArn - AmazonLinux2AmiId ParameterLabels: KmsCmkArn: default: 'Enter the Parmeter Store key of the KMS CMK ARN:' SnsTopicArn: default: 'Enter the Parameter Store key of the AWS SNS topic:' AmazonLinux2AmiId: default: 'Enter the Parameter Store key of the Amazon Linux 2 AMI (do not change):' MariaDBName: default: 'Enter the name of the database:' MariaDBPort: default: 'Enter the TCP port for the database endpoint:' MariaDBSecretName: default: 'Enter the name of the database secret:' BastionKeyPair: default: 'Choose a key pair:' BastionCidr: default: 'Enter the CIDR Block to allow for SSH access:' Parameters: # KmsCmkArn - Amazon Resource Name for the KMS CMK to use for encrypting # volumes for Amazon RDS and AWS EBS. KmsCmkArn: Type : 'AWS::SSM::Parameter::Value' Default: 'data-protection-cmk-arn' # SnsTopicArn - Amazon Resource Name for the SNS topic to use for sending # messages about CloudTrail events SnsTopicArn: Type : 'AWS::SSM::Parameter::Value' Default: 'data-protection-topic-arn' # MariaDBName - The RDS database instance name for the MySQL database. # You would typically accept the default. MariaDBName: Type: String MinLength: 1 MaxLength: 64 AllowedPattern: '[a-zA-Z][a-zA-Z0-9]*' ConstraintDescription: 'Up to 64 alphanumerics beginning with a letter' Default: smdemo # AmazonLinux2AmiId - The AMI for Amazon Linux 2. Note that this uses the # reserved AWS space in Systems Manager Parameter Store. AmazonLinux2AmiId: Type: 'AWS::SSM::Parameter::Value' Default: '/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2' # MariaDBPort - The TCP port for the mySQL RDS database. You should typically # accept the default. MariaDBPort: Default: 3306 Type: Number MinValue: 1024 MaxValue: 65535 ConstraintDescription: 'Must be between 1024 and 65535' # MariaDBSecretName - The secret name to hold the database credentials # You would typically accept the default. MariaDBSecretName: Type: String MinLength: 1 MaxLength: 64 AllowedPattern: '[a-zA-Z][a-zA-Z0-9]*' ConstraintDescription: 'Up to 64 alphanumerics beginning with a letter' Default: smdemo # BastionKeyPair - The EC2 keypair for the bastion host. BastionKeyPair: Type: AWS::EC2::KeyPair::KeyName # BastionCidr: BastionCidr: Type: String AllowedPattern: '^([0-9]{1,3}\.){3}[0-9]{1,3}(\/([0-9]|[1-2][0-9]|3[0-2]))?$' Default: '127.0.0.1/32' Resources: # S3Bucket - General S3 bucket for CloudTrail, etc. # # Notes: # # (1) A lifecycle rule expires objects after 1 day to help reduce costs in case # the stack is not deleted. This does not delete the bucket itself. S3Bucket: DeletionPolicy: Retain Type: AWS::S3::Bucket Properties: LifecycleConfiguration: Rules: - Id: DeletionRule ExpirationInDays: '1' Status: Enabled Tags: - Key: Name Value: !Join - '' - - !ImportValue NamePrefix - '-bucket' - Key: Project Value: !ImportValue ProjectTag # S3BucketPolicy - set up the policy on the S3 Bucket # # Configure the bucket policy to allow the following: # # (1) CloudTrail use S3BucketPolicy: Type: AWS::S3::BucketPolicy Properties: Bucket: !Ref S3Bucket PolicyDocument: Version: '2012-10-17' Statement: - Sid: AWSCloudTrailAclCheck Effect: Allow Principal: Service: cloudtrail.amazonaws.com Action: s3:GetBucketAcl Resource: !Join [ '', ['arn:aws:s3:::', Ref: S3Bucket ] ] - Sid: AWSCloudTrailWrite Effect: Allow Principal: Service: cloudtrail.amazonaws.com Action: s3:PutObject Resource: !Join [ '', [ 'arn:aws:s3:::', Ref: S3Bucket, '/AWSLogs/', !Ref 'AWS::AccountId', '/*' ] ] Condition: StringEquals: s3:x-amz-acl: bucket-owner-full-control # LambdaExecutionRole - Lambda excution role for our Lambda functions # # I am using the same Lambda execution role for all of the functions. # In a production scenario, I would use separate Lambda roles tailored to # each Lambda function to enforce the principle of least privilege. LambdaExecutionRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole Path: '/' Policies: - PolicyName: root PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - logs:CreateLogGroup - logs:CreateLogStream - logs:PutLogEvents Resource: arn:aws:logs:*:*:* - Effect: Allow Action: - ec2:DescribeVpcEndpointServices Resource: '*' - Effect: Allow Action: - sns:Publish Resource: arn:aws:sns:*:*:* - Effect: Allow Action: - secretsmanager:* Resource: arn:aws:secretsmanager:*:*:* # CTrailLogGroup - Log Group for all CloudTrail logs # # Creating the LogGroup in CloudFormation allows us to set the # retention time period for entries. CTrailLogGroup: Type: AWS::Logs::LogGroup Properties: RetentionInDays: 1 # CTrailLogGroupRole - Role for allowing CloudTrail to send logs to the # above log group. CTrailLogGroupRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: - cloudtrail.amazonaws.com Action: - sts:AssumeRole Path: '/' Policies: - PolicyName: root PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - logs:CreateLogStream - logs:PutLogEvents Resource: !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:${CTrailLogGroup}:log-stream:* # CTrail - CloudTrail for API Logging CTrail: Type: AWS::CloudTrail::Trail DependsOn: - S3BucketPolicy Properties: CloudWatchLogsLogGroupArn: !GetAtt CTrailLogGroup.Arn CloudWatchLogsRoleArn: !GetAtt CTrailLogGroupRole.Arn S3BucketName: !Ref S3Bucket IsLogging: True Tags: - Key: Name Value: !Join - '' - - !ImportValue NamePrefix - '-trail' - Key: Project Value: !ImportValue ProjectTag # CTrailEventRuleFunction - Lambda function for processing CloudTrail Events # # Notes: # # (1) Decrypt events are filtered because they happen so frequently. # # (2) Only SNS e-mail targets receive the message. CTrailEventRuleFunction: Type: AWS::Lambda::Function Properties: Description: 'Handle selected API calls from CloudTrail/CloudWatch Events' Environment: Variables: SnsTopicArn: !Ref SnsTopicArn Handler: index.handler MemorySize: 128 Role: !GetAtt LambdaExecutionRole.Arn Runtime: 'python3.6' Timeout: 30 Code: ZipFile: | import boto3 import json import logging import os from botocore.exceptions import ClientError def handler(event, context): eventname = event['detail']['eventName'] snstopicarn = os.environ['SnsTopicArn'] snsclient = boto3.client('sns') logger=logging.getLogger(__name__) logger.setLevel(logging.DEBUG) logger.debug('CTrailEventRuleFunction Event: ' + json.dumps(event)) if eventname == 'Decrypt': return try: response = snsclient.publish( TargetArn = snstopicarn, Subject=("CloudTrail event received: " + eventname), Message=json.dumps({'default': json.dumps(event)}), MessageStructure='json') logger.debug('SNS Publish Response: ' + json.dumps(response)) except ClientError as e: logger.error('An error occured: ' + e.response['Error']['Code']) # CTrailEventRule - CloudWatch Events rule # # Catch security related events and process them. CTrailEventRule: Type: AWS::Events::Rule Properties: Description: 'Rule for monitoring CloudTrail APIs' EventPattern: detail: eventSource: - cloudtrail.amazonaws.com - secretsmanager.amazonaws.com - kms.amazonaws.com - cloudhsm.amazonaws.com State: ENABLED Targets: - Arn: !GetAtt CTrailEventRuleFunction.Arn Id: !Join [ '', [ Ref: 'AWS::StackName', '-eventrule' ]] # CloudTrailEventRuleLogGroup - CloudWatch Logs log group for CTrailEventRuleFunction # # Creating the LogGroup in CloudFormation allows us to set the # retention time period for entries. CloudTrailEventRuleLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Join [ '', [ '/aws/lambda/', Ref: CTrailEventRuleFunction ] ] RetentionInDays: 1 # LambdaPermissionRule - grant CloudWatch Events # rule permissions to invoke Lambda function. LambdaPermissionRule: Type: AWS::Lambda::Permission DependsOn: CloudTrailEventRuleLogGroup Properties: Action: lambda:InvokeFunction FunctionName: !Ref CTrailEventRuleFunction Principal: events.amazonaws.com SourceArn: !GetAtt CTrailEventRule.Arn # BastionSG - The security group for http/https on the bastion host. # # Note: Port 80 must be open to the internet for the Certbot validation # to work. BastionSG: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: Enable SSH SecurityGroupIngress: - IpProtocol: tcp FromPort: '22' ToPort: '22' CidrIp: !Ref BastionCidr Tags: - Key: Name Value: !Join - '' - - !ImportValue NamePrefix - '-sg' - Key: Project Value: !ImportValue ProjectTag VpcId: !ImportValue VpcId # BastionSGSelfReference - We need to make the security group self- # referential to handle communication to and from Secrets Manager. # Doing this for the bastion host allows us to test Secrets Manager # from the AWS CLI. BastionSGSelfReference: Type: "AWS::EC2::SecurityGroupIngress" Properties: FromPort: 0 GroupId: !GetAtt BastionSG.GroupId IpProtocol: tcp SourceSecurityGroupId: !GetAtt BastionSG.GroupId ToPort: 65535 # BastionRole - The instance role for the bastion host. We only need # permissions for Secrets Manager and KMS for this demo. BastionRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: - ec2.amazonaws.com Action: - sts:AssumeRole ManagedPolicyArns: - 'arn:aws:iam::aws:policy/service-role/AmazonEC2RoleforSSM' - 'arn:aws:iam::aws:policy/AmazonSSMFullAccess' Path: '/' Policies: - PolicyName: !Join - '' - - !ImportValue NamePrefix - '-polcloudwatch' PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - cloudwatch:PutMetricData Resource: '*' - PolicyName: !Join - '' - - !ImportValue NamePrefix - '-polcloudwatchlogs' PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - logs:CreateLogGroup - logs:CreateLogStream - logs:PutLogEvents - logs:DescribeLogStreams Resource: arn:aws:logs:*:*:* - PolicyName: !Join - '' - - !ImportValue NamePrefix - '-polsecretsmgr' PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - secretsmanager:* Resource: '*' - PolicyName: !Join - '' - - !ImportValue NamePrefix - '-polkmsr' PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - kms:* Resource: '*' - PolicyName: !Join - '' - - !ImportValue NamePrefix - '-polec2' PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - ec2:Describe* - ec2:AssociateAddress Resource: '*' # BastionProfile - The instance profile for the bastion host which just # contains the corresponding IAM role. BastionProfile: Type: AWS::IAM::InstanceProfile Properties: Path: '/' Roles: - !Ref BastionRole BastionEncryptedVolume: Type: AWS::EC2::Volume Properties: AutoEnableIO: True AvailabilityZone: !ImportValue SecretsMgrSubnet01Az Encrypted: True KmsKeyId: !Ref KmsCmkArn Size: 100 Tags: - Key: Name Value: !Join - '' - - !ImportValue NamePrefix - '-volume' - Key: Project Value: !ImportValue ProjectTag # Bastion - Our bastion host. Note that we launch this with the BastionProfile # instance profile so we get access to some AWS services. We also create the # take advantage of CloudFormation::Init to do some initialization create files, # install packages, and run commands. # # Files created for Secrets Manager: # # mysql.oldway.sh - shows the old way of connecting to the database with # hardcoded credentials # # mysql.newway.sh - shows the new way of connecting to the database with # AWS Secrets Manager # # displaysecretversions.sh - displays the versions of a secret # # Other files created: # # Packages installed: # # jq - for parsing the strings from Secrets Manager # python3 - for building the newest AWS CLI which has the most updated commands # # Additionally: # # (1) Install the LAMP (Linux-Apache-MariaDB-PHP) stack using # amazon-linux-extras. We just need the mysql client and this is an easy # way of getting it. # # (2) Build the latest AWS CLI which has the latest commands # # (3) Update the Systems Manager agent to the most recent version. Bastion: Type: AWS::EC2::Instance DependsOn: - MariaDBInstance Metadata: AWS::CloudFormation::Init: configSets: default: - BastionSetup BastionSetup: packages: yum: jq: [] python3: [] httpd: [] mariadb-server: [] php: [] php-gd: [] php-mbstring: [] php-mysqlnd: [] php-xml: [] php-xmlrpc: [] files: /home/ec2-user/mysql.oldway.sh: content: !Sub | #/bin/bash # mysql.oldway.sh # This is the old way of accessing a database, with hard-coded passwords. # This script will only work right after the CloudFormation script runs. # After you store and rotate the secret, you will need to use the # mysql.newway.sh script. mysql \ -p${MariaDBPassword.RandomString} \ -u ${MariaDBUser.RandomString} \ -P ${MariaDBPort} \ -h ${MariaDBInstance.Endpoint.Address} mode: '755' owner: ec2-user group: ec2-user /home/ec2-user/mysql.newway.sh: content: !Sub | #/bin/bash # This is the new way of accessing a database, with AWS Secrets Manager. secret=$(aws secretsmanager get-secret-value --secret-id ${MariaDBSecretName} --region ${AWS::Region} | jq .SecretString | jq fromjson) user=$(echo $secret | jq -r .username) password=$(echo $secret | jq -r .password) endpoint=$(echo $secret | jq -r .host) port=$(echo $secret | jq -r .port) mysql \ -p$password \ -u $user \ -P $port \ -h $endpoint mode: '755' owner: ec2-user group: ec2-user /home/ec2-user/displaysecretversions.sh: content: !Sub | #/bin/bash # display the versions of a secret VERSIONIDS=($(aws secretsmanager list-secret-version-ids --secret-id ${MariaDBSecretName} --region ${AWS::Region} --query '[Versions[*].[VersionId]]' --output text)) for V in ${!VERSIONIDS[@]} do SECRETINFO=`aws secretsmanager get-secret-value --secret-id ${MariaDBSecretName} --region ${AWS::Region} --version-id $V` SECRETSTRING=`echo $SECRETINFO|jq -r .SecretString` USERNAME=`echo $SECRETSTRING|jq -r .username` PASSWORD=`echo $SECRETSTRING|jq -r .password` VERSIONSTAGES=`echo $SECRETINFO|jq -r .VersionStages | tr -d ' \n\[\]\"'` echo Version: $V echo echo username = $USERNAME echo password = $PASSWORD echo versionstages = $VERSIONSTAGES echo echo ---- echo done mode: '755' owner: ec2-user group: ec2-user /home/ec2-user/rdsmariadbsecretstring.sh: content: !Sub | #/bin/bash # rdsmariadbsecretstring.sh # Create a secretstring value for AWS Secrets Manager # for an Amazon RDS MariaDB database. If no arguments # are supplied, use the values that were created at the # time the stack was built, otherwise use the following # command line parameters (which you may want to quote). # # $1 = database user name # $2 = database password # $3 = database engine # $4 = database host (FQDN) # $5 = database port # $6 = database name # $7 = database instance identifier (the first part of $4) echo \ '{'\ '"'username'"':'"'$1'"',\ '"'password'"':'"'$2'"',\ '"'engine'"':'"'$3'"',\ '"'host'"':'"'$4'"',\ '"'port'"':'"'$5'"',\ '"'dbname'"':'"'$6'"',\ '"'dbInstanceIdentifier'"':'"'$7'"'\ '}' mode: '755' owner: ec2-user group: ec2-user /tmp/awscli-bundle.zip: source: https://s3.amazonaws.com/aws-cli/awscli-bundle.zip mode: '755' commands: cmd10mariadb: command: 'amazon-linux-extras install lamp-mariadb10.2-php7.2' cmd20unzipcli: command: 'unzip awscli-bundle.zip' cwd: '/tmp' cmd30installcli: command: './awscli-bundle/install -i /usr/local/aws -b /usr/local/bin/aws' cwd: '/tmp' CreationPolicy: ResourceSignal: Timeout: PT15M Properties: AvailabilityZone: !ImportValue SecretsMgrSubnet01Az IamInstanceProfile: !Ref BastionProfile ImageId: !Ref AmazonLinux2AmiId InstanceInitiatedShutdownBehavior: stop InstanceType: t2.small KeyName: !Ref BastionKeyPair Monitoring: true NetworkInterfaces: - AssociatePublicIpAddress: true DeviceIndex: '0' GroupSet: - !Ref BastionSG SubnetId: !ImportValue SecretsMgrSubnet01Id Tags: - Key: Name Value: !Join - '' - - !ImportValue NamePrefix - '-host' - Key: Project Value: !ImportValue ProjectTag Tenancy: default Volumes: - VolumeId: !Ref BastionEncryptedVolume Device: '/dev/sdf' UserData: Fn::Base64: !Sub | #!/bin/bash -xe HOMEDIR=/home/ec2-user EPELRPM=https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm HTTPDCONF=/etc/httpd/conf/httpd.conf yum install -y deltarpm yum update -y # Mount the encrypted volume so the web site # installation will be on an encrypted file system. mkfs /dev/sdf mkdir -p /var/www/html mount /dev/sdf /var/www/html # Update the AWS Systems Manager Agent EC2_INSTANCE_ID=`wget -q -O - http://169.254.169.254/latest/meta-data/instance-id` aws ssm create-association \ --targets Key=instanceids,Values=$EC2_INSTANCE_ID \ --name AWS-UpdateSSMAgent \ --schedule-expression "cron(0 0 2 ? * SUN *)" \ --region ${AWS::Region} amazon-linux-extras install lamp-mariadb10.2-php7.2 echo Installing EPEL... cd /tmp wget -O epel.rpm -nv $EPELRPM yum install -y ./epel.rpm /opt/aws/bin/cfn-init -v --stack ${AWS::StackName} --resource Bastion --region ${AWS::Region} mysql -p${MariaDBPassword.RandomString} -u ${MariaDBUser.RandomString} -P ${MariaDBPort} -h ${MariaDBInstance.Endpoint.Address} </,/<\/Directory>/s/AllowOverride None/AllowOverride All/' \ $HTTPDCONF usermod -a -G apache ec2-user chown -R apache:apache /var/www chmod 2775 /var/www && find /var/www -type d -exec chmod 2775 {} \; find /var/www -type d -exec chmod 2775 {} \; find /var/www -type f -exec chmod 0664 {} \; echo Starting Apache... sudo systemctl start httpd sudo systemctl enable httpd /opt/aws/bin/cfn-signal -e $? --stack ${AWS::StackName} --resource Bastion --region ${AWS::Region} # RandomStrFunction - Generate a string of random characters # # This AWS Lambda function is used to generate a random string # of letters. We'll use the Python string module to do this. # You can change the composition of the string by changing the # methods that are used. RandomStrFunction: Type: AWS::Lambda::Function Properties: Description: 'Generate a random string of characters' Handler: index.handler MemorySize: 128 Role: !GetAtt LambdaExecutionRole.Arn Runtime: 'python3.6' Timeout: 30 Tags: - Key: Name Value: !Join - '' - - !ImportValue NamePrefix - '-rndstrcunction' - Key: Project Value: !ImportValue ProjectTag Code: ZipFile: | import json import boto3 import cfnresponse import string import random def handler(event, context): if event['RequestType'] == 'Delete': responseData = {} cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData) return StringLength=int(event['ResourceProperties']['StringLength']) if StringLength <= 0: responseData = {} cfnresponse.send(event, context, cfnresponse.FAILED) else: responseData = {} chars=string.ascii_letters # change this to use other kinds of characters responseData['RandomString'] = ''.join(random.choice(chars) for _ in range(StringLength)) cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData) return # RandomStrFunctionLogGroup - CloudWatch Logs log group for RandomStrFunction # # Creating the LogGroup in CloudFormation allows us to set the # retention time period for entries. RandomStrFunctionLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Join [ '', [ '/aws/lambda/', Ref: RandomStrFunction ] ] RetentionInDays: 1 # MariaDBUser - The AWS Lambda-backed resource for generating a random # database user # # Parameters # # ServiceToken - a pointer to the AWS Lambda function # StringLength - the length of the random string to generate MariaDBUser: Type: Custom::MariaDBUser DependsOn: RandomStrFunctionLogGroup Properties: ServiceToken: !GetAtt RandomStrFunction.Arn StringLength: '16' # MariaDBPassword - The AWS Lambda-backed resource for generating a random # database password # # Parameters # # ServiceToken - a pointer to the AWS Lambda function # StringLength - the length of the random string to generate MariaDBPassword: Type: Custom::MariaDBPassword DependsOn: RandomStrFunctionLogGroup Properties: ServiceToken: !GetAtt RandomStrFunction.Arn StringLength: '32' # SecretLambdaFunction - Create Secret for database SecretLambdaFunction: Type: AWS::Lambda::Function Properties: Description: 'Create secret for RDS database' Handler: index.handler MemorySize: 128 Role: !GetAtt LambdaExecutionRole.Arn Runtime: 'python3.6' Timeout: 30 Code: ZipFile: | import boto3 import json import logging import cfnresponse from botocore.exceptions import ClientError def handler(event, context): smclient = boto3.client('secretsmanager') logger=logging.getLogger(__name__) logger.setLevel(logging.DEBUG) responseData = {} SecretName = event['ResourceProperties']['SecretName'] logger.debug('SecretLambdaFunction Event: ' + json.dumps(event)) if event['RequestType'] == 'Create': SecretDBUsername = event['ResourceProperties']['SecretDBUsername'] SecretDBPassword = event['ResourceProperties']['SecretDBPassword'] SecretDBEngine = event['ResourceProperties']['SecretDBEngine'] SecretDBHost = event['ResourceProperties']['SecretDBHost'] SecretDBPort = event['ResourceProperties']['SecretDBPort'] SecretDBName = event['ResourceProperties']['SecretDBName'] SecretDBIdentifier = event['ResourceProperties']['SecretDBIdentifier'] SecretStringValue = ('{' + '"username":"' + SecretDBUsername + '",' + '"password":"' + SecretDBPassword + '",' + '"engine":"' + SecretDBEngine + '",' + '"host":"' + SecretDBHost + '",' + '"port":"' + SecretDBPort + '",' + '"dbname":"'+ SecretDBName + '",' + '"dbInstanceIdentifier":"' + SecretDBIdentifier + '"' + '}') logger.debug('SecretString: ' + SecretStringValue) try: response=smclient.create_secret( Name=SecretName, SecretString=SecretStringValue ) responseData['Status'] = cfnresponse.SUCCESS cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData) return except ClientError as e: logger.error('An error occured creating secret: ' + e.response['Error']['Code']) responseData['Status'] = cfnresponse.FAILED cfnresponse.send(event, context, cfnresponse.FAILED, responseData) return if event['RequestType'] == 'Delete': try: response=smclient.delete_secret( SecretId=SecretName, ForceDeleteWithoutRecovery = True ) responseData['Status'] = cfnresponse.SUCCESS cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData) return except ClientError as e: logger.error('An error occured deleting secret: ' + e.response['Error']['Code']) responseData['Status'] = cfnresponse.FAILED cfnresponse.send(event, context, cfnresponse.FAILED, responseData) return if event['RequestType'] == 'Update': responseData['Status'] = cfnresponse.FAILED cfnresponse.send(event, context, cfnresponse.FAILED, responseData) return # SecretLambdaLogGroup - CloudWatch Logs group for SecretLambdaFunction # # Creating the LogGroup in CloudFormation allows us to set the # retention time period for entries. SecretLambdaLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Join [ '', [ '/aws/lambda/', Ref: SecretLambdaFunction ] ] RetentionInDays: 1 MariaDBSecret: Type: Custom::MariaDBSecret DependsOn: SecretLambdaLogGroup Properties: ServiceToken: !GetAtt SecretLambdaFunction.Arn SecretName: !Ref MariaDBSecretName SecretDBUsername: !GetAtt MariaDBUser.RandomString SecretDBPassword: !GetAtt MariaDBPassword.RandomString SecretDBEngine: mariadb SecretDBHost: !GetAtt MariaDBInstance.Endpoint.Address SecretDBPort: !Ref MariaDBPort SecretDBName: !Ref MariaDBName SecretDBIdentifier: !Ref MariaDBInstance # DBSG - The security group for the RDS database DBSG: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: Security group for the RDS database SecurityGroupIngress: - IpProtocol: tcp FromPort: !Ref MariaDBPort ToPort: !Ref MariaDBPort CidrIp: !Join - '' - - !ImportValue VpcCidrOctet1 - '.' - !ImportValue VpcCidrOctet2 - '.0.0/16' Tags: - Key: Name Value: !Join - '' - - !ImportValue NamePrefix - '-dbsg' - Key: Project Value: !ImportValue ProjectTag VpcId: !ImportValue VpcId # DBSGSelfReference - We need to make the security group self- # referential to handle communication to and from Secrets Manager. DBSGSelfReference: Type: "AWS::EC2::SecurityGroupIngress" Properties: FromPort: 0 GroupId: !GetAtt DBSG.GroupId IpProtocol: tcp SourceSecurityGroupId: !GetAtt DBSG.GroupId ToPort: 65535 # DBSubnetGroup - Database subnet group # # We need this even for single AZ databases. DBSubnetGroup: Type: AWS::RDS::DBSubnetGroup Properties: DBSubnetGroupDescription: Subnet group for RDS database SubnetIds: - !ImportValue SecretsMgrSubnet01Id - !ImportValue SecretsMgrSubnet02Id Tags: - Key: Name Value: !Join - '' - - !ImportValue NamePrefix - '-dbsubg' - Key: Project Value: !ImportValue ProjectTag # MariaDBInstance - Our test database. # # Note that this is a private RDS database instance. The database resides # on subnets that we have created based on the presence of an AWS Secrets # Manager endpoint. MariaDBInstance: Type: AWS::RDS::DBInstance DeletionPolicy: Delete Properties: AllocatedStorage: 5 AllowMajorVersionUpgrade: false AutoMinorVersionUpgrade: true AvailabilityZone: !ImportValue SecretsMgrSubnet01Az BackupRetentionPeriod: 0 DBInstanceClass: db.t2.small DBName: !Ref MariaDBName DBSubnetGroupName: !Ref DBSubnetGroup Engine: 'mariadb' EngineVersion: '10.3' KmsKeyId: !Ref KmsCmkArn MasterUsername: !GetAtt MariaDBUser.RandomString MasterUserPassword: !GetAtt MariaDBPassword.RandomString MultiAZ: false Port: !Ref MariaDBPort PubliclyAccessible: False StorageEncrypted: True StorageType: gp2 Tags: - Key: Name Value: !Join - '' - - !ImportValue NamePrefix - '-db' - Key: Project Value: !ImportValue ProjectTag VPCSecurityGroups: - !Ref DBSG SecretsManagerEndpoint: Type: 'AWS::EC2::VPCEndpoint' Properties: VpcEndpointType: Interface PrivateDnsEnabled: True SubnetIds: - !ImportValue SecretsMgrSubnet01Id - !ImportValue SecretsMgrSubnet02Id SecurityGroupIds: - !Ref DBSG - !Ref BastionSG ServiceName: !Join - '' - - com.amazonaws. - !Ref 'AWS::Region' - .secretsmanager VpcId: !ImportValue VpcId SessionManagerEndpoint: Type: 'AWS::EC2::VPCEndpoint' Properties: VpcEndpointType: Interface PrivateDnsEnabled: True SubnetIds: - !ImportValue SessionMgrSubnet01Id - !ImportValue SessionMgrSubnet02Id SecurityGroupIds: - !Ref BastionSG ServiceName: !Join - '' - - com.amazonaws. - !Ref 'AWS::Region' - .ssmmessages VpcId: !ImportValue VpcId Outputs: BastionIP: Description: Bastion IP address Value: !GetAtt Bastion.PublicIp BastionInstanceId: Description: Bastion instance ID Value: !Ref Bastion BastionSGId: Description: Bastion instance security group Value: !Ref BastionSG MariaDBUser: Description: MariaDB master username Value: !GetAtt MariaDBUser.RandomString MariaDBPassword: Description: MariaDB master user password Value: !GetAtt MariaDBPassword.RandomString MariaDBEndpoint: Description: MariaDB endpoint Value: !GetAtt MariaDBInstance.Endpoint.Address MariaDBIdentifier: Description: MariaDB identifier Value: !Ref MariaDBInstance MariaDBPort: Description: MariaDB port Value: !Ref MariaDBPort MariaDBName: Description: MariaDB name Value: !Ref MariaDBName