# Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. # # MIT No Attribution # Permission is hereby granted, free of charge, to any person obtaining a copy of this # software and associated documentation files (the "Software"), to deal in the Software # without restriction, including without limitation the rights to use, copy, modify, # merge, publish, distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, # INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A # PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. AWSTemplateFormatVersion: "2010-09-09" Description: "Example of using AWS DNS Firewall for automatic blocking of suspicious traffic detected by GuardDuty. " Parameters: AdminEmail: Type: String Default: "me@example.com" DnsFireWallBlockDomainListName: Type: String Description: "DNS Firewall Blocked Domain List Name" Default: "DemoBlockedDomainListAutoUpdated" DnsFirewallBlockAction: Type: String AllowedValues: - "NODATA" - "NXDOMAIN" Default: "NODATA" DnsFireWallAlertDomainListName: Type: String Description: "DNS Firewall Alert Domain List Name" Default: "DemoAlertDomainListAutoUpdated" Resources: ################################################################################################ # Demo VPCs Setup # ################################################################################################ # Creation of a pair of dummy test VPCs to be associated with our DNS firewall Rule Group DemoVPCOne: Type: AWS::EC2::VPC Properties: CidrBlock: "10.0.0.0/16" EnableDnsHostnames: true EnableDnsSupport: true Tags: - Key: "Name" Value: "demoDnsFirewallVpc" InternetGatewayVpcOne: Type: AWS::EC2::InternetGateway InternetGatewayAttachmentVpcOne: Type: AWS::EC2::VPCGatewayAttachment Properties: InternetGatewayId: !Ref InternetGatewayVpcOne VpcId: !Ref DemoVPCOne PublicSubnetVpcOne: Type: AWS::EC2::Subnet Properties: CidrBlock: "10.0.0.0/24" VpcId: !Ref DemoVPCOne AvailabilityZone: Fn::Select: - 0 - Fn::GetAZs: "" PublicRouteTableVpcOne: Type: AWS::EC2::RouteTable Properties: VpcId: !Ref DemoVPCOne PublicSubnetVpcOneRouteTableAssoc: Type: AWS::EC2::SubnetRouteTableAssociation Properties: RouteTableId: !Ref PublicRouteTableVpcOne SubnetId: !Ref PublicSubnetVpcOne PublicDefaultRouteVpcOne: Type: AWS::EC2::Route Properties: RouteTableId: Ref: PublicRouteTableVpcOne DestinationCidrBlock: 0.0.0.0/0 GatewayId: Ref: InternetGatewayVpcOne DemoVPCTwo: Type: AWS::EC2::VPC Properties: CidrBlock: "10.1.0.0/16" EnableDnsHostnames: true EnableDnsSupport: true Tags: - Key: "Name" Value: "demoDnsFirewallVpcTwo" InternetGatewayVpcTwo: Type: AWS::EC2::InternetGateway InternetGatewayAttachmentVpcTwo: Type: AWS::EC2::VPCGatewayAttachment Properties: InternetGatewayId: !Ref InternetGatewayVpcTwo VpcId: !Ref DemoVPCTwo PublicSubnetVpcTwo: Type: AWS::EC2::Subnet Properties: CidrBlock: "10.1.0.0/24" VpcId: !Ref DemoVPCTwo AvailabilityZone: Fn::Select: - 0 - Fn::GetAZs: "" PublicRouteTableVpcTwo: Type: AWS::EC2::RouteTable Properties: VpcId: !Ref DemoVPCTwo PublicSubnetVpcTwoRouteTableAssoc: Type: AWS::EC2::SubnetRouteTableAssociation Properties: RouteTableId: !Ref PublicRouteTableVpcTwo SubnetId: !Ref PublicSubnetVpcTwo PublicDefaultRouteVpcTwo: Type: AWS::EC2::Route Properties: RouteTableId: Ref: PublicRouteTableVpcTwo DestinationCidrBlock: 0.0.0.0/0 GatewayId: Ref: InternetGatewayVpcTwo ################################################################################################ # DNS Resolver Query Logs # ################################################################################################ CloudWatchDnsResolverLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: "dnsFirewallLogGroup" DnsResolverQueryLoggingConfig: Type: AWS::Route53Resolver::ResolverQueryLoggingConfig Properties: DestinationArn: !GetAtt CloudWatchDnsResolverLogGroup.Arn Name: "DnsFirewallQueryLogging" DnsResQueryLogAssocVpcOne: Type: AWS::Route53Resolver::ResolverQueryLoggingConfigAssociation Properties: ResolverQueryLogConfigId: !Ref DnsResolverQueryLoggingConfig ResourceId: !Ref DemoVPCOne DnsResQueryLogAssocVpcTwo: Type: AWS::Route53Resolver::ResolverQueryLoggingConfigAssociation Properties: ResolverQueryLogConfigId: !Ref DnsResolverQueryLoggingConfig ResourceId: !Ref DemoVPCTwo ################################################################################################ # DNS Resolver Firewall Rule Group Domain Lists and VPC association # ################################################################################################ DnsFirewallBlockDomainList: Type: AWS::Route53Resolver::FirewallDomainList Properties: Name: !Ref DnsFireWallBlockDomainListName DnsFirewallAlertDomainList: Type: AWS::Route53Resolver::FirewallDomainList Properties: Name: !Ref DnsFireWallAlertDomainListName DnsFirewallRuleGroup: Type: AWS::Route53Resolver::FirewallRuleGroup Properties: FirewallRules: - Action: BLOCK BlockResponse: !Ref DnsFirewallBlockAction FirewallDomainListId: !GetAtt DnsFirewallBlockDomainList.Id Priority: 1 - Action: ALERT FirewallDomainListId: !GetAtt DnsFirewallAlertDomainList.Id Priority: 2 Name: "demoDnsFirewallRuleGroup" DnsFirewallRuleGroupAssociation: Type: AWS::Route53Resolver::FirewallRuleGroupAssociation Properties: FirewallRuleGroupId: !Ref DnsFirewallRuleGroup Name: "demoFirewallRuleGroupAssociation" Priority: 102 VpcId: !Ref DemoVPCOne MutationProtection: DISABLED DnsFirewallRuleGroupAssociationTwo: Type: AWS::Route53Resolver::FirewallRuleGroupAssociation Properties: FirewallRuleGroupId: !Ref DnsFirewallRuleGroup Name: "demoFirewallRuleGroupAssociationVpcTwo" Priority: 102 VpcId: !Ref DemoVPCTwo MutationProtection: DISABLED ################################################################################################ # DynamoDB Table for Findings and Domain details # ################################################################################################ GuardDutytoDNSFirewallDDBTable: Type: "AWS::DynamoDB::Table" Properties: BillingMode: PAY_PER_REQUEST AttributeDefinitions: - AttributeName: "DomainName" AttributeType: "S" KeySchema: - AttributeName: "DomainName" KeyType: "HASH" # SNS topic used for sending messages to admins GuardDutyToDNSFirewallSNSTopic: Type: "AWS::SNS::Topic" Properties: Subscription: - Endpoint: !Ref AdminEmail Protocol: "email" ################################################################################################ # Lambda function code and execution roles # # ################################################################################################ CheckAndUpdateDnsFirewallDomainListFunction: Type: "AWS::Lambda::Function" Properties: Handler: "index.lambda_handler" Role: !GetAtt [ CheckAndUpdateDnsFirewallDomainListLambdaExecutionRole, Arn ] Runtime: python3.8 Environment: Variables: fwBlockedDomainListId: !Ref DnsFirewallBlockDomainList fwAlertDomainListId: !Ref DnsFirewallAlertDomainList Timeout: 60 Code: ZipFile: | #### Trick to update boto3 to the latest version which supports DNS Firewall #### Use it if you don't use a layer with an up to date boto3 library import sys from pip._internal import main main(['install', '-I', '-q', 'boto3', '--target', '/tmp/', '--no-cache-dir', '--disable-pip-version-check']) sys.path.insert(0,'/tmp/') # import json import boto3 import os,logging from botocore.exceptions import ClientError logger = logging.getLogger() logger.setLevel(logging.INFO) firewallBlockedDomainListId = os.environ['fwBlockedDomainListId'] firewallAlertDomainListId = os.environ['fwAlertDomainListId'] client = boto3.client('route53resolver') def checkifDomainInDomainList(client,event,firewallDomainListId): """ """ try: logger.info("CheckingDomain") response = client.list_firewall_domains( FirewallDomainListId= firewallDomainListId ) if (event['DomainName']+".") in response['Domains']: event['DomainInDomainList'] = True return event else: event['DomainInDomainList'] = False event['DomainListAction'] ='ADD' return event except Exception as e: raise def updateDomainList(client,event,firewallDomainListId,operation="ADD"): """ Parameters: operation (str): Update Operation valid values 'ADD','REMOVE', 'REPLACE' updatedDomainList (list(str)): A List of dns domains to use in the update operation firewallDomainListId (str): The ID of the domain list whose domains you want to update """ try: updatedDomainList = [event["DomainName"]] if len(updatedDomainList) > 0: response = client.update_firewall_domains( FirewallDomainListId = firewallDomainListId, Operation= operation, Domains= updatedDomainList ) event['DomainInDomainList'] = True return event else: logger.info("No Operation performed: empty input domain list") event['DomainInDomainList'] = False return event except Exception as e: logger.error(e) raise def lambda_handler(event, context): if event['Severity'] == 'HIGH' or event['Severity'] == 'CRITICAL' : firewallDomainListId = firewallBlockedDomainListId else: firewallDomainListId = firewallAlertDomainListId if event['DomainListAction'] == 'ADD': return updateDomainList(client,event,firewallDomainListId,"ADD") else: return checkifDomainInDomainList(client,event,firewallDomainListId) CheckAndUpdateDnsFirewallDomainListLambdaExecutionRole: Type: "AWS::IAM::Role" Properties: AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Principal: Service: lambda.amazonaws.com Action: "sts:AssumeRole" Policies: - PolicyName: UpdateDnsFirewallDomainListLambdaExecutionRolePolicy PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - logs:CreateLogGroup - logs:CreateLogStream - logs:PutLogEvents Resource: "arn:aws:logs:*:*:*" - Effect: Allow Action: - "route53resolver:GetFirewallDomainList" - "route53resolver:UpdateFirewallDomains" - "route53resolver:ListFirewallDomains" "Resource": "*" # state machine that orchestrates Lambda functions to block traffic and record CheckDomainInDynamoLambdaFunction: Type: "AWS::Lambda::Function" Properties: Handler: "index.lambda_handler" Role: !GetAtt [ UpdateDynamoWithDomainInfoLambdaExecutionRole, Arn ] Runtime: python3.8 Environment: Variables: dynamoTableName: !Ref GuardDutytoDNSFirewallDDBTable Timeout: 60 Code: ZipFile: | #==========================================================================================================# # Lambda function used to check if a Domain Name already exist in the DynamoDB # #==========================================================================================================# import boto3 import os, json, logging from botocore.exceptions import ClientError from boto3.dynamodb.conditions import Key, Attr logger = logging.getLogger() logger.setLevel(logging.INFO) #==========================================================================================================# # Variables # #==========================================================================================================# dynamoTableName= os.environ['dynamoTableName'] resource = boto3.resource('dynamodb') table = resource.Table(dynamoTableName) def getDomain(record): """ Retrieve the Domain Record in a DynamoDB If entry does not exist return False return True otherwise """ response = table.get_item( Key={ 'DomainName': record['DomainName'] } ) if "Item" in response: logger.info("log -- Domain %s already in table" %record['DomainName']) record['DomainInDynamo'] = True record['DomainListAction'] = "Check" return record else: logger.info("log -- Domain %s not in table" %record['DomainName']) record['DomainInDynamo'] = False record['DomainListAction'] = "ADD" return record def lambda_handler(event, context): logger.info("log -- Event: %s " % json.dumps(event)) return getDomain(event) UpdateDynamoWithDomainInfoLambdaFunction: Type: "AWS::Lambda::Function" Properties: Handler: "index.lambda_handler" Role: !GetAtt [ UpdateDynamoWithDomainInfoLambdaExecutionRole, Arn ] Runtime: python3.8 Environment: Variables: dynamoTableName: !Ref GuardDutytoDNSFirewallDDBTable Timeout: 60 Code: ZipFile: | import boto3 import os, json, logging from botocore.exceptions import ClientError from boto3.dynamodb.conditions import Key, Attr logger = logging.getLogger() logger.setLevel(logging.INFO) dynamoTableName= os.environ['dynamoTableName'] try: resource = boto3.resource('dynamodb') table = resource.Table(dynamoTableName) except Exception as e: logger.error('Error Connecting to DynamoDB') raise def insertDomainName(record): """ Insert or Update Record in DynamoDB """ if record['DomainInDynamo'] == True: recordlist = [record['FindingId']] response = table.update_item ( Key= { "DomainName" : record['DomainName']}, UpdateExpression="SET #f = list_append(#f,:r)", ExpressionAttributeNames={ "#f": "FindingsList", }, ExpressionAttributeValues={ ':r': recordlist } ) return response else: response = table.put_item( Item = { "DomainName" : record['DomainName'], "FindingsList" : [record['FindingId']] } ) return response def lambda_handler(event, context): logger.info("log -- Event: %s " % json.dumps(event)) return insertDomainName(event) UpdateDynamoWithDomainInfoLambdaExecutionRole: Type: "AWS::IAM::Role" Properties: AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Principal: Service: lambda.amazonaws.com Action: "sts:AssumeRole" Policies: - PolicyName: GuardDutytoFirewallRecordLambdaPolicy PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - "logs:CreateLogGroup" - "logs:CreateLogStream" - "logs:PutLogEvents" Resource: "arn:aws:logs:*:*:*" - Effect: Allow Action: - dynamodb:GetItem - dynamodb:PutItem - dynamodb:Query - dynamodb:Scan - dynamodb:DeleteItem - dynamodb:UpdateItem Resource: !GetAtt GuardDutytoDNSFirewallDDBTable.Arn ################################################################################################ # Step Function # ################################################################################################ SecurityHubtoDnsFirewallStateMachine: Type: "AWS::StepFunctions::StateMachine" Properties: RoleArn: !GetAtt 'SecurityHubtoDnsFirewallStateMachineExecutionRole.Arn' DefinitionString: !Sub | { "StartAt": "getDomainFromDynamo", "Comment": "Triggered by GuardDuty finding and Security Hub ", "States": { "getDomainFromDynamo": { "Type": "Task", "Resource": "${CheckDomainInDynamoLambdaFunction.Arn}", "Parameters": { "Comment": "Relevant fields from the GuardDuty / Security Hub finding", "DomainName.$": "$.detail.findings[0].ProductFields.aws/guardduty/service/action/dnsRequestAction/domain", "Timestamp.$": "$.detail.findings[0].ProductFields.aws/guardduty/service/eventLastSeen", "FindingId.$": "$.id", "AccountId.$": "$.detail.findings[0].AwsAccountId", "Region.$": "$.detail.findings[0].Resources[0].Region", "SourceUrl.$": "$.detail.findings[0].SourceUrl", "Types.$": "$.detail.findings[0].FindingProviderFields.Types", "Severity.$": "$.detail.findings[0].Severity.Label" }, "ResultPath": "$", "Retry": [ { "ErrorEquals": [ "States.TaskFailed" ], "IntervalSeconds": 2, "MaxAttempts": 2, "BackoffRate": 2 } ], "Catch": [ { "ErrorEquals": [ "States.ALL" ], "Next": "notifyFailure" } ], "Next": "isDomainInDynamo" }, "isDomainInDynamo": { "Type": "Choice", "Choices": [ { "Variable": "$.DomainInDynamo", "BooleanEquals": false, "Next": "addDomainToDnsFirewallDomainList" }, { "Variable": "$.DomainInDynamo", "BooleanEquals": true, "Next": "getDomainFromDomainList" } ] }, "addDomainToDnsFirewallDomainList": { "Type": "Task", "Resource": "${CheckAndUpdateDnsFirewallDomainListFunction.Arn}", "Retry": [ { "ErrorEquals": [ "States.TaskFailed" ], "IntervalSeconds": 2, "MaxAttempts": 2, "BackoffRate": 2 } ], "Catch": [ { "ErrorEquals": [ "States.ALL" ], "Next": "notifyFailure" } ], "Next": "updateDynamoDB" }, "updateDynamoDB": { "Type": "Task", "Resource": "${UpdateDynamoWithDomainInfoLambdaFunction.Arn}", "Retry": [ { "ErrorEquals": [ "States.TaskFailed" ], "IntervalSeconds": 2, "MaxAttempts": 2, "BackoffRate": 2 } ], "Catch": [ { "ErrorEquals": [ "States.ALL" ], "Next": "notifyFailure" } ], "Next": "notifySuccess" }, "getDomainFromDomainList": { "Type": "Task", "Resource": "${CheckAndUpdateDnsFirewallDomainListFunction.Arn}", "ResultPath": "$", "Retry": [ { "ErrorEquals": [ "States.TaskFailed" ], "IntervalSeconds": 2, "MaxAttempts": 2, "BackoffRate": 2 } ], "Catch": [ { "ErrorEquals": [ "States.ALL" ], "Next": "notifyFailure" } ], "Next": "isDomainInDomainList" }, "isDomainInDomainList": { "Type": "Choice", "Choices": [ { "Variable": "$.DomainInDomainList", "BooleanEquals": false, "Next": "addDomainToDnsFirewallDomainList" }, { "Variable": "$.DomainInDomainList", "BooleanEquals": true, "Next": "updateDynamoDB" } ] }, "notifySuccess": { "Type": "Task", "Resource": "arn:aws:states:::sns:publish", "Parameters": { "Message": { "Blocked": "true", "Input.$": "$" }, "TopicArn": "${GuardDutyToDNSFirewallSNSTopic}" }, "End": true }, "notifyFailure": { "Type": "Task", "Resource": "arn:aws:states:::sns:publish", "Parameters": { "Message": { "Blocked": "false", "Input.$": "$" }, "TopicArn": "${GuardDutyToDNSFirewallSNSTopic}" }, "End": true } } } SecurityHubtoDnsFirewallStateMachineExecutionRole: Type: "AWS::IAM::Role" Properties: AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Principal: Service: - !Sub states.${AWS::Region}.amazonaws.com Action: "sts:AssumeRole" Path: "/" Policies: - PolicyName: GuardDutytoFirewallStateMachineExecutionPolicy PolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Action: - lambda:InvokeFunction Resource: - !GetAtt CheckDomainInDynamoLambdaFunction.Arn - !GetAtt CheckAndUpdateDnsFirewallDomainListFunction.Arn - !GetAtt UpdateDynamoWithDomainInfoLambdaFunction.Arn - Effect: Allow Action: - sns:Publish Resource: !Ref GuardDutyToDNSFirewallSNSTopic - Effect: Allow Action: - logs:CreateLogGroup - logs:CreateLogStream - logs:PutLogEvents Resource: "arn:aws:logs:*:*:*" ################################################################################################ # EventBridge Event Rule # ################################################################################################ # EventBridge Event Rule - For Security Hub event published to EventBridge: SecurityHubtoFirewallStateMachineEvent: Type: "AWS::Events::Rule" Properties: Description: "Security Hub - GuardDuty findings with DNS Domain" EventPattern: source: - aws.securityhub detail: findings: ProductFields: aws/guardduty/service/action/dnsRequestAction/blocked: - "exists": true State: "ENABLED" Targets: - Arn: !GetAtt SecurityHubtoDnsFirewallStateMachine.Arn RoleArn: !GetAtt SecurityHubtoFirewallStateMachineEventRole.Arn Id: "GuardDutyEvent-StepFunctions-Trigger" # Permissions for EventBridge to invoke the state machine SecurityHubtoFirewallStateMachineEventRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Statement: - Effect: Allow Principal: Service: events.amazonaws.com Action: sts:AssumeRole Policies: - PolicyName: GuardDutytoFirewallStateMachineStartExecution PolicyDocument: Statement: - Effect: Allow Action: - states:StartExecution Resource: - !GetAtt SecurityHubtoDnsFirewallStateMachine.Arn