こんにちは、みかみです。
仕事中に犬が「遊べ」とおもちゃの紐を持ってくるので、無視を貫き通していたら、肩に紐を置いて去ってゆきました(このくさい紐をどうしろと?
はじめに
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 で管理しているケースが多いのではないかと思うので、自動ローテーションも簡単に実装できます。
サービスアカウントキー期限の組織ポリシーの有効化と合わせて、ぜひご検討ください。