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