クラウドの「安全神話」を破壊する:VPCピアリングを悪用した横展開(Lateral Movement)の現実
クラウド環境を構築する際、エンジニアが最も陥りやすい罠がある。「VPCを分けているから安全だ」「セキュリティグループで守られているから大丈夫だ」という慢心だ。
しかし、攻撃者の視点から見れば、VPCピアリングは「隔離された城と城を繋ぐ、無防備な跳ね橋」に他ならない。今日は、インフラの設計ミスがどのように致命的な横展開を許すのか、そしてそれを物理的に封じ込めるための要塞化手法について解説する。
—
1. 攻撃者が狙う「ピアリング」の盲点
VPCピアリングは、異なるVPC間の通信をプライベートIP経由で実現する便利な機能だ。しかし、多くの現場では「とりあえず繋ぐ」ことが優先され、ネットワークのアクセス制御が後回しにされている。
攻撃シナリオ:踏み台からの侵入
1. 初期侵入: 公開されているWebサーバー(VPC-A)の脆弱性を突き、リバースシェルを奪取。
2. 偵察: ip route や arp -a コマンドを叩き、VPCピアリング先のサブネット範囲(VPC-B)を特定。
3. 横展開: セキュリティグループ(SG)が「VPC-AのCIDRからのトラフィックを全許可」している場合、攻撃者はVPC-B内のデータベースや内部管理画面へ直接アクセスを開始する。
防御側の最大のミスは、「ピアリング先からの通信を、内部ネットワークと同等の信頼レベルで扱ってしまうこと」だ。
—
2. ネットワークACLによる多層防御の実装
セキュリティグループ(SG)はステートフルで便利だが、誤設定のリスクが高い。そこで登場するのが「ネットワークACL(NACL)」だ。これはステートレスな境界防御であり、SGのルールが突破された後の最終防衛ラインとなる。
VPC-Bのサブネットにおいて、VPC-Aからのトラフィックを「必要なポートのみ」に絞り込む設定例を見てみよう。
Terraformでの厳格なNACL設定例
# VPC-B側のNACL設定:VPC-Aからの通信を特定のポート(例: 5432/PostgreSQL)のみ許可
resource "aws_network_acl_rule" "allow_db_from_vpc_a" {
network_acl_id = aws_network_acl.vpc_b_main.id
rule_number = 100
egress = false
protocol = "6" # TCP
rule_action = "allow"
cidr_block = "10.0.1.0/24" # 攻撃元となるVPC-Aのサブネット
from_port = 5432
to_port = 5432
}
# 最後にそれ以外を全て拒否するルールを必ず追加(暗黙的拒否を明示)
resource "aws_network_acl_rule" "deny_all_others" {
network_acl_id = aws_network_acl.vpc_b_main.id
rule_number = 32766
egress = false
protocol = "-1"
rule_action = "deny"
cidr_block = "0.0.0.0/0"
}
—
3. アプリケーション層での「ゼロトラスト」な認証
ネットワークを閉じたとしても、万が一Webサーバーが侵害されたら終わりではない。データベースや内部APIへの接続時に、「どのインスタンスから来たか」だけではなく「何の権限で来たか」を検証する必要がある。
以下は、Pythonで実装する内部API認証の簡素な例だ。ピアリング経由の通信であっても、JWT(JSON Web Token)による署名検証を必須とすることで、ネットワークの境界を越えてきた不正なパケットを弾くことができる。
Python (FastAPI) による内部API認証のサンプル
from fastapi import FastAPI, Depends, HTTPException, Header
import jwt
app = FastAPI()
SECRET_KEY = "super-secret-internal-key"
def verify_internal_token(x_internal_token: str = Header(...)):
try:
# トークンを検証し、許可されたスコープのみを通す
payload = jwt.decode(x_internal_token, SECRET_KEY, algorithms=["HS256"])
if payload.get("scope") != "internal-db-access":
raise HTTPException(status_code=403, detail="権限不足")
except jwt.PyJWTError:
raise HTTPException(status_code=401, detail="無効なトークン")
@app.get("/api/v1/private-data", dependencies=[Depends(verify_internal_token)])
async def get_private_data():
return {"data": "機密情報"}
—
4. セキュリティチーフからの最後のアドバイス
現場でよくある失敗は、「運用が面倒だから」という理由でSGのルールを 0.0.0.0/0 や広範囲のCIDRに設定することだ。ピアリング先との通信は、「ピアリング先の特定のIPアドレス(単一ホスト)」まで絞り込むのが鉄則だ。
- SGの設定は常に最小権限:
anyを使わない。 - ログの可視化: VPCフローログを有効にし、Athena等で「本来あるはずのない通信」を毎日監視する。
- IaCの徹底: 手動設定はヒューマンエラーの温床。必ずTerraformやCloudFormationでコード化し、コードレビューでネットワーク境界の設計を監査すること。
「クラウドだから安全」ではない。「設定した通りにしか動かない」のがクラウドだ。攻撃者はあなたの「面倒くさい」という一瞬の隙を、数ヶ月かけて狙っている。強固なセグメンテーションで、その隙を物理的に消し去ってほしい。
コメント