AWS Cloud Operations & Migrations Blog

How to enable VPC Flow Logs automatically using AWS Config rules

This post discusses an automated process for enabling Amazon Virtual Private Cloud (Amazon VPC) Flow Logs using AWS Config rule remediation. Customers use Amazon VPC Flow logs to capture information about the IP traffic going to and from network interfaces in an Amazon VPC. You can deploy this solution with the help of AWS Control Tower. AWS Control Tower will help enforce governance for centralizing Amazon VPC Flow Logs in an Amazon Simple Storage Service (S3) bucket in the log archive account for monitoring, troubleshooting, anomaly detection, and archival purposes at scale in a AWS multi-account environment. The log archive account works as a repository for logs from all accounts in your organization.

In this solution we will be using AWS Config to evaluate the configurations of VPC Flow logs and resolve it if they are missing. AWS Config is a service that enables you to assess, audit, and evaluate the configurations of your AWS resources. AWS Config continuously monitors and records your AWS resource configurations and allows you to automate the evaluation of recorded configurations against desired configurations. The benefit of using AWS Config in this solution is to provide the ability to configure resource evaluation rules to validate configuration of the resource and trigger an automated process to remediate the configuration. An AWS Systems Manager automation document is used to perform the remediation task.

This setup is deployed in new AWS Accounts with the help of AWS Control Tower life cycle events and AWS CloudFormation stack. Additionally, you could leverage this solution to enable VPC Flow logs on the existing AWS accounts by manually running the CloudFormation StackSets on the selected AWS accounts. This solution can be used with individual AWS accounts by deploying this as a CloudFormation stack.

Please note that data ingestion and archival charges for vended logs apply when you publish flow logs to CloudWatch Logs or to Amazon S3. See AWS documentation for VPC Flow logs pricing for more information and examples.

Solution Overview

When a VPC is created without enabling VPC flow logs configuration in any account, this solution will identify and mark the non-compliant VPC in AWS Config, and automatically initiate the remediation task to enable the VPC flow logs. Once the VPC flow logs are enabled, the logs will be stored in a centralized S3 bucket in the log archive account.

The following diagram illustrates the solution architecture:

Figure 1: Diagram explaining the solution architecture for VPC flow logs remediation

Workflow:

  1. When a new AWS account is created, an AWS Control tower lifecycle event will be triggered.
  2. The lifecycle event further triggers a Lambda function, which updates the CloudFormation StackSets with new account ID.
  3. The CloudFormation StackSet deploys
    • VPC_FLOW_LOGS_ENABLED managed AWS Config rule with AWS-EnableVPCFlowLogs SSM Automation document Remediation action.
    • An IAM role executes the automation on behalf of the SSM Automation document.
  4.  AWS Config rule VPC_FLOW_fLOGS_ENABLED evaluates all VPCs in a given region for flow log enabled status, and it flags the ones missing with flow log as “non-compliant” resource. The SSM automation document, AWS-EnableVPCFlowLogs, is executed when the resource becomes “non-compliant”.
  5. The RESOURCE_ID (VPC id) is passed to the SSM automation document as a parameter. The VPC flow log will be enabled on the VPC using permission defined in the IAM role.
  6. VPC Flow log is configured with “LogDestinationType” as an Amazon S3 bucket (this is configurable and can be changed to CloudWatch if required), along with a “Traffic type”.

Note: There is another approach to achieving a similar use case by using AmazonEventBridge and AWS Lambda for orchestration, which is explained in this blog.

Getting Started

Prerequisites

Here are the prerequisites before you deploy this solution:

  1. You must have AWS Control Tower setup with an AWS account for centralized logging.
  2. You must have an S3 bucket in the centralized logging account with the following bucket policy to allow VPC flow logs to be delivered to the S3 bucket. Please refer to creating S3 Bucket if this is your first time creating an S3 bucket.
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Service": "delivery.logs.amazonaws.com"
            },
            "Action": "s3:PutObject",
            "Resource": "arn:aws:s3:::<s3-bucket-name>/*",
            "Condition": {
                "StringEquals": {
                    "s3:x-amz-acl": "bucket-owner-full-control"
                }
            }
        },
        {
            "Effect": "Allow",
            "Principal": {
                "Service": "delivery.logs.amazonaws.com"
            },
            "Action": "s3:GetBucketAcl",
            "Resource": "arn:aws:s3:::<s3-bucket-name>"
        }
    ]
}

Deploying the Solution

To deploy this solution across multiple accounts and Regions, we will use AWS CloudFormation StackSets.

    1. Copy the CloudFormation template below and save in a YAML file (aws-config-remediate-vpc-flow-logs.yaml).
      AWSTemplateFormatVersion: '2010-09-09'
      Description: 'This template will deploy an AWS Config rule to automatically remediate VPC Flow Logs enablement'
      Parameters:
        CustomConfigRuleName:
          Description: Name that you want to give to the AWS Config Rule.
          Type: String
          Default: ConfigRuleForEnableVpcFlowLogs
        TrafficType:
          Type: String
          AllowedValues:
            - ACCEPT
            - REJECT
            - ALL
          Description: The value for the VPC Flow Logs traffic type.
          Default: ALL
        MaxExecutionFrequency:
          Type: String
          AllowedValues:
            - One_Hour
            - Three_Hours
            - Six_Hours
            - Twelve_Hours
            - TwentyFour_Hours
          Description: The maximum frequency with which AWS Config runs evaluations for a rule.
          Default: One_Hour
        CentralizedS3LoggingBucket:
          Description: Name of the S3 bucket in the logging account to send VPC Flow Logs.
          Type: String
      
      Resources:
      
        ConfigRemediationRole:
          Type: 'AWS::IAM::Role'
          Properties:
            AssumeRolePolicyDocument:
              Version: '2012-10-17'
              Statement:
                - Effect: 'Allow'
                  Principal:
                    Service:
                      - 'ssm.amazonaws.com'
                  Action:
                    - 'sts:AssumeRole'
            Path: '/'
            Policies:
              - PolicyName: aws-config-remediate-vpc-flow-logs-policy
                PolicyDocument:
                  Version: '2012-10-17'
                  Statement:
                    - Effect: 'Allow'
                      Action:
                        - logs:CreateLogDelivery
                        - logs:DeleteLogDelivery
                        - logs:CreateLogGroup
                      Resource: arn:aws:logs:*:*:*
                    - Effect: 'Allow'
                      Action:
                        - ec2:CreateFlowLogs
                      Resource: '*'
      
        ConfigRuleForEnableVpcFlowLogs: 
          Type: AWS::Config::ConfigRule
          Properties: 
            ConfigRuleName: !Ref CustomConfigRuleName
            Description: ConfigPermissionToInvokeAnAutomaticRemediation
            InputParameters: 
              trafficType: !Ref TrafficType
            MaximumExecutionFrequency: !Ref MaxExecutionFrequency
            Scope: 
              ComplianceResourceTypes: 
                - AWS::EC2::VPC
            Source: 
              Owner: AWS
              SourceIdentifier: VPC_FLOW_LOGS_ENABLED
      
        VpcFlowLogsRemediationConfiguration:
          DependsOn: ConfigRuleForEnableVpcFlowLogs
          Type: AWS::Config::RemediationConfiguration
          Properties:
              ConfigRuleName: !Ref CustomConfigRuleName
              Automatic: true
              MaximumAutomaticAttempts: 5 #minutes
              RetryAttemptSeconds: 50 #seconds
              ResourceType: AWS::EC2::VPC
              Parameters:
                VPCIds: 
                  ResourceValue:
                    Value: 'RESOURCE_ID'
                LogDestinationType: 
                  StaticValue:
                    Values: 
                      - s3
                LogDestinationArn: 
                  StaticValue:
                    Values: 
                      - !Sub 'arn:aws:s3:::${CentralizedS3LoggingBucket}'
                TrafficType: 
                  StaticValue:
                    Values: 
                      - !Ref TrafficType
                AutomationAssumeRole:
                  StaticValue:
                    Values: 
                      - !GetAtt ConfigRemediationRole.Arn
              TargetId: AWS-EnableVPCFlowLogs
              TargetType: SSM_DOCUMENT
              TargetVersion: 1
      
      Outputs:
        ConfigRuleForEnableVpcFlowLogsArn:
          Description: Arn of the AWS Config Rule to enable VPC Flow Logs
          Value: !GetAtt ConfigRuleForEnableVpcFlowLogs.Arn
        ConfigRemediationRoleArn:
          Description: Arn of the IAM Role to perform auto-emediation
          Value: !GetAtt ConfigRemediationRole.Arn
    2. Log in to the AWS Control Tower management account and select the AWS Region where AWS Control Tower is deployed.
    3. In the AWS CloudFormation StackSets console, select Create StackSet.
    4. Under Permissions select “Service-managed permissions”.
    5. Choose Template is ready, and Upload a template file. Select Choose file and upload the template file you created in Step 1.Figure 2 :- Configuring StackSet permission and template
    6. Provide StackSet details and Parameters as shown in the below screenshot.
      • CentralizedS3LoggingBucket – Name of the S3 Bucket dedicated to store VPC Flow logs in the log archival account
      • CustomConfigRuleName – Name of the AWS Config Rule Example :- ConfigRuleForEnableVpcFlowLogs
      • MaxExecutionFrequency – Select the frequency with which AWS Config rule evaluation should occurs. Default :- One_Hour
      • TrafficType – The type of traffic that VPC Flow logs should capture, default :- ALL

Figure 3 :- Configure StackSet name and parameters

  1. On the Accounts section you have 2 deployment options to choose as described below.

Option 1 : To deploy the StackSet in OUs, select Deploy stacks in organizational units.

Figure 4 :- Configure StackSet deployment option to selected Organization Units

Option 2 : To deploy stacks in selected AWS accounts, choose Deploy Stacks in accounts and provide comma delimited list of AWS account IDs. you want to include.

Figure 5 :- Configure StackSet deployment option to selected AWS Accounts

  1. On the Specify Regions section, select AWS regions where you want to deploy the automation.

Figure 6 :- Choose AWS regions for StackSet deployment

  1. On the Review page, validate all of the parameters and settings. Make sure you select the checkbox I acknowledge that AWS CloudFormation might create IAM resource with custom names. When you’re ready, select Submit.

Figure 7 :- Review and Launch Stackset

  1. Navigate to the StackSets on the CloudFormation console and select the StackSet that was created in Step 9. Click on the StackSet name and go to StackSet info tab and notedown  the StackSet ARN. You will need this for the next step.

Figure 8 :- Copy StackSet ARN from StackSet info page

  1. Setup the lifecycle event.
    • Copy the CloudFormation template below and save in a YAML file (ct-lifecycle-event.yaml).
      AWSTemplateFormatVersion: 2010-09-09
      Description: FlowLog - Infrastructure at the Control tower Management account for setting up Control tower life cycle event
      Parameters:
        StackSetArn:
          Type: String
          Description: ARN of the StackSet deployed from Control Tower Management account
      Resources:
        CTLifeCycleTrigger:
          Type: AWS::Lambda::Function
          Properties:
            Description: CT Lifecycle Trigger Lambda function
            Handler: lambda_handler
            Runtime: python3.8
            Role: !GetAtt 'FlowLogLifeCycleRole.Arn'
            Timeout: 600
            Environment:
              Variables:
                ct_stack_set_arn: !Ref StackSetArn
            Code:
              ZipFile: |
                import json
                import boto3
                import logging
                import os
                from botocore.exceptions import ClientError
      
                LOGGER = logging.getLogger()
                LOGGER.setLevel(logging.INFO)
                logging.getLogger('boto3').setLevel(logging.CRITICAL)
                logging.getLogger('botocore').setLevel(logging.CRITICAL)
      
                session = boto3.Session()
      
                def list_stack_instance_by_account(target_session, stack_set_name, account_id):
                    '''
                    List all stack instances based on the StackSet name and Account Id
                    '''
                    try:
                        cfn_client = target_session.client('cloudformation')
                        cfn_paginator = cfn_client.get_paginator('list_stack_instances')
                        operation_parameters = {
                            'StackSetName': stack_set_name,
                            'StackInstanceAccount': account_id
                        }
                        stackset_result = cfn_paginator.paginate(**operation_parameters)
                        stackset_list = []
                        
                        for page in stackset_result:
                            if 'Summaries' in page:
                                stackset_list.extend(page['Summaries'])
                        
                        return stackset_list
                        
                    except Exception as e:
                        LOGGER.error("List Stack Instance error: %s" % e)
                        return []
      
                def list_stack_instance_region(target_session, stack_set_name):
                    '''
                    List all stack instances based on the StackSet name
                    '''
                    try:
                        cfn_client = target_session.client('cloudformation')
                        cfn_paginator = cfn_client.get_paginator('list_stack_instances')
                        operation_parameters = {
                            'StackSetName': stack_set_name
                        }
                        stackset_result = cfn_paginator.paginate(**operation_parameters)
                        stackset_list_region = []
                        
                        for page in stackset_result:
                            for instance in page['Summaries']:
                                stackset_list_region.append(instance['Region'])
                                
                        stackset_list_region=list(set(stackset_list_region))
                    
                        return stackset_list_region
      
                    except Exception as e:
                        LOGGER.error("List Stack Instance error: %s" % e)
                        return []
      
                def create_stack_instance(target_session, stackset_name, account, regions):
                    '''
                    Create stackset in particular account + region
                    '''
                    try:
                        cfn_client = target_session.client('cloudformation')
                        response = cfn_client.create_stack_instances(
                            StackSetName=stackset_name,
                            Accounts=account,
                            Regions=regions
                            )
                        LOGGER.debug(response)
                        LOGGER.info("Launched stackset instance {} for account {} in regions: {} with Operation id: {}".format(stackset_name, account, regions, response["OperationId"]))
                        return True
                    except Exception as e:
                        LOGGER.error("Could not create stackset instance : {}".format(e))
                        return False
      
                def get_accounts_by_ou(target_session, ou_id):
                    '''
                    List all active accounts by the OU id
                    '''
                    try:
                        org_client = target_session.client('organizations')
                        org_paginator = org_client.get_paginator('list_accounts_for_parent')
                        operation_parameters = {
                            'ParentId': ou_id
                        }
                        accounts_response = org_paginator.paginate(**operation_parameters)
                        accounts_list = []
                        active_accounts_list = []
      
                        for page in accounts_response:
                            if 'Accounts' in page:
                                accounts_list.extend(page['Accounts'])
      
                        for account in accounts_list:
                            if account['Status'] == 'ACTIVE':
                                active_accounts_list.append(account['Id'])
                        
                        return active_accounts_list
                    
                    except ClientError as e:
                        LOGGER.error("Organization get accounts by OU error : {}".format(e))
                        return []
      
                def lambda_handler(event, context):
                    LOGGER.info('Lambda Handler - Start')
                    LOGGER.info('REQUEST RECEIVED: {}'.format(json.dumps(event, default=str)))
      
                    # Check if lifecycle even matches
                    if 'detail' in event and event['detail']['eventName'] == 'CreateManagedAccount':
                        if event['detail']['serviceEventDetails']['createManagedAccountStatus']['state'] == 'SUCCEEDED':
                            account_id = event['detail']['serviceEventDetails']['createManagedAccountStatus']['account']['accountId']
                            
                            #find if existing stackset instance for this account already exist            
                            stackset_name = (str(os.environ["ct_stack_set_arn"]).split(":")[5]).split("/")[1]
                            stackset_instances = list_stack_instance_by_account(session, stackset_name, account_id)
                            stackset_instances_regions = list_stack_instance_region(session, stackset_name)
                            
                            #stackset instance does not exist, create a new one
                            if len(stackset_instances) == 0:
                                create_stack_instance(session, stackset_name, [account_id], stackset_instances_regions)
                            
                            #stackset instance already exist, check for missing region
                            elif len(stackset_instances) > 0:
                                stackset_region = []
                                for instance in stackset_instances:
                                    stackset_region.append(instance['Region'])
                                next_region = list(set(stackset_instances_regions) - set(stackset_region))
                                if len(next_region) > 0:
                                    create_stack_instance(session, stackset_name, [account_id], next_region)
                                else:
                                    LOGGER.info("Stackset instance already exist : {}".format(stackset_instances))
                        else:
                            LOGGER.error("Invalid event state, expected: SUCCEEDED : {}".format(event))
                    else:
                        LOGGER.error("Invalid event received : {}".format(event))
      
                    LOGGER.info('Lambda Handler - End') 
        FlowLogLifeCycleRole:
          Type: AWS::IAM::Role
          Properties:
            Description: FlowLog - Role used by lambda for life cycle / new account creation
            AssumeRolePolicyDocument:
              Version: "2012-10-17"
              Statement:
                -
                  Effect: "Allow"
                  Principal:
                    Service:
                      - "lambda.amazonaws.com"
                  Action:
                    - "sts:AssumeRole"
            Path: "/"
            Policies:
            - PolicyName: StackSetPolicy
              PolicyDocument:
                Version: 2012-10-17
                Statement:
                - Effect: Allow
                  Action:
                    - cloudformation:ListStackInstances
                    - cloudformation:CreateStackInstances
                  Resource:
                    -  !Ref StackSetArn
                - Effect: Allow
                  Action:
                    - logs:CreateLogGroup
                    - logs:CreateLogStream
                    - logs:PutLogEvents
                  Resource:
                    -  !Sub 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/*'
      
        CreateAccountLifeCycleRule:
          Type: AWS::Events::Rule
          Properties:
            Description: FlowLog - CT Life Cycle for CreateManageAccount
            EventPattern:
              {
                "source": [
                  "aws.controltower"
                ],
                "detail-type": [
                  "AWS Service Event via CloudTrail"
                ],
                "detail": {
                  "eventSource": [
                    "controltower.amazonaws.com"
                  ],
                  "eventName": [
                    "CreateManagedAccount"
                  ]
                }
              }
            State: ENABLED
            Targets:
            - Arn: !GetAtt CTLifeCycleTrigger.Arn
              Id: "OrganizationalUnitLifeCycle"
        CreateAccountLifeCycleRulePermission:
          Type: AWS::Lambda::Permission
          Properties:
            FunctionName: !GetAtt CTLifeCycleTrigger.Arn
            Action: "lambda:InvokeFunction"
            Principal: "events.amazonaws.com"
            SourceArn: !GetAtt CreateAccountLifeCycleRule.Arn
    • In this step, we will deploy a CloudFormation stack on the Control Tower management account. This will create an IAM Role, a Lambda function, and an EventBridge rule. These are required to setup the Control Tower lifecycle event.
    • In the AWS CloudFormation console, navigate to Stacks and create new Stack.
    • Choose Template is ready, and Upload a template file. Select Choose file and upload the template file (ct-lifecycle-event.yaml) that you created in the beginning of Step 11.Figure 9 :- Create a CloudFormation Stack
    • In Specify Stack details provide a name for the stack.
    • Provide the parameter for StackSetArn copied from Step 10 and choose Next.Figure 10 :- Specify name and parameters for CloudFormation Stack
    • On the Configure Stack options page, choose Next. On the Review page, select the check box I acknowledge that AWS CloudFormation might create IAM resources with custom names, and choose Create stack.Figure 11 :- Review and launch the CloudFormation Stack

Testing the solution

To test this solution in an AWS Account that is part of your Control Tower environment, create a new VPC without enabling VPC flow logs. The Config rule VPC_FLOW_LOGS_ENABLED will evaluate the missing flow log configuration on this VPC, and it executes the SSM automation document to enable the VPC flow logs.

To validate the solution, verify the centralized S3 bucket in the log archive account for the logs from your test VPC.

Figure 12 :- Verify VPC Flow Logs in S3 bucket in the log archive account

Cleaning up

You would need to pay for the storage cost for the VPC Flow logs being stored in the centralized S3 bucket part of this testing. If you would like to keep the logs for a longer period of time, we encourage you to transition the logs to Cold Storage (S3 Glacier) using S3 Lifecycle policies.

Please perform below steps for the clean-up.

  1. To remove the VPC Flow Logs StackSets, you must first delete the stack instances from the StackSets deployed in Step 3. Follow the instructions from AWS Documentation to remove the Stack instances.
  2. Once all the stack instances are deleted, you can proceed with deleting the StackSets created in Step 3. You can refer to Delete a stack set for the instructions.
  3. To remove the Lifecycle event from the Control Tower management account, navigate to the CloudFormation console and delete the stack that you’ve deployed in Step 11 for the Lifecycle event.
  4. Delete the VPC flow logs configuration of the test VPC that was created by the auto remediation. Follow the instructions here to delete the VPC flow logs configuration.
  5. The logs in the central logging S3 bucket can be deleted if they are not needed. Follow the instructions here to delete the objects from the S3 bucket content.

Conclusion

In this post, we demonstrated how to automate VPC Flow logs setup in a multi-account environment using AWS Config Rule Remediation. CloudFormation template and a Lambda function that deploys the entire solution.

We recommend you to consider packaging this solution as part of your customizations for Control Tower.

Author:

Praveen Haranahalli

Praveen is a Sr. Solutions Architect at AWS, based in Orlando, Florida. Praveen has
helped a diverse set of customers design and operate various workloads using AWS
and has a keen interest in Security and Governance. Outside of work he loves being
outdoors with his family.

Prasad Duvvi

Prasad is a Sr. Cloud Infrastructure Architect at AWS. Prasad has over 13.5 years of
experience in architecting, design, and implementation of solutions for
customers. Prasad enjoys helping customers innovate and accelerate their
journey to the cloud. Prasad’s focus areas include Security, Management, and
Governance.