Network FirewallとNAT Gatewayを使えば、VPCエンドポイントは不要なのか検証してみた

あれ、VPCエンドポイントっていらなくない...??
2021.05.03

VPCエンドポイントっている?

こんにちは、のんピ です。

前回のこちらの記事を書いた後、ふと疑問が湧いてきました。

それは、Network FirewallとNAT Gatewayの環境にはVPCエンドポイントって必要??という疑問です。

VPCエンドポイントの利用シーンとしては、以下があると考えています。

  1. プライベートサブネットにあるリソースからAWSのリソースにインターネットを経由せず、セキュアにアクセスしたい
  2. Direct Connectを経由してオンプレ環境からセキュアにAWSのリソースにアクセスしたい
  3. VPCエンドポイントポリシーを使って、サービスへのアクセスを制限をしたい

この利用シーンの中でも特にVPCエンドポイントを利用する理由になるのが、

1. プライベートサブネットにあるリソースからAWSのリソースにインターネットを経由せず、セキュアにアクセスしたい

だと個人的には感じます。

このような要件がある背景は、セキュリティの観点でインターネットは危険が危ないといった認識があるためです。最強のマルウェア対策がネットワークに繋がないという考えもあると思うので納得です。

ここで、タイムリーな話題として先日、AWSのFAQが更新され、AWSのネットワークから発信され送信先もAWSの場合は、インターネットを経由せずAWSのネットワーク内に留まると、明記されるようになりました。

Q:2 つのインスタンスがパブリック IP アドレスを使用して通信する場合、またはインスタンスが AWS のサービスのパブリックエンドポイントと通信する場合、トラフィックはインターネットを経由しますか?

いいえ。パブリックアドレススペースを使用する場合、AWS でホストされているインスタンスとサービス間のすべての通信は AWS のプライベートネットワークを使用します。AWS ネットワークから発信され、AWS ネットワーク上の送信先を持つパケットは、AWS 中国リージョンとの間のトラフィックを除いて、AWS グローバルネットワークにとどまります。

Amazon VPC のよくある質問 - Q:2 つのインスタンスがパブリック IP アドレスを使用して通信する場合、またはインスタンスが AWS のサービスのパブリックエンドポイントと通信する場合、トラフィックはインターネットを経由しますか?

そのため、プライベートサブネットがAWSのAPIを叩くために、NAT Gatewayを経由したとしてもAWSのネットワーク内に完結するということになります。

加えて、以下公式ドキュメントの通り、エンドポイントのFQDNは法則性があります。

ほとんどの Amazon Web Services では、リクエストの実行に使用できるリージョンのエンドポイントを提供しています。リージョンエンドポイントの一般的な構文は次のとおりです。

protocol://service-code.region-code.amazonaws.com
たとえば、https://dynamodb.us-west-2.amazonaws.com は 米国西部 (オレゴン) リージョンの Amazon DynamoDB サービスのエンドポイントです。

AWSサービスエンドポイント - リージョンエンドポイント

そこで、Network Firewallのドメインフィルタリングを上手く使えば、VPCエンドポイントを使わなくても運用出来ちゃうんじゃないかと考えつきました。

いきなりまとめ

許可するドメインを<サービス名>.<リージョン名>.amazonaws.comとすることでAWSのAPIにアクセスすることは可能です。
しかし、以下の理由からVPCエンドポイントを使って運用する必要があると感じました。

  • ドメインによるフィルタリングの設定しかされていない場合、IPアドレスを直接指定した通信は、Network Firewallを通過してしまう
  • 対策としてIPアドレス制限をすると、エンドポイントにアクセスできなくなってしまう
  • 宛先を0.0.0.0/0 tcp/443で許可すると、ドメインフィルタリングのルールがあったとしても、tcp/443であればどんな通信もできてしまう
  • 基本的に課金額はVPCエンドポイントを使った構成の方が安くなる
  • より厳密にアクセス元のリソースを制限したい場合は、VPCエンドポイントのエンドポリシーを使う必要がある
  • S3のGateway型VPCエンドポイントは通信量がかからないというメリットがあるので、大量にS3と通信が発生する場合は、VPCエンドポイントを使った方がお財布に優しい

検証してみた

CDKでデプロイ

CDKの確認

私はCDKが大好きなので、例によってCDKでデプロイします。CDK自体は前回のブログとほぼほぼ一緒です。

ドメインフィルタリングの対象ドメインを変更します。SSMセッションマネージャーでEC2インスタンスにログインしてみたいので、SSMなど必要なエンドポイントを許可するよう以下のように定義しました。

./lib/app-stack.ts

    // Create Network Firewall rule group
    const networkfirewallRuleGroup = new networkfirewall.CfnRuleGroup(
      this,
      "NetworkFirewallRuleGroup",
      {
        capacity: 100,
        ruleGroupName: "WindowsUpdateRuleGroup",
        type: "STATEFUL",
        ruleGroup: {
          rulesSource: {
            rulesSourceList: {
              generatedRulesType: "ALLOWLIST",
              targetTypes: ["TLS_SNI", "HTTP_HOST"],
              targets: [
                "ssm.us-east-1.amazonaws.com",
                "ssmmessages.us-east-1.amazonaws.com",
                "ec2messages.us-east-1.amazonaws.com",
              ],
            },
          },
        },
      }
    );

その他の前回のブログからの変更箇所は以下の通りです。

  • VPCエンドポイント関連の定義を削除
  • CloudWatch Agent関連の定義を削除
  • OSのバージョンをWindows Server 2012 R2から、Amazon Linux 2に変更
  • Multi-AZからSingle-AZに構成を変更
  • 不要なモジュールを削除

全体のアーキテクチャーは以下の通りです。

CDKのデプロイ

CDKのデプロイをします。特にエラーはなく正常に実行完了しました。

> npx cdk deploy 
This deployment will make potentially sensitive changes according to your current security approval level (--require-approval broadening).
Please confirm you intend to make the following modifications:

IAM Statement Changes
┌───┬─────────────────────────────────┬────────┬─────────────────────────────────┬───────────────────────────────────┬───────────┐
│   │ Resource                        │ Effect │ Action                          │ Principal                         │ Condition │
├───┼─────────────────────────────────┼────────┼─────────────────────────────────┼───────────────────────────────────┼───────────┤
│ + │ ${FlowLogsIamrole.Arn}          │ Allow  │ sts:AssumeRole                  │ Service:vpc-flow-logs.amazonaws.c │           │
│   │                                 │        │                                 │ om                                │           │
│ + │ ${FlowLogsIamrole.Arn}          │ Allow  │ iam:PassRole                    │ AWS:${FlowLogsIamrole}            │           │
├───┼─────────────────────────────────┼────────┼─────────────────────────────────┼───────────────────────────────────┼───────────┤
│ + │ ${FlowLogsLogGroup.Arn}         │ Allow  │ logs:CreateLogStream            │ AWS:${FlowLogsIamrole}            │           │
│   │                                 │        │ logs:DescribeLogStreams         │                                   │           │
│   │                                 │        │ logs:PutLogEvents               │                                   │           │
├───┼─────────────────────────────────┼────────┼─────────────────────────────────┼───────────────────────────────────┼───────────┤
│ + │ ${SsmIamRole.Arn}               │ Allow  │ sts:AssumeRole                  │ Service:ec2.${AWS::URLSuffix}     │           │
└───┴─────────────────────────────────┴────────┴─────────────────────────────────┴───────────────────────────────────┴───────────┘
IAM Policy Changes
┌───┬───────────────┬────────────────────────────────────────────────────────────────────┐
│   │ Resource      │ Managed Policy ARN                                                 │
├───┼───────────────┼────────────────────────────────────────────────────────────────────┤
│ + │ ${SsmIamRole} │ arn:${AWS::Partition}:iam::aws:policy/AmazonSSMManagedInstanceCore │
│ + │ ${SsmIamRole} │ arn:${AWS::Partition}:iam::aws:policy/AmazonSSMPatchAssociation    │
│ + │ ${SsmIamRole} │ arn:${AWS::Partition}:iam::aws:policy/CloudWatchAgentAdminPolicy   │
└───┴───────────────┴────────────────────────────────────────────────────────────────────┘
Security Group Changes
┌───┬───────────────────────────────────────────────┬─────┬────────────┬─────────────────┐
│   │ Group                                         │ Dir │ Protocol   │ Peer            │
├───┼───────────────────────────────────────────────┼─────┼────────────┼─────────────────┤
│ + │ ${Ec2Instance0/InstanceSecurityGroup.GroupId} │ Out │ Everything │ Everyone (IPv4) │
└───┴───────────────────────────────────────────────┴─────┴────────────┴─────────────────┘
(NOTE: There may be security-related changes not in this list. See https://github.com/aws/aws-cdk/issues/1299)

Do you wish to deploy these changes (y/n)? y
AppStack: deploying...
AppStack: creating CloudFormation changeset...
[██████████████████████████████████████████████████████████] (35/35)





 ✅  AppStack

Stack ARN:
arn:aws:cloudformation:us-east-1:<AWSアカウントID>:stack/AppStack/9596d0d0-aa40-11eb-ab32-12ae8fb95b53

動作確認

早速セッションマネージャーで接続してみます。 SSMのコンソールを確認すると、インスタンスはマネージドインスタンスとして認識されているので、セッションマネージャーで接続ができそうです。

セッションマネージャーで接続できました。 念のためインターネットに通信できないか確認してみましたが、正しくフィルタリングされており通信はできませんでした。

sh-4.2$ curl -m 5 https://dev.classmethod.jp/
curl: (28) Operation timed out after 5000 milliseconds with 0 out of 0 bytes received
sh-4.2$
sh-4.2$ curl -m 5 http://update.microsoft.com
curl: (28) Operation timed out after 5001 milliseconds with 0 bytes received
sh-4.2$

ドメインフィルタリングしか設定しない場合、IPアドレスによる通信は許可される

ここで新たに疑問が湧いてきました。最初にVPCエンドポイントが使われる理由で、

1. プライベートサブネットにあるリソースからAWSのリソースにインターネットを経由せず、セキュアにアクセスしたい

を列挙しましたが、インターネットにアクセスするときはIPアドレスを直接指定することもありますよね。 そこで、Google Public DNSのIPアドレスである8.8.8.8に対してpingを打ってみます。

sh-4.2$ ping 8.8.8.8 -c 4
PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
64 bytes from 8.8.8.8: icmp_seq=1 ttl=108 time=4.39 ms
64 bytes from 8.8.8.8: icmp_seq=2 ttl=108 time=2.39 ms
64 bytes from 8.8.8.8: icmp_seq=3 ttl=108 time=2.42 ms
64 bytes from 8.8.8.8: icmp_seq=4 ttl=108 time=2.53 ms

--- 8.8.8.8 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3004ms
rtt min/avg/max/mdev = 2.390/2.937/4.396/0.846 ms
sh-4.2$

なんと通信できてしまいました。ドメインを使ってアクセスしていない場合は、ドメインフィルタリングは機能しないようです。 対応としてIPアドレスの制御ができる5-tupleで以下のように全ての通信を拒否するような制限をかけてみました。

すると、マネージドインスタンスには登録されてはいるようですが、SSMエージェントのpingの状態接続が失われましたとなっていました。

この状態でセッションマネージャーで接続しようとしても以下のようなエラーが出力され、接続できませんでした。

一旦、cdk destroyをして再度cdk deployしましたが、今度はいつまで待ってもマネージインスタンスとして登録されませんでした。

クライアント側でIPアドレスをキャッシュして、最初に名前解決した後はIPアドレスでアクセスしに行く事もあるのかな?と思ったので必要な通信を許可をしてみます。

エンドポイントにはtcp/443でアクセスする必要があります。しかし、エンドポイントのIPアドレスは公開されていません。そのため、宛先を0.0.0.0/0 tcp/443で許可する必要があります。 5-tupleで許可して、再度確認します。

マネージドインスタンスとして登録されました!!

もちろん、このようなルールを設定してしまうと、tcp/443の通信は全て許可されてしまいます。そのため、AWSのエンドポイントだろうが、野良サーバーだろうが通過してしまいます。 企画倒れ感が半端ないですが、AWS以外のリソースと通信することができてしまうことから、Network FirewallはVPCエンドポイントの代替にはならないことが分かりました。 VPCエンドポイントは必要です。ごめんなさい。

注意点を考える

.amazonaws.comはエンドポイントと限らない

.amazonaws.comで許可したら使いたいサービスが増える毎にフィルタリングのルールを更新しなくても良いんじゃない? と思った時期が私にもありました。 .amazon.comはエンドポイントとは限りません。例えば、パブリックIPアドレスを持ったEC2インスタンスのDNS名はec2-3-139-90-234.us-east-2.compute.amazonaws.comになったりします。 横着せずに心を込めて使用するサービス毎にDomain Listに追加しましょう。

VPCエンドポイントポリシーのように細かいアクセス制限をかけられない

Network FirewallはTCP/IPレベルの単純な制御しかできません。 そのため、VPCエンドポイントのように特定リソースに対して特定アクションしかさせないということはできません。

VPCエンドポイントとはなんぞや?という方は以下のブログが参考になるかと思います。

課金額試算

VPCエンドポイントで構成した場合と、Network Firewallで頑張る構成で課金額を試算してみました。

1. VPCエンドポイントが存在する構成

前提は以下の通りです。

  • 1ヶ月730時間で計算
  • バージニア北部(us-east-1)
  • Single-AZ構成
  • Privatelink型のVPCエンドポイント数: 10
  • Gateway型のVPCエンドポイントの数(S3): 1
  • NAT Gatewayの数: 1
  • 1ヶ月あたりのPrivatelink型のVPCエンドポイントのデータ処理量: 1GB
  • 1ヶ月あたりのS3へのデータ通信量: 10GB
  • 1ヶ月あたりのNAT Gatewayのデータ処理量: 10GB

計算した結果、106.31 USDとなりました。 内訳は以下のようになりました。

  • Privatelink型のVPCエンドポイントについての課金
    • 730 hours in a month x 0.01 USD = 7.30 USD (Hourly cost for endpoint ENI)
    • 10 VPC endpoints x 1 ENIs per VPC endpoint x 7.30 USD = 73.00 USD (Total PrivateLink endpoints and data processing cost)
    • 1 GB per month x 0.01 USD = 0.01 USD (PrivateLink data processing cost)
    • 73.00 USD + 0.01 USD = 73.01 USD (Total PrivateLink endpoints and data processing cost)
  • NAT Gatewayについての課金
    • 730 hours in a month x 0.045 USD = 32.85 USD (Gateway usage hourly cost)
    • 10 GB per month x 0.045 USD = 0.45 USD (NAT Gateway data processing cost)
    • 32.85 USD + 0.45 USD = 33.30 USD (NAT Gateway processing and month hours)
    • 1 NAT Gateways x 33.30 USD = 33.30 USD (Total NAT Gateway usage and data processing cost)

2. Network Firewallで頑張る構成

前提は以下の通りです。

  • 1ヶ月730時間で計算
  • バージニア北部(us-east-1)
  • Single-AZ構成
  • Network Firewallのエンドポイント数: 1
  • NAT Gatewayの数: 1
  • 1ヶ月あたりのNetwork Firewallのデータ処理量: 21GB

計算した結果289.715 USDとなりました。 内訳は以下のようになりました。

  • Network Firewallについての課金
    • 730 hours in a month x 0.395 USD = 288.35 USD (Hourly cost for endpoint ENI)
    • 21 GB per month x 0.065 USD = 1.365 USD (Network Firewall data processing cost)
    • 288.35 USD + 1.365 USD = 289.715 USD

求められる機能・役割が全く異なるので単純な比較はできませんが、VPCエンドポイントを使うためだけにNetwork Firewallを使うのは得策ではないですね。

Network Firewallは本来の使い方で使おう

唐突にVPCエンドポイント不要論を提唱しましたが、いろいろ考えてみるとVPCエンドポイントは必要でした。餅は餅屋、セキュアにアクセスするならVPCエンドポイントです。 次は大人しくIPS機能の記事でも書こうと思います。

以上、東京オフィスの のんピ(@non____97)でした!

CDKのおまけ

あまり前回と変更点はありませんが、5-tupleについてもCDKで書いたので掲載します。

./lib/app-stack.ts

import * as cdk from "@aws-cdk/core";
import * as ec2 from "@aws-cdk/aws-ec2";
import * as logs from "@aws-cdk/aws-logs";
import * as iam from "@aws-cdk/aws-iam";
import * as networkfirewall from "@aws-cdk/aws-networkfirewall";

export class AppStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // Create CloudWatch Logs for VPC Flow Logs
    const flowLogsLogGroup = new logs.LogGroup(this, "FlowLogsLogGroup", {
      retention: logs.RetentionDays.ONE_WEEK,
    });

    // Create CloudWatch Logs for Network Firewall Logs
    const networkFirewallFlowLogsLogGroup = new logs.LogGroup(
      this,
      "NetworkFirewallFlowLogsLogGroup",
      {
        retention: logs.RetentionDays.ONE_WEEK,
      }
    );

    // Create VPC Flow Logs IAM role
    const flowLogsIamrole = new iam.Role(this, "FlowLogsIamrole", {
      assumedBy: new iam.ServicePrincipal("vpc-flow-logs.amazonaws.com"),
    });

    // Create SSM IAM role
    const ssmIamRole = new iam.Role(this, "SsmIamRole", {
      assumedBy: new iam.ServicePrincipal("ec2.amazonaws.com"),
      managedPolicies: [
        iam.ManagedPolicy.fromAwsManagedPolicyName(
          "AmazonSSMManagedInstanceCore"
        ),
        iam.ManagedPolicy.fromAwsManagedPolicyName("AmazonSSMPatchAssociation"),
        iam.ManagedPolicy.fromAwsManagedPolicyName(
          "CloudWatchAgentAdminPolicy"
        ),
      ],
    });

    // Create VPC Flow Logs IAM Policy
    const flowLogsIamPolicy = new iam.Policy(this, "FlowLogsIamPolicy", {
      statements: [
        new iam.PolicyStatement({
          effect: iam.Effect.ALLOW,
          actions: ["iam:PassRole"],
          resources: [flowLogsIamrole.roleArn],
        }),
        new iam.PolicyStatement({
          effect: iam.Effect.ALLOW,
          actions: [
            "logs:CreateLogStream",
            "logs:PutLogEvents",
            "logs:DescribeLogStreams",
          ],
          resources: [flowLogsLogGroup.logGroupArn],
        }),
      ],
    });

    // Atach VPC Flow Logs IAM Policy
    flowLogsIamrole.attachInlinePolicy(flowLogsIamPolicy);

    // Create VPC
    const vpc = new ec2.Vpc(this, "Vpc", {
      cidr: "10.0.0.0/16",
      enableDnsHostnames: true,
      enableDnsSupport: true,
      natGateways: 1,
      maxAzs: 1,
      subnetConfiguration: [
        { name: "Firewall", subnetType: ec2.SubnetType.PRIVATE, cidrMask: 28 },
        { name: "Public", subnetType: ec2.SubnetType.PUBLIC, cidrMask: 24 },
        { name: "Private", subnetType: ec2.SubnetType.PRIVATE, cidrMask: 24 },
      ],
    });

    // Setting VPC Flow Logs
    new ec2.CfnFlowLog(this, "FlowLogToLogs", {
      resourceId: vpc.vpcId,
      resourceType: "VPC",
      trafficType: "ALL",
      deliverLogsPermissionArn: flowLogsIamrole.roleArn,
      logDestination: flowLogsLogGroup.logGroupArn,
      logDestinationType: "cloud-watch-logs",
      logFormat:
        "${version} ${account-id} ${interface-id} ${srcaddr} ${dstaddr} ${srcport} ${dstport} ${protocol} ${packets} ${bytes} ${start} ${end} ${action} ${log-status} ${vpc-id} ${subnet-id} ${instance-id} ${tcp-flags} ${type} ${pkt-srcaddr} ${pkt-dstaddr} ${region} ${az-id} ${sublocation-type} ${sublocation-id} ${pkt-src-aws-service} ${pkt-dst-aws-service} ${flow-direction} ${traffic-path}",
      maxAggregationInterval: 60,
    });

    // Get Network Firewall Subnet ID
    const firewallSubnetId = new Array();
    vpc
      .selectSubnets({ subnetGroupName: "Firewall" })
      .subnets.forEach((subnet) => {
        firewallSubnetId.push({ subnetId: subnet.subnetId });
      });

    // Create EC2 instance
    vpc
      .selectSubnets({ subnetGroupName: "Private" })
      .subnets.forEach((subnet, index) => {
        new ec2.Instance(this, `Ec2Instance${index}`, {
          machineImage: ec2.MachineImage.latestAmazonLinux({
            generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2,
          }),
          instanceType: new ec2.InstanceType("t3.micro"),
          vpc: vpc,
          keyName: this.node.tryGetContext("key-pair"),
          role: ssmIamRole,
          vpcSubnets: vpc.selectSubnets({ subnetGroupName: "Private" }),
        });
      });

    // Create Network Firewall rule group
    // Domain List
    const networkfirewallRuleGroupDomainList = new networkfirewall.CfnRuleGroup(
      this,
      "NetworkfirewallRuleGroupDomainList",
      {
        capacity: 100,
        ruleGroupName: "AWSAPIDomainListRuleGroup",
        type: "STATEFUL",
        ruleGroup: {
          rulesSource: {
            rulesSourceList: {
              generatedRulesType: "ALLOWLIST",
              targetTypes: ["TLS_SNI", "HTTP_HOST"],
              targets: [
                "ssm.us-east-1.amazonaws.com",
                "ssmmessages.us-east-1.amazonaws.com",
                "ec2messages.us-east-1.amazonaws.com",
              ],
            },
          },
        },
      }
    );

    // 5 tuple
    const networkfirewallRuleGroup5tuple = new networkfirewall.CfnRuleGroup(
      this,
      "NetworkfirewallRuleGroup5tuple",
      {
        capacity: 100,
        ruleGroupName: "AWSAPI5tupletRuleGroup",
        type: "STATEFUL",
        ruleGroup: {
          rulesSource: {
            statefulRules: [
              {
                action: "PASS",
                header: {
                  destination: "0.0.0.0/0",
                  destinationPort: "443",
                  direction: "ANY",
                  protocol: "TCP",
                  source: vpc.vpcCidrBlock,
                  sourcePort: "ANY",
                },
                ruleOptions: [
                  {
                    keyword: `msg:"tcp/443 pass"`,
                  },
                  {
                    keyword: "sid:1000001",
                  },
                  {
                    keyword: "rev:1",
                  },
                ],
              },
            ],
          },
        },
      }
    );

    // Create Network Firewall policy
    const networkfirewallPolicy = new networkfirewall.CfnFirewallPolicy(
      this,
      "NetworkFirewallPolicy",
      {
        firewallPolicyName: "AWSAPIPolicy",
        firewallPolicy: {
          statelessDefaultActions: ["aws:forward_to_sfe"],
          statelessFragmentDefaultActions: ["aws:forward_to_sfe"],
          statefulRuleGroupReferences: [
            {
              resourceArn: networkfirewallRuleGroupDomainList.attrRuleGroupArn,
            },
            {
              resourceArn: networkfirewallRuleGroup5tuple.attrRuleGroupArn,
            },
          ],
        },
      }
    );

    // Create Network Firewall
    const networkFirewall = new networkfirewall.CfnFirewall(
      this,
      "NetworkFirewall",
      {
        firewallName: "NetworkFirewall",
        firewallPolicyArn: networkfirewallPolicy.attrFirewallPolicyArn,
        vpcId: vpc.vpcId,
        subnetMappings: firewallSubnetId,
      }
    );

    // // Setting Network Firewall logs
    new networkfirewall.CfnLoggingConfiguration(
      this,
      "NetworkFirewallFlowLogsToLogs",
      {
        firewallArn: networkFirewall.ref,
        loggingConfiguration: {
          logDestinationConfigs: [
            {
              logDestination: {
                logGroup: networkFirewallFlowLogsLogGroup.logGroupName,
              },
              logDestinationType: "CloudWatchLogs",
              logType: "FLOW",
            },
          ],
        },
      }
    );

    // Routing NAT Gateway to Network Firewall
    vpc
      .selectSubnets({ subnetGroupName: "Public" })
      .subnets.forEach((subnet, index) => {
        const route = subnet.node.children.find(
          (child) => child.node.id == "DefaultRoute"
        ) as ec2.CfnRoute;
        route.addDeletionOverride("Properties.GatewayId");
        route.addOverride(
          "Properties.VpcEndpointId",
          cdk.Fn.select(
            1,
            cdk.Fn.split(
              ":",
              cdk.Fn.select(index, networkFirewall.attrEndpointIds)
            )
          )
        );
      });

    // Routing Network Firewall to Internet Gateway
    vpc
      .selectSubnets({ subnetGroupName: "Firewall" })
      .subnets.forEach((subnet, index) => {
        const route = subnet.node.children.find(
          (child) => child.node.id == "DefaultRoute"
        ) as ec2.CfnRoute;
        route.addDeletionOverride("Properties.NatGatewayId");
        route.addOverride("Properties.GatewayId", vpc.internetGatewayId);
      });

    // Internet Gateway RouteTable
    const igwRouteTable = new ec2.CfnRouteTable(this, "IgwRouteTable", {
      vpcId: vpc.vpcId,
    });

    // Routing Internet Gateway to Network Firewall
    vpc
      .selectSubnets({ subnetGroupName: "Public" })
      .subnets.forEach((subnet, index) => {
        new ec2.CfnRoute(this, `IgwRouteTableToFirewall${index}`, {
          routeTableId: igwRouteTable.ref,
          destinationCidrBlock: subnet.ipv4CidrBlock,
          vpcEndpointId: cdk.Fn.select(
            1,
            cdk.Fn.split(
              ":",
              cdk.Fn.select(index, networkFirewall.attrEndpointIds)
            )
          ),
        });
      });

    // Association Internet Gateway RouteTable
    new ec2.CfnGatewayRouteTableAssociation(this, "IgwRouteTableAssociation", {
      gatewayId: <string>vpc.internetGatewayId,
      routeTableId: igwRouteTable.ref,
    });
  }
}