こんにちは、セキュリティチームのチーフをやっている者だ。
今日も今日とて、AWSやGCPといったクラウド環境のインフラ見直しや、アプリ側の脆弱性診断結果を突き合わせる日々を送っている。さて、君たち日々の開発やインフラ運用で、こんなコードを書いていないか?
「とりあえず動かすために、インスタンスプロファイル(IAMロール)には AdministratorAccess や、せめて AmazonS3FullAccess をぶち込んでおきました!」
……もし、心当たりがあるなら、今すぐその手を止めてくれ。その設計、攻撃者からすれば「どうぞ我が家の金庫を荒らしてください」と合鍵を渡しているようなものだ。
今回は、クラウド環境におけるIAMロールの最小権限付与と、昨今のクラウドインフラで最も狙われやすい「IMDS(インスタンスメタデータサービス)」の保護について、実際の攻撃シナリオとそれを完全に叩き潰すための実践的なコードを交えて徹底的に解説する。
—
なぜ「広すぎる権限」と「メタデータ」が狙われるのか?
クラウドネイティブなアプリケーションにおいて、最大の脅威の一つが SSRF(Server-Side Request Forgery:サーバー側リクエスト偽造) だ。
アプリケーションがユーザーからの入力値(例えば、画像のURLや外部APIのエンドポイントなど)を検証せずにそのまま外部通信に利用してしまうと、攻撃者はアプリケーションを踏み台にして、内部ネットワークにアクセスできるようになる。
ここでクラウド(AWSを例に取ろう)の特性を思い出してほしい。
EC2インスタンスなどのメタデータサービス(IMDSv1)は、以下の特殊なIPアドレスに対してHTTPリクエストを送るだけで、そのインスタンスに紐づいているIAMロールの一時クレデンシャル(AccessKeyId, SecretAccessKey, Token)をノーガードで返してしまう。
http://169.254.169.254/latest/meta-data/iam/security-credentials/【ロール名】
もし、君のアプリにたった1箇所でもSSRFの脆弱性があり、かつそのインスタンスに強力なIAMロールが付与されていたらどうなるか?
攻撃者はメタデータから一時クレデンシャルをいとも簡単に窃取し、手元の端末からその権限を悪用してS3の全バケットをダンプしたり、他のリソースを勝手に操作したりする。アプリケーションの脆弱性が、クラウド環境全体の乗っ取りに直結する瞬間だ。
—
攻撃者の手口:SSRFからクレデンシャル強奪までのPoC
百聞は一見にしかず。脆弱なPHPアプリケーションを例に、攻撃者がどのようにしてクラウドの心臓部を奪うのかを見てみよう。
脆弱なアプリケーションの例(PHP)
以下のコードは、ユーザーが入力したURLのコンテンツを file_get_contents() で取得して表示する、ありがちな「一見便利そうだが最悪な」コードだ。
<?php
// 【危険な実装例】ユーザーからの入力をそのまま外部通信に使っている
if (isset($_GET['url'])) {
$targetUrl = $_GET['url'];
// URLのバリデーションやスキーム制限が一切ない!
// 攻撃者はここに 「http://169.254.169.254/...」 を指定できる
$response = @file_get_contents($targetUrl);
if ($response !== false) {
echo "<pre>" . htmlspecialchars($response, ENT_QUOTES, 'UTF-8') . "</pre>";
} else {
echo "Failed to fetch URL.";
}
}
?>
攻撃者はこのページに対し、ブラウザや攻撃スクリプトから次のようなリクエストを投げる。
https://vulnerable-app.example.com/fetch.php?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/MyOverPermissionedRole
もしインスタンスメタデータが保護されておらず、さらにそのロールが強大な権限を持っていた場合、画面にはIAMの一時クレデンシャルが丸見えになる。ゲームオーバーだ。
—
防御の要:二重のセキュリティ対策
この脅威を完全に無効化するためには、「アプリケーション側のSSRF対策」 と 「クラウドインフラ側のメタデータ保護(IMDSv2強制)」 の二段構えで守る必要がある。
1. クラウド側の対策:IMDSv2の強制とセッション化
AWSであれば、古い「IMDSv1」を無効化し、セッション指向の「IMDSv2」を強制する。IMDSv2では、PUTリクエストによるトークンの取得が必須となるため、単純なGETリクエストしか送れない通常のSSRF脆弱性からはメタデータを保護できる。
Terraformを使っているなら、インフラ定義に以下の設定を必ず含めよう。
# AWS EC2インスタンス設定でのIMDSv2強制
resource "aws_instance" "secure_server" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
# メタデータサービスのオプション設定
metadata_options {
http_endpoint = "enabled"
http_tokens = "required" # ← これが「IMDSv2の強制」!
http_put_response_hop_limit = 1 # ← ルーター経由でのアクセスを禁止(SSRF対策の追加層)
}
tags = {
Name = "SecureServer"
}
}
2. アプリケーション側の対策:URLスキームとIPアドレスの厳格なバリデーション
PHP側でも、SSRFを防ぐための厳格な入力値検証を実装する。外部からのURLを受け取る場合は、必ず許可されたスキーム(https のみなど)を強制し、プライベートIPやリンクローカルアドレス(メタデータIP含む)へのリクエストを弾くロジックを挟む。
以下は、安全にURLをフェッチするためのセキュアな実装サンプル(PHP)だ。
<?php
/**
* SSRFを防御するための安全なURLフェッチ関数
*/
function secureFetchUrl(string $url): string {
// 1. パースして構造を分解する
$parsed = parse_url($url);
if ($parsed === false || !isset($parsed['scheme'], $parsed['host'])) {
throw new InvalidArgumentException("無効なURL形式です。");
}
// 2. スキームのホワイトリスト検証(http/https以外は絶対に許可しない)
if (!in_array(strtolower($parsed['scheme']), ['https'], true)) {
throw new InvalidArgumentException("許可されていないスキームです。HTTPSのみ許可されています。");
}
$host = $parsed['host'];
// 3. ホスト名がIPアドレスの場合、または名前解決してプライベートIPやメタデータIPでないかチェック
$ip = gethostbyname($host);
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false) {
// メタデータIP(169.254.169.254)やローカルループバック(127.0.0.1等)はここで弾かれる
throw new InvalidArgumentException("不正なホスト(プライベートIPまたはメタデータIP)へのアクセスは禁止されています。");
}
// 4. cURLを使用してリダイレクトを追跡しない設定で安全に通信を行う
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 5); // タイムアウトを設定
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false); // SSRFのオープンリダイレクトチェインを防ぐためリダイレクト追跡をオフに
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
$response = curl_exec($ch);
if (curl_errno($ch)) {
$error = curl_error($ch);
curl_close($ch);
throw new RuntimeException("通信エラー: " . $error);
}
curl_close($ch);
return $response;
}
// 実行例
try {
if (isset($_GET['url'])) {
$safeContent = secureFetchUrl($_GET['url']);
echo "<pre>" . htmlspecialchars($safeContent, ENT_QUOTES, 'UTF-8') . "</pre>";
}
} catch (Exception $e) {
http_response_code(400);
echo "エラー: " . htmlspecialchars($e->getMessage(), ENT_QUOTES, 'UTF-8');
}
?>
—
最小権限(Least Privilege)のIAMポリシー設計
最後に、IAMロール自体の設計についてだ。万が一、アプリケーションが完全に突破され、一時クレデンシャルが奪われたとしても、「被害をそのインスタンスの特定のリソースだけに限定する」のがプロの仕事だ。
例えば、あるアプリケーションが特定のS3バケット(my-app-uploads-bucket)の特定フォルダにのみファイルをアップロードする必要がある場合、ポリシーは以下のように記述すべきだ。
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowSpecificS3BucketAccessOnly",
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:GetObject"
],
"Resource": "arn:aws:s3:::my-app-uploads-bucket/uploads/*"
},
{
"Sid": "DenyOtherDangerousActions",
"Effect": "Deny",
"Action": [
"iam:*",
"s3:DeleteBucket",
"ec2:*"
],
"Resource": "*"
}
]
}
Resourceにはワイルドカード(*)を使わず、必要なバケットの特定プレフィックス(/uploads/*)のみを指定する。- これにより、仮にクレデンシャルが漏洩しても、他のバケットやAWSアカウント内の他のサービス(EC2やIAMなど)へは一切触ることができない。
—
チーフからのメッセージ
セキュリティは「これだけやっておけば100%安全」という銀の弾丸はない。だが、今回紹介した 「IMDSv2の強制」「アプリケーション層でのURL/IPバリデーション」「極限まで絞ったIAMポリシー」 の3つを組み合わせることで、攻撃者のコストを跳ね上げ、システムを致命傷から守り抜くことができる。
「開発スピードを落とさないために全権限を渡す」という悪習は、今日この瞬間に断ち切ろう。
レビューの現場で「このIAMポリシー、広すぎないか?」と互いに指摘し合える強固なエンジニアリング文化を、チーム一丸となって作っていってほしい。期待しているぞ。
コメント