【実務・中級編】 Kubernetesにおけるノードの自動スケーリングとセキュリティ – インフラ・ネットワーク & クラウドセキュリティ防御ガイド

「おいおい、まさか『クラスターが重くなったら自動でノードが増えるから安心』なんて、手放しで喜んでないよな?」

現場のメンバーが嬉しそうに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 がどうなっているか確認してみてくれ。もし設定されていなかったら……君が今日、ヒーローになるチャンスだぞ。

コメント

タイトルとURLをコピーしました