EKS モジュール v21 で hop limit が 1 になって出る IMDS エラーを調べてみた
こんにちは。製造ビジネステクノロジー部所属の hongkii です。
Terraform の terraform-aws-modules/eks モジュールに v21 が出ていたのでアップグレードしたところ、一部の Pod の起動時に IMDS 関連のエラーが出ていることに気づきました。
今回はこのエラーについて、原因と対処、そして実際に確認できたことをまとめていきたいと思います。
TL;DR
- eks モジュール v21 から、ノードの IMDS hop limit のデフォルト値が
2から1に変わった - そのため IMDS を参照する Pod が IMDSv2 のトークンを取得できなくなる
- EBS CSI Driver の場合は
context deadline exceededのエラーログが出るが、Kubernetes API へのフォールバックが働くので機能自体は正常 - 対処としては
node.metadataSources = "kubernetes"を設定すれば、hop limit 1 を維持したままエラーログと約 5 秒の起動遅延も解消できる
Pod は Running なのにエラーログが出ている
出ていたのは、ebs-csi-node Pod(EBS CSI Driver のノードプラグイン)の起動時の以下のログです。
I "Initializing metadata"
I "Attempting to retrieve instance metadata from IMDS"
E "Retrieving IMDS metadata failed" err="could not get IMDS metadata: operation error ec2imds: GetInstanceIdentityDocument, canceled, context deadline exceeded"
I "Attempting to retrieve instance metadata from Kubernetes API"
I "Retrieved metadata from Kubernetes"
IMDS の取得失敗がエラーレベルで出ているものの、Pod は正常に Running の状態でした。IMDS を明示的に使っている Pod があるわけでもなく、v20 では出ていなかったログです。
eks モジュール v21 で変わったデフォルト値
まず疑ったのは、直前に実施した terraform-aws-modules/eks モジュールのメジャーアップグレードです。
実はこのモジュール、v21 でいくつかのデフォルト値が変わっており、そのひとつが IMDS の hop limit でした。
v20 から v21 で変わった主なデフォルト値は以下の通りです。
| 項目 | v20 のデフォルト | v21 のデフォルト |
|---|---|---|
| IMDS hop limit | 2 |
1 |
use_latest_ami_release_version |
false |
true |
enable_monitoring |
true |
false |
アドオンの resolve_conflicts_on_create |
OVERWRITE |
NONE |
ami_type |
(未指定) | AL2023_x86_64_STANDARD |
addons.most_recent |
false |
true |
こうしたデフォルト値の変更は、モジュール側で明示的に値を指定していない部分に効いてきます。hop limit を指定していない構成だったため、v21 に上げた瞬間に自動で 2 から 1 に変わったというわけです。
アップグレード時の terraform plan を見返すと、ノードの launch template で http_put_response_hop_limit が 2 から 1 に変わることが、しっかり出ていました。
# ...aws_launch_template.this[0] will be updated in-place
~ resource "aws_launch_template" "this" {
# ...
~ metadata_options {
~ http_put_response_hop_limit = 2 -> 1
# (4 unchanged attributes hidden)
}
# ...
}
デプロイ後に見つけたログにつながる変更は、この 1 行にすでに出ていたことになります。
現在のノードがどちらの値で動いているかは、describe-instances で確認できます。
$ aws ec2 describe-instances --instance-ids i-0123456789abcdef0 \
--query 'Reservations[].Instances[].MetadataOptions.HttpPutResponseHopLimit'
[
1
]
では hop limit とは何で、なぜ値が変わると IMDS の取得が失敗するのかを見ていきます。
IMDS と hop limit とは?
IMDS
IMDS(Instance Metadata Service)は、EC2 インスタンスが自分自身の情報を取得するための特殊な内部アドレス(169.254.169.254)です。
このアドレスにリクエストを送ると、2 種類の情報を取得できます。
- インスタンスメタデータ : instance-id、リージョン、AZ、インスタンスタイプなど「自分はどんなインスタンスか」という情報
- IAM の一時的な認証情報 : インスタンスにアタッチされた IAM ロール(instance profile)の一時キー
- これを取得できれば、そのロールの権限で AWS API を呼び出せる
セキュリティ的に厄介なのは 2つ目の認証情報です。もし Pod がノードの IMDS にアクセスできてしまうと、その Pod はノードにアタッチされた IAM ロールの権限をそのまま使えてしまうことになります。
また IMDS には v1 と v2 があり、IMDSv2 は先に PUT リクエストでセッショントークンを取得し、そのトークンを使って取得する方式です。この一手間があるぶん、単純な SSRF(Server-Side Request Forgery)攻撃には強くなっています。最近の AWS SDK は IMDSv2 を優先して使うため、トークンを取得できるかどうかがそのまま IMDS を使えるかどうかになります。
hop limit
hop limit(http-put-response-hop-limit)は、IMDSv2 のセッショントークンを取得する際に IMDS が返す PUT レスポンスのパケットに設定されるネットワークホップ(hop)数の上限です。
IP パケットの TTL と同じように 1 ホップ通過するごとに 1 ずつ減り、0 になると破棄されます。
ここで注意したいのが、制限がかかるのはリクエストではなくレスポンスの側だという点です。リクエスト側は OS の通常の TTL で送られるので、PUT リクエスト自体は問題なく IMDS まで届きます。行きは届くのに帰りだけ破棄される、というのがこの設定の効き方です。
- hop = 1 : レスポンスがちょうど 1 ホップしか進めない
- ノード(EC2)自身は IMDS と同じホストなので、トークンをそのまま受け取れる
- 一方で Pod はノードよりもう一段内側(ネットワーク名前空間 → veth(仮想インターフェース)→ ノード)にいるため、1 ホップぶん遠い
- PUT リクエスト自体は IMDS に到達するが、返ってきたトークンレスポンス(TTL=1)が Pod まで戻れずに破棄される
- 結果として Pod はトークンを取得できず、IMDSv2 を使えない
- hop = 2 : レスポンスが 2 ホップまで進める
- トークンがノードを経由して Pod まで戻ってこられる
- そのため Pod からもノードの IMDS を利用できる
図にすると以下のようなイメージです。
hop limit = 1 : レスポンスはノードまで戻れるが、veth を越える 1 ホップで TTL を使い切り Pod に届かない
hop limit = 2 : veth を越えたあとも TTL が残るため、レスポンスが Pod まで戻れる
hop limit は「Pod がノードの IMDS(=IAM 認証情報)を使えるかどうか」を大きく左右する設定です。
そして hop 1 がデフォルトなのは偶然ではなく、意図されたセキュリティ設計だとされています。
the default hop limit on EC2 instances is intentionally set to 1 to prevent IP forwarding.
Blocking access to instance metadata will prevent pods that do not use IRSA or EKS Pod Identities from inheriting the role assigned to the worker node. You can block access to instance metadata by requiring the instance to use IMDSv2 only and updating the hop count to 1
見落としやすいのですが、2つ目の引用は条件を2つ挙げています。hop limit が制御するのは IMDSv2 のトークンレスポンスだけなので、IMDSv1(トークン不要の GET)が有効なままでは hop 1 だけでは塞ぎきれません。
とはいえ eks モジュール v21 は metadata_options のデフォルトで http_tokens = "required" と hop limit 1 の両方を設定してくれるので、モジュールを使っている限りこの 2つは揃います(v21.24.0 variables.tf)。
そのうえで、乗っ取られた Pod がノードの IAM ロールの権限を奪う SSRF 系の攻撃を防いでいるわけです。Pod 側で AWS の権限が必要なら、ノードの IMDS ではなく IRSA や EKS Pod Identity で Pod 単位に与える形になります。
hop limit を変える際にもうひとつ注意したいのが、ノードの入れ替えです。この値はノードの launch template に入るため、2 から 1 に変わると launch template が変更され、ノードのローリング(入れ替え)が発生します。デフォルト値がひとつ変わるだけでノードが入れ替わるので、アップグレードの計画にはこの点も含めておく必要があります。
Kubernetes API へのフォールバックで機能は正常だった
背景が揃ったところで、最初のログに戻ります。
ebs-csi-node はコンテナ(Pod)の中で動きますが、起動時にインスタンスメタデータ(instance-id、AZ、インスタンスタイプなど)を取得するために、まず IMDS を試します。しかし今回のように hop limit 1 の環境では IMDSv2 のトークンを取得できないため、タイムアウトのあとに失敗していました。
これは EBS CSI Driver の公式ドキュメントにも明記されている挙動です。
by default, IMDSv2 uses a hop limit of 1. That will prevent the driver from accessing IMDSv2 if run inside a container with the default IMDSv2 configuration.
それでも Pod が動き続けていた理由は、このエラーが致命的ではないという点にあります。同じドキュメントによれば、ドライバーは IMDS に失敗した場合、Kubernetes API を 2 番目のソースとしてフォールバックするように設計されています。
You may override the default metadata behavior of attempting IMDS, then falling back to Kubernetes, through the
--metadata-sourcesflag.
2 番目のソースである Kubernetes API は、自分自身の Node オブジェクトから同じメタデータを取得します。
| フィールド | IMDS(1 番目) | Kubernetes API(2 番目) |
|---|---|---|
| instance-id、AZ | IMDS | Node の spec.providerID(aws:///<az>/<instance-id>) |
| リージョン、AZ、インスタンスタイプ | IMDS | Node のラベル(topology.kubernetes.io/*、node.kubernetes.io/instance-type) |
注目したいのは、どちらのソースでも取得できる値は同じという点です。値の出どころは結局 EC2 のコントロールプレーンで、kubelet がノード登録時にこれらの値を Node オブジェクトに入れておくためです。パースする場所が違うだけです。
また ebs-csi-node は IMDS から認証情報を取得しません(AWS API の呼び出しは controller 側の担当)。ノードプラグインはローカルのボリュームマウントが主な役割なので、IMDS から取るのは上記の識別用メタデータだけです。
実際に確認できたこと
ドキュメントはフォールバックすることまでしか書いていないため、実際にどうなるのかは自分で確認するしかありませんでした。
ebs-csi-nodePod はRunningのままで、機能面での影響もない- IMDS のタイムアウトから Kubernetes API へのフォールバックまでは約 5 秒
- ボリュームの attach 上限(CSINode の allocatable count)も正常に埋まる
- ただし正常なのはインスタンスタイプ基準の静的な値
- IMDS を使って実際の attach 状態を反映する MutableCSINodeAllocatableCount(EKS 1.34 + ドライバー v1.46.0 以上で既定有効)は hop limit 1 では動かない
3 つ目は、IMDS が使えない場合に上限の計算がどうなるかを確認したものです。kubectl で以下のように見られます。
$ kubectl get csinode -o custom-columns=\
'NODE:.metadata.name,COUNT:.spec.drivers[?(@.name=="ebs.csi.aws.com")].allocatable.count'
NODE COUNT
ip-10-0-1-100.ap-northeast-1.compute.internal 25
COUNT が空にならず数値で埋まっていれば、Kubernetes メタデータから上限が計算できています。値そのものはインスタンスタイプによって変わります。
結果として hop limit 1 のせいで動かなくなった Pod はなく、残ったのは起動時の約 5 秒の遅延とエラーログだけでした。
ただしこれは、先ほど触れた IRSA で権限を渡していたからでもあります。ノードの IMDS 経由で IAM ロールの権限を借りている Pod があれば、hop limit が 1 になった時点で認証情報を取得できなくなります。
整理すると、影響を受けるのは「Pod 独自のネットワークで動き、かつ IMDS を参照する」という条件に当てはまるコンポーネントです。hostNetwork で動く Pod はノードのネットワークをそのまま使うため対象外で、IRSA や EKS Pod Identity で権限を受け取っている Pod も IMDS を使いません。
影響がエラーログだけで済むかどうかは、クラスター側で AWS の権限をどう持たせているかによります。v21 に上げる前に、IMDS に依存している Pod がないかを確認しておくのが確実です。
metadataSources を明示して解消する
機能は正常とはいえ、「失敗するとわかっている IMDS を試して、5 秒待ったあとにエラーログを残す」という動作は無駄なので、ここは解消しておきます。
公式ドキュメントは、コンテナから IMDS を使うには 2つの方法のいずれかが必要だと案内しています。
In order for the driver to access IMDS, it either must be run in host networking mode, or with a hop limit of at least 2.
ここで挙げられているのは IMDS を「使えるようにする」2つの方法ですが、そもそも IMDS を使わないという道もあります。あわせて整理すると、選択肢は 3 つです。
- hop limit を 2 に戻す
- IMDS は再び使えるようになる
- ただし先ほど説明した SSRF 対策(hop 1)を手放すことになる
- ノードプラグインを host networking モードで動かす
- Pod がノードのネットワーク名前空間をそのまま使うため veth を経由せず、hop 1 でも IMDS に到達できる
- ただし Pod をノードのネットワークに露出させることになるので、分離の観点では惜しい
- そもそも IMDS を使わず、Kubernetes をメタデータソースに固定する
- hop 1(セキュリティ)を保ったまま、IMDS の試行自体をなくせる
今回は動的な上限更新を必要としない構成だったこともあり、hop 1 を維持できる 3 番を選びました。
EBS CSI Driver は node.metadataSources の設定でソースを上書きできます。有効な値は imds、kubernetes、metadata-labeler(ALPHA)です。
ただし Kubernetes ソースが動作するには、次の 2つが条件になります。
- ドライバーの Pod が Kubernetes API サーバーにアクセスできること
- Node オブジェクトに必要な情報(providerID、リージョン / AZ / インスタンスタイプのラベル)が入っていること
EKS のノードはこの情報をデフォルトで持っているので、特別な準備なしにそのまま使えます。
EKS のマネージドアドオンとして使っている場合は、アドオンの configuration_values に以下のように指定します。
{
"node": {
"metadataSources": "kubernetes"
}
}
Terraform(eks モジュール)では、アドオンの定義に以下のように入れます。
aws-ebs-csi-driver = {
addon_version = "v1.59.0-eksbuild.1"
configuration_values = jsonencode({
node = {
metadataSources = "kubernetes"
}
})
}
先ほどの表のとおり v21 では addons.most_recent のデフォルトが true ですが、上記のように addon_version を明示した場合はそちらが優先されます。most_recent はバージョンを自動で解決するときにだけ使われるためです。
適用前後の比較
設定を適用すると ebs-csi-node Pod がローリングされ、ログから IMDS の試行の行そのものが消えました。
適用前
I "Attempting to retrieve instance metadata from IMDS"
E "Retrieving IMDS metadata failed ... context deadline exceeded" # 約 5 秒の遅延
I "Attempting to retrieve instance metadata from Kubernetes API"
I "Retrieved metadata from Kubernetes"
適用後
I "Attempting to retrieve instance metadata from Kubernetes API" # すぐに Kubernetes
I "Retrieved metadata from Kubernetes"
ログに出ない部分も先ほどの kubectl で確認したところ、attach 上限も Pod の状態も適用前と変わらず正常なままでした。
hop limit 1(SSRF 対策)を維持したまま、エラーログと起動の遅延をどちらも取り除くことができました。
まとめ
hop limit や IMDS のような低レイヤーの挙動は、普段はあまり意識しないまま、モジュールのメジャーアップグレードで急に表に出てくることがあります。terraform plan の差分は小さくても、一度確認しておくとこうしたケースに気づきやすくなります。
また、エラーログが出ていても機能は正常、というケースもあります。ログのレベルだけで判断せず、実際に何が影響を受けるのかまで確認しておくと、対処が必要かどうかを判断しやすくなります。
そして hop limit 1 は、Pod がノードの IAM ロールを借りられないようにするための変更でもあります。エラーログを消すだけなら hop limit を 2 に戻すのが手っ取り早いですが、その前に IRSA や EKS Pod Identity で足りないかを検討するほうが安全です。
同じような状況に出くわした方の参考になれば幸いです。





