AWS Config with RDK (Rule Development Kit)

In this lab you will learn how to use AWS Config RDK (Rule Development Kit) to create AWS Config Custom Rule and deploy it across AWS Control Tower linked account. In this lab you also learn how to send the findings from AWS Config Custom Rule into AWS Security Hub. Example use case: check whether VPC with Internet Gateway (IGW) is deployed on the account. More info about RDK can be found here https://github.com/awslabs/aws-config-rdk.

Overview

The RDK is designed to support a “Compliance-as-Code” workflow that is intuitive and productive. It abstracts away much of the undifferentiated heavy lifting associated with deploying AWS Config rules backed by custom lambda functions, and provides a streamlined develop-deploy-monitor iterative process.

Architecture for the lab: Architecture In this lab, we will create Lambda function and deploy it on AWS Control Tower Management account. AWS Config Custom Rule is deployed to linked account using AWS CloudFormation StackSet. The AWS Config Custom Rule on each linked account sends the event to Lambda function to be evaluated. Lambda returns the results back to each linked account. All AWS Config Rule results will be aggregated to Audit account.

NOTE: : Before you start writing AWS Config Custom Rule, see if the existing AWS Config Managed Rules can satisfy the requirements.

Prerequisites:

  • This lab presume you have basic understanding of Python programming language.
  • This lab requires fully provisioned AWS Control Tower and administrator role access to AWS Control Tower management account.
  • Select one AWS Organization Organizational Units (OU) and record the OU id, you will deploy the AWS Config Custom Rule to all accounts in this OU.

Prerequisites: Setup Cloud9 Workspace

We will use AWS Cloud9 as our workspace (terminal + IDE) through out this workshop. If you would like to use your own workspace, ensure your AWS CLI is configured properly with administrator role access.

  1. Sign in to AWS Control Tower Management account using Administrator role.

  2. Navigate to AWS Cloud9 console, make sure you select the region where AWS Control Tower is deployed.

  3. Click Create environment

  4. Enter the environment name, i.e. AWS RDK and select Next Step

  5. Use the use the default configuration, including default VPC. Select Next Step Architecture

  6. Confirm the setup by clicking Create environment

  7. Wait until the Cloud9 terminal and IDE is ready.

Prerequisites: IAM role for Cloud9

Certain actions in RDK CLI is not permitted via AWS managed temporary credentials, to complete this lab, you need to create IAM role for Cloud9 EC2 instance.

  1. Use this deep link to create IAM role with Admin access in your AWS Control Tower management account.

  2. Ensure that AWS service and EC2 are selected. Select Next:Permissions

  3. Ensure that AdministratorAccess is checked. Select Next:Tags.

  4. You can optionally assign tags. Select Next: Review to proceed.

  5. Enter ctworkshop-rdk for the Role name. Select Create role to complete.

  6. From your Cloud9 terminal, execute the following command to attach role to the Cloud9 EC2 instance

    export instance_id=$(curl -s 169.254.169.254/latest/meta-data/instance-id)
    aws ec2 associate-iam-instance-profile --iam-instance-profile Name=ctworkshop-rdk --instance-id $instance_id
    
  7. From your Cloud9 IDE, click the gear icon (top right corder). Select AWS Settings > Credentials. Disable the AWS managed temporary credentials by selecting the toggle. Cloud9TempCred

  8. To ensure no temporary credentials in-place, let’s remove existing credentials file and set default CLI configuration.

    sudo yum install jq -y
    rm -vf ${HOME}/.aws/credentials
    export ACCOUNT_ID=$(aws sts get-caller-identity --output text --query Account)
    export AWS_REGION=$(curl -s 169.254.169.254/latest/dynamic/instance-identity/document | jq -r '.region')
    test -n "$AWS_REGION" && echo AWS_REGION is "$AWS_REGION" || echo AWS_REGION is not set
    echo "export ACCOUNT_ID=${ACCOUNT_ID}" | tee -a ~/.bash_profile
    echo "export AWS_REGION=${AWS_REGION}" | tee -a ~/.bash_profile
    aws configure set default.region ${AWS_REGION}
    aws configure get default.region
    aws sts get-caller-identity --query Arn | grep ctworkshop-rdk -q && echo "IAM role valid" || echo "IAM role NOT valid"
    

IMPORTANT: ensure that you see the last output stating IAM role valid. Go back and confirm the steps on this pre-requisite if you don’t see this output.

Task 1: Create Custom Config Rule

On this section we will use AWS Config RDK to quickly create new AWS Config Custom Rule.

1.1 In the Cloud 9 terminal, run the following command to install AWS RDK and initialize sample customer rule called MyRule

pip install rdk
rdk init --control-tower --config-bucket-exists-in-another-account
rdk create MyRule --runtime python3.8 --maximum-frequency One_Hour --input-parameters '{"desiredVPCType":"private","ExecutionRoleName":"config-role"}'
rdk deploy MyRule -f

There are couple things going on here, let’s inspect it:

  • RDK will automatically initialize and creates boilerplate for MyRule, including Python lambda functions Myrule.py.
  • RDK deploy the resources via AWS CloudFormation, you can find stack RDK-Config-Rule-Functions and inspect the Lambda function.
  • Notice we specify input parameter here desiredVPCType":"private" and "ExecutionRoleName":"config-role".
  • Lambda function uses parameter ExecutionRoleName to assume role to the account where the rule is running and perform custom checks.
  • Lambda function uses parameter desiredVPCType to compare against list of VPCs in the account where the rule is deployed (more on this later).

1.9 From Cloud9 console, use the side bar to navigate the folder structure, you should see directory MyRule.

1.10 Open file MyRule.py and find the line with statement ASSUME_ROLE_MODE and modify it as below.

ASSUME_ROLE_MODE = True
  • With this changes, the Lambda function will assume role on each Linked account to perform the custom checks.
  • As noted previously, Lambda expects that IAM role called config-role exist on each Linked account. We’ll need to deploy this IAM role later.

1.11 On the same file MyRule.py locate the statement below inside function evaluate_compliance and change the return value to NON_COMPLIANT.


    ###############################
    # Add your custom logic here. #
    ###############################

    return 'NON_COMPLIANT'

  • In this example, the function evaluate_compliance is still empty, we will just return blanket response of NON_COMPLIANT for each triggers.
  • We’ll expand this to perform custom checks, for example by assuming role to describe VPCs on the source account.

1.12 Save all files in Cloud9 and re-run the deployment in the terminal.

$ rdk deploy MyRule -f

So far we have deployed Lambda function that will handle the custom rule, the Lambda didn’t do much except assuming IAM role config-role and returning NON_COMPLIANT as the results.

Task 2: Provision Custom Config Rule to target OUs

On the previous section, you have deployed simple Lambda function to handle the custom rule, on this section you will deploy the AWS Config Custom Rule into the target accounts or OUs. We will use AWS CloudFormation StackSet to scale this deployment.

2.1 Still on your Cloud9 console.

  • create a new file called stackset.yaml
  • copy the template content below into the file stackset.yaml
  • don’t forget to save the file
---
AWSTemplateFormatVersion: '2010-09-09'
Description: AWS CloudFormation template to create custom AWS Config rules. You will
  be billed for the AWS resources used if you create a stack from this template.
Parameters:
  LambdaAccountId:
    Description: Account ID that contains Lambda functions for Config Rules.
    Type: String
    MinLength: '12'
    MaxLength: '12'
  desiredVPCType:
    Description: Desired VPC type (private or public)
    Type: String
    Default: private
Resources:
  ConfigRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: config-role
      Path: "/"
      ManagedPolicyArns:
      - Fn::Sub: arn:${AWS::Partition}:iam::aws:policy/service-role/AWSConfigRole
      - Fn::Sub: arn:${AWS::Partition}:iam::aws:policy/ReadOnlyAccess
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Sid: LOCAL
          Effect: Allow
          Principal:
            Service:
            - config.amazonaws.com
          Action: sts:AssumeRole
        - Sid: REMOTE
          Effect: Allow
          Principal:
            AWS:
              Fn::Sub: arn:${AWS::Partition}:iam::${LambdaAccountId}:root
          Action: sts:AssumeRole
  MyRuleConfigRule:
    Type: AWS::Config::ConfigRule
    Properties:
      ConfigRuleName: MyRule
      Description: MyRule
      Source:
        SourceDetails:
        - EventSource: aws.config
          MessageType: ScheduledNotification
          MaximumExecutionFrequency: One_Hour
        Owner: CUSTOM_LAMBDA
        SourceIdentifier:
          Fn::Sub: arn:${AWS::Partition}:lambda:${AWS::Region}:${LambdaAccountId}:function:RDK-Rule-Function-MyRule
      InputParameters:
        desiredVPCType: private
        ExecutionRoleName: config-role
  • this template consisted of two resources: IAM role called config-role with basic read-only access and AWS Config rule called MyRule

2.2 From Cloud9 console, select the terminal tab.

2.3 Replace the command below with your target region and target OU, then run in on the Cloud9 terminal.

ACCOUNTID=`aws sts get-caller-identity --query Account`
TARGETREGIONS=ENTER_REGION_NAME
TARGETOUID=ENTER_OU_ID
  • For TARGETOUID use comma delimited if you wish to include multiple OUs, i.e. ou-3ia8-XXXXXXXX,ou-3ia8-XXXXXXXX
  • For TARGETREGIONS use space delimited and escape it with “ ‘ ” if you wish to include multiple regions, i.e. 'us-east-1 us-west-2'

TIPS : you can find the OU id by navigating to AWS Organization or AWS Control Tower console.

WARNING : this tutorial only supports deployment in single region, to do multi-region deployment, you need to deploy the Lambda in each region.

2.4 Run command below in the Cloud9 terminal to create the StackSet.

aws cloudformation create-stack-set --stack-set-name CustomConfigRuleLambda --template-body file://stackset.yaml --parameters ParameterKey=LambdaAccountId,ParameterValue=$ACCOUNTID ParameterKey=desiredVPCType,ParameterValue=private --permission-model SERVICE_MANAGED --auto-deployment Enabled=true,RetainStacksOnAccountRemoval=false --capabilities CAPABILITY_NAMED_IAM

2.5 Run command below in the Cloud9 terminal to launch StackSet instance in the target OU:

STACKSETOPSID=`aws cloudformation create-stack-instances --stack-set-name CustomConfigRuleLambda --deployment-targets OrganizationalUnitIds=$TARGETOUID --regions $TARGETREGIONS | jq -r ".OperationId"`

2.6 Monitor the StackSet progress by running the following command in the Cloud9 terminal. Repeat the command until you see "SUCCEEDED" output in the terminal.

aws cloudformation describe-stack-set-operation --stack-set-name CustomConfigRuleLambda --operation-id $STACKSETOPSID | jq ".StackSetOperation.Status"

Alternatively, you can monitor the StackSet progress from AWS CloudFormation stackset console.

AWS CloudFormation StackSet deploys StackSet instance on each account and region that you selected earlier. The AWS Config custom rule is deployed on each accounts under TARGETOUID and on the next step you will verify the custom rule status.

Task 3: Verify AWS Config Custom Rule deployment

On this task, you will navigate to one of the AWS linked accounts under TARGETOUID OU and confirm the new AWS Config custom rule is in place.

3.1 Sign in to one of the AWS linked account under TARGETOUID using Administrator role.

3.2 Navigate to AWS Config rules, ensure you selected AWS region that included in the TARGETREGIONS

3.3 Locate custom rule called MyRule with status Noncompliant. Remember on step 1 earlier, we setup Lambda function to return NON_COMPLIANT regardless the status. We will modify the Lambda function in the next section.

3.4 By default, custom rule MyRule evaluates resources every 1 hour. To trigger immediate evaluation:

  • Select the radio button next to MyRule
  • Select Actions from the drop down, select Re-evaluate
  • The rule status will stay until we do further modification to Lambda function in the AWS Control Tower Management account.

Task 4: Modify AWS Config Custom Rule

On the following section, we will modify the Lambda function called MyRuleLambdaFunction in AWS Control Tower Management account to perform custom checks and return the evaluation to each linked account where the AWS Config custom rule MyRule is deployed.

4.1 Sign in to AWS Control Tower Management account using Administrator role.

4.2 Navigate to AWS Cloud9 console, make sure you select the region where AWS Control Tower is deployed.

4.3 Return to Cloud9 IDE that you created earlier (AWS RDK)

4.4 Open file MyRule.py, find statement for function evaluate_compliance and modify it as below:

    ###############################
    # Add your custom logic here. #
    ###############################
    if evaluate_vpc_igw(event, valid_rule_parameters):
        return 'COMPLIANT'
    else:
        return 'NON_COMPLIANT'
  • With this changes, the custom logic for AWS Config rule evaluation will call a new function called evaluate_vpc_igw
  • If the function evaluate_vpc_igw returns True then the result is COMPLIANT

4.4 Still on file MyRule.py, add the following statement to the end of the file to create new function evaluate_vpc_igw

def evaluate_vpc_igw(event, parameters):
    try:
        vpc_client = get_client('ec2', event)
        target_status = parameters['desiredVPCType']
        igw_paginator = vpc_client.get_paginator('describe_internet_gateways')
        igw_iterator = igw_paginator.paginate(
            Filters=[{'Name': 'attachment.state','Values': ['available']}]
        )
        igw_list = []
        for page in igw_iterator:
            if 'InternetGateways' in page:
                igw_list.extend(page['InternetGateways'])
        if len(igw_list) > 0:
            if target_status == 'private':
                print("Found VPCs with attached IGW : {}".format(igw_list))
                return False
            else:
                print("No VPCs with attached IGW")
                return True
        else:
            print("No IGWs found")
            return True
    except:
        return False
  • Function evaluate_vpc_igw will assume IAM role config-role on the account that initiated the AWS Config rule evaluation, remember earlier you pass parameter "ExecutionRoleName":"config-role"
  • The function evaluate_vpc_igw collects all IGW and relies on parameter desiredVPCType to determine if it should return True or False when it found IGW attached to VPC.
  • Remember when you created StackSet CustomConfigRuleLambda earlier, this template pass the parameter desiredVPCType with default value set to private

4.5 Save the file MyRule.py to save all changes

4.6 From the Cloud9 terminal, re-run the deployment to update the Lambda function:

$ rdk deploy MyRule -f

You have successfully modified the AWS Config custom rule and redeploy the Lambda function. On the next section, you will re-evaluate the rule to check the results.

Task 5: Re-verify AWS Config Custom Rule changes

On this task, you will navigate to one of the AWS linked accounts under TARGETOUID OU and confirm the changes that you made on AWS Config custom rule earlier.

5.1 Sign in to one of the AWS linked account under TARGETOUID using Administrator role.

5.2 Navigate to AWS Config rules, ensure you selected AWS region that included in the TARGETREGIONS

5.3 Locate custom rule called MyRule

  • Select the radio button next to MyRule
  • Select Actions from the drop down, select Re-evaluate
  • Wait for couple minute until the rule evaluation changes.

IMPORTANT : make sure to double check if the account you selected has any VPC with IGW attached. The rule evaluation will return Compliant if your VPC does not have IGW attached.

CHALLENGE: try to test different evaluation criteria, you could update the Config rule parameter desiredVPCType.

Congratulation, you have completed the deployment of AWS Config custom rule using custom Lambda functions. Explore the following lab tasks to expand your lab.

Task 6: Provision Custom Rule as Conformance Pack

As an alternative to single rule deployment via AWS CloudFormation StackSet, we can also use conformance packs. The CloudFormation template below does that for you. In your AWS Control Tower management account, simply deploy it as StackSet in home region where you deployed AWS Control Tower.

Hint : you could use the same CLI command as shown in Task 2 to create the stackset and deploy stackset instance. Remember that you need to delete the previous StackSet instance and StackSet template first.

6.1 Sign in to AWS Control Tower Management account using Administrator role.

6.2 Navigate to AWS Cloud9 console, make sure you select the region where AWS Control Tower is deployed.

6.3 Return to Cloud9 IDE that you created earlier (AWS RDK)

  • create a new file called conformance-pack.yaml
  • copy the template content below into the file conformance-pack.yaml
  • don’t forget to save the file
Description: "Conformance Pack with Custom Rule(s)"
Parameters:
  LambdaAccountId:
    Description: Account ID that contains Lambda functions for Config Rules.
    Type: String
    MinLength: '12'
    MaxLength: '12'
  desiredVPCType:
    Description: Desired VPC type (private or public)
    Type: String
    Default: private
Resources:
  ConfBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub "config-conf-pack-bucket-${AWS::AccountId}-${AWS::Region}"
  ConfPack:
    Type: AWS::Config::ConformancePack
    DependsOn: ConfBucket
    Properties: 
      ConformancePackName: MyFirstCustomConformancePack
      DeliveryS3Bucket: !Ref ConfBucket
      TemplateBody: !Sub |
        Resources:
          IamPolicyInUse:
            Type: "AWS::Config::ConfigRule"
            Properties:
              ConfigRuleName: IamPolicyInUse
              Description: "Checks whether the IAM policy ARN is attached to an IAM user, or an IAM group with one or more IAM users, or an IAM role with one or more trusted entity."
              MaximumExecutionFrequency: TwentyFour_Hours
              InputParameters:
                policyARN: "arn:aws:iam::aws:policy/IAMFullAccess"
              Source:
                Owner: AWS
                SourceIdentifier: IAM_POLICY_IN_USE
          
          SampleMyRule:
            Type: "AWS::Config::ConfigRule"
            Properties:
              ConfigRuleName: SampleMyRule
              Description: "Sample Custom Rule"
              MaximumExecutionFrequency: TwentyFour_Hours
              Source:
                Owner: "CUSTOM_LAMBDA"
                SourceIdentifier:
                  !Sub "arn:${AWS::Partition}:lambda:${AWS::Region}:${LambdaAccountId}:function:RDK-Rule-Function-MyRule"
                SourceDetails: 
                - EventSource: "aws.config"
                  MessageType: "ScheduledNotification"
              InputParameters:
                desiredVPCType: private
                ExecutionRoleName: config-role
  ConfigRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: config-role
      Path: "/"
      ManagedPolicyArns:
      - Fn::Sub: arn:${AWS::Partition}:iam::aws:policy/service-role/AWSConfigRole
      - Fn::Sub: arn:${AWS::Partition}:iam::aws:policy/ReadOnlyAccess
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Sid: LOCAL
          Effect: Allow
          Principal:
            Service:
            - config.amazonaws.com
          Action: sts:AssumeRole
        - Sid: REMOTE
          Effect: Allow
          Principal:
            AWS:
              Fn::Sub: arn:${AWS::Partition}:iam::${LambdaAccountId}:root
          Action: sts:AssumeRole
  • this template consisted of three resources: IAM role called config-role with basic read-only access, AWS Config Conformance pack called MyFirstCustomConformancePack with two rules on it (including the custom rule) and S3 bucket to store the Conformance pack results.

6.4 From Cloud9 console, select the terminal tab.

6.5 Replace the command below with your target region and target OU, then run in on the Cloud9 terminal. Skip this if you still have the terminal session from the previous Task 2.

ACCOUNTID=`aws sts get-caller-identity --query Account`
TARGETREGIONS=ENTER_REGION_NAME
TARGETOUID=ENTER_OU_ID
  • For TARGETOUID use comma delimited if you wish to include multiple OUs, i.e. ou-3ia8-XXXXXXXX,ou-3ia8-XXXXXXXX
  • For TARGETREGIONS use space delimited and escape it with “ ‘ ” if you wish to include multiple regions, i.e. 'us-east-1 us-west-2'

TIPS : you can find the OU id by navigating to AWS Organization or AWS Control Tower console.

WARNING : this tutorial only supports deployment in single region, to do multi-region deployment, you need to deploy the Lambda in each region.

6.6 Run command below in the Cloud9 terminal to delete the previous StackSet instances.

STACKSETOPSID=`aws cloudformation delete-stack-instances --stack-set-name CustomConfigRuleLambda --regions $TARGETREGIONS --deployment-targets OrganizationalUnitIds=$TARGETOUID --no-retain-stacks | jq -r ".OperationId"`

6.7 Run command below to check the deletion status, wait until you receive "SUCCEEDED" before proceed to the next step.

aws cloudformation describe-stack-set-operation --stack-set-name CustomConfigRuleLambda --operation-id $STACKSETOPSID | jq ".StackSetOperation.Status"

6.8 Run command below in the Cloud9 terminal to delete the StackSet.

aws cloudformation delete-stack-set --stack-set-name CustomConfigRuleLambda
  • If you receive error, ensure that the StackSet instances were deleted completely from the previous step.

6.9 Run command below in the Cloud9 terminal to create the new StackSet with Conformance pack.

aws cloudformation create-stack-set --stack-set-name CustomConfigRuleLambda --template-body file://conformance-pack.yaml --parameters ParameterKey=LambdaAccountId,ParameterValue=$ACCOUNTID ParameterKey=desiredVPCType,ParameterValue=private --permission-model SERVICE_MANAGED --auto-deployment Enabled=true,RetainStacksOnAccountRemoval=false --capabilities CAPABILITY_NAMED_IAM

6.10 Run command below in the Cloud9 terminal to launch StackSet instance in the target OU:

STACKSETOPSID=`aws cloudformation create-stack-instances --stack-set-name CustomConfigRuleLambda --deployment-targets OrganizationalUnitIds=$TARGETOUID --regions $TARGETREGIONS | jq -r ".OperationId"`

6.11 Run command below to check the create new StackSet instance status, wait until you receive "SUCCEEDED" before proceed to the next step.

aws cloudformation describe-stack-set-operation --stack-set-name CustomConfigRuleLambda --operation-id $STACKSETOPSID | jq ".StackSetOperation.Status"

If you would like to verify the deployment, follow the steps on Task 3 to login to each individual linked account to verify the AWS Config rule deployment.

IMPORTANT you must login to the individual linked account to verify this.

For more info see also https://docs.aws.amazon.com/config/latest/developerguide/custom-conformance-pack.html

Task 7: Enable Security Hub in the linked accounts

On this task, you will enable Security Hub in the linked account and later uses it to propage AWS Config rules compliance status as Security Hub findings. We will use simple CloudFormation StackSet to enable Security Hub on each linked account.

IMPORTANT Skip task 7 if you have enabled Security Hub on the linked account (i.e. via delegated admin)

7.1 Sign in to AWS Control Tower Management account using Administrator role.

7.2 Navigate to AWS Cloud9 console, make sure you select the region where AWS Control Tower is deployed.

7.3 Return to Cloud9 IDE that you created earlier (AWS RDK)

  • create a new file called security-hub.yaml
  • copy the template content below into the file security-hub.yaml
  • don’t forget to save the file
Description: Example Hub with Tags
Resources:
  ExampleHubWithTags:
    Type: 'AWS::SecurityHub::Hub'
    Properties:
      Tags:
        key1: value1
        key2: value2
Outputs:
  HubArn:
    Value: !Ref ExampleHubWithTags

7.4 From Cloud9 console, select the terminal tab.

7.5 Replace the command below with your target region and target OU, then run in on the Cloud9 terminal. Skip this if you still have the terminal session from the previous Task 2.

ACCOUNTID=`aws sts get-caller-identity --query Account`
TARGETREGIONS=ENTER_REGION_NAME
TARGETOUID=ENTER_OU_ID
  • For TARGETOUID use comma delimited if you wish to include multiple OUs, i.e. ou-3ia8-XXXXXXXX,ou-3ia8-XXXXXXXX
  • For TARGETREGIONS use space delimited and escape it with “ ‘ ” if you wish to include multiple regions, i.e. 'us-east-1 us-west-2'

TIPS : you can find the OU id by navigating to AWS Organization or AWS Control Tower console.

WARNING : this tutorial only supports deployment in single region, to do multi-region deployment, you need to deploy the Lambda in each region.

7.6 Run command below in the Cloud9 terminal to create the new StackSet for Security Hub.

aws cloudformation create-stack-set --stack-set-name SH-StackSet --template-body file://security-hub.yaml --permission-model SERVICE_MANAGED --auto-deployment Enabled=true,RetainStacksOnAccountRemoval=false --capabilities CAPABILITY_NAMED_IAM

7.7 Run command below in the Cloud9 terminal to launch StackSet instance in the target OU:

STACKSETOPSID=`aws cloudformation create-stack-instances --stack-set-name SH-StackSet --deployment-targets OrganizationalUnitIds=$TARGETOUID --regions $TARGETREGIONS | jq -r ".OperationId"`

7.8 Check the StackSet operation by running the command below and repeat it until you see "SUCCEEEDED"

aws cloudformation describe-stack-set-operation --stack-set-name SH-StackSet --operation-id $STACKSETOPSID | jq ".StackSetOperation.Status"

On the following section, you will deploy the additional solution to propagate AWS Config rules compliance status into Security Hub findings.

Task 8: Deploy Security Hub Config Solution

In this step, we will deploy a solution, that allows us to propagate custom rule findings to Security Hub. Please check this blog post for the full detail walkthrough.

8.1 Sign in to AWS Control Tower Management account using Administrator role.

8.2 Navigate to AWS Cloud9 console, make sure you select the region where AWS Control Tower is deployed.

8.3 Return to Cloud9 IDE that you created earlier (AWS RDK)

  • create a new file called config_to_securityhub.yaml
  • copy the template content below into the file config_to_securityhub.yaml
  • don’t forget to save the file
AWSTemplateFormatVersion: 2010-09-09
Description: This CloudFormation template will automate the importing of aws config findings into aws security hub
Resources:
  LambdaZipsBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: 'AES256'

  CopyZips:
    Type: Custom::CopyZips
    Properties:
      ServiceToken: !GetAtt 'CopyZipsFunction.Arn'
      DestBucket: !Ref 'LambdaZipsBucket'
      SourceBucket: 'awsiammedia'
      Prefix: ''
      Objects:
        - 'public/sample/ImportConfigRulesFindingsSecHub/config-cwe-sh.zip'

  CopyZipsRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
      Path: /
      Policies:
        - PolicyName: lambda-copier
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - s3:GetObject
                  - s3:GetObjectTagging
                Resource:
                  - !Sub 'arn:aws:s3:::awsiammedia/public/sample/ImportConfigRulesFindingsSecHub*'
              - Effect: Allow
                Action:
                  - s3:PutObject
                  - s3:DeleteObject
                  - s3:PutObjectTagging
                Resource:
                  - !Sub 'arn:aws:s3:::${LambdaZipsBucket}/public/sample/ImportConfigRulesFindingsSecHub*'

  CopyZipsFunction:
    Type: AWS::Lambda::Function
    Properties:
      Description: Copies objects from a source S3 bucket to a destination
      Handler: index.handler
      Runtime: python3.7
      Role: !GetAtt 'CopyZipsRole.Arn'
      Timeout: 240
      Code:
        ZipFile: |
          import json
          import logging
          import threading
          import boto3
          import cfnresponse
          def copy_objects(source_bucket, dest_bucket, prefix, objects):
              s3 = boto3.client('s3')
              for o in objects:
                  key = prefix + o
                  copy_source = {
                      'Bucket': source_bucket,
                      'Key': key
                  }
                  print('copy_source: %s' % copy_source)
                  print('dest_bucket = %s'%dest_bucket)
                  print('key = %s' %key)
                  s3.copy_object(CopySource=copy_source, Bucket=dest_bucket,
                        Key=key)
          def delete_objects(bucket, prefix, objects):
              s3 = boto3.client('s3')
              objects = {'Objects': [{'Key': prefix + o} for o in objects]}
              s3.delete_objects(Bucket=bucket, Delete=objects)
          def timeout(event, context):
              logging.error('Execution is about to time out, sending failure response to CloudFormation')
              cfnresponse.send(event, context, cfnresponse.FAILED, {}, None)
          def handler(event, context):
              # make sure we send a failure to CloudFormation if the function
              # is going to timeout
              timer = threading.Timer((context.get_remaining_time_in_millis()
                        / 1000.00) - 0.5, timeout, args=[event, context])
              timer.start()
              print('Received event: %s' % json.dumps(event))
              status = cfnresponse.SUCCESS
              try:
                  source_bucket = event['ResourceProperties']['SourceBucket']
                  dest_bucket = event['ResourceProperties']['DestBucket']
                  prefix = event['ResourceProperties']['Prefix']
                  objects = event['ResourceProperties']['Objects']
                  if event['RequestType'] == 'Delete':
                      delete_objects(dest_bucket, prefix, objects)
                  else:
                      copy_objects(source_bucket, dest_bucket, prefix, objects)
              except Exception as e:
                  logging.error('Exception: %s' % e, exc_info=True)
                  status = cfnresponse.FAILED
              finally:
                  timer.cancel()
                  cfnresponse.send(event, context, status, {}, None)

  LambdaServiceRole:
    Type: 'AWS::IAM::Role'
    Properties:
      RoleName: 'config-sechub-lambda-role'
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
            Action:
              - 'sts:AssumeRole'
      Policies:
        - PolicyName: lambda-service-policy
          PolicyDocument:
            Statement:
              - Effect: Allow
                Action:
                  - 'securityhub:BatchImportFindings'
                Resource:
                  - '*'
              - Effect: Allow
                Action:
                  - 'logs:CreateLogGroup'
                  - 'logs:CreateLogStream'
                  - 'logs:PutLogEvents'
                Resource: '*'
              - Effect: Allow
                Action: 
                  - 'config:DescribeConfigRules'
                Resource: '*'

  ConfigSecHubFunction:
    DependsOn: CopyZips
    Type: AWS::Lambda::Function
    Properties:
      Code:
        S3Bucket: !Ref LambdaZipsBucket
        S3Key: 'public/sample/ImportConfigRulesFindingsSecHub/config-cwe-sh.zip'
      FunctionName : 'Config-SecHub-Lambda'
      Handler: 'lambda_function.lambda_handler'
      Role:
        Fn::GetAtt:
          - LambdaServiceRole
          - Arn
      Runtime: python3.7
      Timeout: 300  
  ConfigSecHubCWRule:
    Type: AWS::Events::Rule
    Properties:
      Description: This CW rule integrates AWS Config Compliance events with AWS Lambda as a target
      Name: 'Config-Sechub-CW-Rule'
      EventPattern:
        source:
          - aws.config
        detail-type:
          - Config Rules Compliance Change
        detail:
          messageType:
            - ComplianceChangeNotification
      State: 'ENABLED'
      Targets:
        - 
          Arn: 
            Fn::GetAtt:
              - 'ConfigSecHubFunction'
              - 'Arn'
          Id: 'TargetFunctionV1'      
  PermissionForEventsToInvokeLambda:
    Type: AWS::Lambda::Permission
    Properties:
      FunctionName:
        Ref: 'ConfigSecHubFunction'
      Action: 'lambda:InvokeFunction'
      Principal: 'events.amazonaws.com'
      SourceArn:
        Fn::GetAtt:
          - 'ConfigSecHubCWRule'
          - 'Arn'

8.4 Run command below in the Cloud9 terminal to create the new StackSet for pushing AWS Config Rule to Security Hub

aws cloudformation create-stack-set --stack-set-name Config-to-SH --template-body file://config_to_securityhub.yaml --permission-model SERVICE_MANAGED --auto-deployment Enabled=true,RetainStacksOnAccountRemoval=false --capabilities CAPABILITY_NAMED_IAM

8.5 Run command below in the Cloud9 terminal to launch StackSet instance in the target OU:

STACKSETOPSID=`aws cloudformation create-stack-instances --stack-set-name Config-to-SH --deployment-targets OrganizationalUnitIds=$TARGETOUID --regions $TARGETREGIONS | jq -r ".OperationId"`

This StackSet deploys a new Lambda function ConfigSecHubFunction on each AWS Linked account. This Lambda function listen for any changes to AWS Config rule evaluations and push the evaluation as findings to AWS Security Hub.

8.6 Check the StackSet operation by running the command below and repeat it until you see "SUCCEEEDED"

aws cloudformation describe-stack-set-operation --stack-set-name Config-to-SH --operation-id $STACKSETOPSID | jq ".StackSetOperation.Status"

To validate the results, login to one of the AWS linked account and inspect the non compliant AWS Config rule and it’s respective findings on AWS Security Hub. Tips you might need to re-run the evaluation to gather findings for existing results.

Security Hub

IMPORTANT you must login to the individual linked account to verify this.

Task 9: Enable/Elevate Security Hub in Audit Account

Login to your Audit Account. Navigate to Security Hub and ensure you selected home region where AWS Control Tower is deployed. Go ahead and manually invite a single linked account.

Hint : You will need to logon to the linked account as well to accept the invite.

For more info see https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-accounts-add-invite.html

Note: In a larger environment we recoomend you to utilize AWS Security Hub integration with AWS Organization

Clean Up

Make sure to undeploy the StackSets to remove Security Hub and any Config Rules. Verify that Security Hub is disabled again!

From the Cloud9 terminal, run the following command to remove all stacksets

CustomConfigRuleLambda

STACKSETOPSID=`aws cloudformation delete-stack-instances --stack-set-name CustomConfigRuleLambda --regions $TARGETREGIONS --deployment-targets OrganizationalUnitIds=$TARGETOUID --no-retain-stacks | jq -r ".OperationId"`
aws cloudformation describe-stack-set-operation --stack-set-name CustomConfigRuleLambda --operation-id $STACKSETOPSID | jq ".StackSetOperation.Status"

Wait until you receive "SUCCEEDED" on the previous command before continue:

aws cloudformation delete-stack-set --stack-set-name CustomConfigRuleLambda

SH-StackSet

STACKSETOPSID=`aws cloudformation delete-stack-instances --stack-set-name SH-StackSet --regions $TARGETREGIONS --deployment-targets OrganizationalUnitIds=$TARGETOUID --no-retain-stacks | jq -r ".OperationId"`
aws cloudformation describe-stack-set-operation --stack-set-name SH-StackSet --operation-id $STACKSETOPSID | jq ".StackSetOperation.Status"

Wait until you receive "SUCCEEDED" on the previous command before continue:

aws cloudformation delete-stack-set --stack-set-name SH-StackSet

Config-to-SH

STACKSETOPSID=`aws cloudformation delete-stack-instances --stack-set-name Config-to-SH --regions $TARGETREGIONS --deployment-targets OrganizationalUnitIds=$TARGETOUID --no-retain-stacks | jq -r ".OperationId"`
aws cloudformation describe-stack-set-operation --stack-set-name Config-to-SH --operation-id $STACKSETOPSID | jq ".StackSetOperation.Status"

Wait until you receive "SUCCEEDED" on the previous command before continue:

aws cloudformation delete-stack-set --stack-set-name Config-to-SH

Cloud9

Before deleting the Cloud9 workspace, run the following in Cloud9 terminal to remove the IAM role that you creater earlier.

IAMROLE=ctworkshop-rdk
IAMPOLICY=`aws iam list-attached-role-policies --role-name $IAMROLE | jq -r ".AttachedPolicies[0].PolicyArn"`
aws iam remove-role-from-instance-profile --instance-profile-name $IAMROLE --role-name $IAMROLE
aws iam delete-instance-profile --instance-profile-name $IAMROLE
aws iam detach-role-policy --role-name $IAMROLE --policy-arn $IAMPOLICY
aws iam delete-role --role-name $IAMROLE

Next, navigate to AWS Cloud9 console, make sure you select the region where AWS Control Tower is deployed.

Locate the Cloud9 environment that you created earlier (i.e. AWS RDK) and select Delete