Enabling Security Hub cross-Region aggregation with CloudFormation

AWS Security Hub allows you to designate an aggregation Region and link some or all Regions to that aggregation Region. Ideally we would configure cross-Region aggregation using Infrastructure-as-code. Here I walk through the CloudFormation template to deploy this configuration.

When you link a Region to the aggregation Region, its findings are continuously synchronized between the Regions. Any updates to findings in a linked Region are replicated to the aggregation Region, and any update to a finding in the aggregation Region is replicated to the linked Region where the finding originated.

We can describe this configuration using AWS CloudFormtion but currently CloudFormation does not have a resource type to do this. We will need to use a CLI driven approach to configure the cross-Region aggregation using CloudFormation.

Using Lambda

AWS Lambda is a serverless, event-driven compute service that lets you run code without provisioning or managing servers. You can trigger Lambda from AWS CloudFormation.

AWS Lambda can execute CLI commands for us using the AWS SDK for Python (boto3) and return a success or failure back to CloudFormation. We will need to use three CloudFormation resources to enable cross-Region aggreagtion.

  1. Lambda Function – used to call the AWS CLI via the boto3 SDK
  2. Custom Resource – used to trigger the Lambda function
  3. IAM Role – used to give Lambda the permission required to make changes within our account
Lambda Function

The Lambda function calls the create-finding-aggregator command to configure cross-Region aggregation. We call this command from the aggregation Region to create a finding aggregator.

We print out the event when the lambda function is triggered by CloudFormation, this output it logged to a CloudWatch log group.

When the function is triggered from a Stack Create or Update action the Region is identified, then a securityhub boto3 client is created in the region the function is executing.

The create_finding_aggregator command accepts a parameter which defines the Region linking mode, in this example we are linking all Regions. If the create_finding_aggregator command completes successfully it notifies CloudFormation of the success via the cfnresponse.send command otherwise it returns “FAILED”.

Currently this Function does not handle the DELETE RequestType and simply returns “SUCCESS” without making any changes.

# SPDX-License-Identifier: MIT-0
import json,boto3,os,cfnresponse
from botocore.exceptions import ClientError

def lambda_handler(event, context):
  print('Event: ' + json.dumps(event))
  if (event['RequestType'] == 'Create' or event['RequestType'] == 'Update'):
    try:                
      region = os.environ['AWS_REGION']
      securityhub_client = boto3.client('securityhub', region_name=region)
      response = securityhub_client.create_finding_aggregator(RegionLinkingMode='ALL_REGIONS')
      print(response)
      cfnresponse.send(event, context, cfnresponse.SUCCESS, {},"LambdaFunctionEnableSHXRAggregationInvoke")
    except Exception as ex:
      print(ex)
      cfnresponse.send(event, context, cfnresponse.FAILED, {})
  else:
    cfnresponse.send(event, context, cfnresponse.SUCCESS, {})
Custom Resource

We create a Custom Resource to trigger the above Lambda Function. This resource only requires 1 property, the service token which is the ARN of the Lambda function to call. Here we use the LogicalID of the resource using the intrinsic GetAtt function.

LambdaFunctionEnableSHXRAggregationInvoke:
  Type: AWS::CloudFormation::CustomResource
  Version: "1.0"
  Properties:
    ServiceToken: !GetAtt LambdaFunctionEnableSHXRAggregation.Arn
IAM Role

We need to create an IAM role for the Lambda function to assume with permissions to create the findings aggregator. Our Role needs to permit the lambda service to assume the role via the Trust Policy and needs to grant Allow permissions for securityhub:CreateFindingAggregator against the findingAggregator/create resource in the inline IAM policy. We add this policy to the required in addition to the AWSLambdaBasicExecutionRole and AWSLambdaVPCAccessExecutionRole managed policies.

We subsitute the AWS Account ID and Region for the details of our account using the AWS::Region and AWS::AccountID pseudo parameters

IAMRoleEnableSHXRAggregation:
    Type: AWS::IAM::Role
    Properties:
      ManagedPolicyArns:
        - "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
        - "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole"
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
            Action:
              - sts:AssumeRole
      Policies:
        - PolicyName: SecurityHubManageXRAggregation
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - securityhub:CreateFindingAggregator
                  - securityhub:UpdateFindingAggregator
                Resource:
                  - !Sub "arn:aws:securityhub:${AWS::Region}:${AWS::AccountId}:/findingAggregator/create"
Putting it all together

Now we can put out three resources altogether in a CloudFormation template that we can use to deploy a Stack. You will launch the stack in the AWS account you want to use as the Aggregator. AWS Best Practise tells us to deploy this in our audit account. This is our account where we run all our Security tools. You will also need to launch the stack in the Region you want to be the aggregation Region.

Feel free to download the full AWS CloudFormation template from my github Repo here

Rich Carpenter

Richard is an Information Security Expert, focussed on the implementation and architecture of Digital Transformation and Public Cloud adoption at forward thinking organisations.