【実務・中級編】 ネットワークACL(NACL)のステートレス処理とエフェメラルポート許可の落とし穴 – クラウド&コンテナネットワーク実践ガイド

NACLのステートレス地獄から抜け出せ!エフェメラルポート開放の罠と正しい設計・デバッグ手法

こんにちは。クラウドの裏側でパケットのルーティングやセキュリティグループの制御に日々頭を悩ませているシニアSREの私です。

AWSのVPC設計をしていると、必ずぶつかる壁がありますよねそう、ネットワークACL(NACL)です。「セキュリティグループ(SG)があるんだから、NACLなんてサブネット境界のファイアウォールは 全許可(Allow All) でいいや」と放置していませんか?

しかし、コンプライアンスの要請やゼロトラストアーキテクチャの文脈から、「サブネット間、あるいはインターネット境界で厳格なステートレスフィルタリングをかけろ」という要件は突然降って湧きます。その瞬間、あなたの目の前でこれまで正常に動いていたWeb APIや外部連携システムが、理由もわからずパタリと通信できなくなる――。

今回は、NACLの最大の罠であり、多くのインフラエンジニアが深夜のトラブルシューティングで冷や汗をかく原因となる「ステートレス処理とエフェメラルポートの許可漏れ」について、パケットの旅路を追いながら徹底的に解説します。

—

1. なぜNACLでハマるのか? ステートフルとステートレスの決定的な違い

まず、AWSのセキュリティグループ(以下、SG)とネットワークACL(NACL)の根本的な違いを整理しておきましょう。ここを混同していると、いつまで経ってもネットワークの挙動が理解できません。

  • セキュリティグループ(Stateful / ステートフル):

インバウンド(受信)で許可した通信に対するアウトバウンド(送信)の戻りパケットは、ルールに関係なく自動的に許可されます。賢いファイアウォールです。

  • ネットワークACL(Stateless / ステートレス):

パケットがサブネットの境界を通過する際、インバウンドとアウトバウンドの両方のルールを個別に、かつ厳格に評価します。パケットの文脈(「これはさっきの通信の返答だな」といった文脈)を一切記憶しません。

パケットの往復運動をシミュレーションしてみる

あなたがプライベートサブネット内のEC2インスタンスから、外部の決済APIへ HTTPS(TCP 443) でリクエストを投げたときのパケットの動きを追ってみましょう。

1. 往路(リクエスト):
EC2から外向きにパケットが飛び出します。送信元IPはEC2のプライベートIP、送信元ポートはOSが動的に割り当てたエフェメラルポート(例: 32768)、宛先ポートは 443 です。

  • NACLのアウトバウンドルール: 443 への送信を許可していれば、このパケットはサブネットの外へ出ていけます。

2. 復路(レスポンス):
外部APIサーバーからの応答パケットがサブネットに戻ってきます。送信元IPは 443、宛先IPはEC2、宛先ポートはさっきのエフェメラルポート(32768)になります。

  • NACLのインバウンドルール: ここで問題が発生します。サブネットに入ってくるパケットの「宛先ポート」は 32768 です。もし、NACLのインバウンドルールに「エフェメラルポート範囲(一般的には 1024-65535)」を許可する設定が抜けていれば、この応答パケットは容赦なくドロップされます。

結果として、クライアント側(EC2)からは「接続がタイムアウトしました (ETIMEDOUT)」に見える、というわけです。

—

2. 実務で直面するシナリオと「エフェメラルポート」の正体

「エフェメラルポート(Ephemeral Port)」とは、クライアント側が一時的に使用する一時的なポート番号のことです。OSのTCP/IPスタックが、通信のたびに動的に割り当てます。

OSやクラウドプロバイダーによって、このエフェメラルポートのデフォルト範囲は異なります。

  • Linux(現代の主流): 32768 から 60999 まで(/proc/sys/net/ipv4/ip_local_port_range で確認可能)
  • Windows Server: 49152 から 65535 まで
  • IANA(インターネット標準): 49152 から 65535(Dynamic/Private Ports)
  • AWS等での一般的なNACL設定: 安全を期して広く 1024 から 65535 までをカバーするのが定石

それでは、実務でよくある2つのパターンの設定例を見てみましょう。

パターンA: 外部APIを叩くクライアントサブネットのNACL設定例

プライベートサブネットからインターネット上のAPIへリクエストを送り、そのレスポンスを受け取るための最小限のNACLルールです。

| ルール番号 | 方向 | プロトコル | ポート範囲 | 宛先 / 送信元 | アクション | 説明 |
| :— | :— | :— | :— | :— | :— | :— |
| 100 | インバウンド | TCP | 1024-65535 | 0.0.0.0/0 | ALLOW | 【重要】APIからの応答を受け取るためのエフェメラルポート |
| 250 | インバウンド | TCP | 443 | 0.0.0.0/0 | ALLOW | (必要に応じて) インバウンドのHTTPS通信を受ける場合 |
| 32767 | インバウンド | すべて | すべて | 0.0.0.0/0 | DENY | デフォルト拒否ルール |
| 100 | アウトバウンド | TCP | 443 | 0.0.0.0/0 | ALLOW | 外部APIへ向かうHTTPSリクエスト |
| 110 | アウトバウンド | TCP | 80 | 0.0.0.0/0 | ALLOW | 外部HTTP通信用 |
| 32767 | アウトバウンド | すべて | すべて | 0.0.0.0/0 | DENY | デフォルト拒否ルール |

パターンB: 内部に踏み込み・APIサーバー(Web層)を持つサブネットのNACL設定例

逆に、ロードバランサー(ALB)からトラフィックを受け取るWebサーバーのサブネットではどうなるでしょうか?

今度は、クライアントからのリクエスト(インバウンド)の宛先が自ホストの 443 や 80 になり、レスポンスのアウトバウンドの宛先がクライアントのエフェメラルポート(1024-65535)になります。

  • インバウンドルール: 宛先ポート 443, 80 を許可
  • アウトバウンドルール: 送信先ポート 1024-65535 を許可(ここを忘れると、サーバーは処理を終えて返事をしようとしても、NACLにパケットを捨てられてしまいます)

—

3. コードとコマンドで検証する:なぜ通信が死ぬのか

現場で「NACLの設定ミスが原因か?」と疑ったとき、机上で悩んでいても始まりません。実際にパケットがどこでロスしているかを突き止めるための実践的なコードとデバッグ手順を紹介します。

検証用Pythonスクリプト(外部APIリクエスト)

以下のPythonスクリプトを、NACLが厳格に設定されたプライベートサブネット内のインスタンスで実行してみます。

import urllib.request
import urllib.error
import sys

# テスト用の公開API(例としてJSONPlaceholderを使用)
TARGET_URL = "https://jsonplaceholder.typicode.com/todos/1"

def test_outbound_api():
    print(f"Connecting to {TARGET_URL}...")
    try:
        # タイムアウトを5秒に設定してリクエスト送信
        req = urllib.request.Request(TARGET_URL, headers={'User-Agent': 'SRE-Network-Test'})
        with urllib.request.urlopen(req, timeout=5) as response:
            status_code = response.getcode()
            body = response.read().decode('utf-8')
            print(f"[SUCCESS] Status Code: {status_code}")
            print(f"[Response Body]: {body[:100]}...")
    except urllib.error.URLError as e:
        print(f"[ERROR] Connection failed: {e.reason}", file=sys.stderr)
        print("-> 💡 ヒント: NACLのアウトバウンド(443)またはインバウンドのエフェメラルポート(1024-65535)がブロックされている可能性大です。", file=sys.stderr)
    except Exception as e:
        print(f"[UNEXPECTED ERROR]: {e}", file=sys.stderr)

if __name__ == "__main__":
    test_outbound_api()

現場で使えるデバッグ・トラブルシューティング手順

もし上記スクリプトが Connection timed out で沈黙した場合、以下のステップで原因を特定します。

1. セキュリティグループの確認:
SGの送信元/送信先ルールに問題がないか確認します(SGはステートフルなので、基本的にはアウトバウンド All Traffic なら問題ありません)。
2. VPCフローログ(VPC Flow Logs)の解析:
これが最も確実です。CloudWatch Logs InsightsやAthenaを使い、該当するENI(Elastic Network Interface)のログをクエリします。

-- CloudWatch Logs Insightsで「パケットがREJECTされた原因」を特定するクエリ例
fields @timestamp, srcAddr, dstAddr, srcPort, dstPort, protocol, packets, bytes, action
| filter status = "REJECT" and interfaceId = "eni-xxxxxxxxxxxxxxxxx"
| sort @timestamp desc
| limit 20
  • クエリ結果の dstPort が 1024-65535 の範囲であり、action が REJECT になっているパケットが大量に見つかった場合、それは間違いなくNACLのインバウンドにおけるエフェメラルポートの許可漏れです。

—

4. アーキテクトが教える、NACL設計のベストプラクティス

最後に、日々の運用で涙を流さないための、NACL設計における実践的な知見をいくつか共有します。

  • 原則として「デフォルトNACL(全許可)」をベースにする

特に理由がない限り、AWSが用意しているデフォルトNACL(インバウンド・アウトバウンド共に 全許可 / Allow)をそのままアタッチしておくのが、実用上最もトラブルが少ないです。

  • NACLを適用するのは「真にセキュリティ要件が厳しいサブネット」に限定する

PCI DSSや金融系のレギュレーションなど、「サブネット間での厳格な隔離(マイクロセグメンテーション)」が明示的に求められる場合のみ、カスタムNACLを作成して適用しましょう。

  • エフェメラルポートはケチらずに広く開ける

「セキュリティを厳しくするために、エフェメラルポートの範囲を 32768-60999 に絞ろう」とする人がいますが、OSのバージョンアップやパッチ適用によってエフェメラルポートの動的割り当て範囲が変わった瞬間、システム全体が沈黙する爆弾を抱えることになります。基本的には 1024-65535 で潔く全開放するのがプロの選択です。

  • ステートレスであることを常に意識する

「ルールを追加したら、必ず往復のセット(インバウンドとアウトバウンド)で考える」。この習慣が身につくだけで、ネットワーク起因の障害対応時間は劇的に短縮されます。

—

まとめ

NACLのステートレス処理とエフェメラルポートの関係は、クラウドネットワークの基礎でありながら、多くのエンジニアを悩ませる罠です。

「なぜか外から返事が返ってこない」
「セキュリティグループは完璧なのに、通信がタイムアウトする」

そんなときは、パケットの往復を頭の中で再生し、NACLのインバウンドルールにエフェメラルポートの許可がポツンと抜けていないかを確認してください。この記事が、あなたの次の深夜対応を防ぐ防衛策となれば幸いです。

それでは、快適なクラウドライフを!

コメント

タイトルとURLをコピーしました