NACLの「ステートレス」という罠:AWSネットワークの現場で学ぶルール番号とエフェメラルポートの全知識
こんにちは。クラウドの底なし沼……もとい、AWSのネットワーク構築とKubernetesのクラスター設計に日々頭を悩ませているシニアSREの私です。
夜中に「Web APIのレスポンスが突然返らなくなった!」というアラートで飛び起きたとき、あなたならどこを疑いますか? セキュリティグループ(SG)を確認し、ルートテーブルを確認し、それでも原因が分からずに冷や汗をかいた経験はありませんか?
その原因、もしかすると ネットワークACL(NACL)の「ステートレス」な仕様と、ルール評価順序の罠 にあるかもしれません。
セキュリティグループが「ステートフル(往復の文脈を覚えていてくれる優しい相棒)」であるのに対し、NACLは「ステートレス(過去の記憶を一切持たない冷徹な門番)」です。この特性を理解していないと、ちょっとしたインバウンド・アウトバウンドの設定ミスで、システム全体が沈黙することになります。
今回は、パケットがAWSのサブネット境界を駆け抜けるリアルな挙動を追いながら、実務で絶対に知っておくべきNACLの核心を紐解いていきましょう。
—
1. NACLの基本原理:パケットがサブネット境界で受ける洗礼
AWSのVPCにおいて、セキュリティグループが「インスタンス(ENI)単位」のファイアウォールであるのに対し、NACLは「サブネット単位」で動作するパケットフィルタです。
サブネットに出入りするすべてのパケットは、インスタンスに到達する前(あるいはVPC外に出ていく前)に、必ずNACLの検問を受けます。
ここで重要なのが、NACLは 「ステートレス(Stateless)」 であるという点です。
ステートフル(SG)とステートレス(NACL)の決定的な違い
- セキュリティグループ(ステートフル):
クライアントからサーバーへのインバウンド通信(例: ポート443へのアクセス)を許可すると、サーバーからクライアントへ戻るアウトバウンドのレスポンスは、ルールの記述に関わらず自動的に許可されます。
- ネットワークACL(ステートレス):
インバウンドでパケットを許可しても、サーバーからクライアントへレスポンスを返すためのアウトバウンド通信も、個別のルールで明示的に許可しなければなりません。往路と復路、両方の通行手形がそれぞれ必要なのです。
—
2. ルール番号の評価順序と「最小番号ファースト」の鉄則
NACLには、複数のルールにそれぞれ「ルール番号(Rule Number)」を割り当てます。この番号の扱い方を誤ると、意図しない通信ブロックを引き起こします。
評価のメカニズム
1. NACLのルールは、ルール番号が小さい順(昇順)に評価されます。
2. パケットが最初に一致したルールが適用された時点で評価は終了し、後続のルールは無視されます(ファーストマッチ方式)。
3. すべてのカスタムルールに一致しなかった場合、デフォルトで用意されている「すべてのトラフィックを拒否するルール(通常は番号 32767)」が適用され、パケットはドロップされます。
> SREの現場の教訓:
> 「とりあえず新しいルールを追加したいから、番号は適当に999にしておこう」という雑な変更は厳禁です。既存の「拒否(Deny)」ルールよりも番号が大きければ、追加した許可ルールは永遠に評価されません。ルールの間隔は、後からの挿入を考慮して十番刻み(10, 20, 30…)で確保するのがプロの作法です。
—
3. エフェメラルポートの罠:なぜAPI通信が突如途絶えるのか?
実務で最も頻発するNACLのトラブルが、エフェメラルポート(一時ポート)の解放漏れです。
例えば、プライベートサブネットにあるバックエンドのLambdaやEC2から、外部の決済API(HTTPS)を叩くケースを考えてみましょう。
通信のライフサイクルと必要なNACL設定
1. リクエスト(アウトバウンド):
- 送信元: プライベートサブネットのプライベートIP(任意のポート)
- 送信先: 外部APIサーバーの
443ポート - 必要なNACLアウトバウンドルール: ポート
443への送信を許可
2. レスポンス(インバウンド):
- 送信元: 外部APIサーバーの
443ポート - 送信先: プライベートサブネットのIPと、OSが動的に割り当てたエフェメラルポート(通常
1024~65535のいずれか) - 必要なNACLインバウンドルール: ポート
1024-65535からの受信を許可
もし、この「インバウンドのエフェメラルポート許可」を忘れていると、往路のリクエストは外部に出ていくものの、戻ってきたレスポンスがNACLのインバウンド検問で容赦なくドロップされ、クライアント側ではタイムアウトエラーが発生します。
—
4. 実践:Terraformによる安全なNACL設定例
それでは、実務でそのまま使えるTerraformコードを見てみましょう。ここでは、Web/APIサーバーが安全に外部通信を行いつつ、外部からの不正なアクセスを防ぐためのNACL設定を定義しています。
# AWS VPC Network ACL (NACL) のサンプル定義
resource "aws_network_acl" "api_private_nacl" {
vpc_id = aws_vpc.main.id
subnet_ids = [aws_subnet.private_api.id]
# ==========================================
# インバウンドルール (Inbound Rules)
# ==========================================
# 1. 既に確立された通信や外部からの戻りパケット(エフェメラルポート)を許可
ingress {
protocol = "tcp"
rule_no = 100
action = "allow"
cidr_block = "0.0.0.0/0"
from_port = 1024
to_port = 65535
}
# 2. パブリックサブネットからのALB経由の通信(例: ポート8080)を許可
ingress {
protocol = "tcp"
rule_no = 110
action = "allow"
cidr_block = "10.0.1.0/24" # パブリックサブネットのCIDR
from_port = 8080
to_port = 8080
}
# 3. 明示的な拒否ルール(セキュリティ要件に応じたブラックリスト等)
ingress {
protocol = "-1" # すべてのプロトコル
rule_no = 900
action = "deny"
cidr_block = "192.0.2.0/24" # 例: 悪意ある特定IPレンジ
from_port = 0
to_port = 0
}
# ==========================================
# アウトバウンドルール (Outbound Rules)
# ==========================================
# 1. 外部APIやパブリックリポジトリへのHTTPS通信(ポート443)を許可
egress {
protocol = "tcp"
rule_no = 100
action = "allow"
cidr_block = "0.0.0.0/0"
from_port = 443
to_port = 443
}
# 2. 外部APIやDNSサーバーへのHTTP通信(ポート80)を許可
egress {
protocol = "tcp"
rule_no = 110
action = "allow"
cidr_block = "0.0.0.0/0"
from_port = 80
to_port = 80
}
# 3. 内部DNS(Route 53 Resolver / VPC内DNS)へのUDP 53ポートを許可
egress {
protocol = "udp"
rule_no = 120
action = "allow"
cidr_block = "10.0.0.0/16" # VPCのCIDR
from_port = 53
to_port = 53
}
tags = {
Name = "api-private-nacl"
Environment = "production"
}
}
—
5. 現場で使える!NACLトラブルシューティングとデバッグ手法
もし、あなたの管理するアプリケーションで「特定の外部API連携だけがタイムアウトする」「なぜかパケットが流れない」という事態に直面したら、以下の手順でデバッグを行ってください。
Step 1: 流量の可視化には「VPCフローログ」を使う
パケットがNACLのどこでブロックされているかを特定する最も確実な方法は、VPCフローログ(VPC Flow Logs)の有効化です。
CloudWatch LogsやAthenaに出力されたログの action カラムを確認してください。
ACCEPT: セキュリティグループやNACLを通過したパケットREJECT: どこかで拒否されたパケット
もし REJECT が記録されており、セキュリティグループ側で許可しているはずなのに通信できない場合は、NACLでパケットが落とされています。
Step 2: OS側からの簡易的な疎通確認
インスタンス内部から、対象の宛先に対して curl や Python の urllib を使って通信をテストします。
# curlによる詳細な接続トレース(SSLハンドシェイクやタイムアウトの確認)
curl -v https://api.example.com/v1/healthcheck
もしPythonコードからAPIを叩いている環境であれば、次のようなスクリプトでタイムアウトエラーの詳細をキャッチします。
import urllib.request
import urllib.error
url = "https://api.example.com/v1/healthcheck"
try:
# 5秒でタイムアウトを設定し、APIへリクエストを送信
req = urllib.request.Request(url, headers={"User-Agent": "SRE-Debug-Client/1.0"})
with urllib.request.urlopen(req, timeout=5) as response:
print(f"Status Code: {response.status}")
print(response.read().decode("utf-8"))
except urllib.error.URLError as e:
print(f"通信エラーが発生しました (NACLやセキュリティグループのブロック可能性あり): {e.reason}")
except Exception as e:
print(f"予期せぬエラー: {e}")
—
まとめ
ネットワークACL(NACL)は、VPCのサブネットを守る最後の砦であり、同時にその「ステートレス」な仕様ゆえに多くのエンジニアを悩ませるポイントでもあります。
- ルール番号は小さい順に評価され、最初にヒットしたもので決まる。
- ステートレスであるため、往路だけでなく復路(特にエフェメラルポート
1024-65535)のルールも必ずセットで記述する。 - 迷ったときはVPCフローログを有効化し、パケットがどちらの方向で
REJECTされているかを冷静に追跡する。
この3つを頭に叩き込んでおけば、どんなに複雑なマルチtierアーキテクチャのトラブルであっても、ネットワーク層の原因を迅速に切り分けることができるはずです。
あなたのインフラライフが、静かで平穏なものになりますように。それではまた別の現場でお会いしましょう!
コメント