AWSTemplateFormatVersion: 2010-09-09
Description: AWS Network Firewall Demo using distributed model.
Metadata:
  'AWS::CloudFormation::Interface':
    ParameterGroups:
      - Label:
          default: VPC Parameters
        Parameters:
          - AvailabilityZone1Selection
          - AvailabilityZone2Selection
      - Label:
          default: EC2 Parameters
        Parameters:
          - LatestAmiId
      - Label:
          default: AWS Network Firewall
        Parameters:
          - NetworkFirewallAllowedWebsites
Parameters:
  AvailabilityZone1Selection:
    Description: Availability Zone 1
    Type: 'AWS::EC2::AvailabilityZone::Name'
    Default: us-east-1a
  AvailabilityZone2Selection:
    Description: Availability Zone 2
    Type: 'AWS::EC2::AvailabilityZone::Name'
    Default: us-east-1b
  LatestAmiId:
    Description: Latest EC2 AMI from Systems Manager Parameter Store
    Type: 'AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>'
    Default: /aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2
  NetworkFirewallAllowedWebsites:
    Description: List of websites to allow access from AWS NetworkFirewall
    Type: CommaDelimitedList
    Default: ".amazonaws.com,.docker.io,.docker.com"

Resources:
  SpokeVpcA:
    Type: 'AWS::EC2::VPC'
    Properties:
      CidrBlock: 10.1.0.0/16
      EnableDnsSupport: 'true'
      EnableDnsHostnames: 'true'
      InstanceTenancy: default
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-spokevpca'
  InternetGateway:
    Type: 'AWS::EC2::InternetGateway'
    Properties:
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-spokevpca-igw'
  AttachInternetGateway:
    Type: 'AWS::EC2::VPCGatewayAttachment'
    Properties:
      VpcId: !Ref SpokeVpcA
      InternetGatewayId: !Ref InternetGateway
  NatGw1Eip:
    Type: 'AWS::EC2::EIP'
    Properties:
      Domain: vpc
  NatGw2Eip:
    Type: 'AWS::EC2::EIP'
    Properties:
      Domain: vpc
  NatGw1:
    Type: 'AWS::EC2::NatGateway'
    DependsOn:
      - NatGw1Eip
      - PublicSubnet1
    Properties:
      AllocationId: !GetAtt 
        - NatGw1Eip
        - AllocationId
      SubnetId: !Ref PublicSubnet1
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-spokevpca-natgw-1'
  NatGw2:
    Type: 'AWS::EC2::NatGateway'
    DependsOn:
      - NatGw2Eip
      - PublicSubnet2
    Properties:
      AllocationId: !GetAtt 
        - NatGw2Eip
        - AllocationId
      SubnetId: !Ref PublicSubnet2
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-spokevpca-natgw-2'
  PrivateSubnet1:
    Type: 'AWS::EC2::Subnet'
    Properties:
      VpcId:
        Ref: SpokeVpcA
      CidrBlock: 10.1.0.0/24
      AvailabilityZone:
        Ref: AvailabilityZone1Selection
      MapPublicIpOnLaunch: false
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-private-subnet-1'
  PrivateSubnet2:
    Type: 'AWS::EC2::Subnet'
    Properties:
      VpcId:
        Ref: SpokeVpcA
      CidrBlock: 10.1.2.0/24
      AvailabilityZone:
        Ref: AvailabilityZone2Selection
      MapPublicIpOnLaunch: false
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-private-subnet-2'
  PublicSubnet1:
    Type: 'AWS::EC2::Subnet'
    Properties:
      VpcId:
        Ref: SpokeVpcA
      CidrBlock: 10.1.1.0/24
      AvailabilityZone:
        Ref: AvailabilityZone1Selection
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-public-subnet-1'
  PublicSubnet2:
    Type: 'AWS::EC2::Subnet'
    Properties:
      VpcId:
        Ref: SpokeVpcA
      CidrBlock: 10.1.3.0/24
      AvailabilityZone:
        Ref: AvailabilityZone2Selection
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-public-subnet-2'
  FirewallSubnet1:
    Type: 'AWS::EC2::Subnet'
    Properties:
      VpcId:
        Ref: SpokeVpcA
      CidrBlock: 10.1.16.0/28
      AvailabilityZone:
        Ref: AvailabilityZone1Selection
      MapPublicIpOnLaunch: false
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-firewall-subnet-1'
  FirewallSubnet2:
    Type: 'AWS::EC2::Subnet'
    Properties:
      VpcId:
        Ref: SpokeVpcA
      CidrBlock: 10.1.16.16/28
      AvailabilityZone:
        Ref: AvailabilityZone2Selection
      MapPublicIpOnLaunch: false
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-firewall-subnet-2'
  SpokeVpcAEndpointSecurityGroup:
    Type: 'AWS::EC2::SecurityGroup'
    Properties:
      GroupDescription: Allow instances to get to SSM Systems Manager
      VpcId: !Ref SpokeVpcA
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 443
          ToPort: 443
          CidrIp: 10.1.0.0/16
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-vpce-sg-1'
  SpokeVpcASSMEndpoint:
    Type: 'AWS::EC2::VPCEndpoint'
    Properties:
      PrivateDnsEnabled: true
      SecurityGroupIds:
        - !Ref SpokeVpcAEndpointSecurityGroup
      ServiceName: !Sub 'com.amazonaws.${AWS::Region}.ssm'
      SubnetIds:
        - !Ref PublicSubnet1
        - !Ref PublicSubnet2
      VpcEndpointType: Interface
      VpcId: !Ref SpokeVpcA
  SpokeVpcAEC2MessagesEndpoint:
    Type: 'AWS::EC2::VPCEndpoint'
    Properties:
      PrivateDnsEnabled: true
      SecurityGroupIds:
        - !Ref SpokeVpcAEndpointSecurityGroup
      ServiceName: !Sub 'com.amazonaws.${AWS::Region}.ec2messages'
      SubnetIds:
        - !Ref PublicSubnet1
        - !Ref PublicSubnet2
      VpcEndpointType: Interface
      VpcId: !Ref SpokeVpcA
  SpokeVpcASSMMessagesEndpoint:
    Type: 'AWS::EC2::VPCEndpoint'
    Properties:
      PrivateDnsEnabled: true
      SecurityGroupIds:
        - !Ref SpokeVpcAEndpointSecurityGroup
      ServiceName: !Sub 'com.amazonaws.${AWS::Region}.ssmmessages'
      SubnetIds:
        - !Ref PublicSubnet1
        - !Ref PublicSubnet2
      VpcEndpointType: Interface
      VpcId: !Ref SpokeVpcA
  SubnetRole:
    Type: 'AWS::IAM::Role'
    Properties:
      RoleName: !Sub '${AWS::StackName}-subnet-role'
      Path: /
      ManagedPolicyArns:
        - 'arn:aws:iam::aws:policy/service-role/AmazonEC2RoleforSSM'
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - ec2.amazonaws.com
            Action:
              - 'sts:AssumeRole'
  SubnetInstanceProfile:
    Type: 'AWS::IAM::InstanceProfile'
    Properties:
      Path: /
      Roles:
        - !Ref SubnetRole
  LambdaExecutionRole:
    Type: 'AWS::IAM::Role'
    Properties:
      RoleName: !Sub '${AWS::StackName}-lambda-role'
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
            Action:
              - 'sts:AssumeRole'
      Path: /
      Policies:
        - PolicyName: root
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - 'logs:CreateLogStream'
                  - 'logs:PutLogEvents'
                Resource: !GetAtt RetrieveVpcIdLogGroup.Arn
              - Effect: Allow
                Action:
                  - 'network-firewall:DescribeFirewall'
                Resource: '*'
  RetrieveVpcIdLogGroup:
    Type: 'AWS::Logs::LogGroup'
    Properties:
      LogGroupName: !Sub '/aws/lambda/${AWS::StackName}-retrieve-vpceid'
      RetentionInDays: 1
  RetrieveVpceId:
    Type: 'AWS::Lambda::Function'
    DependsOn: RetrieveVpcIdLogGroup
    Properties:
      FunctionName: !Sub '${AWS::StackName}-retrieve-vpceid'
      Handler: index.handler
      Role: !GetAtt 
        - LambdaExecutionRole
        - Arn
      Code:
        ZipFile: |
          import boto3
          import cfnresponse
          import json
          import logging

          def handler(event, context):
              logger = logging.getLogger()
              logger.setLevel(logging.INFO)
              responseData = {}
              responseStatus = cfnresponse.FAILED
              logger.info('Received event: {}'.format(json.dumps(event)))
              if event["RequestType"] == "Delete":
                  responseStatus = cfnresponse.SUCCESS
                  cfnresponse.send(event, context, responseStatus, responseData)
              if event["RequestType"] == "Create":
                  try:
                      Az1 = event["ResourceProperties"]["Az1"]
                      Az2 = event["ResourceProperties"]["Az2"]
                      FwArn = event["ResourceProperties"]["FwArn"]
                  except Exception as e:
                      logger.info('AZ retrieval failure: {}'.format(e))
                  try:
                      nfw = boto3.client('network-firewall')
                  except Exception as e:
                      logger.info('boto3.client failure: {}'.format(e))
                  try:
                      NfwResponse=nfw.describe_firewall(FirewallArn=FwArn)
                      VpceId1 = NfwResponse['FirewallStatus']['SyncStates'][Az1]['Attachment']['EndpointId']
                      VpceId2 = NfwResponse['FirewallStatus']['SyncStates'][Az2]['Attachment']['EndpointId']

                  except Exception as e:
                      logger.info('ec2.describe_firewall failure: {}'.format(e))

                  responseData['FwVpceId1'] = VpceId1
                  responseData['FwVpceId2'] = VpceId2
                  responseStatus = cfnresponse.SUCCESS
                  cfnresponse.send(event, context, responseStatus, responseData)
      Runtime: python3.7
      Timeout: 30
  FirewallVpceIds:
    Type: 'Custom::DescribeVpcEndpoints'
    Properties:
      ServiceToken: !GetAtt RetrieveVpceId.Arn
      Az1: !Ref AvailabilityZone1Selection
      Az2: !Ref AvailabilityZone2Selection
      FwArn: !Ref SpokeVpcAFirewall
  SubnetSecurityGroup:
    Type: 'AWS::EC2::SecurityGroup'
    Properties:
      GroupDescription: ICMP acess from 10.0.0.0/8
      GroupName: !Sub '${AWS::StackName}-test-instance-sec-group'
      VpcId: !Ref SpokeVpcA
      SecurityGroupIngress:
        - IpProtocol: icmp
          CidrIp: 10.0.0.0/8
          FromPort: '-1'
          ToPort: '-1'
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-test-sg-1'
  TestInstance1:
    Type: 'AWS::EC2::Instance'
    Properties:
      ImageId: !Ref LatestAmiId
      SubnetId: !Ref PrivateSubnet1
      InstanceType: t2.micro
      SecurityGroupIds:
        - !Ref SubnetSecurityGroup
      IamInstanceProfile: !Ref SubnetInstanceProfile
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-test-instance-1'
  TestInstance2:
    Type: 'AWS::EC2::Instance'
    Properties:
      ImageId: !Ref LatestAmiId
      SubnetId: !Ref PrivateSubnet2
      InstanceType: t2.micro
      SecurityGroupIds:
        - !Ref SubnetSecurityGroup
      IamInstanceProfile: !Ref SubnetInstanceProfile
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-test-instance-2'
  SpokeVpcAFirewall:
    Type: 'AWS::NetworkFirewall::Firewall'
    Properties:
      FirewallName: !Sub '${AWS::StackName}-firewall'
      FirewallPolicyArn: !Ref EgressFirewallPolicy
      VpcId: !Ref SpokeVpcA
      SubnetMappings:
        - SubnetId: !Ref FirewallSubnet1
        - SubnetId: !Ref FirewallSubnet2
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-firewall'
  ICMPAlertStatefulRuleGroup:
    Type: 'AWS::NetworkFirewall::RuleGroup'
    Properties:
      RuleGroupName: !Sub '${AWS::StackName}-icmp-alert'
      Type: STATEFUL
      Capacity: 100
      RuleGroup:
        RulesSource:
          StatefulRules:
            - Action: ALERT
              Header:
                Direction: ANY
                Protocol: ICMP
                Destination: ANY
                Source: ANY
                DestinationPort: ANY
                SourcePort: ANY
              RuleOptions:
                - Keyword: 'sid:1'
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-icmp-alert'
  DomainAllowStatefulRuleGroup:
    Type: 'AWS::NetworkFirewall::RuleGroup'
    Properties:
      RuleGroupName: !Sub '${AWS::StackName}-domain-allow'
      Type: STATEFUL
      Capacity: 100
      RuleGroup:
        RuleVariables:
          IPSets:
            HOME_NET:
              Definition:
                - 10.0.0.0/8
        RulesSource:
          RulesSourceList:
            TargetTypes:
              - HTTP_HOST
              - TLS_SNI
            Targets: !Ref NetworkFirewallAllowedWebsites
            #   - .amazon.com
            GeneratedRulesType: ALLOWLIST
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-domain-allow'
  EgressFirewallPolicy:
    Type: 'AWS::NetworkFirewall::FirewallPolicy'
    Properties:
      FirewallPolicyName: !Sub '${AWS::StackName}-firewall-policy'
      FirewallPolicy:
        StatelessDefaultActions:
          - 'aws:forward_to_sfe'
        StatelessFragmentDefaultActions:
          - 'aws:forward_to_sfe'
        StatefulRuleGroupReferences:
          - ResourceArn: !Ref DomainAllowStatefulRuleGroup
          - ResourceArn: !Ref ICMPAlertStatefulRuleGroup
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-firewall-policy'
  SpokeVpcAFirewallLogFlowGroup:
    Type: 'AWS::Logs::LogGroup'
    Properties:
      LogGroupName: !Sub '/${AWS::StackName}/anfw/flow'
  SpokeVpcAFirewallLogAlertGroup:
    Type: 'AWS::Logs::LogGroup'
    Properties:
      LogGroupName: !Sub '/${AWS::StackName}/anfw/alert'
  SpokeVpcAFirewallLog:
    Type: 'AWS::NetworkFirewall::LoggingConfiguration'
    Properties:
      FirewallArn: !Ref SpokeVpcAFirewall
      LoggingConfiguration:
        LogDestinationConfigs:
          - LogType: FLOW
            LogDestinationType: CloudWatchLogs
            LogDestination:
              logGroup: !Sub '/${AWS::StackName}/anfw/flow'
          - LogType: ALERT
            LogDestinationType: CloudWatchLogs
            LogDestination:
              logGroup: !Sub '/${AWS::StackName}/anfw/alert'
  PrivateRtb1:
    Type: 'AWS::EC2::RouteTable'
    Properties:
      VpcId: !Ref SpokeVpcA
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-private-route-table-1'
  PrivateRtb1Association:
    Type: 'AWS::EC2::SubnetRouteTableAssociation'
    DependsOn: PrivateSubnet1
    Properties:
      RouteTableId: !Ref PrivateRtb1
      SubnetId: !Ref PrivateSubnet1
  PrivateRtb1DefaultRoute:
    Type: 'AWS::EC2::Route'
    DependsOn: NatGw1
    Properties:
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref NatGw1
      RouteTableId: !Ref PrivateRtb1
  PrivateRtb2:
    Type: 'AWS::EC2::RouteTable'
    Properties:
      VpcId: !Ref SpokeVpcA
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-private-route-table-2'
  PrivateRtb2Association:
    Type: 'AWS::EC2::SubnetRouteTableAssociation'
    DependsOn: PrivateSubnet2
    Properties:
      RouteTableId: !Ref PrivateRtb2
      SubnetId: !Ref PrivateSubnet2
  PrivateRtb2DefaultRoute:
    Type: 'AWS::EC2::Route'
    DependsOn: NatGw2
    Properties:
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref NatGw2
      RouteTableId: !Ref PrivateRtb2
  PublicRtb1:
    Type: 'AWS::EC2::RouteTable'
    Properties:
      VpcId: !Ref SpokeVpcA
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-public-route-table-1'
  PublicRtb1Association:
    Type: 'AWS::EC2::SubnetRouteTableAssociation'
    DependsOn: PublicSubnet1
    Properties:
      RouteTableId: !Ref PublicRtb1
      SubnetId: !Ref PublicSubnet1
  PublicRtb1DefaultRoute:
    Type: 'AWS::EC2::Route'
    DependsOn: SpokeVpcAFirewall
    Properties:
      DestinationCidrBlock: 0.0.0.0/0
      VpcEndpointId: !GetAtt FirewallVpceIds.FwVpceId1
      RouteTableId: !Ref PublicRtb1
  PublicRtb2:
    Type: 'AWS::EC2::RouteTable'
    Properties:
      VpcId: !Ref SpokeVpcA
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-public-route-table-2'
  PublicRtb2Association:
    Type: 'AWS::EC2::SubnetRouteTableAssociation'
    DependsOn: PublicSubnet2
    Properties:
      RouteTableId: !Ref PublicRtb2
      SubnetId: !Ref PublicSubnet2
  PublicRtb2DefaultRoute:
    Type: 'AWS::EC2::Route'
    DependsOn: SpokeVpcAFirewall
    Properties:
      DestinationCidrBlock: 0.0.0.0/0
      VpcEndpointId: !GetAtt FirewallVpceIds.FwVpceId2
      RouteTableId: !Ref PublicRtb2
  FirewallRtb1:
    Type: 'AWS::EC2::RouteTable'
    Properties:
      VpcId: !Ref SpokeVpcA
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-firewall-route-table-1'
  FirewallRtb1Association:
    Type: 'AWS::EC2::SubnetRouteTableAssociation'
    DependsOn: FirewallSubnet1
    Properties:
      RouteTableId: !Ref FirewallRtb1
      SubnetId: !Ref FirewallSubnet1
  FirewallRtb1DefaultRoute:
    Type: 'AWS::EC2::Route'
    DependsOn: InternetGateway
    Properties:
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway
      RouteTableId: !Ref FirewallRtb1
  FirewallRtb2:
    Type: 'AWS::EC2::RouteTable'
    Properties:
      VpcId: !Ref SpokeVpcA
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-firewall-route-table-2'
  FirewallRtb2Association:
    Type: 'AWS::EC2::SubnetRouteTableAssociation'
    DependsOn: FirewallSubnet2
    Properties:
      RouteTableId: !Ref FirewallRtb2
      SubnetId: !Ref FirewallSubnet2
  FirewallRtb2DefaultRoute:
    Type: 'AWS::EC2::Route'
    DependsOn: InternetGateway
    Properties:
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway
      RouteTableId: !Ref FirewallRtb2
  IngressRtb:
    Type: 'AWS::EC2::RouteTable'
    Properties:
      VpcId: !Ref SpokeVpcA
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-ingress-route-table'
  IngressRtbAssociation:
    Type: 'AWS::EC2::GatewayRouteTableAssociation'
    DependsOn: InternetGateway
    Properties:
      RouteTableId: !Ref IngressRtb
      GatewayId: !Ref InternetGateway
  IngressRtbPublicSubnet1Route:
    Type: 'AWS::EC2::Route'
    DependsOn: SpokeVpcAFirewall
    Properties:
      DestinationCidrBlock: 10.1.1.0/24
      VpcEndpointId: !GetAtt FirewallVpceIds.FwVpceId1
      RouteTableId: !Ref IngressRtb
  IngressRtbPublicSubnet2Route:
    Type: 'AWS::EC2::Route'
    DependsOn: SpokeVpcAFirewall
    Properties:
      DestinationCidrBlock: 10.1.3.0/24
      VpcEndpointId: !GetAtt FirewallVpceIds.FwVpceId2
      RouteTableId: !Ref IngressRtb

Outputs:
  PrivateSubnetA:
    Description: Private Subnet A
    Value: !Ref PrivateSubnet1

  PrivateSubnetB:
    Description: Private Subnet B
    Value: !Ref PrivateSubnet2

  PublicSubnetA:
    Description: Public Subnet A
    Value: !Ref FirewallSubnet1

  PublicSubnetB:
    Description: Public Subnet B
    Value: !Ref FirewallSubnet2