はじめに
金融IT本部 2年目の坂江 克斗です。
業務にてドメインベースでのアウトバウンド通信制限を考えるタイミングがあったため、本記事を書きました。
DNSに関する基本的な内容はこちらの記事に、アウトバウンドセキュリティの概要に関してはこちらの記事に記載しているので、気になる方はぜひ参照してみてください。
- はじめに
- 概要
- ドメインベースのアウトバウンド通信制御の検証
- Amazon Route 53 Resolver DNS Firewallの検証
- AWS Network Firewallの検証
- AWS Network Firewall Proxyの検証
- まとめ
- おわりに
概要
以下の表にAWSにおけるアウトバウンドセキュリティサービスの一覧を示します。
| レイヤ | 制御観点 | AWSサービス | 料金 | 特徴 |
|---|---|---|---|---|
| DNS(名前解決) | ドメイン名 | Amazon Route 53 Resolver DNS Firewall | 低(DNSクエリ) | 早期に遮断可能。ローカルでの名前解決やIP直打ち、独自DNS使用等のRoute 53 Resolverを経由しない通信は防御不可 |
| L3–4 | IP / Port | Security Groups / NACL | 無料 | ネットワークセキュリティのベース |
| L3–7 | IP / Port, TCPヘッダ / HTTP ヘッダ / TLS ヘッダ | AWS Network Firewall, AWS Gateway Load Balancer + サードパーティソフト | 高(AZ毎の常時稼働エンドポイント+処理量) | 柔軟な制御が可能で、マネージドにも運用可能。ドメイン制御において、Host ヘッダのスプーフィングは防御不可、SNIスプーフィングには対応可能※1。 |
| DNS + L3–7 | IP / Port, HTTP ヘッダ / TLS ヘッダ | AWS Network Firewall Proxy | 未発表 | マネージドなフォワードプロキシサービス(通信主体は Proxy)。現状はプレビュー公開中でボディ解析は未提供 |
※1 Network Firewall の TLS インスペクション機能を利用することで対策可能
今回は業務要件で挙がったドメインベースのアウトバウンド制御手法に関して、Amazon Route 53 Resolver DNS Firewall・AWS Network Firewall ・ AWS Network Firewall Proxy で比較検証を行います。
ドメインベースのアウトバウンド通信制御の検証
前提
- 検証のためローカルで実装します。
- EC2をプライベートサブネットに配置し、NATゲートウェイ経由でのAWSサービス・外部サービスへのアクセスを想定します。
- Allow List により検証します。
- 本検証では、公開されている Web サイトに対して、通信制御の挙動を確認する目的で少数のリクエストを送信しており、サービスの可用性や機密性に影響を与える行為は行っていません。
本記事では、検証のシンプルさや負荷の低さ(数リクエスト程度)からテックブログ用ドメインによる検証を行いました。
しかし、本来はテスト手法に問題があり予期せず過大な負荷をかけてしまった場合など、規約違反となる可能性があるため、IANA(現在はICANNの一部)が管理するテスト用ドメイン(example.com等)を使用し検証を行うことが最も適切となります。
Amazon Route 53 Resolver DNS Firewallの検証
Terraformの実装
以下に示すシンプルな構成で検証します。

初めにVPCおよびEC2の定義をします。
terraform { required_version = "~> 1.14.0" required_providers { aws = { version = "6.23.0" source = "hashicorp/aws" } } } provider "aws" { region = "ap-northeast-1" } data "aws_region" "current" {} data "aws_partition" "current" {} data "aws_caller_identity" "current" {} ########################################################################### ## VPC resource "aws_vpc" "main" { cidr_block = "10.0.0.0/16" enable_dns_hostnames = true enable_dns_support = true tags = { Project = "example" } } resource "aws_internet_gateway" "igw" { vpc_id = aws_vpc.main.id tags = { Project = "example" } } ## Subnet resource "aws_subnet" "public_a" { vpc_id = aws_vpc.main.id cidr_block = "10.0.1.0/24" availability_zone = "ap-northeast-1a" map_public_ip_on_launch = true tags = { Project = "example" } } resource "aws_subnet" "private_a" { vpc_id = aws_vpc.main.id cidr_block = "10.0.2.0/24" availability_zone = "ap-northeast-1a" map_public_ip_on_launch = false tags = { Project = "example" } } ## Root Table resource "aws_route_table" "public_a" { vpc_id = aws_vpc.main.id route { cidr_block = "0.0.0.0/0" gateway_id = aws_internet_gateway.igw.id } tags = { Project = "example" } } resource "aws_route_table" "private_a" { vpc_id = aws_vpc.main.id route { cidr_block = "0.0.0.0/0" nat_gateway_id = aws_nat_gateway.ngw.id } tags = { Project = "example" } } resource "aws_route_table_association" "public_a" { subnet_id = aws_subnet.public_a.id route_table_id = aws_route_table.public_a.id } resource "aws_route_table_association" "private_a" { subnet_id = aws_subnet.private_a.id route_table_id = aws_route_table.private_a.id } ## NAT resource "aws_eip" "ngw" { } resource "aws_nat_gateway" "ngw" { depends_on = [aws_internet_gateway.igw] allocation_id = aws_eip.ngw.id subnet_id = aws_subnet.public_a.id tags = { Project = "example" } } ########################################################################### ## Instance data "aws_ami" "amazon_linux_2023" { most_recent = true owners = ["amazon"] filter { name = "name" values = ["al2023-ami-*-x86_64"] } } resource "aws_instance" "amazon_linux" { ami = data.aws_ami.amazon_linux_2023.id instance_type = "t2.micro" subnet_id = aws_subnet.private_a.id iam_instance_profile = aws_iam_instance_profile.ec2.name vpc_security_group_ids = [ aws_security_group.ec2.id, ] tags = { Project = "example" } } ## Security Group resource "aws_security_group" "ec2" { name = "ec2-sg" vpc_id = aws_vpc.main.id egress { from_port = 443 to_port = 443 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] } egress { from_port = 80 to_port = 80 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] } } ## IAM resource "aws_iam_instance_profile" "ec2" { name = "ec2-profile" role = aws_iam_role.ec2.name } resource "aws_iam_role" "ec2" { name = "ec2-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" { role = aws_iam_role.ec2.name policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore" }
次に、Route53 Resolverのログを定義します。
Route53 ResolverはVPC単位で適用されるため、ログの紐付けもVPC単位となります。
## Resolver Log resource "aws_cloudwatch_log_group" "example" { name = "/aws/route53/resolver-firewall/example" tags = { Project = "example" } } ### https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route53_resolver_query_log_config resource "aws_route53_resolver_query_log_config" "example" { name = "example" destination_arn = aws_cloudwatch_log_group.example.arn tags = { Project = "example" } } ### https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route53_resolver_query_log_config_association resource "aws_route53_resolver_query_log_config_association" "example" { resolver_query_log_config_id = aws_route53_resolver_query_log_config.example.id resource_id = aws_vpc.main.id }
Route53 Resolver Firewallを定義します。
使用するリソースは以下の5つです。Route53 Resolverログと同様に、VPC単位での紐付けを行います。
- aws_route53_resolver_firewall_config
- ファイアウォールの基本設定
- aws_route53_resolver_firewall_rule_group
- 複数のファイアウォールルールを紐付けるルールグループ
- aws_route53_resolver_firewall_rule_group_association
- VPCとルールグループを紐付け
- aws_route53_resolver_firewall_rule(Allow List / Deny List方式で変動)
- ドメインリストを基に許可・拒否・アラートのアクションを設定したルール
- aws_route53_resolver_firewall_domain_list(Allow List / Deny List方式で変動)
- フィルタリングに使用するドメインのリスト
## Firewall Config ### https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route53_resolver_firewall_config resource "aws_route53_resolver_firewall_config" "main" { resource_id = aws_vpc.main.id firewall_fail_open = "DISABLED" } ## Firewall Rule Group ### https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route53_resolver_firewall_rule_group resource "aws_route53_resolver_firewall_rule_group" "example" { name = "example" tags = { Project = "example" } } ## Firewall Rule Group Association with VPC ### https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route53_resolver_firewall_rule_group_association resource "aws_route53_resolver_firewall_rule_group_association" "example" { name = "example" vpc_id = aws_vpc.main.id firewall_rule_group_id = aws_route53_resolver_firewall_rule_group.example.id priority = 101 # 100は予約済み }
Allow List では、広範囲のドメインに対するBlockを定義後に、Allowする設定を追加することで実装することが出来ます。
今回は、SSM接続用のAWSドメインと本テックブログtech.dentsusoken.comへのアクセスのみを許可し、その他のドメインをすべて拒否するように設定します。
Allow Listの設定負荷を減らすためfirewall_domain_redirection_action = "TRUST_REDIRECTION_DOMAIN"とすることで、初めに解決しに行ったドメインを信頼し CNAME 後の検証はしない設定にしています。
## Firewall Domain List ### https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route53_resolver_firewall_domain_list resource "aws_route53_resolver_firewall_domain_list" "block_example" { name = "block-example" domains = ["*."] tags = { Project = "example" } } resource "aws_route53_resolver_firewall_domain_list" "allow_example" { name = "allow-example" domains = ["tech.dentsusoken.com", "*.amazonaws.com", "*.cloudfront.net"] tags = { Project = "example" } } ## Firewall Rule (Associated with Firewall Rule Group) ### https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route53_resolver_firewall_rule resource "aws_route53_resolver_firewall_rule" "block_example" { name = "block-example" action = "BLOCK" block_response = "NXDOMAIN" firewall_domain_list_id = aws_route53_resolver_firewall_domain_list.block_example.id firewall_rule_group_id = aws_route53_resolver_firewall_rule_group.example.id firewall_domain_redirection_action = "INSPECT_REDIRECTION_DOMAIN" priority = 200 } resource "aws_route53_resolver_firewall_rule" "allow_example" { name = "allow-example" action = "ALLOW" firewall_domain_list_id = aws_route53_resolver_firewall_domain_list.allow_example.id firewall_rule_group_id = aws_route53_resolver_firewall_rule_group.example.id firewall_domain_redirection_action = "TRUST_REDIRECTION_DOMAIN" priority = 100 }
デプロイ後の検証
terraform applyの実行後、エラーなく3分程度で完了しました。
Allow List において、digコマンドによる名前解決を見ると以下の結果となります。
想定通りtech.dentsusoken.comの名前解決は成功し、www.dentsusoken.comの名前解決は拒否されていることが確認できました。
sh-5.2$ dig tech.dentsusoken.com ; <<>> DiG 9.18.33 <<>> tech.dentsusoken.com ;; global options: +cmd ;; Got answer: ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 62350 ;; flags: qr rd ra; QUERY: 1, ANSWER: 3, AUTHORITY: 0, ADDITIONAL: 1 ;; OPT PSEUDOSECTION: ; EDNS: version: 0, flags:; udp: 4096 ;; QUESTION SECTION: ;tech.dentsusoken.com. IN A ;; ANSWER SECTION: tech.dentsusoken.com. 300 IN CNAME hatenablog.com. hatenablog.com. 54 IN A 35.75.255.9 hatenablog.com. 54 IN A 54.199.90.60 ;; Query time: 0 msec ;; SERVER: 10.0.0.2#53(10.0.0.2) (UDP) ;; WHEN: Wed Dec 17 12:52:28 UTC 2025 ;; MSG SIZE rcvd: 106 sh-5.2$ dig www.dentsusoken.com ; <<>> DiG 9.18.33 <<>> www.dentsusoken.com ;; global options: +cmd ;; Got answer: ;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 17641 ;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 1 ;; OPT PSEUDOSECTION: ; EDNS: version: 0, flags:; udp: 4096 ;; QUESTION SECTION: ;www.dentsusoken.com. IN A ;; Query time: 0 msec ;; SERVER: 10.0.0.2#53(10.0.0.2) (UDP) ;; WHEN: Wed Dec 17 12:52:40 UTC 2025 ;; MSG SIZE rcvd: 48
ローカルでの名前解決として、digコマンドで取得したIPアドレスを基にhostsの書き換えによるドメインアクセスを検証します。
Route 53 Resolverを経由せずに名前解決が可能になったことで、拒否されるはずのwww.dentsusoken.comへの通信が可能になっていることが確認できました。
sh-5.2$ curl https://www.dentsusoken.com curl: (6) Could not resolve host: www.dentsusoken.com sh-5.2$ sudo vi /etc/hosts sh-5.2$ sudo cat /etc/hosts 127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain4 ::1 localhost6 localhost6.localdomain6 3.173.219.57 www.dentsusoken.com sh-5.2$ curl https://www.dentsusoken.com <!DOCTYPE html> (中略) <link rel="canonical" href="https://www.dentsusoken.com/" /> <meta name="description" content="株式会社電通総研の公式ホームページです。2024年1月1日に社名をISID(電通国際情報サービス)から変更しました。 お客様の業務課題に対応するソリューションや導入事例のほか、企業情報、IR情報、採用情報等をご紹介しています。" /> <meta property="og:site_name" content="電通総研" /> (中略) </html>
AWS Network Firewallの検証
Terraformの実装
以下に示すシンプルな構成で検証します。

初めにVPCおよびEC2の定義をします。
VPCに関して、Network Firewall用のサブネットが存在すること、EC2とNATゲートウェイの通信において Firewall Endpoint を経由するルーティングを設定することに注意が必要です。
terraform { required_version = "~> 1.14.0" required_providers { aws = { version = "6.23.0" source = "hashicorp/aws" } } } provider "aws" { region = "ap-northeast-1" } data "aws_region" "current" {} data "aws_partition" "current" {} data "aws_caller_identity" "current" {} ########################################################################### ## VPC resource "aws_vpc" "main" { cidr_block = "10.0.0.0/16" enable_dns_hostnames = true enable_dns_support = true tags = { Project = "example" } } resource "aws_internet_gateway" "igw" { vpc_id = aws_vpc.main.id tags = { Project = "example" } } ## Subnet resource "aws_subnet" "public_a" { vpc_id = aws_vpc.main.id cidr_block = "10.0.1.0/24" availability_zone = "ap-northeast-1a" map_public_ip_on_launch = true tags = { Project = "example" } } resource "aws_subnet" "private_nfw_a" { vpc_id = aws_vpc.main.id cidr_block = "10.0.2.0/24" availability_zone = "ap-northeast-1a" map_public_ip_on_launch = false tags = { Project = "example" } } resource "aws_subnet" "private_a" { vpc_id = aws_vpc.main.id cidr_block = "10.0.3.0/24" availability_zone = "ap-northeast-1a" map_public_ip_on_launch = false tags = { Project = "example" } } ## Root Table resource "aws_route_table" "public_a" { vpc_id = aws_vpc.main.id route { cidr_block = "0.0.0.0/0" gateway_id = aws_internet_gateway.igw.id } route { cidr_block = "10.0.3.0/24" vpc_endpoint_id = tolist(tolist(tolist(aws_networkfirewall_firewall.example.firewall_status)[0].sync_states)[0].attachment)[0].endpoint_id } tags = { Project = "example" } } resource "aws_route_table_association" "public_a" { subnet_id = aws_subnet.public_a.id route_table_id = aws_route_table.public_a.id } resource "aws_route_table" "private_nfw_a" { vpc_id = aws_vpc.main.id route { cidr_block = "0.0.0.0/0" nat_gateway_id = aws_nat_gateway.ngw.id } tags = { Project = "example" } } resource "aws_route_table_association" "private_nfw_a" { subnet_id = aws_subnet.private_nfw_a.id route_table_id = aws_route_table.private_nfw_a.id } resource "aws_route_table" "private_a" { vpc_id = aws_vpc.main.id route { cidr_block = "0.0.0.0/0" vpc_endpoint_id = tolist(tolist(tolist(aws_networkfirewall_firewall.example.firewall_status)[0].sync_states)[0].attachment)[0].endpoint_id } tags = { Project = "example" } } resource "aws_route_table_association" "private_a" { subnet_id = aws_subnet.private_a.id route_table_id = aws_route_table.private_a.id } ## NAT resource "aws_eip" "ngw" { } resource "aws_nat_gateway" "ngw" { depends_on = [aws_internet_gateway.igw] allocation_id = aws_eip.ngw.id subnet_id = aws_subnet.public_a.id tags = { Project = "example" } } ########################################################################### ## Instance data "aws_ami" "amazon_linux_2023" { most_recent = true owners = ["amazon"] filter { name = "name" values = ["al2023-ami-*-x86_64"] } } resource "aws_instance" "amazon_linux" { depends_on = [aws_networkfirewall_firewall.example] ami = data.aws_ami.amazon_linux_2023.id instance_type = "t2.micro" subnet_id = aws_subnet.private_a.id iam_instance_profile = aws_iam_instance_profile.ec2.name vpc_security_group_ids = [ aws_security_group.ec2.id, ] tags = { Project = "example" } } ## Security Group resource "aws_security_group" "ec2" { name = "ec2-sg" vpc_id = aws_vpc.main.id egress { from_port = 443 to_port = 443 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] } egress { from_port = 80 to_port = 80 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] } } ## IAM resource "aws_iam_instance_profile" "ec2" { name = "ec2-profile" role = aws_iam_role.ec2.name } resource "aws_iam_role" "ec2" { name = "ec2-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" { role = aws_iam_role.ec2.name policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore" }
Network Firewallの定義をします。
今回は他VPCへの共有もなく、シンプルな構成となるため必要なリソースは以下の4種類となります。Firewall はサブネット単位で紐付けます。
また、aws_networkfirewall_logging_configurationのログタイプをALERTに設定することで、ALERTまたはDROPルールにマッチした通信のみログに出力されるようになります。
- aws_networkfirewall_firewall
- aws_networkfirewall_logging_configuration
- ログ設定
- aws_networkfirewall_firewall_policy(Allow List / Deny List 方式で変動)
- 1つ以上のルールグループを紐付け、適用する順序や優先度を設定可能なポリシー
- aws_networkfirewall_rule_group(Allow List / Deny List 方式で変動)
- 1つ以上のステートレス/ステートフルルールを設定可能なルールグループ
## Network Firewall (+ Firewall Endpoint) ### https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/networkfirewall_firewall#enabled_analysis_types-1 resource "aws_networkfirewall_firewall" "example" { name = "example" firewall_policy_arn = aws_networkfirewall_firewall_policy.example.arn vpc_id = aws_vpc.main.id enabled_analysis_types = [] # HTTP、HTTPS通信の解析をしてレポートを出力、ドメインリスト作成に活用可能 firewall_policy_change_protection = false # 検証のため subnet_change_protection = false # 検証のため subnet_mapping { subnet_id = aws_subnet.private_nfw_a.id } tags = { Project = "example" } } ## Log Config resource "aws_cloudwatch_log_group" "nfw" { name = "/aws/vpc/network-firewall/example" tags = { Project = "example" } } ### https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/networkfirewall_logging_configuration resource "aws_networkfirewall_logging_configuration" "example" { firewall_arn = aws_networkfirewall_firewall.example.arn logging_configuration { log_destination_config { log_destination = { logGroup = aws_cloudwatch_log_group.nfw.name } log_destination_type = "CloudWatchLogs" log_type = "ALERT" # FLOWの場合はPassルールもログに出力 } } }
Allow List の場合は、明示的にSTRICT_ORDERかつdrop:establishedにすることでデフォルトがDenyになることから、以下の実装となります。
この時、SSM接続のためにAWSサービス用のドメイン名を許可しておく必要があります。
## Firewall Rule Group ### https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/networkfirewall_rule_group resource "aws_networkfirewall_rule_group" "example" { capacity = 100 # ルール毎のキャパシティに合わせて手動設定必要 name = "example" type = "STATEFUL" rule_group { rules_source { rules_source_list { generated_rules_type = "ALLOWLIST" target_types = ["HTTP_HOST", "TLS_SNI"] targets = ["tech.dentsusoken.com", ".amazonaws.com", ".cloudfront.net"] } } stateful_rule_options { rule_order = "STRICT_ORDER" } } tags = { Project = "example" } } ## Network Firewall Policy ### https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/networkfirewall_firewall_policy#stateful-rule-group-reference resource "aws_networkfirewall_firewall_policy" "example" { name = "firewall-policy-example" firewall_policy { stateless_default_actions = ["aws:forward_to_sfe"] stateless_fragment_default_actions = ["aws:forward_to_sfe"] stateful_engine_options { rule_order = "STRICT_ORDER" } stateful_default_actions = ["aws:drop_established"] stateful_rule_group_reference { priority = 1 resource_arn = aws_networkfirewall_rule_group.example.arn } } tags = { Project = "example" } }
デプロイ後の検証
terraform applyの実行後、エラーなく6分程度で完了しました。
Allow List において、curlコマンドによるサイトへのアクセスを実行すると、以下の結果となります。
想定通りtech.dentsusoken.comへのアクセスは許可され、www.dentsusoken.comへのアクセスはブロックされていることが確認できました。
sh-5.2$ curl https://tech.dentsusoken.com <!DOCTYPE html> <html lang="ja" data-admin-domain="//blog.hatena.ne.jp" data-admin-origin="https://blog.hatena.ne.jp" data-author="dentsusoken" data-avail-langs="ja en" data-blog="isid.hatenablog.com" data-blog-host="isid.hatenablog.com" data-blog-is-public="1" data-blog-name="電通総研 テックブログ" data-blog-owner="dentsusoken" data-blog-show-ads="" data-blog-show-sleeping-ads="" data-blog-uri="https://tech.dentsusoken.com/" (中略) </html> sh-5.2$ curl --max-time 10 https://www.dentsusoken.com curl: (28) Connection timed out after 10002 milliseconds
DNS Firewallと同様に、hostsファイルの書き換えによるドメインアクセスも検証してみると、当然ですがDNS解決の有無に関係なく、最終的なパケットのHost・SNIヘッダでブロックしていることが確認できました。
sh-5.2$ sudo vi /etc/hosts sh-5.2$ sudo cat /etc/hosts 127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain4 ::1 localhost6 localhost6.localdomain6 3.173.219.15 www.dentsusoken.com sh-5.2$ curl --max-time 10 https://www.dentsusoken.com curl: (28) Connection timed out after 10002 milliseconds
次に以下の2パターンでスプーフィングを検証しました。
- Hostヘッダ詐称:
curl http://www.dentsusoken.com -H "Host: tech.dentsusoken.com" - SNIヘッダ詐称:
curl https://tech.dentsusoken.com --resolve tech.dentsusoken.com:443:3.173.219.15 -H "Host: www.dentsusoken.com" -k
いずれのケースでも、Allow List の判定上は許可され、ブロックされない(=フィルタを通過する)ことを確認しました。
一方で、Host ヘッダや SNI の不整合により、HTTP/TLS の処理上は正しく応答できず、期待したページ表示には至りませんでした。
以上より、スプーフィングによって「許可判定そのもの」を通過できることが分かりました(ただし、通信の成立や正しいページ表示まで保証されるわけではありません)。
sh-5.2$ curl http://www.dentsusoken.com -H "Host: tech.dentsusoken.com" <html> <head><title>301 Moved Permanently</title></head> <body> <center><h1>301 Moved Permanently</h1></center> <hr><center>CloudFront</center> </body> </html> sh-5.2$ curl https://tech.dentsusoken.com --resolve tech.dentsusoken.com:443:3.173.219.15 -H "Host: www.dentsusoken.com" -k <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> (中略) <H1>421 ERROR</H1> <H2>The request could not be satisfied.</H2> <HR noshade size="1px"> The distribution does not match the certificate for which the HTTPS connection was established with. We can't connect to the server for this app or website at this time. There might be too much traffic or a configuration error. Try again later, or contact the app or website owner. (中略) </BODY></HTML>
今回はCloudFrontを使用したWebサイトであることから、CloudFrontがHTTPS接続時に実装しているドメインフロンティング対策(SNI・Host・証明書・AWSアカウントの整合性検証)が動作した形となります。
この仕組みにより、本検証ではHostヘッダとSNIヘッダの不整合、またはHTTPのHostヘッダがTLSハンドシェイク時に提示された証明書に含まれていないことが検知され、CloudFront側で正しく防御されました。
本記事の主題であるアウトバウンドセキュリティの観点とは直接関係しませんが、サービスを公開する際には、CloudFront や ALB などのエッジサーバ、リバースプロキシによる前面での防御を組み合わせることが有効であることが分かります。
AWS Network Firewall Proxyの検証
本記事では記事のボリュームを考慮しデプロイの手順は割愛します。こちらの記事でNetwork Firewall Proxyのデプロイ手順を含め詳細を記載していますのでご参照お願いします。
以下に示すシンプルな構成(Securing Egress Architectures with Network Firewall Proxyより引用)を作成します。
(プレビュー中のサービスでありTerraform Providerでは未提供のリソースのため、手動での設定も必要となります。)

ルールの設定値としては以下になります。
- Pre-DNS:ドメイン
tech.dentsusoken.com・*.amazonaws.comをAllow。デフォルトアクションはDeny。 - Pre-Request:ルールなし。デフォルトアクションはAllow。
- Post-Response:ルールなし。デフォルトアクションはAllow。
デプロイ後の検証
Allow List において、curlコマンドによるサイトへのアクセスを実行すると、以下の結果となります。
想定通りtech.dentsusoken.comへのアクセスは許可され、www.dentsusoken.comへのアクセスはブロックされていることが確認できました。
sh-5.2$ export https_proxy="https://0a776294cb5ae3886.proxy.nfw.us-east-2.amazonaws.com:443" sh-5.2$ export http_proxy="https://0a776294cb5ae3886.proxy.nfw.us-east-2.amazonaws.com:443" sh-5.2$ curl https://tech.dentsusoken.com <!DOCTYPE html> <html lang="ja" data-admin-domain="//blog.hatena.ne.jp" data-admin-origin="https://blog.hatena.ne.jp" data-author="dentsusoken" data-avail-langs="ja en" data-blog="isid.hatenablog.com" data-blog-host="isid.hatenablog.com" data-blog-is-public="1" data-blog-name="電通総研 テックブログ" data-blog-owner="dentsusoken" data-blog-show-ads="" data-blog-show-sleeping-ads="" data-blog-uri="https://tech.dentsusoken.com/" (中略) </html> sh-5.2$ curl https://www.dentsusoken.com curl: (56) CONNECT tunnel failed, response 403 sh-5.2$
DNS Firewallと同様に、hostsファイルの書き換えによるドメインアクセスも検証してみると、当然ですがDNS解決の有無に関係なく、最終的なパケットのHost・SNIヘッダでブロックしていることが確認できました。
sh-5.2$ sudo vi /etc/hosts sh-5.2$ sudo cat /etc/hosts 127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain4 ::1 localhost6 localhost6.localdomain6 3.173.219.15 www.dentsusoken.com sh-5.2$ curl https://www.dentsusoken.com curl: (56) CONNECT tunnel failed, response 403 sh-5.2$
次に以下の2パターンでスプーフィングを検証しました。
- Hostヘッダ詐称:
curl http://www.dentsusoken.com -H "Host: tech.dentsusoken.com" - SNIヘッダ詐称:
curl https://tech.dentsusoken.com --resolve tech.dentsusoken.com:443:3.173.219.15 -H "Host: www.dentsusoken.com" -k
Hostヘッダ詐称の場合は、Allow List の判定にマッチせずに拒否されたことが確認できました。
SNIヘッダ詐称の場合は、そもそも名前解決がプロキシ側で発生するため実質的にcurl https://tech.dentsusoken.com -H "Host: www.dentsusoken.com" -kと同じ挙動となります。そのため、tech.dentsusoken.comへの名前解決が発生しながら、Hostヘッダがwww.dentsusoken.comになっていることで409エラーを返したと考えられます。
sh-5.2$ curl -v http://www.dentsusoken.com -H "Host: tech.dentsusoken.com" * Uses proxy env variable http_proxy == 'https://0a776294cb5ae3886.proxy.nfw.us-east-2.amazonaws.com:443' * Host 0a776294cb5ae3886.proxy.nfw.us-east-2.amazonaws.com:443 was resolved. * IPv6: (none) * IPv4: 10.0.1.29 * Trying 10.0.1.29:443... * TLSv1.3 (OUT), TLS handshake, Client hello (1): (中略) * TLSv1.3 (IN), TLS handshake, Newsession Ticket (4): * Request completely sent off < HTTP/1.1 403 Forbidden (中略) Forbidden * Connection #0 to host 0a776294cb5ae3886.proxy.nfw.us-east-2.amazonaws.com left intact sh-5.2$ curl -v https://tech.dentsusoken.com --resolve tech.dentsusoken.com:443:3.173.219.15 -H "Host: www.dentsusoken.com" -k * Added tech.dentsusoken.com:443:3.173.219.15 to DNS cache * Uses proxy env variable https_proxy == 'https://0a776294cb5ae3886.proxy.nfw.us-east-2.amazonaws.com:443' * Host 0a776294cb5ae3886.proxy.nfw.us-east-2.amazonaws.com:443 was resolved. * IPv6: (none) * IPv4: 10.0.1.29 * Trying 10.0.1.29:443... * ALPN: curl offers http/1.1 * TLSv1.3 (OUT), TLS handshake, Client hello (1): (中略) < HTTP/1.1 409 Conflict < Connection: close < HTTP/1.1 400 Bad Request Connection: close * TLSv1.3 (IN), TLS alert, close notify (256): * shutting down connection #0 * TLSv1.3 (OUT), TLS alert, close notify (256): * TLSv1.3 (IN), TLS alert, close notify (256): * TLSv1.3 (OUT), TLS alert, close notify (256): sh-5.2$
ただし、本検証のように Network Firewall Proxy を単体で利用している場合、--noproxyオプション等を使用しプロキシを経由しない通信をした場合には、Network Firewall Proxyのルールを無視して通信しに行くことが可能です。
そのため、Network Firewall Proxy単体ではなく他のファイアウォール系サービスとの組み合わせが重要となります。
まとめ
ドメインベースのアウトバウンド通信制御は、サブネット単位での制御要件がない限り、コストと防御効果(スプーフィングにも対応可能)のバランスに優れる Route 53 Resolver DNS Firewall を主軸に据える構成が推奨だと考えられます。
そのうえで、hosts ファイルの書き換え、IP 直打ち、独自DNSの利用などにより Route 53 Resolver を経由しない通信が成立し得る点を踏まえ、Network Firewall や Network Firewall Proxyを併用して補完的にカバーすることが重要です。
おわりに
本記事では、AWSにおける具体的なドメインベース通信制御を検証してみました。
実運用においては、マネージドルールや推奨される Suricata ルールの導入、ログの活用による継続的な改善が重要となります。
今後も、実際の運用や検証を通じて理解を深めていきたいと思います。
余談ですが、Network Firewallのapply後に削除することを忘れてしまい8日間放置してしまいました。
シングルAZ構成でFirewall エンドポイントは1つだけでしたが、それでもエンドポイント料金だけで約76$(12,000円程)のコストが発生してしまいました。
apply 後の削除忘れには改めて注意が必要ですね。
執筆:@sakae.katsuto
レビュー:Ishizawa Kento (@kent)
(Shodoで執筆されました)



