AWSTemplateFormatVersion: "2010-09-09" Description: Ermetic AWS Control Tower management account template # --------------------------------------------------------------------------------------------------------------- # # This template allows newly added Control Tower managed accounts to be monitored automatically by Ermetic. # It provisions infrastructure in the Control Tower management account that allows creation of # Ermetic stack instances in Control Tower managed accounts whenever a new Control Tower managed account is added. # # --------------------------------------------------------------------------------------------------------------- Parameters: ErmeticAccountId: Description: Ermetic AWS Account ID (12 digits). Type: String Default: "081802104111" ErmeticActiveRegions: Description: Comma delimited list of active regions (e.g. us-east-1,us-east-2,eu-central-1 etc.) Type: CommaDelimitedList ErmeticApiKeyToken: AllowedPattern: .+ ConstraintDescription: ErmeticApiKeyToken is required Description: Ermetic API key token NoEcho: true Type: String ErmeticApplicationUrl: AllowedValues: - "https://ca.app.ermetic.com" - "https://eu.app.ermetic.com" - "https://us.app.ermetic.com" Description: Ermetic application URL Type: String Default: "https://us.app.ermetic.com" ErmeticExternalId: AllowedPattern: .+ ConstraintDescription: ErmeticExternalId is required Description: Ermetic external ID Type: String ErmeticManagedAccountTemplateUrl: Description: Ermetic CloudFormation template URL for managed accounts Type: String Default: "https://ermetic-controltower.s3.us-east-2.amazonaws.com/ermetic-controltower-managed-account.yaml" Resources: ErmeticApiKeyTokenSecret: Type: AWS::SecretsManager::Secret Properties: Name: ErmeticApiKeyTokenSecret Description: Ermetic API key token SecretString: !Ref ErmeticApiKeyToken ErmeticManagedAccountsStackSet: Type: AWS::CloudFormation::StackSet Properties: StackSetName: "ErmeticManagedAccountsStackSet" Description: Ermetic managed accounts stack set Parameters: - ParameterKey: ErmeticAccountId ParameterValue: !Ref ErmeticAccountId - ParameterKey: ErmeticExternalId ParameterValue: !Ref ErmeticExternalId AdministrationRoleARN: !Join [":", ["arn", !Ref "AWS::Partition", "iam:", !Ref "AWS::AccountId", "role/service-role/AWSControlTowerStackSetRole" ]] Capabilities: - CAPABILITY_AUTO_EXPAND - CAPABILITY_IAM - CAPABILITY_NAMED_IAM ExecutionRoleName: "AWSControlTowerExecution" PermissionModel: SELF_MANAGED TemplateURL: !Ref ErmeticManagedAccountTemplateUrl ErmeticManagementAccountCloudWatchEventsRule: Type: AWS::Events::Rule Properties: Name: ErmeticManagementAccountCloudWatchEventsRule Description: Ermetic management account CloudWatch Events rule EventPattern: detail: eventName: - CreateManagedAccount - UpdateManagedAccount eventSource: - controltower.amazonaws.com detail-type: - AWS Service Event via CloudTrail source: - aws.controltower State: ENABLED Targets: - Arn: !GetAtt "ErmeticManagementAccountFunction.Arn" Id: ErmeticManagementAccountFunction ErmeticManagementAccountFunction: Type: AWS::Lambda::Function Properties: FunctionName: ErmeticManagementAccountFunction Code: ZipFile: | import boto3 import json import logging import os import requests import time logger = logging.getLogger() logger.setLevel(logging.INFO) def get_api_key_token(): secretsmanager = boto3.client('secretsmanager') return secretsmanager.get_secret_value(SecretId='ErmeticApiKeyTokenSecret')['SecretString'] def add_or_update_account( id, name, region): activeRegions = os.environ['ErmeticActiveRegions'] ermeticApiUrl = os.environ['ErmeticApplicationUrl'] + '/api/graph' partition = os.environ['Partition'] if activeRegions: activeRegions = str([activeRegion.strip('"') for activeRegion in activeRegions.split(",")]).replace("'",'"') else: activeRegions = 'null' createAwsAccountMutation = f""" mutation {{ CreateAwsAccount( input: {{ ActiveRegions: {activeRegions} Id: "{id}" Name: "{name}" ParentGroupId: "awsRoot" RoleArn: "arn:{partition}:iam::{id}:role/ErmeticRole" }}) {{ Account {{ Id }} }} }} """ requestHeaders = { 'Authorization': 'Bearer ' + get_api_key_token() } timeout = time.time() + 120 while True: response = requests.post( ermeticApiUrl, headers = requestHeaders, json = { 'query': createAwsAccountMutation }) if response.status_code == 200 and not response.json().get('errors'): break elif response.json()['errors'][0]['message'] == 'AWS account already exists': break; elif time.time() < timeout: time.sleep(5) logger.info(f'Retrying CreateAwsAccount API {response.json()}') else: raise Exception(f'Failed CreateAwsAccount API {response.json()}') updateAwsAccountMutation = f""" mutation {{ UpdateAwsAccount( input: {{ Id: "{id}" Name: "{name}" TrailArns: ["arn:{partition}:cloudtrail:{region}:{id}:trail/aws-controltower-BaselineCloudTrail"] }}) {{ Account {{ Id }} }} }} """ response = requests.post( ermeticApiUrl, headers = requestHeaders, json = { 'query': updateAwsAccountMutation }) if response.status_code != 200 or response.json().get('errors'): logger.warning(f'Failed UpdateAwsAccount API {response.json()}') def handle_event(event, context): try: eventDetails = event['detail'] eventName = eventDetails['eventName'] eventRegionName = eventDetails['awsRegion'] if eventName == 'CreateManagedAccount' or eventName == 'UpdateManagedAccount': eventDetailsServiceEventDetails = eventDetails['serviceEventDetails'] eventAccountStatus = {} if eventName == 'CreateManagedAccount': eventAccountStatus = eventDetailsServiceEventDetails['createManagedAccountStatus'] if eventName == 'UpdateManagedAccount': eventAccountStatus = eventDetailsServiceEventDetails['updateManagedAccountStatus'] if eventAccountStatus['state'] == 'SUCCEEDED': accountId = eventAccountStatus['account']['accountId'] accountName = eventAccountStatus['account']['accountName'] logger.info(f'Handling event {eventName}. Account: {accountName} ({accountId}), Region: {eventRegionName}') stackSetName = 'ErmeticManagedAccountsStackSet' timeout = time.time() + 780 while True: try: cloudformationClient = boto3.client('cloudformation') cloudformationClient.create_stack_instances( Accounts=[accountId], Regions=[eventRegionName], StackSetName=stackSetName) logger.info(f'Stack instance created sucessfully in {stackSetName}') break except Exception as exception: if time.time() < timeout: time.sleep(10) logger.info(f'Retrying stack instance creation. Exception: {exception} ') else: raise Exception(f'Failed to create stack instance {stackSetName}. Exception: {exception}') try: add_or_update_account( accountId, accountName, eventRegionName) logger.info('Account added/updated sucessfully in Ermetic') except Exception as exception: logger.error(f'Failed to add/update account in Ermetic. Exception: {exception}') else: logger.info(f'Skipping unsuccessful event {event}') else: logger.info(f'Unexpected event {event}') except Exception as exception: logger.error(f'Unexpeted error. Exception: {exception}') Environment: Variables: ErmeticActiveRegions: !Join [",", !Ref "ErmeticActiveRegions"] ErmeticApplicationUrl: !Ref "ErmeticApplicationUrl" Partition: !Ref "AWS::Partition" Handler: index.handle_event MemorySize: 256 Role: !GetAtt "ErmeticManagementAccountFunctionRole.Arn" Runtime: python3.7 Timeout: 900 ErmeticManagementAccountFunctionRole: Type: "AWS::IAM::Role" Properties: RoleName: ErmeticManagementAccountFunctionRole AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: lambda.amazonaws.com Action: sts:AssumeRole Policies: - PolicyName: ErmeticManagementAccountFunctionRolePolicy PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: cloudformation:CreateStackInstances Resource: !Join [":", ["arn", !Ref "AWS::Partition", "cloudformation", !Ref "AWS::Region", !Ref "AWS::AccountId", "stackset/ErmeticManagedAccountsStackSet:*"]] - Effect: Allow Action: - logs:CreateLogGroup - logs:CreateLogStream - logs:DescribeLogStreams - logs:PutLogEvents Resource: !Join [":", ["arn", !Ref "AWS::Partition", "logs", !Ref "AWS::Region", !Ref "AWS::AccountId", "log-group", "/aws/lambda/ErmeticManagementAccountFunction*"]] - Effect: Allow Action: secretsmanager:GetSecretValue Resource: !Join [':',["arn", !Ref "AWS::Partition", "secretsmanager", !Ref "AWS::Region", !Ref "AWS::AccountId", "secret", "ErmeticApiKeyTokenSecret-*"]] ManagedPolicyArns: - !Sub "arn:${AWS::Partition}:iam::aws:policy/ReadOnlyAccess" - !Sub "arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" ErmeticManagementAccountCloudWatchEventsRulePermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction FunctionName: !GetAtt "ErmeticManagementAccountFunction.Arn" Principal: events.amazonaws.com SourceArn: !GetAtt "ErmeticManagementAccountCloudWatchEventsRule.Arn"