Cloud Functions と Secret Manager でサービスアカウントキーローテーションを自動化してみた。

2024.06.29

こんにちは、みかみです。

仕事中に犬が「遊べ」とおもちゃの紐を持ってくるので、無視を貫き通していたら、肩に紐を置いて去ってゆきました(このくさい紐をどうしろと?

はじめに

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 イベントを受信する権限を付与します。

$ 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 で管理しているケースが多いのではないかと思うので、自動ローテーションも簡単に実装できます。

サービスアカウントキー期限の組織ポリシーの有効化と合わせて、ぜひご検討ください。

参考