
AWS Network Firewallが3月に東京リージョンでも使えるようになったので、機会もあるかということで、Terraformでデプロイしてみました。
目次
構成
以下にある「2) AWS Network Firewall is deployed to protect traffic between an AWS service in a public subnet and IGW」を元に作成してます。
画面キャプチャで引用するとこんな感じです。

これを元に、
- 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_actionsとstateless_fragment_default_actionsでステートレスなルールのデフォルトの動作を設定します。公式のドキュメントにある通り、ステートレスなルールがまず先に評価され、どれにもマッチしない場合のデフォルトの動作がここになります。

ここでは、ステートフルなルールのほうが使いやすいのと、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などのためにプロキシ入れたりとかしなくてすみますしね。既存環境への導入も(多少の変更はもちろん必要になりますが)比較的楽なのではないかなと。
参考)