AWSTemplateFormatVersion: '2010-09-09' Description: AWS Control Tower Lifecycle Events for Acme Corporation(MPCT-jqowxsqq) # --------------------------------------------------------------------------------------------------- # SECTION 1: # - Provide input parameters for ISV template in managed account # -------------------------------------------------------------------------------------------------- Parameters: apiKey: Description: REQUIRED. Default Access Key for Acme Type: String AllowedPattern: .+ ConstraintDescription: apiKey is required apiSecret: Description: REQUIRED. Default API Secret for Acme Type: String AllowedPattern: .+ NoEcho: true ConstraintDescription: apiSecret is required url: Description: REQUIRED. Default url for Acme Type: String AllowedPattern: .+ Default: "app.acme.io" ConstraintDescription: url is required AcmeIAMRoleName: Description: IAM role to be provisioned in the managed account Type: String Default: IAM_R_ACME_SECURITY AcmeMonitoringAccountId: Description: 12 digit AWS Account ID where Acme's monitoring appliance is installed Type: String Default: '1234123412341234' AcmeTemplateURL: Description: >- Base URL for Acme's CloudFormation template for managed accounts Type: String Default: 'https://acme-controltower.s3.amazonaws.com/acme-managedaccount.yaml' Resources: # -------------------------------------------------------------------------------------------------- # SECTION 2: # Create a Acme Secrets Manager SecretString to store ISV API Keys/Secrets # - Accessible while provisioning managed accounts with Control Tower Lifecycle events # -------------------------------------------------------------------------------------------------- AcmeControlTowerKMSKey: Type: AWS::KMS::Key Properties: Description: "This is KMS Key Id used to encrypt/decrypt the Secret" EnableKeyRotation: true KeyPolicy: Version: '2012-10-17' Id: acme-key-default-1 Statement: - Sid: Allow administration of the key Effect: Allow Principal: AWS: !Sub arn:aws:iam::${AWS::AccountId}:root Action: - kms:Create* - kms:Describe* - kms:Enable* - kms:List* - kms:Put* - kms:Update* - kms:Revoke* - kms:Disable* - kms:Get* - kms:Delete* - kms:ScheduleKeyDeletion - kms:CancelKeyDeletion Resource: '*' - Sid: Allow use of the key Effect: Allow Principal: AWS: !Sub ${AWS::AccountId} Action: - kms:Encrypt - kms:Decrypt - kms:ReEncrypt - kms:GenerateDataKey - kms:CreateGrant - kms:DescribeKey Resource: '*' Condition: StringEquals: kms:ViaService: !Sub secretsmanager.${AWS::Region}.amazonaws.com kms:CallerAccount: !Sub ${AWS::AccountId} AcmeControlTowerKMSAlias: Type: AWS::KMS::Alias Properties: AliasName: !Sub "alias/Acme-Control-Tower-${AWS::StackName}" TargetKeyId: Ref: AcmeControlTowerKMSKey #Create Secret AcmeSecretString: Type: AWS::SecretsManager::Secret Properties: Description: Credentials required for Acme Name: AcmeSecretString KmsKeyId: !Ref AcmeControlTowerKMSKey SecretString: Fn::Join: - '' - - '{"apiKey":"' - Ref: apiKey - '","apiSecret": "' - Ref: apiSecret - '","apiSecret": "' - '","url": "' - Ref: url - '"}' # --------------------------------------------------------------------------------------------------- # SECTION 3: # Create a Acme StackSet in the Control Tower Management Account # - The Acme StackSet is the basis for the Acme template to be provisioned in the managed accounts # -------------------------------------------------------------------------------------------------- AcmeStackSet: Type: AWS::CloudFormation::StackSet Properties: Description: StackSet for creating Acme Integration Role StackSetName: 'AcmeMemberRolev1' Parameters: - ParameterKey: AcmeIAMRoleName ParameterValue: !Ref AcmeIAMRoleName - ParameterKey: AcmeMonitoringAccountId ParameterValue: !Ref AcmeMonitoringAccountId PermissionModel: SELF_MANAGED AdministrationRoleARN: !Join [':', ['arn:aws:iam:', !Ref 'AWS::AccountId', 'role/service-role/AWSControlTowerStackSetRole']] ExecutionRoleName: "AWSControlTowerExecution" Capabilities: - CAPABILITY_NAMED_IAM - CAPABILITY_IAM - CAPABILITY_AUTO_EXPAND TemplateURL: !Ref AcmeTemplateURL # -------------------------------------------------------------------------------------------------- # SECTION 4: # 1- Provision a CloudWatchEvents Rule that is triggered based on a Control Tower Lifecycle Event # 2- Provision a Lifecyle Lambda as a target for the CloudWatch Events Rule. # -------------------------------------------------------------------------------------------------- AcmeCaptureControlTowerLifeCycleEvents: Type: AWS::Events::Rule Properties: Description: Capture Control Tower LifeCycle Events for Acme and Trigger an Action EventPattern: detail: eventName: - CreateManagedAccount - UpdateManagedAccount - EnableGuardrail - DisableGuardrail - SetupLandingZone - UpdateLandingZone - RegisterOrganizationalUnit - DeregisterOrganizationalUnit eventSource: - controltower.amazonaws.com detail-type: - AWS Service Event via CloudTrail source: - aws.controltower Name: AcmeCaptureControlTowerLifeCycleEvents State: ENABLED Targets: - Arn: !GetAtt "TriggerCustomizationsOnLifeCycleEvent.Arn" Id: IDCaptureControlTowerLifeCycleEvents #Acme TriggerLifecyleEvent Lambda TriggerCustomizationsOnLifeCycleEvent: Type: AWS::Lambda::Function Properties: Code: ZipFile: | import json import os import boto3 import logging import urllib.request logger = logging.getLogger() logger.setLevel(logging.INFO) stackset_list = ['AcmeMemberRolev1'] result = {"ResponseMetadata":{"HTTPStatusCode":"400"}} def get_secret_value(key='AcmeSecretString'): secretsmanager = boto3.client('secretsmanager') secret_list = secretsmanager.list_secrets()['SecretList'] output = {} for s in secret_list: if key in s.values(): output = secretsmanager.get_secret_value(SecretId=key)['SecretString'] return(output) def register_acme_account(accountid): secretList = json.loads(get_secret_value('AcmeSecretString')) apiKey="" apiSecret="" url = "" if 'apiKey' in secretList: apiKey = secretList['apiKey'] if 'apiSecret' in secretList: apiSecret = secretList['apiSecret'] if 'url' in secretList: url = secretList['url'] account_id = accountid acmePOSTUrl = 'https://'' + url + '/api/v1/integration/aws?api_key=' + apiKey + '&secret_key=' + apiSecret payload = { 'account_id': account_id } data_json=json.dumps(payload) req = urllib.request.Request(url = acmePOSTUrl, data = bytes(data_json.encode("utf-8")), method = "POST") response_data = {} with urllib.request.urlopen(req) as resp: response_data = json.loads(resp.read().decode("utf-8")) print(response_data) output = response_data['status'] return output def lambda_handler(event, context): masterAcct = event['account'] eventDetails = event['detail'] regionName = eventDetails['awsRegion'] eventName = eventDetails['eventName'] srvEventDetails = eventDetails['serviceEventDetails'] if eventName == 'CreateManagedAccount' or eventName == 'UpdateManagedAccount': newAccInfo = {} logger.info('Event Processed Sucessfully') if eventName == 'CreateManagedAccount': newAccInfo = srvEventDetails['createManagedAccountStatus'] if eventName == 'UpdateManagedAccount': newAccInfo = srvEventDetails['updateManagedAccountStatus'] cmdStatus = newAccInfo['state'] if cmdStatus == 'SUCCEEDED': '''Sucessful event recieved''' accId = newAccInfo['account']['accountId'] # register_acme_account(accId) cloudformation = boto3.client('cloudformation') for item in stackset_list: try: result = cloudformation.create_stack_instances(StackSetName=item, Accounts=[accId], Regions=[regionName]) logger.info('Processed {} Sucessfully'.format(item)) except Exception as e: logger.error('Unable to launch in:{}, REASON: {}'.format(item, e)) else: '''Unsucessful event recieved''' logger.info('Unsucessful Event Recieved. SKIPPING :{}'.format(event)) return(False) else: logger.info('Control Tower Event Captured :{}'.format(event)) Handler: index.lambda_handler MemorySize: 256 Role: !GetAtt "AcmeTriggerLifecycleEventLambdaRole.Arn" Runtime: python3.7 Timeout: 60 #Acme Trigger LifecyleEvent Lambda Role AcmeTriggerLifecycleEventLambdaRole: Type: 'AWS::IAM::Role' DependsOn: AcmeSecretString Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Sid: AllowLambdaAssumeRole Effect: Allow Principal: Service: lambda.amazonaws.com Action: 'sts:AssumeRole' Policies: - PolicyName: AcmeLifecycleLambdaPolicy PolicyDocument: Version: 2012-10-17 Statement: - Sid: '1' Action: - 's3:GetObject' Effect: Allow Resource: - !Sub 'arn:${AWS::Partition}:s3:::cribl-controltower-8zwvs0yfk6at-${AWS::Region}' - !Sub 'arn:${AWS::Partition}:s3:::cribl-controltower-8zwvs0yfk6at-${AWS::Region}/*' - Sid: '2' Effect: Allow Action: - 'cloudformation:CreateStackInstances' Resource: !Join [':',['arn:aws:cloudformation', !Ref 'AWS::Region', !Ref 'AWS::AccountId', 'stackset/AcmeMemberRolev1:*']] - Sid: '3' Action: - 'logs:CreateLogGroup' - 'logs:CreateLogStream' - 'logs:PutLogEvents' - 'logs:DescribeLogStreams' Effect: Allow Resource: !Join [':',['arn:aws:logs', !Ref 'AWS::Region', !Ref 'AWS::AccountId', 'log-group', '/aws/lambda/Acme_TriggerLifecyleEvent:*']] - Sid: '4' Action: - 'secretsmanager:GetSecretValue' - 'secretsmanager:ListSecrets' Effect: Allow Resource: !Join [':',['arn:aws:secretsmanager', !Ref 'AWS::Region', !Ref 'AWS::AccountId','secret','AcmeSecretString*']] - Sid: '5' Action: - 'kms:Decrypt' Effect: Allow Resource: !GetAtt "AcmeControlTowerKMSKey.Arn" ManagedPolicyArns: - !Sub 'arn:${AWS::Partition}:iam::aws:policy/ReadOnlyAccess' - !Sub 'arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole' PermissionForEventsToInvokeLambdachk: Type: AWS::Lambda::Permission DependsOn: AcmeSecretString Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt "TriggerCustomizationsOnLifeCycleEvent.Arn" Principal: events.amazonaws.com SourceArn: !GetAtt "AcmeCaptureControlTowerLifeCycleEvents.Arn"