【実務・中級編】 セキュリティグループ(SG)のステートフル動作とインスタンスレベルの保護 – クラウド&コンテナネットワーク実践ガイド

クラウドのインフラを設計し、日々の運用でKubernetesやマイクロサービスの海を泳いでいると、避けて通れないのが「ネットワークの機嫌取り」だ。

「あれ、なんでステージング環境から外部の決済APIに繋がらないんだ?」
「ログを見る限り、アプリはリクエストを投げているのに、タイムアウトで沈黙している……」

こんな夜、君ならどこを疑うだろうか? ルートテーブルか? ネットワークACL(NACL)か? それともアプリケーション層のタイムアウト設定か?
だいたいのケースで、真犯人はセキュリティグループ(Security Group: SG)のステートフルな挙動の誤解にある。

今日は、AWSやGCP(GCPの場合はVPCファイアウォールルールだが、概念はほぼ同じステートフルモデルだ)の根幹を支える、ENI(Elastic Network Interface)単位のセキュリティグループについて、パケットの挙動から実務的なトラブルシューティングまで、現場のシニアエンジニアの視点で徹底的に解説しよう。教科書には載っていない「生きた知識」を持ち帰ってほしい。

—

セキュリティグループは「仮想NICの門番」である

まず大前提として、セキュリティグループとは何かを再確認しておこう。
VPCにおけるセキュリティグループは、サブネット単位ではなく、インスタンスの仮想NIC(AWSならENI)単位でアタッチされる仮想ファイアウォールだ。

サブネットレベルでトラフィックを制御するNACLが「マンションのエントランスの自動ドア」だとすれば、セキュリティグループは「各部屋の玄関の鍵」に例えられる。パケットは、ENIに到達する直前(インバウンド)と、ENIから送り出される直前(アウトバウンド)のまさにその瞬間で、この門番による厳格な審査を受ける。

デフォルト拒否の原則(Default Deny)

セキュリティグループの基本思想は極めてシンプルかつ厳格だ。

  • インバウンド(Inbound): すべての通信がデフォルトで「拒否(Deny)」されている。明示的に許可ルール(Allow)を追加しない限り、外からパケットは入ってこない。
  • アウトバウンド(Outbound): クラウドプロバイダーによってデフォルトが異なるが、AWSのEC2用セキュリティグループのデフォルトは「すべての通信を許可(All Traffic Outbound)」だ。しかし、セキュリティを厳格に担保する環境では、このアウトバウンドもデフォルト拒否に変え、必要な宛先だけを絞るのがプロの作法である。

—

「ステートフル(Stateful)」という魔法の挙動

初心者が最もハマり、同時にベテランがその恩恵に何度も救われるのが、セキュリティグループの「ステートフル動作」だ。

NACLは「ステートレス(Stateless)」である。つまり、インバウンドで通信を許可しても、レスポンス(戻りトラフィック)を通したければ、アウトバウンド側でも明示的に許可ルールを書かなければならない。往復のパケットを両方で管理する必要があるため、NACLのルール管理はすぐにカオス化する。

一方、セキュリティグループはステートフルだ。
どういうことか? インバウンドで許可されて確立されたコネクションの「戻りパケット」は、アウトバウンドのルールに関わらず、自動的に許可されるのだ。逆に、内側から外へ向けて確立したアウトバウンド通信に対する「戻りパケット」も、インバウンドのルールを気にせず自動で通る。

この挙動を、実際のパケットの往来に沿ってシミュレーションしてみよう。

通信フロー(シーケンス)の裏側

Webサーバー(EC2インスタンス)が外部のAPIサーバーとHTTPS通信を行うシーンを想像してほしい。

1. SYNパケットの送信(アウトバウンド)

  • アプリケーションが https://api.example.com(IP: 203.0.113.50)に対してリクエストを投げる。
  • インスタンスのENIからパケットが出る際、SGのアウトバウンドルールが評価される(デフォルト許可であればそのまま通過)。

2. コネクション追跡(Connection Tracking)の開始

  • クラウドのハイパーバイザー層にある仮想ルーター(ステートフルインスペクションエンジン)は、この往信パケットを検知し、「このIPとポートの組み合わせで通信が始まったな」という状態(State)をメモリ上に記憶(コネクション・トラッキング)する。

3. SYN-ACKパケットの受信(インバウンド)

  • 外部APIサーバーからの応答パケットがENIに到着する。
  • 通常、インバウンドはデフォルト拒否のはずだ。しかし、ハイパーバイザーはコネクション・トラッキングのテーブルを参照し、「あ、これはさっき内側から出て行った通信の正当な返事だな」と即座に判断する。
  • インバウンドの許可ルールに一致していなくても、パケットは無条件でスルーされ、インスタンスに到達する。

この仕組みがあるおかげで、私たちは「外から入ってくる通信のポート範囲はこれで、戻りのポートは……」なんて面倒な計算をせずに済んでいるのだ。

—

実務で直面する「落とし穴」とコード例

しかし、このステートフル動作が仇になる瞬間もある。例えば、エフェメラルポート(一時ポート)の枯渇や、厳格なアウトバウンド制限をかけたマイクロサービス環境だ。

ここでは、Python(requestsライブラリ)や curl を使って、外部APIを叩くコンテナやインスタンスを運用している現場を想定しよう。

トラブル事例:アウトバウンドを絞ったらAPI通信が死んだ

セキュリティ要件の監査が入り、「アウトバウンドの通信もすべてホワイトリスト方式にせよ」というお達しが出たとしよう。そこでインフラエンジニアは、次のようなTerraformまたはAWS CLIの設定を投入した。

# 【アンチパターン】アウトバウンドを特定の宛先IP・ポート(443)だけに絞ったつもりの設定
resource "aws_security_group" "strict_egress" {
  name        name = "strict-app-sg"
  description = "Strict egress security group"

  # インバウンドは一旦すべて閉じる
  ingress = []

  # アウトバウンドはHTTPS(443)のみを許可
  egress = [
    {
      description      = "Allow HTTPS to outside"
      from_port        = 443
      to_port          = 443
      protocol         = "tcp"
      cidr_blocks      = ["0.0.0.0/0"]
      ipv6_cidr_blocks = []
      prefix_list_ids  = []
      security_groups  = []
      self             = false
    }
  ]
}

この設定、一見すると安全で完璧に見える。しかし、このインスタンス上で動くアプリケーションから外部APIへリクエストを投げると、時々、あるいは完全に名前解決や通信エラー(Connection Timeout)が発生する。なぜか?

原因:DNSクエリ(UDP/TCP 53番ポート)の抜け落ち

アプリケーションは https://api.example.com というホスト名で通信しようとする。その際、最初に何をするか? そう、DNSサーバー(社内DNSや 8.8.8.8 など)への名前解決(DNS Query)だ。
DNSクエリは通常、UDP(またはTCP)の53番ポートを使って行われる。

上記のセキュリティグループの egress ルールを見ると、許可されているのは 443 番ポートのみ。53番ポートへのアウトバウンド通信が明示的に拒否されているため、パケットはENIから一歩も外に出られず、アプリはDNSの名前解決すらできずに沈没していたのだ。

正しい設定アプローチ

この問題を解決するには、アウトバウンドルールにDNSの名前解決用トラフィックを明示的に追加するか、あるいはVPC内DNSリゾルバー(AmazonProvidedDNSなど)のIPレンジに絞ったルールを追加する必要がある。

以下は、Pythonスクリプトから外部APIを安全に呼び出す環境を想定した、実用的なセキュリティグループのアウトバウンド設計例だ。

# 例:Python (requests) を使った外部API呼び出し
import requests
from requests.exceptions import RequestException

def call_external_api():
    url = "https://api.example.com/v1/data"
    try:
        # この通信が成功するためには、
        # 1. 53番ポート(DNS)のアウトバウンドが許可されていること
        # 2. 443番ポート(HTTPS)のアウトバウンドが許可されていること
        # の両方が必要不可欠。
        response = requests.get(url, timeout=5)
        response.raise_for_status()
        return response.json()
    except RequestException as e:
        print(f"Network or API Error occurred: {e}")
        raise

このコードを実行するインスタンスのSGには、最低限以下のアウトバウンドルールが必要になる。

# AWS CLIでDNSとHTTPSのアウトバウンドを正しく許可する例

# 1. すでに存在するデフォルトの全許可アウトバウンドルールを削除(必要な場合)
# aws ec2 revoke-security-group-egress --group-id sg-xxxxxx --protocol -1 --port -1 --cidr 0.0.0.0/0

# 2. HTTPS (443) のアウトバウンドを許可
aws ec2 authorize-security-group-egress \
    --group-id sg-xxxxxx \
    --protocol tcp \
    --port 443 \
    --cidr 0.0.0.0/0

# 3. DNS (UDP 53) のアウトバウンドを許可(VPCのローカルDNSサーバーや外部パブリックDNS宛て)
aws ec2 authorize-security-group-egress \
    --group-id sg-xxxxxx \
    --protocol udp \
    --port 53 \
    --cidr 10.0.0.0/16  # ※VPC CIDRに合わせて調整、または 0.0.0.0/0

—

ベーシックなようで、実務ではこうした「DNSを忘れてハマる」ようなトラブルが後を絶たない。

現場で役立つ実践的デバッグTips

最後に、インフラの現場で「あ、これセキュリティグループのブロックだな」と瞬時に見抜き、迷わず原因を特定するための実践的なTipsを授けよう。

1. IPレイヤーの疎通確認には nc や telnet ではなく curl -v を使え
ICMP(ping)が通らないからといってネットワークが死んでいるとは限らない。現代のクラウド環境ではセキュリティグループやNACLでICMPが塞がれていることは日常茶飯事だ。
HTTP/HTTPSの通信を確認したいなら、curl -v https://<target-ip> を叩き、TCPのハンドシェイク(Connected to...)まで進むか、それとも Connection timed out で即死するのかを確認せよ。即死する場合は、ほぼ100%セキュリティグループのアウトバウンド、あるいは相手側のインバウンド(または途中のルーティング)でパケットがドロップされている。
2. フローログ(VPC Flow Logs)を有効化し、REJECT パケットを追え
原因不明の通信断に直面したら、迷わずVPC Flow Logs(あるいはCloudWatch Logs Insights)を開き、以下のクエリを投げてみろ。

# CloudWatch Logs Insights用クエリの例
fields @timestamp, srcAddr, dstAddr, dstPort, protocol, action, packets, bytes
| filter action = "REJECT"
| sort @timestamp desc
| limit 20

action = REJECT のレコードを見れば、どの送信元IPから、どの宛先ポート(dstPort)に向かったパケットが、セキュリティグループやNACLによって葬り去られたのかが一目瞭然でわかる。ログは嘘をつかない。

—

まとめ

セキュリティグループは、クラウドネットワークにおける「最後の砦」であり、インスタンスレベルの安全性を担保する最も強力なツールだ。
その「ステートフル動作」のメカニズムと、往復のパケットフロー、そしてDNSなどの暗黙的な依存関係を正しく理解していれば、どんなに複雑なマイクロサービスのネットワーク設計であっても、迷うことはなくなるはずだ。

トラブルシューティングの夜、パケットの気持ちになって「今、こいつはどの門番に止められているんだ?」と想像を巡らせてみてほしい。答えは必ず、そのパケットの足元にある。

コメント

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