AWSTemplateFormatVersion: "2010-09-09" Description: Inspection VPC and AWS Network Firewall Parameters: CoreNetworkId: Type: String Default: "" CoreNetworkArn: Type: String Default: "" CreateCWANAttachment: Type: String Description: Create the Cloud WAN VPC attachments Default: "false" AllowedValues: [true, false] CreateVPCRoutes: Type: String Description: Create VPC routes to Core Network attachments Default: "false" AllowedValues: [true, false] RouteCidr: Description: CIDR range for Inspection VPC RTs to point to Cloud WAN attachment Type: String Default: "10.0.0.0/8" AllowedPattern: ^([0-9]{1,3}\.){3}[0-9]{1,3}/[0-9]{1,2}$ Conditions: CWANValuesAdded: !And - !Not [!Equals [!Ref CoreNetworkId, ""]] - !Not [!Equals [!Ref CoreNetworkArn, ""]] CreateAttachment: !And - !Equals [!Ref CreateCWANAttachment, 'true'] - !Condition CWANValuesAdded CreateRoutes: !And - !Condition CWANValuesAdded - !Equals [!Ref CreateCWANAttachment, 'true'] - !Equals [!Ref CreateVPCRoutes, 'true'] Resources: # ---------- INSPECTION VPC ---------- # VPC resource VPCInspection: Metadata: cfn_nag: rules_to_suppress: - id: W60 reason: VPC Flow Logs not required for workshop Type: AWS::EC2::VPC Properties: CidrBlock: "100.64.0.0/16" EnableDnsSupport: true EnableDnsHostnames: true Tags: - Key: Name Value: !Join - "-" - - "inspection" - !Ref AWS::Region - "vpc" # Subnets VPCInspectionSubnetFirewall1: Type: AWS::EC2::Subnet Properties: VpcId: !Ref VPCInspection CidrBlock: !Select [0, !Cidr [!GetAtt VPCInspection.CidrBlock, 6, 8]] AvailabilityZone: !Select - 0 - !GetAZs Ref: "AWS::Region" MapPublicIpOnLaunch: false Tags: - Key: Name Value: !Join - "-" - - "inspection-" - !Ref AWS::Region - "firewall-subnet-1" VPCInspectionSubnetFirewall2: Type: AWS::EC2::Subnet Properties: VpcId: !Ref VPCInspection CidrBlock: !Select [1, !Cidr [!GetAtt VPCInspection.CidrBlock, 6, 8]] AvailabilityZone: !Select - 1 - !GetAZs Ref: "AWS::Region" MapPublicIpOnLaunch: false Tags: - Key: Name Value: !Join - "-" - - "inspection-" - !Ref AWS::Region - "firewall-subnet-2" VPCInspectionSubnetPublic1: Type: AWS::EC2::Subnet Properties: VpcId: !Ref VPCInspection CidrBlock: !Select [2, !Cidr [!GetAtt VPCInspection.CidrBlock, 6, 8]] AvailabilityZone: !Select - 0 - !GetAZs Ref: "AWS::Region" MapPublicIpOnLaunch: true Tags: - Key: Name Value: !Join - "-" - - "inspection-" - !Ref AWS::Region - "public-subnet-1" VPCInspectionSubnetPublic2: Type: AWS::EC2::Subnet Properties: VpcId: !Ref VPCInspection CidrBlock: !Select [3, !Cidr [!GetAtt VPCInspection.CidrBlock, 6, 8]] AvailabilityZone: !Select - 1 - !GetAZs Ref: "AWS::Region" MapPublicIpOnLaunch: true Tags: - Key: Name Value: !Join - "-" - - "inspection-" - !Ref AWS::Region - "public-subnet-2" VPCInspectionSubnetCWAN1: Type: AWS::EC2::Subnet Properties: VpcId: !Ref VPCInspection CidrBlock: !Select [4, !Cidr [!GetAtt VPCInspection.CidrBlock, 6, 8]] AvailabilityZone: !Select - 0 - !GetAZs Ref: "AWS::Region" MapPublicIpOnLaunch: false Tags: - Key: Name Value: !Join - "-" - - "inspection-" - !Ref AWS::Region - "cwan-subnet-1" VPCInspectionSubnetCWAN2: Type: AWS::EC2::Subnet Properties: VpcId: !Ref VPCInspection CidrBlock: !Select [5, !Cidr [!GetAtt VPCInspection.CidrBlock, 6, 8]] AvailabilityZone: !Select - 1 - !GetAZs Ref: "AWS::Region" MapPublicIpOnLaunch: false Tags: - Key: Name Value: !Join - "-" - - "inspection-" - !Ref AWS::Region - "cwan-subnet-2" # IGW and NATGWs IGW: Type: AWS::EC2::InternetGateway Properties: Tags: - Key: Name Value: Inspection-IGW IGWAttachment: Type: AWS::EC2::VPCGatewayAttachment Properties: InternetGatewayId: !Ref IGW VpcId: !Ref VPCInspection NATGW1: Type: AWS::EC2::NatGateway Properties: AllocationId: Fn::GetAtt: - EIP1 - AllocationId SubnetId: Ref: VPCInspectionSubnetPublic1 Tags: - Key: Name Value: Inspection-NATGW1 EIP1: DependsOn: IGWAttachment Type: AWS::EC2::EIP Properties: Domain: vpc NATGW2: Type: AWS::EC2::NatGateway Properties: AllocationId: Fn::GetAtt: - EIP2 - AllocationId SubnetId: Ref: VPCInspectionSubnetPublic2 Tags: - Key: Name Value: Inspection-NATGW2 EIP2: DependsOn: IGWAttachment Type: AWS::EC2::EIP Properties: Domain: vpc # Route Tables VPCInspectionRouteTableFirewall1: Type: AWS::EC2::RouteTable Properties: VpcId: !Ref VPCInspection Tags: - Key: Name Value: !Join - "-" - - "inspection" - !Ref AWS::Region - "firewall-rt-1" VPCInspectionFirewallSubnet1RouteTableAssociation: Type: AWS::EC2::SubnetRouteTableAssociation Properties: RouteTableId: !Ref VPCInspectionRouteTableFirewall1 SubnetId: !Ref VPCInspectionSubnetFirewall1 VPCInspectionRouteTableFirewall2: Type: AWS::EC2::RouteTable Properties: VpcId: !Ref VPCInspection Tags: - Key: Name Value: !Join - "-" - - "inspection" - !Ref AWS::Region - "firewall-rt-2" VPCInspectionFirewallSubnet2RouteTableAssociation: Type: AWS::EC2::SubnetRouteTableAssociation Properties: RouteTableId: !Ref VPCInspectionRouteTableFirewall2 SubnetId: !Ref VPCInspectionSubnetFirewall2 VPCInspectionRouteTablePublic1: Type: AWS::EC2::RouteTable Properties: VpcId: !Ref VPCInspection Tags: - Key: Name Value: !Join - "-" - - "inspection" - !Ref AWS::Region - "public-rt-1" VPCInspectionPublicSubnet1RouteTableAssociation: Type: AWS::EC2::SubnetRouteTableAssociation Properties: RouteTableId: !Ref VPCInspectionRouteTablePublic1 SubnetId: !Ref VPCInspectionSubnetPublic1 VPCInspectionRouteTablePublic2: Type: AWS::EC2::RouteTable Properties: VpcId: !Ref VPCInspection Tags: - Key: Name Value: !Join - "-" - - "inspection" - !Ref AWS::Region - "public-rt-2" VPCInspectionPublicSubnet2RouteTableAssociation: Type: AWS::EC2::SubnetRouteTableAssociation Properties: RouteTableId: !Ref VPCInspectionRouteTablePublic2 SubnetId: !Ref VPCInspectionSubnetPublic2 VPCInspectionRouteTableCWAN1: Type: AWS::EC2::RouteTable Properties: VpcId: !Ref VPCInspection Tags: - Key: Name Value: !Join - "-" - - "inspection" - !Ref AWS::Region - "cwan-rt-1" VPCInspectionCWANSubnet1RouteTableAssociation: Type: AWS::EC2::SubnetRouteTableAssociation Properties: RouteTableId: !Ref VPCInspectionRouteTableCWAN1 SubnetId: !Ref VPCInspectionSubnetCWAN1 VPCInspectionRouteTableCWAN2: Type: AWS::EC2::RouteTable Properties: VpcId: !Ref VPCInspection Tags: - Key: Name Value: !Join - "-" - - "inspection" - !Ref AWS::Region - "cwan-rt-1" VPCInspectionCWANSubnet2RouteTableAssociation: Type: AWS::EC2::SubnetRouteTableAssociation Properties: RouteTableId: !Ref VPCInspectionRouteTableCWAN2 SubnetId: !Ref VPCInspectionSubnetCWAN2 # Route: Public Subnets to Internet Gateway PublicToIGW1: Type: AWS::EC2::Route DependsOn: IGWAttachment Properties: RouteTableId: !Ref VPCInspectionRouteTablePublic1 DestinationCidrBlock: 0.0.0.0/0 GatewayId: !Ref IGW PublicToIGW2: Type: AWS::EC2::Route DependsOn: IGWAttachment Properties: RouteTableId: !Ref VPCInspectionRouteTablePublic2 DestinationCidrBlock: 0.0.0.0/0 GatewayId: !Ref IGW # Route: Firewall subnets to Internet via NATGW FirewallToNATGW1: Type: AWS::EC2::Route Properties: RouteTableId: !Ref VPCInspectionRouteTableFirewall1 DestinationCidrBlock: 0.0.0.0/0 NatGatewayId: !Ref NATGW1 FirewallToNATGW2: Type: AWS::EC2::Route Properties: RouteTableId: !Ref VPCInspectionRouteTableFirewall2 DestinationCidrBlock: 0.0.0.0/0 NatGatewayId: !Ref NATGW2 # ---------- AWS NETWORK FIREWALL ---------- # Network Firewall resource NetworkFirewall: Type: AWS::NetworkFirewall::Firewall Properties: FirewallName: !Join - "-" - - "network-firewall" - !Ref AWS::Region FirewallPolicyArn: !GetAtt NetworkFirewallRulesPolicy.FirewallPolicyArn SubnetMappings: - SubnetId: !Ref VPCInspectionSubnetFirewall1 - SubnetId: !Ref VPCInspectionSubnetFirewall2 VpcId: !Ref VPCInspection # Firewall Policy NetworkFirewallRulesPolicy: Type: AWS::NetworkFirewall::FirewallPolicy Properties: FirewallPolicy: StatelessDefaultActions: - "aws:forward_to_sfe" StatelessFragmentDefaultActions: - "aws:forward_to_sfe" StatefulRuleGroupReferences: - ResourceArn: !GetAtt NetworkFirewallStatefulICMPAlert.RuleGroupArn FirewallPolicyName: !Join - "-" - - "firewall-policy" - !Ref AWS::Region # Stateful Rule Group - ICMP Alert NetworkFirewallStatefulICMPAlert: Type: AWS::NetworkFirewall::RuleGroup Properties: Capacity: 100 RuleGroupName: ICMPAlert Type: STATEFUL Description: Alerting ICMP traffic RuleGroup: RulesSource: StatefulRules: - Action: ALERT RuleOptions: - Keyword: "sid:1" Header: Protocol: ICMP Source: ANY Destination: ANY Direction: ANY DestinationPort: ANY SourcePort: ANY # Stateful Rule Group (PLACEHOLDER) # Logging configuration NetworkFirewallLoggingConfiguration: Type: AWS::NetworkFirewall::LoggingConfiguration Properties: FirewallArn: !Ref NetworkFirewall LoggingConfiguration: LogDestinationConfigs: - LogType: FLOW LogDestinationType: CloudWatchLogs LogDestination: logGroup: !Ref FWFlowLogGroup - LogType: ALERT LogDestinationType: CloudWatchLogs LogDestination: logGroup: !Ref FWAlertLogGroup FWAlertLogGroup: Metadata: cfn_nag: rules_to_suppress: - id: W84 reason: Encryption not required for this log group Type: AWS::Logs::LogGroup Properties: LogGroupName: NetworkFirewallAlertLogs RetentionInDays: 7 UpdateReplacePolicy: Delete DeletionPolicy: Delete FWFlowLogGroup: Metadata: cfn_nag: rules_to_suppress: - id: W84 reason: Encryption not required for this log group Type: AWS::Logs::LogGroup Properties: LogGroupName: NetworkFirewallFlowLogs RetentionInDays: 7 UpdateReplacePolicy: Delete DeletionPolicy: Delete # ---------- CLOUD WAN ATTACHMENT ---------- VPCInspectionCWANAttachment: Condition: CreateAttachment Type: AWS::NetworkManager::VpcAttachment Properties: CoreNetworkId: !Ref CoreNetworkId SubnetArns: - Fn::Join: - "" - - "arn:aws:ec2:" - Ref: AWS::Region - ":" - Ref: AWS::AccountId - ":subnet/" - Ref: VPCInspectionSubnetCWAN1 - Fn::Join: - "" - - "arn:aws:ec2:" - Ref: AWS::Region - ":" - Ref: AWS::AccountId - ":subnet/" - Ref: VPCInspectionSubnetCWAN2 Tags: - Key: sharedservices Value: true VpcArn: Fn::Join: - "" - - "arn:" - Ref: AWS::Partition - ":ec2:" - Ref: AWS::Region - ":" - Ref: AWS::AccountId - ":vpc/" - Ref: VPCInspection # ---------- VPC ROUTES TO CLOUD WAN AND NETWORK FIREWALL (CUSTOM RESOURCES) ---------- # Routes from Firewall subnets to Cloud WAN attachment RouteFirewallToCWAN1: Condition: CreateRoutes Type: Custom::RouteTableEntry DependsOn: - VPCInspectionCWANAttachment - FunctionLogGroup Properties: ServiceToken: !GetAtt CloudWanRouteFunction.Arn Cidr: !Ref RouteCidr RouteTableId: !Ref VPCInspectionRouteTableFirewall1 CoreNetworkArn: !Ref CoreNetworkArn RouteFirewallToCWAN2: Condition: CreateRoutes Type: Custom::RouteTableEntry DependsOn: - VPCInspectionCWANAttachment - FunctionLogGroup Properties: ServiceToken: !GetAtt CloudWanRouteFunction.Arn Cidr: !Ref RouteCidr RouteTableId: !Ref VPCInspectionRouteTableFirewall2 CoreNetworkArn: !Ref CoreNetworkArn # Routes from Public subnets to network via the Firewall endpoints RoutePublicToFirewall1: Type: Custom::RouteTableEntry DependsOn: - FunctionLogGroup Properties: ServiceToken: !GetAtt FirewallRouteFunction.Arn FirewallArn: !GetAtt NetworkFirewall.FirewallArn SubnetAz: !GetAtt VPCInspectionSubnetFirewall1.AvailabilityZone DestinationCidr: !Ref RouteCidr RouteTableId: !Ref VPCInspectionRouteTablePublic1 RoutePublicToFirewall2: Type: Custom::RouteTableEntry DependsOn: - FunctionLogGroup Properties: ServiceToken: !GetAtt FirewallRouteFunction.Arn FirewallArn: !GetAtt NetworkFirewall.FirewallArn SubnetAz: !GetAtt VPCInspectionSubnetFirewall2.AvailabilityZone DestinationCidr: !Ref RouteCidr RouteTableId: !Ref VPCInspectionRouteTablePublic2 # Routes from CWAN subnets to Internet via the Firewall endpoints RouteCWANToFirewall1: Type: Custom::RouteTableEntry DependsOn: - FunctionLogGroup Properties: ServiceToken: !GetAtt FirewallRouteFunction.Arn FirewallArn: !GetAtt NetworkFirewall.FirewallArn SubnetAz: !GetAtt VPCInspectionSubnetFirewall1.AvailabilityZone DestinationCidr: "0.0.0.0/0" RouteTableId: !Ref VPCInspectionRouteTableCWAN1 RouteCWANToFirewall2: Type: Custom::RouteTableEntry DependsOn: - FunctionLogGroup Properties: ServiceToken: !GetAtt FirewallRouteFunction.Arn FirewallArn: !GetAtt NetworkFirewall.FirewallArn SubnetAz: !GetAtt VPCInspectionSubnetFirewall2.AvailabilityZone DestinationCidr: "0.0.0.0/0" RouteTableId: !Ref VPCInspectionRouteTableCWAN2 # Lambda functions to create routes to Cloud WAN and Firewall endpoints RouteFunctionRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole Policies: - PolicyName: AllowLambdaVPC PolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Action: - ec2:CreateRoute - ec2:UpdateRoute - ec2:DeleteRoute Resource: - !Sub arn:aws:ec2:*:*:route-table/* - Effect: Allow Action: - network-firewall:DescribeFirewall Resource: - !GetAtt NetworkFirewall.FirewallArn ManagedPolicyArns: - !Sub arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole FunctionLogGroup: Metadata: cfn_nag: rules_to_suppress: - id: W84 reason: Encryption not required for this log group Type: AWS::Logs::LogGroup Properties: LogGroupName: '/aws/lambda/VPCRouteFunctions' RetentionInDays: 7 CloudWanRouteFunction: Condition: CreateRoutes Metadata: cfn_nag: rules_to_suppress: - id: W58 reason: CWL permissions granted by use of AWSLambdaBasicExecutionRole - id: W89 reason: No requirement for this function to be in a VPC - id: W92 reason: No requirement to limit simultaneous executions Type: AWS::Lambda::Function Properties: Description: Manage route table entry for CoreNetwork Runtime: python3.9 Timeout: 10 Role: !GetAtt RouteFunctionRole.Arn Handler: index.lambda_handler Code: ZipFile: |- import logging import boto3 import json import cfnresponse from botocore.exceptions import ClientError log = logging.getLogger("handler") log.setLevel(logging.INFO) def lambda_handler(event, context): try: log.info("Received event: %s", json.dumps(event)) action = event["RequestType"] cidr = event["ResourceProperties"]["Cidr"] table_id = event["ResourceProperties"]["RouteTableId"] core_arn = event["ResourceProperties"]["CoreNetworkArn"] ec2 = boto3.client("ec2") response = {} if action == "Create": log.info( "Creating route to %s with arn %s for table %s", cidr, core_arn, table_id, ) response = ec2.create_route( DestinationCidrBlock=cidr, RouteTableId=table_id, CoreNetworkArn=core_arn, ) print(response) if action == "Delete": log.info("Deleting route to %s in table %s", cidr, table_id) try: response = ec2.delete_route( DestinationCidrBlock=cidr, RouteTableId=table_id ) print(response) except ClientError as error: if error.response["Error"]["Code"] == "InvalidRoute.NotFound": response = {"Return": True} else: raise error if action == "Update": old_cidr = event["OldResourceProperties"]["Cidr"] old_table_id = event["OldResourceProperties"]["RouteTableId"] if old_cidr == cidr and old_table_id == table_id: log.info( "Updating route table %s entry for %s to %s", table_id, cidr, core_arn, ) response = ec2.replace_route( DestinationCidrBlock=cidr, RouteTableId=table_id, CoreNetworkArn=core_arn, ) print(response) response["Return"] = True else: log.info( "Replacing route with interruption due to change in cidr and/or table id" ) try: response = ec2.delete_route( DestinationCidrBlock=old_cidr, RouteTableId=old_table_id ) except ClientError as error: if error.response["Error"]["Code"] != "InvalidRoute.NotFound": raise error log.info( "Creating replacement route %s to %s in table %s", cidr, core_arn, table_id, ) response = ec2.create_route( DestinationCidrBlock=cidr, RouteTableId=table_id, CoreNetworkArn=core_arn, ) if "Return" in response: if response["Return"]: cfnresponse.send(event, context, cfnresponse.SUCCESS, {}) return cfnresponse.send( event, context, cfnresponse.FAILED, {}, reason="API request failed" ) return cfnresponse.send(event, context, cfnresponse.SUCCESS, {}) except: log.exception("whoops") cfnresponse.send( event, context, cfnresponse.FAILED, {}, reason="Caught exception, check logs", ) FirewallRouteFunction: Metadata: cfn_nag: rules_to_suppress: - id: W58 reason: CWL permissions granted by use of AWSLambdaBasicExecutionRole - id: W89 reason: No requirement for this function to be in a VPC - id: W92 reason: No requirement to limit simultaneous executions Type: AWS::Lambda::Function Properties: Description: Manage route table entry for CoreNetwork Runtime: python3.9 Timeout: 10 Role: !GetAtt RouteFunctionRole.Arn Handler: index.on_event Code: ZipFile: |- import logging from typing import Dict import boto3 import cfnresponse logger = logging.getLogger(__name__) nfw = boto3.client("network-firewall") ec2 = boto3.client("ec2") response = {} def get_data(firewall_arn: str) -> Dict[str, str]: response = nfw.describe_firewall(FirewallArn=firewall_arn) return { k: v["Attachment"]["EndpointId"] for k, v in response["FirewallStatus"]["SyncStates"].items() } def create(event): logical_id: str = event["LogicalResourceId"] request_id: str = event["RequestId"] physical_resource_id = f'{logical_id}-{request_id.replace("-", "")}.txt' firewall_arn = event["ResourceProperties"]["FirewallArn"] subnet_az = event["ResourceProperties"]["SubnetAz"] destination_cidr = event["ResourceProperties"]["DestinationCidr"] route_table_id = event["ResourceProperties"]["RouteTableId"] endpoints = get_data(firewall_arn) ec2.create_route( DestinationCidrBlock=destination_cidr, RouteTableId=route_table_id, VpcEndpointId=endpoints[subnet_az], ) return physical_resource_id def update(event): logical_id: str = event["LogicalResourceId"] request_id: str = event["RequestId"] physical_resource_id = f'{logical_id}-{request_id.replace("-", "")}.txt' return physical_resource_id def delete(event): route_table_id = event["ResourceProperties"]["RouteTableId"] destination_cidr = event["ResourceProperties"]["DestinationCidr"] ec2.delete_route(DestinationCidrBlock=destination_cidr, RouteTableId=route_table_id) def on_event(event, context): if event["RequestType"] == "Create": physical_resource_id = create(event) cfnresponse.send(event,context,cfnresponse.SUCCESS, {}) return elif event["RequestType"] == "Update": update(event), cfnresponse.send(event,context, cfnresponse.SUCCESS, {}) return elif event["RequestType"] == "Delete": delete(event) cfnresponse.send(event, context, cfnresponse.SUCCESS, {}) return