On each AWS account created there is contact information configured to identify the account owner. In addition to the primary contact information, you can specify dedicated contacts for Billing, Operations and Security events. These contacts are used in case AWS needs to contact your organization for a specific event. When setting contact information, make sure to not assign personal mailboxes and phone numbers as these generally change over time. Instead, make use of group mailboxes which are actively monitored by a team.

When creating new AWS accounts through AWS Organizations the primary contact information is inherited from the AWS Organizations Management account, the alternate contact information is not. Until recently there was no way to update this information without logging in to the newly created account. This year AWS released the AWS Account Management APIs that make this very easy to update, so no more excuses.

Updating Existing Accounts

Using the script below you can update all existing accounts in your organization.

The script will:

  • Enable trusted access from AWS Organizations to AWS Account Management.
def enable_trusted_access():
    trusted_services = organizations_client.list_aws_service_access_for_organization()['EnabledServicePrincipals']
    if not any(x['ServicePrincipal'] == 'account.amazonaws.com' for x in trusted_services):
        organizations_client.enable_aws_service_access(
            ServicePrincipal='account.amazonaws.com'
        )
  • Collect a list of all accounts in your Organization. Pagination is used in case you have a large number of accounts
def get_account_list():
    print("Getting list of accounts in AWS Organizations")
    account_list = []
    management_account_id = organizations_client.describe_organization()['Organization']['MasterAccountId']
    next_page = True
    next_token = ''
    while next_page:
        if next_token:
            response = organizations_client.list_accounts(NextToken="{}".format(next_token))
        else:
            response = organizations_client.list_accounts()

        accounts = response['Accounts']
        
        for account in accounts:
            if account['Status'] == 'ACTIVE' and account['Id'] != management_account_id:
                account_list.append(account['Id'])

        if 'NextToken' in response:
            next_token = response['NextToken'].encode('utf-8')
        else:
            next_page = False
    return account_list
  • Update the contacts for each account.
def set_account_contacts(account_id, contacts):
    for contact in contacts:
        print("Updating: {}".format(contact['type']))
        account_client.put_alternate_contact(
            AccountId=account_id,
            AlternateContactType=contact['type'],
            EmailAddress=contact['email'],
            Name=contact['name'],
            PhoneNumber=contact['phone'],
            Title=contact['title']
        )

You can download the full script from my GitHub, but make sure to update the following sections to reflect your contact details:

billing = {
    'type': 'BILLING',
    'name': 'Finance Department',
    'email': 'finance@acme.com',
    'phone': '+31648522680',
    'title': 'Finance Department'
}

security = {
    'type': 'SECURITY',
    'name': 'CISO Office',
    'email': 'security@acme.com',
    'phone': '+316xxxxxxxx',
    'title': 'CISO Office'
}

operations = {
    'type': 'OPERATIONS',
    'name': 'Cloud Platform Team',
    'email': 'operations@acme.com',
    'phone': '+316xxxxxxxx',
    'title': 'Cloud Platform Team'
}

Updating future accounts

Unfortunately, alternate contacts are not inherited when creating new accounts. If you don’t want to be bothered running the script above on all new accounts, you can monitor events generated by AWS to invoke a simple Lambda function. To achieve this you will need to monitor for 2 different events:

If one of these events is detected you can use an AWS EventBridge rule to invoke a Lambda function that will update the contact information. Note that these events are generated by AWS CloudTrail and are only visible in the North Virginia (us-east-1) region. As a result, the EventBridge rules will also need to be deployed to this region. The end-to-end flow will look like this:

Alternate Contacts Flow

To allow the update of alternate contact information through AWS Organizations you will first need to enable trusted service access for the AWS Account Management APIs. You can do this through the AWS Organizations console, by navigating to the Services section and enabling AWS Account Managenement.

Trusted Access

Alternatively, you can use the AWS CLI for this.

$ aws organizations enable-aws-service-access --service-principal account.amazonaws.com

Next, you can use the following snippets to create the above events in CDK:

organizations_rule = aws_events.Rule(
    scope=self, 
    id="OrganizationsRule",
    event_pattern=aws_events.EventPattern(
        source=["aws.organizations"],
        detail=dict(
            eventName=["CreateAccountResult"],
            eventSource=["organizations.amazonaws.com"],
            serviceEventDetails=dict(
                createAccountStatus=dict(
                    state=['SUCCEEDED']
                )
            )
        )
    )
)   
organizations_rule.add_target(target=aws_events_targets.LambdaFunction(update_contacts_function))
control_tower_rule = aws_events.Rule(
    scope=self, 
    id="ControlTowerRule",
    event_pattern=aws_events.EventPattern(
        source=["aws.controltower"],
        detail=dict(
            eventName=["CreateManagedAccount"],
            eventSource=["controltower.amazonaws.com"],
            serviceEventDetails=dict(
                createAccountStatus=dict(
                    state=['SUCCEEDED']
                )
            )
        )
    )
)   
control_tower_rule.add_target(target=aws_events_targets.LambdaFunction(update_contacts_function))

The above events invoke a Lambda function:

update_contacts_function = aws_lambda.Function(
    scope=self,
    id="UpdateContactFunction",
    runtime=aws_lambda.Runtime.PYTHON_3_9,
    code=aws_lambda.Code.from_asset('lambda/update-contacts'),
    memory_size=128,
    environment={
        "BILLING_CONTACT_NAME": "Finance",
        "BILLING_EMAIL": "finance@acme.com",
        "BILLING_CONTACT_PHONE": "+316xxxxxxxx",
        "BILLING_CONTACT_TITLE": "Finance",
        "SECURITY_CONTACT_NAME": "CISO Office",
        "SECURITY_EMAIL": "security@acme.com",
        "SECURITY_CONTACT_PHONE": "+316xxxxxxxx",
        "SECURITY_CONTACT_TITLE": "CISO Office",
        "OPERATIONS_CONTACT_NAME": "Cloud Platform Team",
        "OPERATIONS_EMAIL": "operations@acme.com",
        "OPERATIONS_CONTACT_PHONE": "+316xxxxxxxxxx",
        "OPERATIONS_CONTACT_TITLE": "Cloud Platform Team",
    },
    timeout=Duration.minutes(1),
    handler='app.lambda_handler',
)      

# Allow the function to update account contact information
update_contacts_function.role.add_to_principal_policy(
    aws_iam.PolicyStatement(
        effect=aws_iam.Effect.ALLOW,
        actions=[
            'account:PutAlternateContact'
        ],
        resources=[
            "arn:aws:account::{}:account/*".format(self.account)
        ],
    )
)

The Lambda function to update the contact information is very similar to the script above. When the AWS EventBridge invokes the Lambda it will send the following event:

{
    "detail-type": "AWS Service Event via CloudTrail", 
    "source": "aws.organizations", 
    ...
    "detail": {
        ...
        "eventSource": "organizations.amazonaws.com", 
        ...
        "serviceEventDetails": {
            "createAccountStatus": {
                "state": "SUCCEEDED",
                "accountId": "xxxxxxxxxxxxx",
                ...
            }
        }, 
        "eventCategory": "Management"
    }
}

Or similar, when created through AWS Control Tower:

{
    "detail-type": "AWS Service Event via CloudTrail", 
    "source": "aws.controltower", 
    ...
    "detail": {
        ...
        "eventSource": "controltower.amazonaws.com", 
        ...
        "serviceEventDetails": {
            "createManagedAccountStatus": {
                "state": "SUCCEEDED",
                "account": {
                  "accountId": "xxxxxxxxxxxxx",
                  ...
                }
                ...
            }
        }
    }
}

When can then simply extract the account Id from the newly created account from this event and update the alternate contacts.

import boto3
import os

account_client = boto3.client('account')

def set_account_contacts(account_id, contacts):
    for contact in contacts:
        print("Updating: {}".format(contact['type']))
        account_client.put_alternate_contact(
            AccountId=account_id,
            AlternateContactType=contact['type'],
            EmailAddress=contact['email'],
            Name=contact['name'],
            PhoneNumber=contact['phone'],
            Title=contact['title']
        )

def lambda_handler(event, context):

    billing_contact = {
        'type': 'BILLING',
        ...
    }

    security_contact = {
        'type': 'SECURITY',
        ...
    }
    
    operations_contact = {
        'type': 'OPERATIONS',
        ...
    }
    
    if event["detail-type"] == "AWS Service Event via CloudTrail":
        detail = event["detail"]
        if detail.get("serviceEventDetails",{}).get("createAccountStatus",{}).get('state') == "SUCCEEDED":
            account_id = detail['serviceEventDetails']['createAccountStatus']['accountId']
        if detail.get("serviceEventDetails",{}).get("createManagedAccountStatus",{}).get('state') == "SUCCEEDED":
            account_id = detail['serviceEventDetails']['createManagedAccountStatus']['account']['accountId']

        print("Updating alternate contacts for: {}".format(account_id))
        set_account_contacts(account_id, [security_contact, billing_contact, operations_contact])

I’ve uploaded the full CDK project to my GitHub.

Conclusion

In this post I’ve shown you how to keep the alternate contact information on your AWS accounts up-to-date. This is very important when AWS want to reach out to an account owner when they detect issues, like security incidents.

Photo by Pavan Trikutam on Unsplash

comments powered by Disqus