ハードコードされた秘密情報の「終わりの始まり」
おい、ちょっと手を止めて聞いてくれ。
先日、あるクライアントのインシデント調査で、海外のIPからAWSやAzureの環境へ不正アクセスがあり、数千万円規模のクラウド資源がマイニングに悪用されるという現場に立ち会った。
原因を辿っていくと、犯人は高度なゼロデイ攻撃を使ったわけじゃない。開発環境の片隅に放置されていた、リポジトリ内の.envファイル、あるいはソースコードの奥深くにベタ書きされていた「平文のパスワードやシークレットキー」を、GitHubの漏洩スキャンから拾い上げただけだった。
「いやぁ、本番環境のクレデンシャルは環境変数に入れているから大丈夫です」
そう言って胸を張るエンジニアによく出会うが、OSのプロセスリストやメモリダンプ、コンテナのイメージレイヤーを覗かれたら、環境変数なんて丸見えだ。アプリケーションがデータベースや外部APIに接続する際、「パスワードを知っていること」を証明するために、わざわざ人間がその秘密情報をコードや設定ファイルに持たせる――この悪習こそが、現代のクラウドインフラにおける最大の爆弾なんだよ。
今日のテーマは、この泥臭くて危険なパスワード管理の呪縛を断ち切る「マネージドID(Managed Identity)とワークロードID(Workload Identity)の活用」だ。これを使えば、そもそも「秘密情報」という概念自体をコードから消し去ることができる。
—
攻撃者はどうやってシークレットを奪うのか?(PoCの現実)
なぜパスワードをコードや設定ファイルに持たせてはいけないのか。攻撃者がどのような手順でそれらを強奪し、インフラの深部へ侵入するのか、その冷徹な現実を追体験してみよう。
1. リポジトリのゴミ漁りとシークレットスキャン
GitHubやGitLabなどのパブリック/プライベートリポジトリにおいて、誤ってコミットされた .env や config.json は、攻撃者にとってのゴールドラッシュだ。彼らは git のコミット履歴(git log -p)や、過去のバージョンにまで残されたシークレットを自動化スクリプトで秒速で回収する。
2. SSRFやLFIを通じた環境変数・設定ファイルの窃取
仮にコード内にシークレットがなくとも、アプリケーションにサーバー側リクエスト偽造(SSRF)やローカルファイルインクルージョン(LFI)の脆弱性が存在する場合、攻撃者は次のような攻撃を仕掛ける。
例えば、AWSのEC2インスタンスメタデータサービス(IMDSv1)にアクセス可能な脆弱性がある場合、アプリケーションを踏み台にして以下のリクエストを送りつける。
# 攻撃者がSSRF等を用いてEC2のメタデータから一時クレデンシャルを強奪する例
curl http://169.254.169.254/latest/meta-data/iam/security-credentials/MyEC2Role
このリクエストが通ってしまった瞬間、攻撃者はインスタンスにアタッチされたIAMロールの一時アクセスキーを手に入れ、そのクラウド環境の「正当な利用者」になりすますことができる。パスワードをハッシュ化しようが、暗号化しようが、アプリケーション自体がその鍵を持っている限り、このリスクはゼロにならないのだ。
—
マネージドID / ワークロードIDというパラダイムシフト
このジレンマを根本から解決するのが、マネージドID(Azure Managed Identity)やワークロードID(GCP Workload Identity / Azure Workload Identity)だ。
これの本質は何か?
それは、「アプリケーションに認証情報を覚え込ませるのではなく、プラットフォーム(クラウド基盤)側がアプリケーションの身元を保証し、自動的に短命なトークンを発行する」という仕組みに他ならない。
- 開発者: 接続文字列やパスワードをコードや設定ファイルに書く必要が一切なくなる。
- アプリケーション: プラットフォームのAPI(ローカルエンドポイント等)を叩くだけで、自動更新されるアクセストークンを受け取れる。
- 攻撃者: 仮にソースコードが流出しても、そこには「鍵」が一切存在しないため、何も盗み出せない。
それでは、実務で即座に使える具体的な実装パターンを見ていこう。
—
【実装サンプル】マネージドIDを使ったセキュアなデータベース接続
ここでは、Azure App ServiceまたはAzure VM上で稼働するPython(Flask / SQLAlchemy)アプリを想定し、パスワードを一切使わずにAzure Managed IdentityとAzure AD(Microsoft Entra ID)認証を用いてMySQL/PostgreSQLやAzure SQLに接続するサンプルコードを示す。
1. Pythonアプリケーション側の実装(app.py)
コード内には、クライアントIDやシークレットの記述が一切ないことに注目してほしい。DefaultAzureCredential を使うことで、ローカル開発環境では開発者のAzure CLI認証を使い、本番のAzure上では自動的にマネージドIDにシームレスに切り替わる。
import os
from azure.identity import DefaultAzureCredential
import pymysql
def get_db_connection():
"""
マネージドIDを使用してAzure上のデータベースへ接続する関数。
パスワードをハードコードせず、Azure ADのアクセストークンを動的取得する。
"""
# 1. DefaultAzureCredentialで環境に応じた認証情報を自動取得
# (本番環境ではマネージドID、ローカルではAzure CLIの権限を使用)
credential = DefaultAzureCredential()
# 2. データベース用のアクセストークンを動的に取得(有効期限は通常数時間)
# スコープにはデータベースサービスに応じたリソースIDを指定
token = credential.get_token("https://ossrdbms-aad.database.windows.net/.default")
# 3. 取得したトークンをパスワードの代わりに渡して接続を確立
connection = pymysql.connect(
host=os.environ.get("DB_HOST"),
user=os.environ.get("DB_USER"),
database=os.environ.get("DB_NAME"),
password=token.token, # パスワードの代わりにアクセストークンを使用!
ssl_ca=os.environ.get("DB_SSL_CA_PATH")
)
return connection
if __name__ == "__main__":
try:
conn = get_db_connection()
print("マネージドIDによる認証成功:データベースに接続しました。")
conn.close()
except Exception as e:
print(f"接続エラー: {e}")
2. 必要パッケージのインストール要件(requirements.txt)
azure-identity==1.15.0
pymysql==1.1.0
cryptography==42.0.0
この実装であれば、万が一このソースコードがパブリックなリポジトリに誤ってプッシュされたとしても、データベースの安全性は微動だにしない。なぜなら、コード内に悪用の手がかり(鍵)が全く存在しないからだ。
—
【インフラ設定】GCP Workload Identity Pool のTerraform構築例
次に、Kubernetes(GKE)上で動くワークロード(Pod)から、シークレットキーのJSONファイル(サービスアカウントキー)を完全に排除し、GCPの各リソース(Cloud StorageやBigQueryなど)にアクセスさせるためのTerraform設定を見てみよう。
従来の「サービスアカウントのJSONキーをK8sのSecretに保存する」という手法は、K8sの権限設定を誤った瞬間にキーが丸見えになる地獄への切符だった。Workload Identityを使えば、K8sのサービスアカウントとGCPのサービスアカウントを安全に紐付けることができる。
# 1. GCP側でWorkload Identityプールを作成
resource "google_iam_workload_identity_pool" "k8s_pool" {
workload_identity_pool_id = "prod-k8s-identity-pool"
display_name = "Production K8s Workload Identity Pool"
description = "GKEクラスタからの安全な認証用プール"
disabled = false
}
# 2. プロバイダーの設定(Kubernetes OIDCプロバイダーとの連携)
resource "google_iam_workload_identity_pool_provider" "k8s_provider" {
workload_identity_pool_id = google_iam_workload_identity_pool.k8s_pool.workload_identity_pool_id
workload_identity_pool_provider_id = "gke-oidc-provider"
# GKEクラスタのエンドポイントURLを指定
oidc {
issuer_uri = "https://container.googleapis.com/v1/projects/my-project-id/locations/us-central1/clusters/my-gke-cluster"
}
attribute_mapping = {
"google.subject" = "assertion.sub"
"attribute.k8s_ns" = "assertion.kubernetes.io/namespace"
"attribute.k8s_sa" = "assertion.kubernetes.io/serviceaccount/name"
}
}
# 3. GCPサービスアカウントに対するIAMバインディングの設定
# 特定のK8s名前空間・サービスアカウントのみがGCPサービスアカウントになりすませるように制限
resource "google_service_account_iam_member" "workload_identity_binding" {
service_account_id = "projects/my-project-id/serviceAccounts/app-backend-sa@my-project-id.iam.gserviceaccount.com"
role = "roles/iam.workloadIdentityUser"
member = "serviceAccount:my-project-id.svc.id.goog[production/backend-service-account]"
}
このインフラ構成を適用すれば、Kubernetesのコンテナ内にはGCPの認証情報を一切保持させる必要がなくなる。PodはGoogleのメタデータサーバーを通じて自動的に短命なOAuth2トークンを取得し、Google CloudのAPIを安全に叩き続けることができるのだ。
—
セキュリティチーフからの実務アドバイス
現場のエンジニアたちにいつも伝えていることがある。セキュリティとは、「強固な鍵を作るゲーム」ではなく、「鍵そのものを無くすゲーム」なのだと。
どんなに厳重に暗号化しても、どんなに複雑なパスワードを設定しても、「人間がそれを管理し、コードや設定ファイルに記述する」というプロセスが存在する限り、ヒューマンエラーやサプライチェーン攻撃による漏洩のリスクはゼロにならない。
今日から君たちのチームでも、以下のルールを徹底してほしい。
1. 「サービスアカウントキーのJSONファイル」や「データベースの平文パスワード」を新規に発行することを禁止する。
2. クラウド環境間での連携には、必ずマネージドIDまたはワークロードIDを採用する。
3. ローカル開発環境においても、クラウドベンダーが提供するCLI認証(Azure CLIやGCloud CLI、AWS SSO)をベースにしたクレデンシャルプロバイダーを利用し、コードやローカルファイルに永続的な鍵を置かないワークフローを構築する。
セキュリティの強さは、複雑な防御壁の数ではなく、「いかに攻撃者に付け入る隙(アタックサーフェス)を与えないか」で決まる。コードから不要な秘密情報を一掃し、モダンで堅牢なインフラストラクチャを構築してくれ。期待しているぞ。
コメント