kun432's blog

Alexaなどスマートスピーカーの話題中心に、Voiceflowの日本語情報を発信してます。たまにAWSやkubernetesなど。

〜スマートスピーカーやVoiceflowの記事は右メニューのカテゴリからどうぞ。〜

Network FirewallをTerraformでデプロイする

f:id:kun432:20210228193004p:plain

AWS Network Firewallが3月に東京リージョンでも使えるようになったので、機会もあるかということで、Terraformでデプロイしてみました。

目次

構成

以下にある「2) AWS Network Firewall is deployed to protect traffic between an AWS service in a public subnet and IGW」を元に作成してます。

画面キャプチャで引用するとこんな感じです。

f:id:kun432:20210516233332p:plain

これを元に、

  • 2AZ構成
  • EC2インスタンスはALB+AutoScaling、Packerで作ったWebサーバのAMIを使う
  • アウトバウンドはNAT Gateway

で作っています。

Terraform

以下でコードを公開してます。

Network FirewallのVPC周りについては説明を割愛します。↓がとてもわかりやすかったのでオススメです。

あと、ALB/Auto Scaling、Packerなどについても説明を割愛します。

では早速見てみましょう。

aws_networkfirewall_firewallでNetwork Firewallを作成します。

resource "aws_networkfirewall_firewall" "firewall" {
  name                = "${var.prj_name}-firewall"
  firewall_policy_arn = aws_networkfirewall_firewall_policy.firewall.arn
  vpc_id              = aws_vpc.vpc.id

  subnet_mapping {
    subnet_id     = aws_subnet.subnet_firewall_c.id
  }
  subnet_mapping {
    subnet_id     = aws_subnet.subnet_firewall_d.id
  }

  tags = {
    Name = "${var.prj_name}-firewall"
  }
}

subnet_mappingでそれぞれのAZのFirewallサブネットにFirewall Endpointを配置します。firewall_policy_arnでNetwork Firewallのポリシーと紐付けます。

aws_networkfirewall_firewall_policyでポリシーを作成します。

resource "aws_networkfirewall_firewall_policy" "firewall" {
  name = "${var.prj_name}-firewall-policy"

  firewall_policy {
    stateless_default_actions          = ["aws:forward_to_sfe"]
    stateless_fragment_default_actions = ["aws:forward_to_sfe"]
    stateful_rule_group_reference {
      resource_arn = aws_networkfirewall_rule_group.ips.arn
    }
  }

  tags = {
    Name = "${var.prj_name}-subnet-firewall-initial-policy"
  }
}

stateless_default_actionsstateless_fragment_default_actionsでステートレスなルールのデフォルトの動作を設定します。公式のドキュメントにある通り、ステートレスなルールがまず先に評価され、どれにもマッチしない場合のデフォルトの動作がここになります。

f:id:kun432:20210518012718p:plain

ここでは、ステートフルなルールのほうが使いやすいのと、IPSとしてのルールを試したいので、デフォルトでステートフルなルールに転送するように設定しています。その場合の転送先がstateful_rule_group_referenceで指定したステートフルルールグループになります。

aws_networkfirewall_rule_groupでルールグループを作成します。

resource "aws_networkfirewall_rule_group" "ips" {
  capacity = 100
  name     = "ips"
  type     = "STATEFUL"
  rule_group  {
    rules_source {
      rules_string = file("${path.module}/rules/sample-rules.txt")
    }
}

上で述べたとおり、今回はIPSとして使いたいのでSuricata互換のIPSルールを設定したファイルを読み込んでます。こういう感じのフォーマットです。

alert tcp any any -> any any (msg:"TCP traffic detected"; sid:200001; rev:1;)

単純にTCPの通信があればalertするだけのものですね。

こういったルールを記載したルールファイルを読み込んで設定する場合の設定サンプルがTerraformのドキュメントにあります。

resource "aws_networkfirewall_rule_group" "example" {
  capacity = 100
  name     = "example"
  type     = "STATEFUL"
  rules    = file("example.rules")
}

この書き方だと一応最初の構築はできるのですが、記載されているルールファイルの中身を更新してapplyすると、Terraformとしては更新を検知するもののエラーになります。上のルールに1行追加してUDPの通信をalertするようにして適用するとこんな感じのエラーになります。

Error: error updating NetworkFirewall Rule Group (arn:aws:network-firewall:ap-northeast-1:XXXXXXXXXXXX:stateful-rulegroup/ips): InvalidRequestException: Exactly one of Rules or RuleGroup must be set

なんかルールがないというようなメッセージに見えますね。apply時の出力を見てみるとファイルの変更は検知しているのですが、その下のところです。

  ~ resource "aws_networkfirewall_rule_group" "ips" {
...
      ~ rules        = <<~EOT
            alert tcp any any -> any any (msg:"TCP traffic detected"; sid:200001; rev:1;)
          + alert udp any any -> any any (msg:"UDP traffic detected"; sid:200002; rev:1;)
...
        rule_group {

            rules_source {
                rules_string = "alert tcp any any -> any any (msg:\"TCP traffic detected\"; sid:200001; rev:1;)"
            }
        }
    }

Plan: 0 to add, 1 to change, 0 to destroy.

どうやらrulesで読みこんだルールは、rule_group.rules_source.rules_stringに置き換えられるようで、おそらくここがうまくいかないのだと思われます。したがって、こういうふうに書き変えれば、ちゃんと更新が反映されるようになります。

resource "aws_networkfirewall_rule_group" "example" {
  capacity = 100
  name     = "example"
  type     = "STATEFUL"
  rule_group  {
    rules_source {
      rules_string = file("example.rules")
    }
  }
}

さらに、IPSのルールはいちいち自分で設定するのではなく、公開されているものを使いたいですよね。suricataやsnortではオープンソースとしてルールファイルの公開が行われています。

サンプルで上記のルールの一つを抜粋します。

alert http $HOME_NET any -> $EXTERNAL_NET any (msg:"ET INFO Suspicious GET Request for .x64"; flow:established,to_server; content:"GET"; http_method; content:".x64"; isdataat:!1,relative; fast_pattern; http_uri; http_header_names; content:!"Referer"; classtype:bad-unknown; sid:2032925; rev:1; metadata:affected_product Linux, affected_product IoT, attack_target Client_Endpoint, created_at 2021_05_10, deployment Perimeter, former_category HUNTING, signature_severity Informational, updated_at 2021_05_10;)

ルール内に変数が指定されていますが、これらはsuricataやsnortでは設定ファイル内に定義することで動作します。Terraformだと以下のようにrule_variablesを使って設定します。

(snip)
  rule_group  {
    rules_source {
      rules_string = file("${path.module}/rules/sample-suricata-rules.txt")
    }
    rule_variables {
      ip_sets {
        key = "EXTERNAL_NET"
        ip_set {
          definition = ["0.0.0.0/0"]
        }
      }

      ip_sets {
        key = "HOME_NET"
        ip_set {
          definition = [var.vpc_cidr]
        }
      }
      port_sets {
        key = "HTTP_PORTS"
        port_set {
          definition = ["[80,443]"]
        }
      }
    }
  }
(snip)

ちょっとつらそうなところ

このあたりはまだ未確認も含みます。

  • ルールグループのキャパシティは事前に設定する必要があり、これは変更できない、つまりキャパシティを変更する場合は再作成が必要になります。SuricataやSnortの公開ルールは結構なボリュームがあり、ざっと見た限りは20000行ぐらいあります。ステートフルの場合、1ルール=1キャパシティなので、上限30000の2/3ぐらいは使うことになりそうです。キャパシティを超えると以下のようなエラーになります。
Error: error updating NetworkFirewall Rule Group (arn:aws:network-firewall:ap-northeast-1:XXXXXXXXXXXX:stateful-rulegroup/ips): InvalidRequestException: StatefulRules capacity exceeded, parameter: [628], context: RulesSource.StatefulRules
  • Terraformからルールファイルを読み込ませる場合、1回で読み込ませれるサイズの上限があります。(記録してないですが、1000000バイトぐらいだったような。エラーになります。)
    なので、公開されているルールを全部を読み込ませようと思うと、多分分割してルールグループの設定を書く必要が出てくるのではないかと思いますし、キャパシティも意識しつつうまく分割しないと、更新とかのときにめんどくさくなる気がします。
  • Terraformだとモジュール化したくなると思いますが、VPC周りのリソースとかなり密な感じなので、Network FirewallとVPCを別モジュールで分けるのは難しい気がしました(すくなくとも私は諦めた)

まとめ

Network FirewallはVPCの一番外側で動かせるのでとても便利だと思います。IPS/IDSなどのためにプロキシ入れたりとかしなくてすみますしね。既存環境への導入も(多少の変更はもちろん必要になりますが)比較的楽なのではないかなと。

参考)