はじめに
EC2を使っていると、「IAMロールを付けておけばSDKが勝手に認証してくれる」というのを当たり前のように使っていると思います。自分もそうでした。
でもふと「この認証情報、どこから来てるの?」「アクセスキーを設定してないのになんでS3にアクセスできるの?」と思って調べてみると、裏側にIMDS(Instance Metadata Service)という仕組みがあることがわかりました。
この記事は、IMDSの正体から、v1とv2の違い、cloud-initとの関係、設計上の注意点までを整理したものです。
IMDSとは何か
IMDS(Instance Metadata Service)は、EC2インスタンスの内部からだけアクセスできるメタデータ提供サービスです。
EC2の中から http://169.254.169.254/ にHTTPリクエストを投げると、そのインスタンスに紐づいた情報が返ってきます。
$ curl http://169.254.169.254/latest/meta-data/instance-id
i-0abcd1234efgh5678
ただし、このHTTPサーバはOS上のプロセスではありません。ps aux | grep imds で探しても何も出てきません。なぜかというと、IMDSはAWS基盤側が提供しているもので、OS内で起動しているわけではないからです。
ホテルの内線電話で例えると
あなたの部屋(ゲストOS)
└── 内線電話(IMDS)← ホテル(AWS基盤)が設置済み
部屋に入った瞬間から使える。
あなたが電話機のプロセスを起動したわけではない。
IMDSはこれと同じです。インスタンスが起動したらAWS基盤側が「使えるようにしてくれる」ものであって、OSが何か立ち上げているわけではありません。
169.254.169.254 はリンクローカルアドレス
このアドレスは 169.254.0.0/16 の範囲で、リンクローカルアドレスと呼ばれます。
特徴はこうです。
- ルーターを超えて外には出ない(インターネットには行かない)
- 同一セグメント内でのみ有効
- 通常はDHCP失敗時にOSが自動で割り当てるアドレス帯
EC2の世界では、この「すぐ隣」がAWS基盤側(ホスト)になります。だからインスタンスの中からしかアクセスできないし、外部からは到達できません。
IMDSで取得できる情報
EC2の内部から以下のような情報を取得できます。
| 情報 | 用途 |
|---|---|
| インスタンスID | ログ出力、識別 |
| AZ / リージョン | 動的なリージョン判定 |
| AMI ID | 起動元の確認 |
| IAMロールの一時クレデンシャル | AWS API実行 |
この中で圧倒的に重要なのが、IAMロールの一時クレデンシャルの取得です。
EC2にIAMロールを付与すると、アクセスキー不要、シークレットキー不要、ハードコード不要でAWS SDKが自動的にIMDS経由で認証情報を取得してくれます。これが「IAMロールをEC2にアタッチすればSDKが勝手に動く」仕組みの正体です。
IMDSはどこにいるのか
ここが一番誤解されやすいポイントです。
┌──────────────────────────────────┐
│ EC2インスタンス(ゲストOS) │
│ │
│ アプリ / SDK / cloud-init │
│ │ │
│ │ HTTP GET/PUT │
│ ▼ │
│ 仮想NIC(ENAドライバ) │
└──────────┬───────────────────────┘
│
│ 169.254.169.254 宛
▼
┌──────────────────────────────────┐
│ AWS基盤(Nitro / ホスト側) │
│ │
│ メタデータ提供機構(IMDS) │
│ ← ここがHTTPを返している │
└──────────────────────────────────┘
つまり、あなたのOSで何もサーバを立てていないのにHTTPが返ってくるのは、AWS基盤側がリクエストを受け取って応答しているからです。
「起動する」というより「提供される」が正確な表現です。
IMDSv1 と IMDSv2 の違い
IMDSv1 の問題
IMDSv1はシンプルなGETリクエストだけでメタデータを取得できました。
$ curl http://169.254.169.254/latest/meta-data/iam/security-credentials/MyRole
これ自体は便利なのですが、SSRF(Server Side Request Forgery)攻撃に弱いという重大な問題がありました。
SSRF攻撃のイメージ
攻撃者
│
│ 「この画像URLを表示して」
│ → http://169.254.169.254/latest/meta-data/iam/security-credentials/MyRole
▼
Webアプリ(EC2上で動作)
│
│ 「画像を取りに行くか...」
│ → IMDSにアクセスしてしまう
▼
IMDS
│
│ 「はい、IAM認証情報です」
▼
攻撃者がIAM認証情報を取得
外部ユーザーがWebアプリに細工したURLを渡して、サーバ自身に内部URLへアクセスさせる攻撃です。これによりIAM認証情報が盗まれる事故が実際に発生しました。
IMDSv2 のセッション化
IMDSv2ではHTTPアクセスをセッション化して、この問題に対処しています。
■ ステップ1: PUTでトークン取得
TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" \
-H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
→ TTL付き(最大6時間)
→ PUT メソッドが必要(GETでは取得不可)
■ ステップ2: トークン付きGETでメタデータ取得
curl -H "X-aws-ec2-metadata-token: $TOKEN" \
http://169.254.169.254/latest/meta-data/instance-id
→ トークンがないと拒否される
なぜこれでSSRFを防げるかというと、SSRF攻撃は通常GETリクエストしか発行させられません。IMDSv2ではトークン取得にPUTが必要なので、GETだけでは認証情報にたどり着けません。
さらにhop limit(TTL相当)で、コンテナや別ネットワークを経由した中継も抑止されます。
v1 と v2 の比較
| 項目 | IMDSv1 | IMDSv2 |
|---|---|---|
| アクセス方法 | GETのみ | PUT→GET の2段階 |
| トークン | 不要 | 必須(TTL付き) |
| SSRF耐性 | 弱い | 強い |
| hop制御 | なし | あり |
重要なポイントとして、IMDSv2のトークンはEC2起動時に自動生成されるわけではありません。アプリやSDKが必要になった瞬間にPUTで取得し、TTL切れで破棄されます。
誰がIMDSにアクセスしているのか
普段意識しませんが、EC2上のいろいろなソフトウェアがIMDSを使っています。
パターン1: AWS SDK / CLI(最も多い)
import boto3
s3 = boto3.client('s3')
s3.list_buckets()
このコードを実行すると、SDKは内部でこういう順番で認証情報を探します。
環境変数にアクセスキーがある? → なし
~/.aws/credentials にある? → なし
IAMロールがある? → IMDSへアクセス
└→ PUT(トークン取得)
└→ GET(一時クレデンシャル取得)
ここでIMDSが登場します。
パターン2: ユーザーデータ
#!/bin/bash
aws s3 ls
ユーザーデータの中でAWS CLIを呼ぶと、CLIの内部でSDKがIMDSにアクセスして認証情報を取得します。ユーザーデータ自体がIMDSを直接叩いているわけではなく、きっかけを提供しているだけです。
パターン3: AWSエージェント
CloudWatch Agent、SSM Agent、Inspector、GuardDutyなどのエージェントも、AWS APIを呼ぶためにIMDSを利用しています。
ユーザーデータとは何か
ここでよく混乱するのが「ユーザーデータ」です。
ユーザーデータ(User Data)とは、EC2起動時にインスタンスに対して実行させたい初期化スクリプトを渡す仕組みです。
どこに保存されるのか
ユーザーデータはインスタンスの属性としてAWS側に保存されます。OS内に最初から存在するわけではなく、OSが起動してからIMDS経由で取りに行きます。
誰が実行するのか
ここが誤解されやすいところです。AWSがサーバ側で実行してくれるわけではありません。実行主体はOS側です。
Linuxの場合、多くの環境ではcloud-initがユーザーデータをIMDSから取得して、OS内で実行します。
OS起動
└→ cloud-init 起動
└→ IMDSから user-data を取得
└→ /var/lib/cloud/instance/scripts/ に保存
└→ シェルスクリプトとして実行
cloud-init とは何か
cloud-initはクラウド環境でOSを初期化するための標準的な仕組みです。EC2に限らず、Azure、GCPなど多くのクラウドで使われています。
cloud-initがやることはこのあたりです。
- 「ここはEC2だ」と認識する(データソース検出)
- IMDSからメタデータを取得する
- ホスト名を設定する
- SSH公開鍵を配置する
- ネットワーク設定を補助する
- ユーザーデータを取得して実行する
OS起動とcloud-initの関係(ここが一番実務に効く)
EC2が起動するとき、OS(systemd)とcloud-initは並行して動きます。順番に1つずつ完了するのではなく、同時進行です。
■ OS起動(systemd) ■ cloud-init(段階実行)
kernel 起動
↓
systemd 起動
↓
basic.target init-local(ネット不要の処理)
↓ ↓
network.target init(最低限のネットワーク)
↓ ↓
multi-user.target config(user-data実行)
↓
final(後処理)
← これらは並列で進む →
ここで注意すべきなのは、以下の2つです。
- multi-user.target に到達した = ネットワークが完全に使える、ではない
- cloud-init の config 段階に到達した = ネットワークが使える、でもない
つまり、ユーザーデータの中でAWS APIを叩こうとしたとき、ネットワークがまだ準備できていなかったり、IMDSにまだ到達できなかったりするタイミング事故が起きる可能性があります。
じゃあどう設計すればいいのか
原則: ユーザーデータでAWS APIに依存しすぎない
ユーザーデータは便利ですが、タイミング事故が起きやすいです。できればユーザーデータはローカルで完結する最小限の処理に限定して、AWS APIが必要な処理は後段に回すのが安全です。
systemd で network-online.target を待つ
アプリをsystemdサービスとして起動するなら、こう書きます。
[Unit]
After=network-online.target
Wants=network-online.target
[Service]
ExecStart=/usr/local/bin/myapp
Restart=on-failure
これで「ネットワークが実用状態になってから起動」を狙えます。
ただし注意点として、network-online.target はIMDSへの到達を保証するものではありません。
認証取得前提の処理はリトライ設計にする
最終的にはアプリ側でリトライするのが一番確実です。
初回アクセス → 失敗(まだIMDS到達不可)
↓
数秒待つ
↓
再試行 → 成功
起動直後の一回だけ失敗するケースは実際にあります。IMDSv2が絡むほど、トークン取得のPUTを含めたリトライ設計が重要になります。
まとめ
IMDS = AWS基盤がEC2に提供する"内線電話"
= アクセスキー不要でAWS APIを呼べる世界を支える仕組み
IMDSv1 → GETだけで取れる → SSRFに弱い
IMDSv2 → PUT+GET の2段階 → SSRFに強い
認証情報を取りに行くのは SDK / CLI / エージェント
ユーザーデータは「きっかけ」、実行主体はOS側(cloud-init)
起動直後はIMDS到達不可のタイミングがある → リトライ設計が重要
EC2を使っているなら、裏側でIMDSが何をしているか知っておくと、「なぜ認証エラーが起きるのか」「なぜ起動直後だけ失敗するのか」がわかるようになります。
参考:
AWS公式 - インスタンスメタデータとユーザーデータ
AWS公式 - IMDSv2の使用
cloud-init ドキュメント