AWS Network Firewallの導入とロギング
Network FirewallはAZ単位の設計とパブリックIPの有無に注意しつつ、ステートレス・ステートフル両方のルールとCloudWatch Logsへのログ出力による高い可視化が可能です。一方で、NATゲートウェイとの組み合わせや運用コストにも留意が必要なため、実装前の十分な検討が推奨されます。
目次
はじめに
- ANS勉強して出てきた、Network Firewallとそのロギングを試してみた
- ステートフルルール、ステートレスルールごとにロギングできて細かにログを吐き出すことが可能
- それ以前にNetwork Firewallを入れるところが結構大変だったので記録していく
参考記事
Network Firewallの理論的な記事はいくつも優秀なのがあるので適宜参照
アーキテクチャー図
- 作ったものは以下の通り。プライベートサブネットにEC2をおいて、いつものようにパブリックサブネットのNATゲートウェイに通すのではなく、一旦Network Firewall用のサブネットに通してパブリックサブネットにパスさせる。
- 間にNetwork Firewallが入っていても、NATゲートウェイがあれば(別途VPCエンドポイントは必要なしで)Session Managerには接続可能。
ハマった点
- 原理的には、Network FirewallをNATの前(Public SubnetとInternet Gatewayの間)に置くことは可能。しかし、これだとNetwork FirewallがパブリックIPを持たないため、インターネットに出ていくことができなかった。
- 観測される事象としては、
curl www.example.com
とやるとタイムアウトする - 単に自分のやり方が悪かったのかもしれない。これで数時間ハマって入れ替えたら素直にできたので、おそらくパブリックIPを持つかどうかは考えないといけなそう。
- 観測される事象としては、
- Network FirewallはAZ単位でデプロイする必要があり(AWSもこれを推奨)、NATゲートウェイのように1個のサブネットにいるリソースに寄せるということは実質的にできなそう。
- Network Firewallをルートテーブルに通すときは、Gateway LoadBalancer型のVPCエンドポイントを指すが、これは複数のAZにまたがらせることは可能。しかし、これで試したところEC2に対してSession Managerに接続できなくなってしまった
Network Firewallの値段の注意
- Network Firewall Endpoint:USD 0.395/時間
- Network Firewall Traffic Processing:USD 0.065/GB
- NAT ゲートウェイの料金:Network Firewall エンドポイントについて請求される 1 時間および 1 GB ごとに、1 時間および 1 GB の NAT ゲートウェイを追加料金なしで使用できます。
ぶっちゃけていうと結構高い。3つのAZで立ち上げていると、1時間に1.2ドルの課金でGPUインスタンス1個つけているようなもの。
NATゲートウェイの割引はあるものの、気休め程度だろう。
Network Firewallのルールについて
- ステートレスルールと、ステートフルルールがある。
- ステートレスルールが先に判定され、次にステートフルルールで判定できる。ステートフルのほうが凝った判定ができる。
- ステートレスのみ各ルールを優先度を設定でき、昇順で評価される
Network Firewall stateless and stateful rules engines
この例では、
- ステートフルルールとして、HTTP(HTTP 80)とHTTPS(TCP 443)を許可
- ステートレスルールとして、HTTP(TCP 80)とHTTPS(TCP 443)とエフェメラルポート(1024-65535)を許可
結果
正常にデプロイされたところで、マネジメントコンソールでいろんなところを見ていく
Private SubnetのEC2
各EC2にSession Managerで接続し、インターネットに出ていくか確認する。
この通り、間にファイアーウォールのサブネットを挟んだとしても、通常どおりロールで設定してしまえばSession Managerで接続することは可能。
インターネットにも普通に出て行けて、curl https://www.example.com
にアクセスできる。
ルートテーブル
普通のプライベートサブネット、パブリックサブネットのときのルートテーブルは、プライベートサブネット→NAT、パブリックサブネット→インターネットゲートウェイだった。
これがファイアーウォールのサブネットが入った場合は以下のようになる。
プライベートサブネットはAZ単位で定義する。なぜなら、AZごとに飛んでいNetwork Firewallのサブネットが異なるため。最初のAWSの資料でもそうなっていた。このときのルート先がVPCエンドポイントであるというのを気に留めておこう。
ファイアーウォールルートテーブルはNATに転送する。ここは通常のプライベートサブネットと同じで、NATが1個の場合はルートテーブルは1個で良い。
ファイアーウォールのサブネットのCIDRはどの程度取るかという点だが、/27
ぐらい取れば良さそうな気がした。/27
で取ったときの利用可能なIPv4のアドレスは26だった。VPCが10.0.0.0/20
であったとき、各AZのサブネットは10.0.10.0/27, 10.0.10.32/27, 10.0.10.64/27
というCIDRになる。
パブリックサブネットのルートテーブルはいつもどおりインターネットゲートウェイにルートする。
VPCエンドポイント
興味深いのはVPCエンドポイントで、GatewayLoadBalancerという珍しい型が出てくる。GatewayLoadBalancerはファイアーウォールやセキュリティのアプライアンスの場面でしかほぼ使わないので、Network Firewallの内部ではGLBを使っているのだと想像される。
このGatewayLoadBalancerは実はENIを持っており、ここを経由して通信が行われているのだと思われる。プライベート→ファイアーウォール→パブリック→IGWとおいたときは、ここのENIはプライベートIPでよかったので通じた。
しかし、自分がプライベート→パブリック→ファイアーウォール→IGWという構成でうまくいかなかったのは、GLBのVPCエンドポイントに紐づいているENIがパブリックIPアドレスを持っていなかったからではないかと思われた(理由は通信がインターネットに出ていけなかったから)。なので、自分は試せていないが、Network Firewallに紐づいているENIにElastic IPを紐づけてしまえばおそらく自分がうまくいかなかったパターンでもうまくいく可能性があると思われる。
Network Firewallの画面
以下のような画面になっている。なかなか見慣れないものだと思う
サブネットとVPCエンドポイントが表示されている。
設定の動機状況や、ログの記録設定がある。アラートとフローの両方で記録できる。別途TLSのログも設定できるようだ。
ファイアーウォールポリシー。個々のルール設定に飛べる。ここはキャパシティユニットを持っているためWAFに近いような印象がある。
モニタリング画面。パケット単位でのメトリクスをトラッキングできる。
ログはどうなっているか
フローログ、アラートログがCloudWatch Logsに記録されている。この設定だとおそらくアラートログは出ないので、フローログを見てみる。このようなフォーマットになっているようだ。
{
"firewall_name": "nfw-vpc-firewall",
"availability_zone": "ap-northeast-1a",
"event_timestamp": "1740931497",
"event": {
"tcp": {
"tcp_flags": "1b",
"syn": true,
"fin": true,
"psh": true,
"ack": true
},
"app_proto": "unknown",
"src_ip": "10.10.4.251",
"src_port": 40546,
"netflow": {
"pkts": 17,
"bytes": 3827,
"start": "2025-03-02T15:58:55.091200+0000",
"end": "2025-03-02T15:59:04.173268+0000",
"age": 9,
"min_ttl": 126,
"max_ttl": 126
},
"event_type": "netflow",
"flow_id": 2080554071012014,
"dest_ip": "52.119.XXX.XX",
"proto": "TCP",
"dest_port": 443,
"timestamp": "2025-03-02T16:04:57.455833+0000"
}
}
全体コード
全体のコードは以下の通り
main.tf
variable "vpc_name" {
type = string
description = "Name of VPC"
default = "nfw-vpc"
}
variable "vpc_cidr_block" {
type = string
description = "CIDR block of VPC"
default = "10.10.0.0/20"
}
variable "availability_zones" {
type = list(string)
description = "List of availability zones"
default = ["ap-northeast-1a", "ap-northeast-1c"]
# default = ["ap-northeast-1a", "ap-northeast-1c", "ap-northeast-1d"]
}
# Define a VPC
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr_block
enable_dns_support = true
enable_dns_hostnames = true
assign_generated_ipv6_cidr_block = true
tags = {
Name = var.vpc_name
}
}
private.tf
プライベートサブネットとルートテーブル
# Private subnet
# (10.0.0.0/20 -> [10.0.4.0/23, 10.0.6.0/23, 10.0.8.0/23])
resource "aws_subnet" "private" {
count = length(var.availability_zones)
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(aws_vpc.main.cidr_block, 3, 2 + count.index)
ipv6_cidr_block = cidrsubnet(aws_vpc.main.ipv6_cidr_block, 8, 4 + count.index)
availability_zone = element(var.availability_zones, count.index)
map_public_ip_on_launch = false
assign_ipv6_address_on_creation = true
tags = {
Name = "${var.vpc_name}-private-${count.index + 1}"
}
}
# プライベートサブネット用ルートテーブル(サブネットごとに定義)
resource "aws_route_table" "private" {
count = length(var.availability_zones)
vpc_id = aws_vpc.main.id
tags = {
Name = "${var.vpc_name}-private-route-table-${count.index}"
}
}
# Firewallエンドポイントの取得
data "aws_networkfirewall_firewall" "endpoint_ids" {
name = aws_networkfirewall_firewall.main.name
depends_on = [aws_networkfirewall_firewall.main]
}
locals {
firewall_endpoints = {
for ss in data.aws_networkfirewall_firewall.endpoint_ids.firewall_status[0].sync_states :
ss.availability_zone => ss.attachment[0].endpoint_id
}
}
# 各プライベートサブネットからの通信をファイアーウォールサブネットに振り分ける
resource "aws_route" "private_through_firewall" {
count = length(var.availability_zones)
route_table_id = aws_route_table.private[count.index].id
destination_cidr_block = "0.0.0.0/0"
# プライベートサブネットの AZ と同じ AZ の Firewall エンドポイントを使用
vpc_endpoint_id = lookup(local.firewall_endpoints, aws_subnet.private[count.index].availability_zone, null)
depends_on = [data.aws_networkfirewall_firewall.endpoint_ids]
}
# 各ルートテーブルを各サブネットに関連付ける
resource "aws_route_table_association" "private_assoc" {
count = length(aws_subnet.private)
subnet_id = aws_subnet.private[count.index].id
route_table_id = aws_route_table.private[count.index].id
}
output "firewall_endpoints" {
value = data.aws_networkfirewall_firewall.endpoint_ids.firewall_status
}
firewall.tf
Network Firewallの定義とサブネット、ルートテーブル
# Firewall subnet (for Network Firewall)
# (10.0.0.0/20 -> [10.0.10.0/27, 10.0.10.32/27, 10.0.10.64/27])
resource "aws_subnet" "firewall" {
count = length(var.availability_zones)
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(aws_vpc.main.cidr_block, 7, 96 + count.index)
ipv6_cidr_block = cidrsubnet(aws_vpc.main.ipv6_cidr_block, 8, 12 + count.index)
availability_zone = var.availability_zones[count.index]
map_public_ip_on_launch = false
assign_ipv6_address_on_creation = true
tags = {
Name = "${var.vpc_name}-firewall-${count.index + 1}"
}
}
# Network Firewall
resource "aws_networkfirewall_firewall" "main" {
name = "${var.vpc_name}-firewall"
firewall_policy_arn = aws_networkfirewall_firewall_policy.main.arn
vpc_id = aws_vpc.main.id
// 各 Firewall サブネットを subnet_mapping ブロックとして追加
dynamic "subnet_mapping" {
for_each = aws_subnet.firewall
content {
subnet_id = subnet_mapping.value.id
}
}
tags = {
Name = "${var.vpc_name}-network-firewall"
}
}
# Firewall Policy
resource "aws_networkfirewall_firewall_policy" "main" {
name = "${var.vpc_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.example.arn
}
# ステートレスルールグループの参照
stateless_rule_group_reference {
priority = 10
resource_arn = aws_networkfirewall_rule_group.stateless_example.arn
}
# ログ設定
stateful_engine_options {
rule_order = "DEFAULT_ACTION_ORDER"
}
}
tags = {
Name = "${var.vpc_name}-firewall-policy"
}
}
# Firewall Rule Group (ステートフルルール)
resource "aws_networkfirewall_rule_group" "example" {
capacity = 100
name = "${var.vpc_name}-rule-group"
type = "STATEFUL"
rule_group {
rules_source {
stateful_rule {
action = "PASS"
header {
destination = "ANY"
destination_port = "80"
direction = "ANY"
protocol = "HTTP"
source = "ANY"
source_port = "ANY"
}
rule_option {
keyword = "sid"
settings = ["1"]
}
}
# HTTPS トラフィックを許可するルールを追加
stateful_rule {
action = "PASS"
header {
destination = "ANY"
destination_port = "443" # HTTPS のポート
direction = "ANY"
protocol = "TCP" # HTTPS は TCP
source = "ANY"
source_port = "ANY"
}
rule_option {
keyword = "sid"
settings = ["2"]
}
}
}
}
tags = {
Name = "${var.vpc_name}-rule-group"
}
}
# ステートレスルールグループの作成
resource "aws_networkfirewall_rule_group" "stateless_example" {
capacity = 100
name = "${var.vpc_name}-stateless-rule-group"
type = "STATELESS"
rule_group {
rules_source {
stateless_rules_and_custom_actions {
# 80番(HTTP)を許可するルール
stateless_rule {
priority = 1
rule_definition {
actions = ["aws:pass"]
match_attributes {
protocols = [6] # TCP
source {
address_definition = "10.0.0.0/16"
}
destination {
address_definition = "0.0.0.0/0"
}
destination_port {
from_port = 80
to_port = 80
}
}
}
}
# 443番(HTTPS)を許可するルール
stateless_rule {
priority = 2
rule_definition {
actions = ["aws:pass"]
match_attributes {
protocols = [6] # TCP
source {
address_definition = "10.0.0.0/16"
}
destination {
address_definition = "0.0.0.0/0"
}
destination_port {
from_port = 443
to_port = 443
}
}
}
}
# エフェメラルポート(1024~65535)を許可するルール
stateless_rule {
priority = 3
rule_definition {
actions = ["aws:pass"]
match_attributes {
protocols = [6] # TCP
source {
address_definition = "10.0.0.0/16"
}
destination {
address_definition = "0.0.0.0/0"
}
destination_port {
from_port = 1024
to_port = 65535
}
}
}
}
}
}
}
tags = {
Name = "${var.vpc_name}-stateless-rule-group"
}
}
# Firewall用のルートテーブル(全てNATに振り分ける)
resource "aws_route_table" "firewall" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_nat_gateway.nat_gw.id
}
tags = {
Name = "${var.vpc_name}-firewall-route-table"
}
}
# ルートテーブルを各サブネットに関連付ける
resource "aws_route_table_association" "firewall_assoc" {
count = length(aws_subnet.firewall)
subnet_id = aws_subnet.firewall[count.index].id
route_table_id = aws_route_table.firewall.id
}
# ファイアウォールのログ設定
resource "aws_cloudwatch_log_group" "flow" {
name = "/aws/network-firewall/${var.vpc_name}/flow"
retention_in_days = 7
}
resource "aws_cloudwatch_log_group" "alert" {
name = "/aws/network-firewall/${var.vpc_name}/alert"
retention_in_days = 7
}
# ファイアウォールにログ設定を追加
resource "aws_networkfirewall_logging_configuration" "logging" {
firewall_arn = aws_networkfirewall_firewall.main.arn
# 3個までしかロギングできない
logging_configuration {
# フローログ
log_destination_config {
log_destination = {
logGroup = aws_cloudwatch_log_group.flow.name
}
log_destination_type = "CloudWatchLogs"
log_type = "FLOW"
}
# アラートログ
log_destination_config {
log_destination = {
logGroup = aws_cloudwatch_log_group.alert.name
}
log_destination_type = "CloudWatchLogs"
log_type = "ALERT"
}
}
}
public.tf
パブリックサブネットとルートテーブル
# Internet gateway
resource "aws_internet_gateway" "igw" {
vpc_id = aws_vpc.main.id
tags = {
Name = "${var.vpc_name}-igw"
}
}
# Public subnet
# (10.0.0.0/20 -> [10.0.0.0/24, 10.0.1.0/24, 10.0.2.0/24])
# Network firewallが複数AZ分必要になるのでパブリックは1個にする
resource "aws_subnet" "public" {
count = length(var.availability_zones)
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(aws_vpc.main.cidr_block, 4, count.index)
ipv6_cidr_block = cidrsubnet(aws_vpc.main.ipv6_cidr_block, 8, count.index)
availability_zone = element(var.availability_zones, count.index)
map_public_ip_on_launch = false
assign_ipv6_address_on_creation = true
tags = {
Name = "${var.vpc_name}-public-${count.index + 1}"
}
}
# Elastic IPの割り当て(NATゲートウェイ用)
resource "aws_eip" "nat" {
domain = "vpc"
tags = {
Name = "${var.vpc_name}-nat-eip"
}
}
# NATゲートウェイ(パブリックサブネットの1個目のAZのみおく)
resource "aws_nat_gateway" "nat_gw" {
allocation_id = aws_eip.nat.id
subnet_id = aws_subnet.public[0].id
tags = {
Name = "${var.vpc_name}-nat-eip"
}
}
# Public route table (インターネットゲートウェイに通す)
resource "aws_route_table" "public" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.igw.id
}
tags = {
Name = "${var.vpc_name}-public-route-table"
}
}
# 各サブネットを関連付ける
resource "aws_route_table_association" "public_assoc" {
count = length(aws_subnet.public)
subnet_id = aws_subnet.public[count.index].id
route_table_id = aws_route_table.public.id
}
ec2.tf
実験用のEC2
# Amazon Linux 2023
data "aws_ami" "amazon_linux" {
most_recent = true
owners = ["137112412989"] # AmazonのAMI所有者ID
filter {
name = "name"
# Amazon Linux 2023 AMIの名前パターン。minimumを除外する
values = ["al2023-ami-2023*-kernel-*-x86_64"]
}
filter {
name = "architecture"
values = ["x86_64"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
}
# Security Group for Sesson Mangaer Instances
resource "aws_security_group" "ssm_instance_sg" {
name = "ssm-instance-sg"
description = "Security group for session manager instances"
vpc_id = aws_vpc.main.id
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
description = "Allow all outbound traffic"
}
tags = {
Name = "ssm-instance-sg"
}
}
# Session Manager 利用用IAMロールとインスタンスプロファイル
resource "aws_iam_role" "ec2_ssm_role" {
name = "ec2-session-manager-role"
assume_role_policy = jsonencode({
Version = "2012-10-17",
Statement = [{
Effect = "Allow",
Principal = {
Service = "ec2.amazonaws.com"
},
Action = "sts:AssumeRole"
}]
})
}
resource "aws_iam_role_policy_attachment" "ssm_attach" {
role = aws_iam_role.ec2_ssm_role.name
policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}
resource "aws_iam_instance_profile" "ec2_instance_profile" {
name = "ec2-instance-profile"
role = aws_iam_role.ec2_ssm_role.name
}
# EC2 Instance to test the connection
resource "aws_instance" "test_instance" {
for_each = {
for idx, subnet in aws_subnet.private : "private-${idx}" => subnet.id
}
ami = data.aws_ami.amazon_linux.id
instance_type = "t3.micro"
subnet_id = each.value
vpc_security_group_ids = [aws_security_group.ssm_instance_sg.id]
iam_instance_profile = aws_iam_instance_profile.ec2_instance_profile.name
tags = {
Name = "test-instance-${each.key}"
}
}
所感
- Network Firewallが重要になる場面はあるんだろうけど、普段使いのVPCでアプリケーションと併用するのは結構大変そう
- よく問題に出てる例のように、Transit Gatewayで一回中央集権的なFirewall Subnetにルーティングして、そこからインターネットゲートウェイに出すというふうにVPC自体を疎結合にしてしまったほうが楽そうな感じはした
- Network Firewall使うの初めてで、割とこれめっちゃハマったので素直に動いて嬉しい。これやってみるとどういう感じかわかる
- Network Firewallは高めなので
terraform destroy
をお忘れずに
Shikoan's ML Blogの中の人が運営しているサークル「じゅ~しぃ~すくりぷと」の本のご案内
技術書コーナー
北海道の駅巡りコーナー