NEWS
執筆活動:2024.08.16

CloudFormationを用いたAWS Network Firewallのデプロイについて

初めに

S&J株式会社コアテクノロジーグループの新井です。
普段は社内AWS環境のセキュリティ向上の対応やIaC化など担当しています。
今回は当社のプロジェクトでAWS Network Firewallを利用した際のナレッジとして、インフラ構成やデプロイ方法の当社事例をご紹介させていただきます。

当社事例紹介

前提

・プライベートな EC2 インスタンスから外部ネットワーク通信を行う際にIPSを通過させる
・IPSでは許可リスト形式で *.google.comのドメインへのアクセスのみ許可する
・構築はIaCのCloudFormationで行う
・検証のためシングルAZでの構築を行う

構成図

以下のインフラ構成で作成します。PrivateサブネットのEC2インスタンスから外部ネットワーク通信を行います。
「IPアドレスとドメイン名でアウトバウント通信を制限したい要件」・「許可リスト形式でドメイン名を指定したい要件」はよくある構成ではないでしょうか。


デプロイ方法

以下CloudFormationのテンプレートです。

テンプレート
AWSTemplateFormatVersion: '2010-09-09'
Parameters :
  ProjectName :
    Type: String
    Default: network-firewall-poc
  VpcCidr:
    Type: String
    Default: 192.170.2.0/24
  PrivateSubnet1aCidr :
    Type: String
    Default: 192.170.2.0/28
  PublicSubnet1aCidr :
    Type: String
    Default: 192.170.2.32/28
  FirewallSubnet1aCidr :
    Type: String
    Default: 192.170.2.64/28
Resources :
  VPC :
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref VpcCidr
      EnableDnsHostnames: true
      EnableDnsSupport: true
      Tags:
      - Key: Name
        Value: !Sub ${ProjectName}-vpc
      - Key: Project
        Value: !Ref ProjectName
  PrivateSubnet1a :
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: ap-northeast-1a
      CidrBlock: !Ref PrivateSubnet1aCidr
      VpcId: !Ref VPC
      Tags:
      - Key: Name
        Value: !Sub ${ProjectName}-private-subnet-1a
      - Key: Project
        Value: !Ref ProjectName
  PublicSubnet1a:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: ap-northeast-1a
      CidrBlock: !Ref PublicSubnet1aCidr
      VpcId: !Ref VPC
      Tags:
      - Key: Name
        Value: !Sub ${ProjectName}-public-subnet-1a
      - Key: Project
        Value: !Ref ProjectName
  FirewallSubnet1a :
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: ap-northeast-1a
      CidrBlock: !Ref FirewallSubnet1aCidr
      VpcId: !Ref VPC
      Tags:
      - Key: Name
        Value: !Sub ${ProjectName}-firewall-subnet-1a
      - Key: Project
        Value: !Ref ProjectName
  InternetGateway :
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
      - Key: Name
        Value: !Sub ${ProjectName}-internet-gateway
      - Key: Project
        Value: !Ref ProjectName
  AttachGateway:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      InternetGatewayId: !Ref InternetGateway
      VpcId: !Ref VPC
  NatGateway1a:
    Type: AWS::EC2::NatGateway
    Properties:
      AllocationId: !GetAtt NATGatewayEIP1a.AllocationId
      ConnectivityType: public
      SubnetId: !Ref PublicSubnet1a
      Tags:
      - Key: Name
        Value: !Sub ${ProjectName}-nat-1a
      - Key: Project
        Value: !Ref ProjectName
  NATGatewayEIP1a:
    Type: "AWS::EC2::EIP"
    Properties:
      Domain: vpc
  PrivateRouteTable1a:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
      - Key: Name
        Value: !Sub ${ProjectName}-private-route-table-1a
      - Key: Project
        Value: !Ref ProjectName
  PublicRouteTable1a:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
      - Key: Name
        Value: !Sub ${ProjectName}-public-route-table-1a
      - Key: Project
        Value: !Ref ProjectName
  FirewallRouteTable1a:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
      - Key: Name
        Value: !Sub ${ProjectName}-firewall-route-table-1a
      - Key: Project
        Value: !Ref ProjectName
  PublicRoute1aIGW:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PublicRouteTable1a
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway
  FirewallRouteNat1a:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref FirewallRouteTable1a
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref NatGateway1a
  RouteTableAssocPublic1a:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnet1a
      RouteTableId: !Ref PublicRouteTable1a
  RouteTableAssocPrivate1a:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PrivateSubnet1a
      RouteTableId: !Ref PrivateRouteTable1a
  RouteTableAssocFirewall1a:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref FirewallSubnet1a
      RouteTableId: !Ref FirewallRouteTable1a
  VPCSsmEndpoint:
    Type: "AWS::EC2::VPCEndpoint"
    Properties:
      SubnetIds:
        - !Ref PrivateSubnet1a
      SecurityGroupIds:
        - !Ref SecurityGroup
      ServiceName: !Sub com.amazonaws.${AWS::Region}.ssm
      VpcId: !Ref VPC
      VpcEndpointType: Interface
      PrivateDnsEnabled: true
  VPCSsmmessagesEndpoint:
    Type: "AWS::EC2::VPCEndpoint"
    Properties:
      SubnetIds:
        - !Ref PrivateSubnet1a
      SecurityGroupIds:
        - !Ref SecurityGroup
      ServiceName: !Sub com.amazonaws.${AWS::Region}.ssmmessages
      VpcId: !Ref VPC
      VpcEndpointType: Interface
      PrivateDnsEnabled: true
  Ec2messagesEndpoint:
    Type: "AWS::EC2::VPCEndpoint"
    Properties:
      SubnetIds:
        - !Ref PrivateSubnet1a
      SecurityGroupIds:
        - !Ref SecurityGroup
      ServiceName: !Sub com.amazonaws.${AWS::Region}.ec2messages
      VpcId: !Ref VPC
      VpcEndpointType: Interface
      PrivateDnsEnabled: true
  SecurityGroup:
    Type: "AWS::EC2::SecurityGroup"
    Properties:
      VpcId: !Ref VPC
      GroupName: "vpc-endpoint-sg"
      GroupDescription: "-"
      Tags:
        - Key: "Name"
          Value: "vpcendpoint-sg"
        - Key: Project
          Value: !Ref ProjectName
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 443
          ToPort: 443
          CidrIp: !GetAtt VPC.CidrBlock
  NetworkFirewall:
    Type: AWS::NetworkFirewall::Firewall
    Properties:
      FirewallName: !Sub ${ProjectName}-network-firewall
      FirewallPolicyArn: !Ref NetworkFirewallPolicy
      VpcId: !Ref VPC
      SubnetMappings:
      - SubnetId: !Ref FirewallSubnet1a
      Tags:
        - Key: Project
          Value: !Ref ProjectName
  NetworkFirewallPolicy:
    Type: AWS::NetworkFirewall::FirewallPolicy
    Properties:
      FirewallPolicyName: !Sub ${ProjectName}-network-firewall-policy
      FirewallPolicy:
        StatelessDefaultActions:
        - aws:forward_to_sfe
        StatelessFragmentDefaultActions:
        - aws:forward_to_sfe
        StatefulDefaultActions:
        - aws:drop_established
        - aws:alert_established
        StatefulEngineOptions:
          RuleOrder: STRICT_ORDER
        StatefulRuleGroupReferences:
        - ResourceArn: !Ref RuleGroup
          Priority: 1
      Tags:
        - Key: Project
          Value: !Ref ProjectName
  RuleGroup:
    Type: "AWS::NetworkFirewall::RuleGroup"
    Properties:
      RuleGroupName: !Sub ${ProjectName}-rule-group
      Type: "STATEFUL"
      Capacity: 50
      RuleGroup:
        RulesSource:
          RulesSourceList:
            Targets:
            - ".ap-northeast-1.amazonaws.com"
            - ".google.com"
            TargetTypes:
            - "HTTP_HOST"
            - "TLS_SNI"
            GeneratedRulesType: "ALLOWLIST"
        StatefulRuleOptions:
          RuleOrder: "STRICT_ORDER"
      Tags:
        - Key: Project
          Value: !Ref ProjectName
  Instance:
    Type: 'AWS::EC2::Instance'
    Properties:
      InstanceType: t3.micro
      ImageId: ami-05a56ce08feadf9c4
      IamInstanceProfile: !Ref InstanceProfile
      NetworkInterfaces:
        - AssociatePublicIpAddress: false
          DeviceIndex: '0'
          SubnetId: !Ref PrivateSubnet1a
      Tags:
        - Key: Name
          Value: !Sub ${ProjectName}-01
        - Key: Project
          Value: !Ref ProjectName
  InstanceRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub ${ProjectName}-role
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: ec2.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
  InstanceProfile:
    Type: 'AWS::IAM::InstanceProfile'
    Properties:
      Path: /
      InstanceProfileName: !Sub ${ProjectName}-profile
      Roles:
        - !Ref InstanceRole
  Role:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub ${ProjectName}-fix-route-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:
                  - network-firewall:DescribeFirewall
                Resource: "*"
              - Effect: Allow
                Action:
                  - ec2:ReplaceRoute
                  - ec2:CreateRoute
                  - ec2:DescribeVpcs
                Resource: "*"
  Function:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: !Sub ${ProjectName}-fix-route
      Handler: "index.handler"
      Role: !GetAtt Role.Arn
      Code:
        ZipFile: |
          import boto3
          import cfnresponse

          def handler(event, context):
              responseData = {}
              responseStatus = cfnresponse.FAILED
              if event["RequestType"] == "Delete":
                  responseStatus = cfnresponse.SUCCESS
                  cfnresponse.send(event, context, responseStatus, responseData)
              if event["RequestType"] == "Create":
                  try:

                      responseStatus = cfnresponse.SUCCESS
                      cfnresponse.send(event, context, responseStatus, responseData)

                      PrivateRouteTable1a = event["ResourceProperties"]["PrivateRouteTable1a"]
                      PublicRouteTable1a = event["ResourceProperties"]["PublicRouteTable1a"]
                      VpcCidr = event["ResourceProperties"]["VpcCidr"]
                      NetworkFirewallArn = event["ResourceProperties"]["NetworkFirewallArn"]

                      ec2 = boto3.client('ec2')
                      nfw = boto3.client('network-firewall')

                      NfwResponse=nfw.describe_firewall(FirewallArn=NetworkFirewallArn)
                      VpceId = NfwResponse['FirewallStatus']['SyncStates']['ap-northeast-1a']['Attachment']['EndpointId']
                     ec2.create_route(
                          DestinationCidrBlock='0.0.0.0/0',
                          RouteTableId=PrivateRouteTable1a,
                          VpcEndpointId=VpceId
                      )
                     ec2.replace_route(
                          DestinationCidrBlock=VpcCidr,
                          RouteTableId=PublicRouteTable1a,
                          VpcEndpointId=VpceId
                      )
                  except Exception as e:
                      responseStatus = cfnresponse.FAILED
                      cfnresponse.send(event, context, responseStatus, responseData)
      Runtime: python3.12
      Timeout: 30

  FixRoutes:
    Type: Custom::FixRoutes
    Properties:
      ServiceToken: !GetAtt Function.Arn
      PrivateRouteTable1a: !Ref PrivateRouteTable1a
      PublicRouteTable1a: !Ref PublicRouteTable1a
      NetworkFirewallArn: !Ref NetworkFirewall
      VpcCidr: !GetAtt VPC.CidrBlock


デプロイ方法
以下のコマンドでデプロイします。
$ aws cloudformation deploy --template-file <前セクションのCloudFormationテンプレート>.yaml --stack-name NetworkFirewallPoc --capabilities CAPABILITY_NAMED_IAM

実行後、以下の出力結果が表示されればデプロイ成功となります。
Waiting for changeset to be created..
Waiting for stack create/update to complete
Successfully created/updated stack – NetworkFirewallPoc

解説
CloudFormationのコードの中で重要な点をいくつかピックアップしてご紹介します。
・FirewallPolicyでステートフルデフォルトアクションをdrop_establishedに設定します。これによって暗黙のDenyを実現できます。
・RuleGroupで許可したいドメイン名のルールグループをALLOWLISTに設定することで、そのルールを許可リストとして扱うことができます。
NetworkFirewallPolicy:
    Type: AWS::NetworkFirewall::FirewallPolicy
    Properties:
      FirewallPolicyName: !Sub ${ProjectName}-network-firewall-policy
      FirewallPolicy:
        StatelessDefaultActions:
        - aws:forward_to_sfe
        StatelessFragmentDefaultActions:
        - aws:forward_to_sfe
        StatefulDefaultActions:
        - aws:drop_established
        - aws:alert_established
        StatefulEngineOptions:
          RuleOrder: STRICT_ORDER
        StatefulRuleGroupReferences:
        - ResourceArn: !Ref RuleGroup
          Priority: 1
      Tags:
        - Key: Project
          Value: !Ref ProjectName
  RuleGroup:
    Type: "AWS::NetworkFirewall::RuleGroup"
    Properties:
      RuleGroupName: !Sub ${ProjectName}-rule-group
      Type: "STATEFUL"
      Capacity: 50
      RuleGroup:
        RulesSource:
          RulesSourceList:
            Targets:
            - ".ap-northeast-1.amazonaws.com"
            - ".google.com"
            TargetTypes:
            - "HTTP_HOST"
            - "TLS_SNI"
            GeneratedRulesType: "ALLOWLIST"
        StatefulRuleOptions:
          RuleOrder: "STRICT_ORDER"
      Tags:
        - Key: Project
          Value: !Ref ProjectName

・CloudFormation の仕様では NetworkFirewall リソースの戻り値に AWS Network Firewall のエンドポイント ID が渡されます。マルチ AZ で構築した場合はその戻り値が配列形式で渡されるのですが、配列内のエンドポイント ID の順番は AZ 順などではなくランダムです。
そのため構築を CloudFormation で完結するには下記のようにカスタムリソースを作成し、スクリプトを実行する必要があります。今回はシングル AZ なため不要ではありますが、本番環境などでマルチ AZ にしたい時があるので、カスタムリソースを用いるのが良いかと思います。
FixRoutes:
    Type: Custom::FixRoutes
    Properties:
      ServiceToken: !GetAtt Function.Arn
      PrivateRouteTable1a: !Ref PrivateRouteTable1a
      PublicRouteTable1a: !Ref PublicRouteTable1a
      NetworkFirewallArn: !Ref NetworkFirewall
      VpcCidr: !GetAtt VPC.CidrBlock

動作確認
EC2 インスタンスに Session Manager で接続し curl コマンドを実行します。
aws ssm start-session –target <<今回作成したインスタンス ID>>
sh-5.2$ curl www.google.com -o /dev/null -w '%{http_code}\n' -s #HTTPステータスコードのみ標準出力
200
sh-5.2$ curl www.sandj.co.jp
curl: (52) Empty reply from server

以上の結果から、許可リストによるドメイン名の外部ネットワーク通信の制限ができていると判断できます。

まとめ

今回はAWS Network Firewallの構成図・デプロイ方法の当社事例ついてご紹介しました。
AWS Network Firewallはルーティングの設定やFirewallPolicyの設定項目の難しさがあるサービスです。そのため必要な要件を満たすよう検証が必要になると思います。
しかし、要件に対して適切な設定をすることでマネージドでIDS/IPSを実現できる強みを持っています。
また今回Network FirewallのデプロイにCloudFormarmationを利用しました。
そのため、当記事を見てくださった方も容易にNetwork Firewallのデプロイおよび検証を行うことができるため、ぜひ試していただければと思います。
詳しいAWS Network Firewallのマネージドルールについて次回の記事で記載いたします。
ここまでご覧いただき、ありがとうございました。