はじめに:なぜ、そのNATインスタンスは沈黙したのか
クラウドの世界でインフラを構築していると、マネージドサービスであるAWSの NAT Gateway ではなく、あえてAmazon EC2インスタンスを使った「自前NAT(NATインスタンス)」を組まざるを得ない場面に直面します。
コスト削減、複雑なルーティング制御、あるいは特定のセキュリティアプライアンスとの統合など、理由はさまざまです。OSのルーティング設定(net.ipv4.ip_forward=1)を書き換え、いざプライベートサブネットからの通信を流してみる……しかし、外の世界へパケットが1バイトも出ていかない。VPCフローログを確認すると、プライベートインスタンスからのパケットはNATインスタンスに届いているのに、そこから先へピクリとも動いていない。
この絶望的な状況を引き起こしている犯人こそ、AWSの仮想ネットワークレイヤーに隠された「送信元/宛先チェック(Source/Destination Check)」という名の厳格な検問です。
今回は、数々の修羅場をくぐり抜けてきたシニアSREの視点から、このチェック機構がパケットの運命をどうねじ曲げルのか、そしてなぜそれを無効化しなければならないのかを、TCP/IPの基本からAWSの内部挙動まで徹底的に紐解いていきます。
—
1. パケットがたどる運命:なぜ「送信元/宛先チェック」が存在するのか
まずは、AWSの仮想プライベートクラウド(VPC)の裏側で何が起きているのかをイメージしてください。
通常、EC2インスタンスは「自分が送信元であるパケット」または「自分宛てのパケット」しか処理しません。これはネットワークの基本であり、セキュリティの観点からも極めて真っ当な挙動です。
送信元/宛先チェックの基本動作
AWSの各ENI(Elastic Network Interface)には、デフォルトで Source/Destination Check が有効になっています。
- 送信元チェック: EC2から送信されるIPパケットの送信元IPアドレス(Source IP)が、そのENIに割り当てられたプライベートIPアドレスと一致しているかを検証します。
- 宛先チェック: EC2宛てのパケットの宛先IPアドレスが、そのENIのプライベートIPアドレスと一致しているかを検証します。
もし、このチェックに引っかかった場合(例えば、ENIに割り当てられていないIPアドレスを送信元としてパケットを送り出そうとした場合)、AWSの仮想ルーター(ハイパーバイザー層)は容赦なくそのパケットをドロップします。
NATインスタンスにおける致命的な矛盾
ここで、自前で構築したNATインスタンスの通信フローを考えてみてください。
1. プライベートサブネットのインスタンス(例: 10.0.1.100) が、インターネット上のWeb API(例: 8.8.8.8)へリクエストを投げる。
2.パケットはNATインスタンス(例: 10.0.2.50)を経由してインターネットへ向かう。
3. この時、NATインスタンスは IPマスカレード(SNAT) を行い、パケットの送信元IPアドレスを「自分のプライベートIP(10.0.2.50)」に書き換えて送り出す。
お気づきでしょうか? NATインスタンスが処理しているパケットの送信元IPアドレスは、もはや「プライベートインスタンスのIP(10.0.1.100)」であり、NATインスタンス自身のENIに割り当てられたIPとは異なります(厳密には、パケットがNAT処理される瞬間、または処理された直後の境界でこの矛盾が生じます)。
もしAWSの Source/Destination Check が有効なままだと、ハイパーバイザーは「お前、自分以外のIPアドレスを名乗ってパケットを出そうとしているな? 偽装(スプーフィング)だ!」と判断し、パケットをブラックホール送りにしてしまうのです。
—
2. 実務で必須!NATインスタンス構築の全手順とパラメータ
理屈が分かったところで、実務で迷いがちなAWS CLIによる設定変更と、OS側のカーネルパラメータの設定手順を整理します。
ステップ1: AWS側で送信元/宛先チェックを無効化する
EC2インスタンスを立ち上げた後(あるいは起動テンプレート設定時)、そのENIに紐づくフラグを明示的に false に変更します。
# 対象のインスタンスIDまたはENI IDを指定してチェックを無効化する
aws ec2 modify-instance-attribute \
--instance-id i-0123456789abcdef0 \
--no-source-dest-check
# もしENIのID(eni-xxxxxxxxx)を直接指定する場合
aws ec2 modify-network-interface-attribute \
--network-interface-id eni-0123456789abcdef0 \
--no-source-dest-check
ステップ2: Linux OS側でIPパケット転送を有効にする
AWS側で「ほかの人のパケットを通してよし」と許可を出したら、次はOS(Linux)側で「届いたパケットをルーティングして外へ出す」設定を有効にします。
/etc/sysctl.conf または専用の設定ファイルを編集します。
# /etc/sysctl.d/custom-nat.conf
# IPv4のパケットフォワーディングを有効化する
net.ipv4.ip_forward = 1
# (推奨)パケットの送信元が厳密なリバースパスと一致しない場合のドロップを防ぐ設定
# 非対称ルーティングが発生する複雑なネットワーク構成の場合に調整が必要
net.ipv4.conf.eth0.rp_filter = 0
設定を即時反映させるには、以下のコマンドを実行します。
# sysctlの設定をただちに反映
sudo sysctl --system
ステップ3: iptablesによるIPマスカレード(SNAT)の設定
最後に、NATインスタンスとしての魂であるパケット変換ルールを iptables(または nftables)に書き込みます。
# プライベートサブネット(例: 10.0.1.0/24)からのトラフィックを、NATインスタンスのインターフェース(eth0)経由で外へ出す際にSNATする
sudo iptables -t nat -A POSTROUTING -o eth0 -s 10.0.1.0/24 -j MASQUERADE
# 転送ルールを許可(FORWARDチェイン)
sudo iptables -A FORWARD -i eth0 -o eth0 -j ACCEPT
# ルールを永続化する(Ubuntu/Debian系の場合の例)
sudo apt-get install -y iptables-persistent
sudo netfilter-persistent save
—
3. 動作検証:コードから疎通を確認する
構築が完了したら、実際にプライベートサブネット内のインスタンスから外部APIを叩いて、パケットが正しくNATされているかを確認します。今回はPythonの requests ライブラリと、定番の curl コマンドを使った検証コードを用意しました。
検証用Pythonスクリプト
外部の「自分のグローバルIPアドレスを返すAPI(例: https://httpbin.org/ip)」を叩き、返ってきたIPがNATインスタンスに割り当てられたElastic IP(EIP)のグローバルIPと一致することを確認します。
#!/usr/bin/env python3
import requests
import sys
def test_nat_outbound():
target_url = "https://httpbin.org/ip"
print(f"[*] 外部API ({target_url}) へリクエストを送信しています...")
try:
# プライベートサブネットからNAT経由で外へリクエストを飛ばす
response = requests.get(target_url, timeout=5)
response.raise_for_status()
data = response.json()
origin_ip = data.get("origin")
print(f"[+] 成功: 外部から観測された送信元IPアドレス -> {origin_ip}")
print("[i] このIPが、NATインスタンスにアタッチされたEIP(Elastic IP)と一致しているか確認してください。")
except requests.exceptions.RequestException as e:
print(f"[-] 失敗: 通信エラーが発生しました: {e}", file=sys.stderr)
sys.stderr.write("ヒント: 送信元/宛先チェックが無効化されているか、ルートテーブル(0.0.0.0/0 -> NATインスタンス)が正しいか再確認してください。\n")
sys.sys.exit(1)
if __name__ == "__main__":
test_nat_outbound()
検証用ワンライナー(curl)
インフラのデバッグ現場で真っ先に叩くおなじみのコマンドです。
# 自分のグローバルIPを確認する
curl -s https://ipinfo.io/ip
もし、このコマンドがタイムアウトする場合は、以下のチェックリストを上から順に確認してください。
1. ルートテーブルの確認: プライベートサブネットのルートテーブルに、0.0.0.0/0 のルーティング先としてNATインスタンスのENI(またはインスタンスID)が正しく設定されているか?
2. セキュリティグループの確認: NATインスタンスのセキュリティグループが、内部からのアウトバウンド通信(HTTP/HTTPS)と、プライベートサブネットからのインバウンド通信を許可しているか?
3. 送信元/宛先チェック: 本当にAWS側で source-dest-check が無効化されているか?(CLIで aws ec2 describe-instances --instance-id <id> を叩いて SourceDestCheck: false になっているか目視確認する)
—
まとめ:ネットワークの挙動を「点」ではなく「線」で捉える
マネージドな NAT Gateway は非常に便利で、可用性も高く、基本的にはそちらを使うべきです。しかし、アーキテクチャの制約やコスト最適化の観点から、EC2ベースのNATインスタンスを避けて通れない現場は今なお存在します。
今回解説した 「送信元/宛先チェックの無効化」 は、単なるおまじないの設定ではありません。クラウドプラットフォームが持つセキュリティのデフォルト挙動と、自前でルーターを構築するという「本来想定されていないパケット改変(SNAT)」の仕様を正しく調停するための、極めて重要な架け橋です。
パケットがどのレイヤーを通過し、どの段階でIP書き換えが行われ、どのチェックポイントで弾かれているのか。その「パケットの旅路」を頭の中で完全にトレースできるようになれば、どんな複雑なクラウドネットワークのトラブルに直面しても、必ず原因を突き止めることができます。
現場のインフラストラクチャに、幸運と安定したルーティングを!
コメント