【実務・中級編】 プライベートサブネットの定義とインターネット遮断のセキュリティ要件 – クラウド&コンテナネットワーク実践ガイド

クラウド要塞の作り方:プライベートサブネットとインターネット遮断の鉄則

こんにちは、インフラの泥臭いトラブルシューティングから這い上がってきたシニアSREの私です。

Web APIの設計やインフラ構築に携わっていると、一度は必ず直面するのが「このコンポーネント、本当にインターネットに直接露出させて大丈夫か?」というセキュリティの問いです。データベース、バッチ処理ワーカー、内部向けマイクロサービス……これらは外部からの直接アクセスを完全に遮断しつつ、必要な時だけ安全にアップデートや外部API連携を行いたい。

今回は、AWSやGCPといったメガクラウドのネットワーク根幹を支える「プライベートサブネット」の定義と、インターネット遮断のセキュリティ要件について、パケットの挙動や実務で役立つ設定の裏側まで徹底的に解説します。教科書的なコピペではなく、現場のリアルな知見を共にお届けしましょう。

—

1. パブリックとプライベートの本質:なぜ「遮断」が必要なのか

クラウドのVPC(Virtual Private Cloud)を設計する際、私たちは必ずサブネットを「パブリック」と「プライベート」に分割します。この二者の決定的な違いは、インターネットゲートウェイ(IGW)への直接のルーティングパスを持っているかどうか、これに尽きます。

[Internet]
    │
    ▼
[Internet Gateway (IGW)]
    │
    ├─ (ルーティングあり) ──> [Public Subnet]  (ALB, 踏み台など)
    │
    └─ (ルーティングなし) ──> [Private Subnet] (DB, 内部APIサーバーなど)

パブリックサブネットに配置されたリソースは、IGWを通じてグローバルIPアドレスを直接持ち、インターネットからの入出力(Inbound/Outbound)が可能です。ロードバランサー(ALB/NLB)や踏み台サーバー(Bastion Host)がここに配置されるのはそのためです。

一方、プライベートサブネットの本質は、「外部からの侵入経路(Inbound)を物理的・論理的に断つこと」にあります。RFC 1918で定められたプライベートIPアドレス(10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)のみを付与し、インターネット側からルートを見えないようにする。これにより、万が一アプリケーション層にゼロデイ脆弱性が存在したとしても、外部の悪意あるスキャナーから直接叩かれるリスクをゼロに抑え込めるのです。

—

2. インターネット遮断のセキュリティ要件と「外への通信」のジレンマ

「じゃあ、プライベートサブネット内のサーバーは、完全に外部と隔離すればいいんだな?」
若かりし頃の私もそう考えました。しかし、現実はそう甘くありません。

現代のインフラストラクチャにおいて、完全に外界から孤立したシステムなどほぼ存在しません。プライベートサブネットにあるリソースであっても、以下のような「外向きの通信(Outbound)」がどうしても必要になります。

  • OSやミドルウェアのセキュリティパッチ適用(apt update や yum update)
  • 外部SaaS(Stripe、SendGrid、AWS SDKを通じた各種APIなど)の呼び出し
  • 外部コンテナレジストリからのイメージプル

ここでセキュリティのジレンマが生じます。「外からのアクセスは絶対に拒絶したいが、中からの外へのアクセス(Outbound)は許可したい」。

この背反する要件をエレガントに解決するのが、NAT(Network Address Translation)ゲートウェイやVPCエンドポイントの存在です。パケットはプライベートから外へ出ていくことはできますが、外から自発的にプライベートの中へ入ってくることはステートフルファイアウォール(セキュリティグループやNACL)によって厳格にブロックされます。

—

3. 通信フローの裏側:パケットはどこをどう流れるか

ここで、プライベートサブネットから外部APIを叩くときのパケットの挙動を、頭の中に思い描いてみましょう。

例えば、Python製のバックエンドAPIサーバー(10.0.1.50)が、外部の決済APIサーバー(203.0.113.50)へHTTPSリクエストを送るシナリオを考えてみます。

1. ルーティングの判定: サーバーのOSカーネルは、宛先が同一VPC内ではないため、デフォルトルート(0.0.0.0/0)に従ってパケットをデフォルトゲートウェイ(VPCルーター)へ投げます。
2. ルートテーブルの参照: プライベートサブネットのルートテーブルには、0.0.0.0/0 の転送先としてNATゲートウェイ(またはNATインスタンス)のインターフェースが指定されています。
3. NATによる送信元IPの変換(SNAT): パケットがNATゲートウェイを通過する際、送信元IPアドレスである 10.0.1.50 が、NATゲートウェイに割り当てられたパブリックIPアドレス(例: 198.51.100.10)に書き換えられます(ポート番号も動的ポートに変換されます)。
4. インターネット経由の到達: パケットはIGWを経由してインターネットへ飛び出し、宛先の決済APIに到達します。
5. レスポンスの返却: 決済APIからのレスポンスは 198.51.100.10 宛てに返ってきます。NATゲートウェイはコネクションステートテーブルを参照し、元のプライベートIP 10.0.1.50 にパケットを正確に書き戻してサーバーに届けます。

この一連の流れにおいて、外のインターネットから 10.0.1.50 というプライベートIPへ直接パケットを送ることは、インターネット側でルーティングが存在しないため、物理的に不可能です。これが「安全な遮断」の正体です。

—

4. 実務で役立つ設定例とコードスニペット

口で言うだけではなく、実際にインフラを定義するコードと、アプリケーションから安全に外部通信を行う実装例を見ていきましょう。

TerraformによるセキュアなプライベートサブネットとNATの定義例

AWS環境を想定し、プライベートサブネットからIGWへの直接ルートを持たせず、NATゲートウェイを経由させる標準的なTerraformコードの抜粋です。

# プライベートサブネットの定義
resource "aws_subnet "private_backend" {
  vpc_id            = aws_vpc.main.id
  cidr_block        = "10.0.10.0/24"
  availability_zone = "ap-northeast-1a"

  tags = {
    Name = "prod-private-subnet-1a"
  }
}

# プライベートサブネット用のルートテーブル
# ※ あえてインターネットゲートウェイ(IGW)へのルートは含めず、NAT Gatewayのみを指定する
resource "aws_route_table" "private" {
  vpc_id = aws_vpc.main.id

  route {
    cidr_block     = "0.0.0.0/0"
    nat_gateway_id = aws_nat_gateway.main.id
  }

  tags = {
    Name = "prod-private-rt"
  }
}

resource "aws_route_table_association" "private" {
  subnet_id      = aws_subnet.private_backend.id
  route_table_id = aws_route_table.private.id
}

Python (Requests) による外部API呼び出しの実装例

プライベートサブネット内で稼働するアプリケーションコンテナから、安全に外部APIを叩く際のサンプルコードです。実務ではタイムアウト設定や例外処理を必ず組み込みます。

import logging
import requests
from requests.exceptions import RequestException

logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO)

def call_external_payment_api(payload: dict) -> dict:
    """
    プライベートサブネット上のコンテナから外部決済APIを呼び出す関数。
    NATゲートウェイ経由で外向き通信を行う。
    """
    api_url = "https://api.example-payment.com/v1/charge"
    
    # セキュリティ要件として、必ずタイムアウトを明示する(無応答によるスレッド枯渇を防ぐ)
    timeout_sec = 5.0 

    try:
        response = requests.post(
            api_url, 
            json=payload, 
            timeout=timeout_sec,
            headers={"Content-Type": "application/json"}
        )
        
        # HTTPステータスコードが40x, 50xの場合に例外を発生させる
        response.raise_for_status()
        
        logger.info("外部APIの呼び出しに成功しました。")
        return response.json()

    except RequestException as e:
        logger.error(f"外部APIとの通信中にエラーが発生しました: {e}")
        # 実務ではここで適切なカスタム例外の送出やリトライキュー(SQS等)への退避を検討する
        raise

—

5. シニアSREが教える!現場のトラブルシューティングTips

最後に、現場でよくある「プライベートサブネットまわりの罠」と、そのデバッグ手法を伝授します。

罠1:突然、外部APIと通信できなくなった

  • 原因の多く: NATゲートウェイのバグやリソース枯渇ではなく、セキュリティグループ(SG)やネットワークACL(NACL)のルール見落とし、あるいはDNSの名前解決失敗がほとんどです。
  • デバッグ手順:

1. プライベートサブネット内の踏み台やセッションマネージャー経由で対象サーバーにログインします。
2. まずIPアドレス直指定で疎通確認をします(例: curl -Iv https://203.0.113.50)。これで通ればDNSの問題です(VPCのDNS解決設定やVPCエンドポイントを確認)。
3. 次に traceroute や tcptraceroute を使って、パケットがどこまで飛んでいるか確認します。

# パケットがNATゲートウェイやプロキシでドロップしていないか確認
  nc -zv api.example-payment.com 443

罠2:コストの罠(NATゲートウェイのデータ処理費用)

  • 現場の知見: NATゲートウェイは「時間課金」だけでなく「データ処理量課金(GBあたり)」が発生します。プライベートサブネットからパブリックなS3やDynamoDBへ大量のデータを送受信している場合、NATゲートウェイを経由すると多額の通信費が発生してマネージャーから怒られます。
  • 対策: AWSであれば VPCエンドポイント(Gateway型 / Interface型) を活用しましょう。S3やDynamoDBへの通信はNATを経由せず、AWSのバックボーン内を通るため、コスト削減とセキュリティ向上の両方を達成できます。

—

まとめ

プライベートサブネットの定義とインターネット遮断の要件は、単に「外から見えなくして安心する」という精神論ではありません。

1. 明確な境界線の定義: 外部からのインバウンド通信を完全にシャットアウトする。
2. 制御されたアウトバウンド: NATゲートウェイやVPCエンドポイントを介して、必要な外部通信だけをステートフルに許可する。
3. コストとパフォーマンスの最適化: 通信経路を正しく理解し、無駄なトラフィックを生み出さない設計を行う。

この基本原則をしっかりと押さえておけば、どんなに大規模で複雑なマイクロサービスアーキテクチャを構築することになっても、揺るぎないセキュアな基盤を維持し続けることができます。

あなたのインフラストラクチャが、今日も安全で健やかなパケットに満ち溢れていますように。それではまた、次の現場でお会いしましょう!

コメント

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