【実務・中級編】 Workload Identityによるクラウドサービスへの安全なアクセス – インフラ・ネットワーク & クラウドセキュリティ防御ガイド

おい、ちょっと手を止めてこっちを向いてくれ。

先日、とあるクライアントのKubernetesクラスタのペネトレーションテスト(模擬攻撃)を完了したんだが、またやらかしていたよ。何だと思う? WebアプリケーションのPod内から、AWSのフル権限を持った永続的なアクセスキー(AWS_ACCESS_KEY_ID と AWS_SECRET_ACCESS_KEY)が環境変数としてゴロゴロ発掘されたんだ。

攻撃者目線で言わせてもらえば、こんなの「どうぞ金庫のマスターキーを持って行ってください」と言っているようなものだ。Podが1台でもRCE(リモートコード実行)系の脆弱性や、アプリケーションの依存関係(脆弱なライブラリ)を踏み台にされて侵攻されたら、一瞬でクラウド環境全体の制圧(クラスタエスケープからの全バケット強奪など)が完了する。

「うちは環境変数をKubernetesのSecretで暗号化しているから大丈夫です」なんて言い訳は、現場のセキュリティレビューでは一切通用しない。メモリ上に露出した平文クレデンシャルは、ダンプを取るかプロセスを覗き見れば一発で抜けるからだ。

だからこそ、現代のクラウドネイティブインフラにおいて「Workload Identity(ワークロードアイデンティティ)」の導入は、オプションではなく絶対的なマスト要件なのだ。今日は、なぜ静的クレデンシャルが最悪の選択肢なのか、そしてOIDC(OpenID Connect)を用いた動的な一時クレデンシャルへの移行をどう実装すべきか、俺が現場のリアルなコードと共に叩き込んでやる。

—

1. 静的クレデンシャルという「時限爆弾」の正体

まずは、攻撃者がPodの内部に入り込んだときに何を狙うかを知っておこう。彼らは真っ先に環境変数を調べる。

# 攻撃者がPod内で実行するお決ろのコマンド
env | grep -iE 'aws|key|secret|token'

ここに AWS_ACCESS_KEY_ID や AZURE_CLIENT_SECRET が入っていたら、攻撃者はその瞬間にクラウドAPIへの永続的なアクセス権を手に入れる。キーがローテーションされない限り、その権限は永遠に生き続ける。これが「静的クレデンシャルの呪い」だ。

これに対して、Workload IdentityはクラウドプロバイダーのIAMとKubernetesのServiceAccount(KSA)をOIDCで信頼関係(Federation)で結ぶ。Podには永続的なキーを一切持たせず、Kubernetesが発行する短命なJWT(JSON Web Token)をクラウド側が検証することで、「その瞬間、そのPodだけに許可された最小限の一時権限(Session Token)」を動的に払い出す仕組みだ。

これにより、万が一Podが踏み台にされても、被害は最小限に抑えられ、キーの漏洩リスク自体が物理的に消滅する。

—

2. 【実践】AWS (EKS) と Kubernetes間でのWorkload Identity(IRSA)構築

では、具体的な実装に踏み込もう。今回はAWSのEKS環境をベースに、IAM Roles for Service Accounts(IRSA)を用いた設定手順を解説する。

ステップ1: OIDCプロバイダーの有効化

EKSクラスタに対して、独自のOIDCプロバイダーURLを関連付ける。これはAWS CLIやTerraformで一発だ。以下のTerraformスニペットを見てほしい。

# TerraformによるEKS OIDCプロバイダーの設定例
data "aws_eks_cluster" "this" {
  name = "production-cluster"
}

# クラスタに紐づくOIDC発行体のURLからプロバイダーを作成
resource "aws_iam_openid_connect_provider" "eks" {
  url             = data.aws_eks_cluster.this.identity[0].oidc[0].issuer
  client_id_list  = ["sts.amazonaws.com"]
  thumbprint_list = ["9e99a48a9960b14926bb7f3b02e22da2b0ab7280"] # ルートCAのサムプリント
}

ステップ2: アプリケーション用IAMロールの作成と信頼関係の定義

次に、Kubernetesの特定のServiceAccountからのみ引き受け(Assume)が可能なIAMロールを作成する。ここでのポイントは、条件(StringEquals)にクラスタのOIDCと対象のNamespace・ServiceAccount名を厳密に指定することだ。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::123456789012:oidc-provider/oidc.eks.us-east-1.amazonaws.com/id/EXAMPLED539D4633E53DE1B71EXAMPLE"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "oidc.eks.us-east-1.amazonaws.com/id/EXAMPLED539D4633E53DE1B71EXAMPLE:sub": "system:serviceaccount:default:my-secure-app-sa",
          "oidc.eks.us-east-1.amazonaws.com/id/EXAMPLED539D4633E53DE1B71EXAMPLE:aud": "sts.amazonaws.com"
        }
      }
    }
  ]
}

ステップ3: Kubernetes側のマニフェスト設定

いよいよKubernetes側の設定だ。ServiceAccountに先ほど作成したIAMロールのARNをアノテーションとして付与し、アプリケーションのDeploymentでそのServiceAccountを指定する。

apiVersion: v1
kind: ServiceAccount
metadata:
  name: my-secure-app-sa
  namespace: default
  annotations:
    # AWSのIAMロールと紐付けるマジックアノテーション
    eks.amazonaws.com/role-arn: "arn:aws:iam::123456789012:role/MySecureAppS3AccessRole"
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: secure-backend-app
  namespace: default
spec:
  replicas: 2
  selector:
    matchLabels:
      app: secure-backend
  template:
    metadata:
      labels:
        app: secure-backend
    spec:
      serviceAccountName: my-secure-app-sa # ここで紐付けたSAを指定する
      containers:
        - name: app
          image: 123456789012.dkr.ecr.us-east-1.amazonaws.com/my-app:v1.0.0
          # 環境変数にAWS_ACCESS_KEY_IDは一切書かない!これが鉄則。
          env:
            - name: TARGET_S3_BUCKET
              value: "my-company-secure-storage"
          resources:
            limits:
              cpu: "500m"
              memory: "512Mi"

—

3. セキュアなアプリケーション実装コード(Python / AWS SDK)

インフラ側でWorkload Identityを整えたら、アプリケーション側(ここではPythonのBoto3を想定)のコードも確認しておこう。

勘違いしやすいが、SDKの初期化コードに特別な変更は必要ない。AWS SDKのデフォルトのクレデンシャルチェーン(DefaultAWSCredentialsProviderChain)は、環境変数や設定ファイルにキーが見つからない場合、自動的にKubernetesがコンテナ内にマウントしてくれたOIDCトークンを検知し、AWS STSの AssumeRoleWithWebIdentity をバックグラウンドで実行して一時トークンを取得してくれる。

以下のPythonスクリプトは、静的キーを一切持たずにS3から安全にデータを取得する実務的なサンプルコードだ。

import os
import boto3
from botocore.exceptions import ClientError

def fetch_secure_document(file_key: str) -> bytes:
    """
    Workload Identity (IRSA) を利用して一時クレデンシャルでS3にアクセスする関数。
    ソースコード内や環境変数に静的クレデンシャルはハードコードしない。
    """
    # 環境変数からバケット名を取得(機密情報ではない公開情報や設定値のみ)
    bucket_name = os.getenv("TARGET_S3_BUCKET")
    if not bucket_name:
        raise ValueError("TARGET_S3_BUCKET environment variable is missing.")

    # boto3クライアントの生成
    # ※ クレデンシャル(aws_access_key_id等)は明示的に渡さず、
    #    環境変数のAWS_WEB_IDENTITY_TOKEN_FILEを経由して自動取得される。
    s3_client = boto3.client("s3")

    try:
        print(f"Fetching {file_key} from bucket {bucket_name} using dynamic credentials...")
        response = s3_client.get_object(Bucket=bucket_name, Key=file_key)
        
        # オブジェクトのデータを読み込む
        file_content = response['Body'].read()
        return file_content

    except ClientError as e:
        # エラーハンドリング(ログに機密情報を含めないよう注意)
        error_code = e.response.get("Error", {}).get("Code", "Unknown")
        print(f"Failed to fetch object from S3. Error Code: {error_code}")
        raise

if __name__ == "__main__":
    # 実行テスト
    try:
        data = fetch_secure_document("confidential/report.pdf")
        print("Successfully retrieved secure data.")
    except Exception as ex:
        print(f"Operation failed: {ex}")

このコードを実行すると、AWS SDKは自動的にコンテナ内の /var/run/secrets/eks.amazonaws.com/serviceaccount/token(Kubernetesが自動マウントするOIDCトークン)を読み込み、AWS側へ一時セッションを要求する。開発者はクレデンシャルのローテーションや管理の苦しみから完全に解放されるというわけだ。

—

セキュリティチーフからの総括

クラウドセキュリティの本質は、「侵害されることを前提とした多層防御(Zero Trust Architecture)」にある。「絶対に不正アクセスされないシステム」などこの世に存在しない。だからこそ、侵入されたときに攻撃者が得られる利得(Blast Radius)を極限まで小さく設計することが、プロのエンジニアの仕事なのだ。

今日から君たちのチームでも、アプリケーションコードや設定ファイルにある静的アクセスキーをすべて洗い出し、クラウドベンダーが提供するWorkload Identityへのリプレースを急いでくれ。

運用コストは下がり、セキュリティレベルは跳ね上がる。やらない理由はないはずだ。実装で詰まったら、いつでも俺のところへ聞きに来い。セキュアで強靭なシステムを一緒に作り上げていこうぜ。

コメント

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