おい、ちょっと手を止めてこっちを向いてくれ。
先日、とあるクライアントの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へのリプレースを急いでくれ。
運用コストは下がり、セキュリティレベルは跳ね上がる。やらない理由はないはずだ。実装で詰まったら、いつでも俺のところへ聞きに来い。セキュアで強靭なシステムを一緒に作り上げていこうぜ。
コメント