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.
- Lambda Function – used to call the AWS CLI via the boto3 SDK
- Custom Resource – used to trigger the Lambda function
- 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