Cloud Functions と Secret Manager でサービスアカウントキーローテーションを自動化してみた。
こんにちは、みかみです。
仕事中に犬が「遊べ」とおもちゃの紐を持ってくるので、無視を貫き通していたら、肩に紐を置いて去ってゆきました(このくさい紐をどうしろと?
はじめに
2024/04以降に Google Cloud の組織を作成した場合、デフォルトで有効になる組織ポリシーがいくつか追加になりました。 いずれもセキュリティ強化に関する項目です。 2024/04以前に作成した組織で Google Cloud プロジェクトをお使いの場合には、一部のポリシーを除き自動で有効にはならないので、一度組織ポリシーを見直してみるのも良いのではないかと思います。
デフォルトで有効化されることになった組織ポリシーの中に、下記、サービスアカウントに関するポリシーがあります。
サービスアカウントキー作成を無効にする: iam.disableServiceAccountKeyCreation
その名の通り、サービスアカウントキーの発行を禁止する組織ポリシーです。 Google Cloud では、近年、サービスアカウントの認証には、キー情報ではなく Workload Identity 連携を利用することが推奨されています。
とはいえ、これまでずっとサービスアカウントキーを利用して本番運用しているシステムなど、認証方法の変更が簡単にできないケースもあるのではないでしょうか。 そんな時には、以下の組織ポリシーを有効にして、サービスアカウントキーローテーションを強制することを検討しても良いのではないかと思います。 なお、こちらのポリシーは2024/04以降もデフォルト有効にはなりません。
サービス アカウント キーの有効期限(時間): constraints/iam.serviceAccountKeyExpiryHours
ですが、人間は忘れる生き物です。 手動でのキーローテーションを想定している場合、作業を忘れてシステム障害が発生するリスクも否定できません。 また、定期的なローテーション作業には労力がかかることになります。
ということで。
やりたいこと
- サービスアカウントキーを定期的に自動でローテーションしたい
- Cloud Functions をスケジュール起動して、Secret Manager に登録済みのサービスアカウントキーを新しいキーに置き換えたい
前提
Google Cloud SDK(gcloud コマンド)の実行環境は準備済みであるものとします。 本エントリでは、Cloud Shell を使用しました。
また、Cloud Functions や Secret Manager など各サービス操作に必要な API の有効化と必要な権限は付与済みです。
なお、文中、プロジェクトIDに関する一部の文字は伏字に変更しています。
[準備]Secret Manager のシークレットを作成
以下のコマンドで、サービスアカウントキーを保管しておく Secret Manager のシークレットを作成しておきます。
gcloud secrets create temp-sa-rotation
$ gcloud secrets create temp-sa-rotation Created secret [temp-sa-rotation]. $ gcloud secrets describe temp-sa-rotation createTime: '2024-06-28T15:12:37.493832Z' etag: '"161bf4ad864c48"' name: projects/797147019523/secrets/temp-sa-rotation replication: automatic: {}
[準備]サービスアカウントを作成
以下のコマンドで、検証用のサービスアカウントを作成しました。
gcloud iam service-accounts create temp-sa-rotation
$ gcloud iam service-accounts create temp-sa-rotation Created service account [temp-sa-rotation]. $ gcloud iam service-accounts describe temp-sa-rotation@cm-da-mikami-yuki-****.iam.gserviceaccount.com email: temp-sa-rotation@cm-da-mikami-yuki-****.iam.gserviceaccount.com etag: MDEwMjE5MjA= name: projects/cm-da-mikami-yuki-****/serviceAccounts/temp-sa-rotation@cm-da-mikami-yuki-****.iam.gserviceaccount.com oauth2ClientId: '105759542851247631330' projectId: cm-da-mikami-yuki-**** uniqueId: '105759542851247631330'
[準備]サービスアカウントキーを作成して Secret Manager に登録する
ローテーションするサービスアカウントキーを作成して、Secret Manager に登録します。 後ほど Cloud Functions で実行するため、以下の Pyhon コードで実行しました。
from google.cloud import iam_admin_v1 from google.cloud.iam_admin_v1 import types from google.cloud import secretmanager import google_crc32c def create_key(project_id: str, account: str) -> types.ServiceAccountKey: iam_admin_client = iam_admin_v1.IAMClient() request = types.CreateServiceAccountKeyRequest() request.name = f"projects/{project_id}/serviceAccounts/{account}" key = iam_admin_client.create_service_account_key(request=request) return key def add_secret_version( project_id: str, secret_id: str, payload: str ) -> secretmanager.SecretVersion: print(f"{str}") client = secretmanager.SecretManagerServiceClient() parent = client.secret_path(project_id, secret_id) crc32c = google_crc32c.Checksum() crc32c.update(payload) response = client.add_secret_version( request={ "parent": parent, "payload": { "data": payload, "data_crc32c": int(crc32c.hexdigest(), 16), }, } ) print(f"Added secret version: {response.name}") return response project_id = 'cm-da-mikami-yuki-****' account = 'temp-sa-rotation@cm-da-mikami-yuki-****.iam.gserviceaccount.com' secret_id = 'temp-sa-rotation' key = create_key(project_id, account) add_secret_version(project_id, secret_id, key.private_key_data)
念のため、正常にシークレットバージョンが登録されたか確認しておきます。
$ gcloud secrets versions list temp-sa-rotation NAME: 1 STATE: enabled CREATED: 2024-06-28T15:31:31 DESTROYED: - $ gcloud secrets versions describe 1 --secret=temp-sa-rotation clientSpecifiedPayloadChecksum: true createTime: '2024-06-28T15:31:31.492600Z' etag: '"161bf4f11dbef8"' name: projects/797147019523/secrets/temp-sa-rotation/versions/1 replicationStatus: automatic: {} state: ENABLED
サービスアカウントキーが Secret Manager の バージョン:1
に登録できました。
新しいサービスアカウントキーを発行して、Secret Manager に登録する Cloud Functions をデプロイする
Cloud Functions 実行用サービスアカウントを準備
Cloud Functions 実行用のサービスアカウントを作成します。
$ gcloud iam service-accounts create temp-sa-rotation-gcf Created service account [temp-sa-rotation-gcf]. $ gcloud iam service-accounts describe temp-sa-rotation-gcf@cm-da-mikami-yuki-****.iam.gserviceaccount.com email: temp-sa-rotation-gcf@cm-da-mikami-yuki-****.iam.gserviceaccount.com etag: MDEwMjE5MjA= name: projects/cm-da-mikami-yuki-****/serviceAccounts/temp-sa-rotation-gcf@cm-da-mikami-yuki-****.iam.gserviceaccount.com oauth2ClientId: '112114946874824196312' projectId: cm-da-mikami-yuki-**** uniqueId: '112114946874824196312'
作成したサービスアカウントに、サービスアカウントキーを作成する権限と、Cloud Run を起動するための権限、Eventarc イベントを受信する権限を付与します。
- 必要なロール | IAM ドキュメント
- トリガーを使用して Cloud Functions の関数を作成する | Cloud Scheduler ドキュメント
- Cloud Run ターゲットのロールと権限 | Eventarc ドキュメント
$ gcloud projects add-iam-policy-binding cm-da-mikami-yuki-**** \ --member="serviceAccount:temp-sa-rotation-gcf@cm-da-mikami-yuki-****.iam.gserviceaccount.com" \ --role="roles/iam.serviceAccountKeyAdmin" (省略) $ gcloud projects add-iam-policy-binding cm-da-mikami-yuki-**** \ --member="serviceAccount:temp-sa-rotation-gcf@cm-da-mikami-yuki-****.iam.gserviceaccount.com" \ --role="roles/run.invoker" (省略) $ gcloud projects add-iam-policy-binding cm-da-mikami-yuki-**** \ --member="serviceAccount:temp-sa-rotation-gcf@cm-da-mikami-yuki-****.iam.gserviceaccount.com" \ --role="roles/eventarc.eventReceiver" Updated IAM policy for project [cm-da-mikami-yuki-****]. bindings: (省略) - members: - serviceAccount:797147019523-compute@developer.gserviceaccount.com - serviceAccount:temp-sa-rotation-gcf@cm-da-mikami-yuki-****.iam.gserviceaccount.com role: roles/eventarc.eventReceiver (省略) - members: - serviceAccount:temp-sa-rotation-gcf@cm-da-mikami-yuki-****.iam.gserviceaccount.com role: roles/iam.serviceAccountKeyAdmin (省略) - members: - serviceAccount:service-797147019523@gcp-sa-pubsub.iam.gserviceaccount.com - serviceAccount:temp-sa-rotation-gcf@cm-da-mikami-yuki-****.iam.gserviceaccount.com role: roles/run.invoker (省略) etag: BwYb9xkSOM4= version: 1
続いて、先ほど準備しておいた Secret Manager シークレットに、Cloud Functions 実行用サービスアカウントがシークレットバージョンを追加する権限を付与します。
$ gcloud secrets add-iam-policy-binding temp-sa-rotation \ --member="serviceAccount:temp-sa-rotation-gcf@cm-da-mikami-yuki-****.iam.gserviceaccount.com" \ --role="roles/secretmanager.secretVersionAdder" Updated IAM policy for secret [temp-sa-rotation]. bindings: - members: - serviceAccount:temp-sa-rotation-gcf@cm-da-mikami-yuki-****.iam.gserviceaccount.com role: roles/secretmanager.secretVersionAdder etag: BwYb9TQ2C7c= version: 1
Cloud Functions スケジュール実行用の Pub/Sub トピックと Scheduler ジョブを作成する
Cloud Scheduler ジョブからのメッセージを受け取る Pub/Sub トピックを作成します。
$ gcloud pubsub topics create temp-sa-rotation Created topic [projects/cm-da-mikami-yuki-****/topics/temp-sa-rotation]. mikami_yuki@cloudshell:~/20240628/gcf (cm-da-mikami-yuki-****)$ gcloud pubsub topics describe temp-sa-rotation name: projects/cm-da-mikami-yuki-****/topics/temp-sa-rotation
毎時0分に、作成したトピックにメッセージを送信する、Cloud Scheduler ジョブを作成します。
$ gcloud scheduler jobs create pubsub temp-sa-rotation \ --location=asia-northeast1 \ --schedule="0 * * * *" \ --topic=temp-sa-rotation \ --message-body="test" name: projects/cm-da-mikami-yuki-****/locations/asia-northeast1/jobs/temp-sa-rotation pubsubTarget: data: dGVzdA== topicName: projects/cm-da-mikami-yuki-****/topics/temp-sa-rotation retryConfig: maxBackoffDuration: 3600s maxDoublings: 16 maxRetryDuration: 0s minBackoffDuration: 5s schedule: 0 * * * * state: ENABLED timeZone: Etc/UTC userUpdateTime: '2024-06-28T16:35:58Z' $ gcloud scheduler jobs describe temp-sa-rotation --location=asia-northeast1 name: projects/cm-da-mikami-yuki-****/locations/asia-northeast1/jobs/temp-sa-rotation pubsubTarget: data: dGVzdA== topicName: projects/cm-da-mikami-yuki-****/topics/temp-sa-rotation retryConfig: maxBackoffDuration: 3600s maxDoublings: 16 maxRetryDuration: 0s minBackoffDuration: 5s schedule: 0 * * * * scheduleTime: '2024-06-28T17:00:00.856608Z' state: ENABLED timeZone: Etc/UTC userUpdateTime: '2024-06-28T16:35:58Z'
動作確認目的のため実行頻度は毎時に設定していますが、実際はサービスアカウントキーのローテートが必要なタイミングでスケジュール設定する想定です。
Cloud Functions をデプロイ
以下のコードを、main.py
というファイル名で保存しました。
from google.cloud import iam_admin_v1 from google.cloud.iam_admin_v1 import types from google.cloud import secretmanager import google_crc32c import functions_framework import os def create_key(project_id: str, account: str) -> types.ServiceAccountKey: iam_admin_client = iam_admin_v1.IAMClient() request = types.CreateServiceAccountKeyRequest() request.name = f"projects/{project_id}/serviceAccounts/{account}" key = iam_admin_client.create_service_account_key(request=request) return key def add_secret_version( project_id: str, secret_id: str, payload: str ) -> secretmanager.SecretVersion: client = secretmanager.SecretManagerServiceClient() parent = client.secret_path(project_id, secret_id) crc32c = google_crc32c.Checksum() crc32c.update(payload) response = client.add_secret_version( request={ "parent": parent, "payload": { "data": payload, "data_crc32c": int(crc32c.hexdigest(), 16), }, } ) print(f"Added secret version: {response.name}") return response @functions_framework.cloud_event def rotate_sa_key(cloud_event): # 環境変数取得 GCP_PROJECT = os.getenv('GCP_PROJECT') ACCOUNT = os.getenv('ACCOUNT') SECRET_ID = os.getenv('SECRET_ID') if not GCP_PROJECT or not ACCOUNT or not SECRET_ID: raise Exception('invalid env val.') key = create_key(GCP_PROJECT, ACCOUNT) add_secret_version(GCP_PROJECT, SECRET_ID, key.private_key_data)
また、実行に必要なライブラリを、requirements.txt
ファイルとして保存しました。
functions-framework==3.* google-cloud-iam==2.15.0 google-cloud-secret-manager==2.20.0 google-crc32c==1.5.0
以下のコマンドで Cloud Functions をデプロイします。
gcloud functions deploy rotate_sa_key \ --gen2 \ --region asia-northeast1 \ --runtime python312 \ --service-account temp-sa-rotation-gcf@cm-da-mikami-yuki-****.iam.gserviceaccount.com \ --entry-point=rotate_sa_key \ --trigger-resource temp-sa-rotation \ --trigger-event google.pubsub.topic.publish \ --set-env-vars GCP_PROJECT=cm-da-mikami-yuki-****,ACCOUNT=temp-sa-rotation@cm-da-mikami-yuki-****.iam.gserviceaccount.com,SECRET_ID=temp-sa-rotation
正常にデプロイできました。
$ gcloud functions deploy rotate_sa_key \ --gen2 \ --region asia-northeast1 \ --runtime python312 \ --service-account temp-sa-rotation-gcf@cm-da-mikami-yuki-****.iam.gserviceaccount.com \ --entry-point=rotate_sa_key \ --trigger-resource temp-sa-rotation \ --trigger-event google.pubsub.topic.publish \ --set-env-vars GCP_PROJECT=cm-da-mikami-yuki-****,ACCOUNT=temp-sa-rotation@cm-da-mikami-yuki-****.iam.gserviceaccount.com,SECRET_ID=temp-sa-rotation Preparing function...done. OK Deploying function... (省略) buildConfig: automaticUpdatePolicy: {} build: projects/797147019523/locations/asia-northeast1/builds/58fb3deb-d755-4fd6-b803-87aadd657ba6 dockerRegistry: ARTIFACT_REGISTRY dockerRepository: projects/cm-da-mikami-yuki-****/locations/asia-northeast1/repositories/gcf-artifacts entryPoint: rotate_sa_key runtime: python312 source: storageSource: bucket: gcf-v2-sources-797147019523-asia-northeast1 generation: '1719592923047177' object: rotate_sa_key/function-source.zip sourceProvenance: resolvedStorageSource: bucket: gcf-v2-sources-797147019523-asia-northeast1 generation: '1719592923047177' object: rotate_sa_key/function-source.zip createTime: '2024-06-28T16:42:03.476873486Z' environment: GEN_2 eventTrigger: eventType: google.cloud.pubsub.topic.v1.messagePublished pubsubTopic: projects/cm-da-mikami-yuki-****/topics/temp-sa-rotation retryPolicy: RETRY_POLICY_DO_NOT_RETRY serviceAccountEmail: temp-sa-rotation-gcf@cm-da-mikami-yuki-****.iam.gserviceaccount.com trigger: projects/cm-da-mikami-yuki-****/locations/asia-northeast1/triggers/rotate-sa-key-997498 triggerRegion: asia-northeast1 labels: deployment-tool: cli-gcloud name: projects/cm-da-mikami-yuki-****/locations/asia-northeast1/functions/rotate_sa_key serviceConfig: allTrafficOnLatestRevision: true availableCpu: '0.1666' availableMemory: 256M environmentVariables: ACCOUNT: temp-sa-rotation@cm-da-mikami-yuki-****.iam.gserviceaccount.com GCP_PROJECT: cm-da-mikami-yuki-**** LOG_EXECUTION_ID: 'true' SECRET_ID: temp-sa-rotation ingressSettings: ALLOW_ALL maxInstanceCount: 100 maxInstanceRequestConcurrency: 1 revision: rotate-sa-key-00001-zuy service: projects/cm-da-mikami-yuki-****/locations/asia-northeast1/services/rotate-sa-key serviceAccountEmail: temp-sa-rotation-gcf@cm-da-mikami-yuki-****.iam.gserviceaccount.com timeoutSeconds: 60 uri: https://rotate-sa-key-rfdxqagmja-an.a.run.app state: ACTIVE updateTime: '2024-06-28T16:43:18.271245487Z' url: https://asia-northeast1-cm-da-mikami-yuki-****.cloudfunctions.net/rotate_sa_key
Cloud Functions スケジュール実行結果を確認
Cloud Scheduler ジョブに指定した時間まで待って、想定通り Secret Manager に新しいサービスアカウントキーが登録されたか確認してみます。
Cloud Function 実行前のシークレットバージョンは1
で、登録していたキー情報は下記画像の通りです。
スケジュール設定しておいた日時経過後、再度シークレットバージョンを確認してみます。
Cloud Functions を使って自動でサービスアカウントキーのローテートを行うことができました。
まとめ(所感)
Google Cloud のサービスアカウントキーに限った話ではないですが、認証キーの管理はセキュリティ上悩ましい問題かと思います。
繰り返しになりますが、Google Cloud では現在、サービスアカウントキーを発行することは非推奨になっています。 まずは Workload Identity 連携のご利用をご検討ください。
ですが、どうしてもサービスアカウントキーを使う必要がある場合は、共有範囲に十分注意すると共に、定期的にローテーションするルールを整備しておいた方が安心だと思います。 Google Cloud 環境内でサービスアカウントキーを使用している場合には、キー情報は Secret Manager で管理しているケースが多いのではないかと思うので、自動ローテーションも簡単に実装できます。
サービスアカウントキー期限の組織ポリシーの有効化と合わせて、ぜひご検討ください。