「おいおい、まさか『クラスターが重くなったら自動でノードが増えるから安心』なんて、手放しで喜んでないよな?」
現場のメンバーが嬉しそうにKubernetes(K8s)のCluster AutoscalerやKarpenterの導入を報告してくれるたびに、私は決まってこの言葉を投げかける。
クラウドの恩恵である「オートスケーリング」。負荷に応じてインフラが自律的に伸縮する様子は、エンジニアリングの極みのように思える。だが、攻撃者の視点に立ってみろ。彼らにとって、自動スケーリングは「脆弱な、あるいは初期設定のままの丸裸のサーバー(ノード)が、自動的に、かつ無限に増殖して燃料を投下してくれるボーナスタイム」に他ならないんだ。
特に、ノードが起動(プロビジョニング)してからK8sクラスターにジョインし、準備完了(Ready)になるまでの「ブートストラップ」と呼ばれる数分間。ここには、設計者が見落としがちな恐ろしい盲点が潜んでいる。
今回は、数々のインシデント現場で泥水をすすってきた私が、自動スケーリングにおけるノードハーデニングと認証情報保護のリアルなリスク、そして今日からコピペで現場に即投入できる鉄壁の防御コードを伝授する。
—
1. 攻撃シナリオ:なぜ、自動スケーリングしたノードは秒で乗っ取られるのか?
教科書には「コンテナは隔離されているから安全」と書いてあるかもしれないが、現実はそんなに甘くない。攻撃者が狙う典型的なシナリオを解説しよう。
ターゲットは「IMDSv1」と「UserDataに放置されたトークン」
あなたが開発したWebアプリケーションに、URLを指定するとその中身を取得してくる機能(例えば、OGP画像の取得機能や外部API連携)があったとする。ここに脆弱性があり、SSRF(Server-Side Request Forgery)が可能だった場合、攻撃者は真っ先にどこを叩くと思う?
答えは、クラウドのインスタンスメタデータサービス(IMDS)のリンクローカルアドレス http://169.254.169.254 だ。
特に古い設定のままのIMDSv1が有効になっている場合、攻撃者はコンテナ内、あるいはSSRF経由で以下のような単純なリクエストを送るだけで、ノードに付与されたIAMロールの一時クレデンシャルをいとも簡単に奪取できてしまう。
# 攻撃者がSSRFやコンテナ侵入後に実行するコマンドのイメージ
$ curl http://169.254.169.254/latest/meta-data/iam/security-credentials/eks-node-group-role
これだけで、AWSの AccessKeyId, SecretAccessKey, Token が画面に表示される。
もし、このノードのIAMロールに「EC2の操作権限」や「K8sクラスターへの強いアクセス権限」が紐づいていたら? 攻撃者はその瞬間に、クラスター全体の支配権(管理者権限)を手に入れることになる。
さらに最悪なのは、ノードが起動する際の「UserData(ユーザーデータスクリプト)」に、K8sクラスターに参加するためのジョイントークンや、プライベートリポジトリを引くためのGitHubのGitHub Appsトークン、認証鍵などをハードコードしてしまっているケースだ。
攻撃者がノードのメタデータにアクセスできれば、UserDataの中身も丸見えになる。自動スケーリングで新しいノードが立ち上がるたびに、攻撃者にとっての「鍵の自動配布祭り」が開催されているようなものなんだ。
—
2. 防御の極意1:Packerによる「黄金イメージ(Golden Image)」の事前堅牢化
「ノードが起動してから、UserDataで yum update や apt-get install を走らせて、セキュリティ設定を適用すればいいや」
もし君のチームがこの運用をしているなら、今すぐやめさせよう。
起動時に外部リポジトリからパッケージを落とす設計は、以下の3つの致命的なリスクを孕んでいる。
1. スケールアウトの遅延: パッケージのダウンロードとインストールに数分かかり、スパイクアクセスへの対応が間に合わない。
2. 起動失敗(DoS): 外部リポジトリやDNSが一時的に落ちていた場合、スケーリングしたノードがクラスターに参加できず、システム全体が沈む。
3. 中間者攻撃(MITM): 起動時の無防備な状態で、不正なパッケージを掴まされるリスク。
正解は、「あらかじめ不要なサービスを削ぎ落とし、セキュリティパッチを適用し終えた『黄金イメージ(AMIやマシンイメージ)』を事前にビルドしておき、オートスケーリングではそれを起動するだけにする」ことだ。
以下は、HashiCorpのPackerとAnsibleを使って、不要なサービス(avahi-daemon や rpcbind など)を停止し、CISベンチマークに準拠した堅牢なUbuntuベースのEKSノードイメージを作成するためのテンプレートコードだ。
node-hardened-ami.pkr.hcl(Packer設定ファイル)
packer {
required_plugins {
amazon = {
version = ">= 1.0.0"
source = "github.com/hashicorp/amazon"
}
}
}
source "amazon-ebs" "eks_node" {
ami_name = "eks-hardened-node-{{timestamp}}"
instance_type = "t3.medium"
region = "ap-northeast-1"
# ベースとなるEKS最適化AMIを検索
source_ami_filter {
filters = {
name = "amazon-eks-node-1.28-v*"
root-device-type = "ebs"
virtualization-type = "hvm"
}
most_recent = true
owners = ["602401143452"] # Amazon EKSの公式AWSアカウントID
}
ssh_username = "ec2-user"
}
build {
sources = ["source.amazon-ebs.eks_node"]
# Ansible等を使ってOSのハーデニング(不要サービスの無効化、ログ転送設定など)を実行
provisioner "shell" {
inline = [
# 1. パッケージのアップデート
"sudo yum update -y",
# 2. 攻撃者に悪用されやすい不要なネットワークサービスの停止と無効化
"sudo systemctl disable avahi-daemon",
"sudo systemctl stop avahi-daemon || true",
"sudo systemctl disable rpcbind",
"sudo systemctl stop rpcbind || true",
# 3. ユーザーデータのログにパスワードやトークンが残らないよう、コンソールの出力を制限
"sudo chmod 600 /var/log/cloud-init-output.log",
"sudo chmod 600 /var/log/cloud-init.log",
# 4. SSHのパスワード認証を明示的に禁止(公開鍵認証のみ)
"sudo sed -i 's/^#PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config",
"sudo sed -i 's/^PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config",
"sudo systemctl restart sshd"
]
}
}
このPackerスクリプトでビルドしたAMIをオートスケーリンググループ(ASG)やKarpenterの Provisioner(現 NodePool)に登録する。これで、起動するノードは最初から「要塞化された状態」で戦場に投入されるわけだ。
—
3. 防御の極意2:TerraformによるIMDSv2の強制とホップ制限(Hop Limit)
ここからが、今回の最も重要なハイライトだ。
仮にコンテナに侵入されたりSSRFが発生したりしても、ノードのIAMロールを絶対に盗ませないための設定を行う。
AWSにおける特効薬は「IMDSv2(Instance Metadata Service Version 2)の強制」と「ホップ制限(Hop Limit)を 1 にすること」だ。
なぜホップ制限(Hop Limit)を「1」にするのか?
IMDSv2は、認証のために最初に PUT リクエストを送り、セッショントークンを取得することを要求する。これだけでもSSRF対策として非常に強力だが、さらに外堀を埋める。
コンテナ(Pod)から送信されるIPパケットは、ホスト(ノード)のネットワーク名前空間を跨ぎ、ブリッジ接続や仮想インターフェースを経由して外に出る。このネットワークの境界を越える際、パケットのIPヘッダにある TTL(Time To Live / ホップ数) が「1」減少する。
- ホストOS(ノード自体)からIMDSへのアクセス: ホップ数は
1。 - コンテナ(Pod)からIMDSへのアクセス: ネットワークの境界を1つ超えるため、ホップ数は最低でも
2必要。
つまり、EC2の起動テンプレートで「IMDSのレスポンスホップ制限(http_put_response_hop_limit)を 1」に設定しておくと、ホストOS上(Kubeletなど)からのメタデータアクセスは許可しつつ、コンテナ(Pod)内からのメタデータアクセスは物理的に遮断することができるんだ。
これをTerraformで実装したコードが以下だ。明日から構築するすべてのクラスターにこの設定を組み込んでくれ。
eks-node-group.tf(Terraformでの実装例)
# セキュアな起動テンプレート(Launch Template)の定義
resource "aws_launch_template" "secure_eks_node" {
name_prefix = "secure-eks-node-"
image_id = "ami-xxxxxxxxxxxxxxxxx" # 先ほどPackerで作成した要塞化済みのAMI IDを指定
instance_type = "t3.medium"
# ネットワーク・セキュリティ設定
network_interfaces {
associate_public_ip_address = false # パブリックIPは絶対に直接付与しない(プライベートサブネットに配置)
security_groups = [aws_security_group.node_sg.id]
}
# 【超重要】メタデータオプションのセキュア設定
metadata_options {
http_endpoint = "enabled" # メタデータサービス自体は有効(Kubeletの起動に必要)
http_tokens = "required" # IMDSv2を「必須」にし、IMDSv1を完全無効化
http_put_response_hop_limit = 1 # ホップ制限を「1」に設定し、コンテナ内からのアクセスを物理遮断
instance_metadata_tags = "disabled" # インスタンスのタグから機密情報が漏洩するのを防ぐ
}
user_data = base64encode(<<-EOF
#!/bin/bash
set -o xtrace
# クラスターへのジョインスクリプトを実行する。
# ここには絶対にデータベースのパスワードや、管理者特権を持つAPIキーなどを直書きしてはならない。
/etc/eks/bootstrap.sh my-kubernetes-cluster
EOF
)
lifecycle {
create_before_destroy = true
}
}
# EKSセルフマネージド・ノードグループ、またはASGに上記テンプレートを適用
resource "aws_autoscaling_group" "eks_asg" {
desired_capacity = 2
max_size = 10
min_size = 2
vpc_zone_identifier = ["subnet-private-1a", "subnet-private-1c"] # 必ずプライベートサブネットを指定
launch_template {
id = aws_launch_template.secure_eks_node.id
version = "$Latest"
}
tag {
key = "kubernetes.io/cluster/my-kubernetes-cluster"
value = "owned"
propagate_at_launch = true
}
}
—
4. 防御の極意3:K8sネットワークポリシーによる多層防御
インフラ(AWS/クラウド)レイヤーで守りを固めたら、次はK8sレイヤーでの多層防御だ。
もし「ホップ制限を設定できない事情がある(一部のPodでどうしてもノードのIAMロールを一時的に必要とするなど)」場合や、インフラ設定の漏れに対する保険として、NetworkPolicyを使ってPodからメタデータIP(169.254.169.254)への通信を明示的に禁止しよう。
以下のマニフェストを適用することで、対象のネームスペース内のすべてのPodから、クラウドのメタデータエンドポイントへの通信をデフォルトで拒否する。
deny-metadata-egress.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: deny-metadata-endpoint-egress
namespace: default # 開発アプリが動くネームスペースを指定
spec:
podSelector: {} # ネームスペース内の「すべてのPod」を対象にする
policyTypes:
- Egress
egress:
# 1. 通常の通信(DNSや他のPod、インターネットへのアクセス)は許可する
- to:
- ipBlock:
cidr: 0.0.0.0/0
except:
- 169.254.169.254/32 # 【超重要】メタデータエンドポイントのIPだけを「除外(ブロック)」に指定
これを適用しておけば、万が一WebアプリケーションにSSRFの脆弱性が混入し、かつインフラ側のIMDSv2設定が漏れていたとしても、K8sのCNI(CalicoやCiliumなど)がメタデータへのパケットをネットワークレベルでドロップしてくれる。
—
5. チーフエンジニアからのアドバイス:自動化とセキュリティは「表裏一体」
最後に、君たちに覚えておいてほしいことがある。
「自動化は、セキュリティの自動化を伴わなければ、単なる『脆弱性の自動拡散器』にしかならない」
オートスケーリングを導入して「インフラ管理が楽になった!」と喜ぶのは素人だ。プロは「これによってアタックサーフェス(攻撃対象領域)がどう変化し、ブートストラップのタイミングにどんな隙が生まれるか」を真っ先に考える。
今回紹介した3つのアプローチ:
1. PackerによるOSハーデニング(黄金イメージ化)
2. IMDSv2の強制 + ホップ制限「1」によるクレデンシャル奪取の完全防御
3. NetworkPolicyによるコンテナ・ネットワークの多層防御
これらはどれか一つをやればいいというものではない。すべてを組み合わせて初めて、ビクともしない堅牢なK8sクラスターが完成する。
さあ、今すぐ自分たちのクラスターの設定ファイルを開いて、http_put_response_hop_limit がどうなっているか確認してみてくれ。もし設定されていなかったら……君が今日、ヒーローになるチャンスだぞ。
コメント