【実践編】CircleCIからOIDCを用いて安全にGoogle Cloudにアクセスする

SREチーム(新卒)の市川恭佑です。これはカヤックSRE連載の1月号です。

みなさんの記憶に新しいと思いますが、先日CircleCIで大きなセキュリティインシデントがありました1。 このインシデントを受けて、環境変数やContextに保存された外部サービスへの認証情報を更新しながら「自分もOIDC対応しておけば......」と後悔した方も少なくないと思います。

外部サービスの例として、Amazon Web Services (AWS)に関しては、公式OrbにてOIDC連携がサポートされていたり、AWS側のIdentity and Access Management (IAM)設定を含む情報がインターネット上に数多く公開されています。 ゆえに、CircleCIのパイプラインからAWSにアクセスする箇所について、OIDC連携に対応する難易度は比較的低いです。

これに対して、Google Cloudに関しては、公式Orbが存在せず、再利用しやすい形でまとめられた正確な情報はインターネット上に見つかりませんでした2。 そのため、今回はCircleCIからOIDCを用いて安全にGoogle Cloudにアクセスする方法を紹介します。

本ブログについて

このような記事を書かずとも、OpenID Connect 1.0 (OIDC)の仕様に対する理解を持ち合わせているエンジニアが、CircleCIおよびGoogleのドキュメントに分散している情報を探索・整理・検証すれば本ブログの目的は達成可能です。 または、執筆時点で誤植が見られるCircleCI公式ドキュメントが修正・更新された場合3、それを参照するだけで十分参考になるかもしれません。

しかし、このようなセキュリティ系のベストプラクティスは、多くの人の関心があるうちに導入しやすい形で提供されることに大きな意味があります。 それゆえに、本ブログは筆者が動作を確認したサンプルコード等、実践的な情報の提供に努めます。

もちろん、曖昧な理解のままでは満足できない方や、導入するのが不安な方もいると思います。 しかし、詳しい解説は長くなってしまうので、カヤックSRE連載2月号のエントリに譲ります。 楽しみにしていただいている方には恐縮ですが、少々お待ちください。

更新: SRE連載2月号が出ました。ぜひご覧ください!!

techblog.kayac.com

OIDC連携のための下準備をする

さっそく作業に取り掛かりたいところですが、まずはCircleCIおよびGoogle Cloudの双方で下準備が必要です。

CircleCIにおけるID取得

CircleCIのWeb画面にログインして、対象となるOrganizationを選択したら「Organization Settings > Overview」からOrganization IDを見つけてメモしてください。 その後、ホームに戻って「Projects」から対象となるProjectを選択したら「Project Settings > Overview」からProject IDを見つけてメモしてください。

どちらも、NameではなくIDであることに注意が必要です。 これらは16進数のUUIDで、ハイフンを含めて36文字になるはずです。

IDの例: AAAA1234-BB56-CC78-DD90-EEEEEE123456

Google CloudにおけるAPI有効化

Google Cloud側では、関連するAPIの有効化が必要になります。 とは言っても、すでに運用中のプロジェクトであれば、IAM Service Account Credentials APIの有効化を確認しておけば十分であると考えられます。

もしCloud Resource Manager APIなど他のAPIについて無効エラーが発生した場合は逐一有効化してください。 その時はステータスコード403と一緒に以下のようなメッセージが返ってくるはずです。

Google Foo Bar API has not been used in project <your-project> before or it is disabled.

Google CloudでWorkload Identity連携の設定をする

下準備を終えたらGoogle CloudのWorkload Identity連携の設定に取り掛かりましょう。

詳しい解説は、先述のとおりカヤックSRE連載2月号をご覧ください。

Terraformのサンプルコード

まずは論よりコードです。 ここではTerraformを利用しますが、平易な記述なので、他のツールを利用している方も雰囲気と精神力で読み取ってください。

locals {
  circleci_oidc_domain = "oidc.circleci.com"

  # 下準備でメモしたIDを埋めてください
  circleci_organization_id = "AAAA1234-BB56-CC78-DD90-EEEEEE123456"
  circleci_project_id      = "FFFF7890-GG12-HH34-II56-JJJJJJ789012"
}

resource "google_iam_workload_identity_pool" "circleci" {
  provider                  = google-beta
  workload_identity_pool_id = "circleci"
}

# fooという名前はプロジェクト名(GitHubと連携している場合はリポジトリ名と同じ)を想定しています。
# このままでも動作しますが、適宜書き換えてください。
resource "google_iam_workload_identity_pool_provider" "circleci_foo_project" {
  provider                           = google-beta
  workload_identity_pool_id          = google_iam_workload_identity_pool.circleci.workload_identity_pool_id
  workload_identity_pool_provider_id = "foo-project"

  attribute_mapping = {
    "google.subject"       = "assertion.sub"
    "attribute.project_id" = "assertion['oidc.circleci.com/project-id']"
  }

  attribute_condition = "attribute.project_id==\"${local.circleci_project_id}\""

  oidc {
    allowed_audiences = [local.circleci_organization_id]
    issuer_uri        = "https://oidc.circleci.com/org/${local.circleci_organization_id}"
  }
}

# 既存のService Account情報を取得します。
# emailの "@" より前の部分が "deploy" という名前になっていることを仮定した記述になっています。
# 実際に利用するService Accountに合わせてください。
# もし「CircleCIパイプラインから利用したい権限」を持っているService Accountが存在しない場合は、作成してください。
data "google_service_account" "deploy" {
  account_id = "deploy"
}

resource "google_service_account_iam_member" "circleci_impersonate_deploy" {
  service_account_id = data.google_service_account.deploy.name
  role               = "roles/iam.workloadIdentityUser"
  member             = "principalSet://iam.googleapis.com/${google_iam_workload_identity_pool.circleci.name}/attribute.project_id/${local.circleci_project_id}"
}

CircleCIのパイプラインで認証情報を設定する

CircleCIのパイプライン定義ファイル(以降config.yml)の記述例は以下のとおりです。

具体的な解説は後述しますが、これを模倣してconfig.ymlを記載するだけでは完結せず、追加のアクションが必要です。 サンプルコードをプロジェクトに反映するだけではなく、必ず「追加のアクション」に関する記載もご確認ください

サンプルコード(config.yml)

version: 2.1

orbs:
  gcp-cli: circleci/gcp-cli@3.0.1

commands:
  setup_google_cloud:
    description: gcloudコマンドのインストールとOIDCを用いたログイン
    parameters:
      project_number:
        type: env_var_name
        default: GOOGLE_PROJECT_NUMBER
      workload_identity_pool_id:
        type: env_var_name
        default: GOOGLE_WIP_ID
      workload_identity_pool_provider_id:
        type: env_var_name
        default: GOOGLE_WIP_PROVIDER_ID
      service_account_email:
        type: env_var_name
        default: GOOGLE_SERVICE_ACCOUNT_EMAIL
      gcp_cred_config_file_path:
        type: string
        default: /home/circleci/gcp_cred_config.json
      oidc_token_file_path:
        type: string
        default: /home/circleci/oidc_token.json
    steps:
      - gcp-cli/install
      - run:
          name: Authenticate with Google Cloud
          command: |
            echo $CIRCLE_OIDC_TOKEN > << parameters.oidc_token_file_path >>
            gcloud iam workload-identity-pools create-cred-config \
                "projects/${<< parameters.project_number >>}/locations/global/workloadIdentityPools/${<< parameters.workload_identity_pool_id >>}/providers/${<< parameters.workload_identity_pool_provider_id >>}" \
                --output-file="<< parameters.gcp_cred_config_file_path >>" \
                --service-account="${<< parameters.service_account_email >>}" \
                --credential-source-file=<< parameters.oidc_token_file_path >>
            echo "export GOOGLE_APPLICATION_CREDENTIALS='<< parameters.gcp_cred_config_file_path >>'" | tee -a "$BASH_ENV"
            gcloud auth login --brief --cred-file "<< parameters.gcp_cred_config_file_path >>"

jobs:
  deploy:
    executor: gcp-cli/default
    steps:
      - setup_google_cloud
      - run: 
          name: Do something with Google Cloud
          command: ... # 実際にGoogle Cloudにアクセスする処理を書く

workflows:
  main:
    jobs:
      - deploy:
          name: Run deploy scripts
          context: foo-stg

追加のアクション

第一に、Contextの作成が必要です。これがないと、OIDCが動作しません(具体的には$CIRCLECI_OIDC_TOKENが実行環境に渡されません)。 CircleCIのWeb画面にアクセスして、「Organization Settings > Contexts」から作成してください。

作成したContextは、config.ymlのワークフローセクションにおいて実行ジョブを定義する箇所で指定することになります。 サンプルコードではfoo-stgという名前のContextを指定しており、一般に${プロジェクト名}-${環境名}としておくと便利でしょう4

第二に、サンプルコードに記載されたGOOGLE_から始まる4つの変数について、それぞれ実行環境に渡す必要があります。 これを達成する方法は複数挙げられますが、先ほど作成したContextに与えるのが手軽です。

CircleCIのWeb画面で「Organization Settings > Contexts」から作成済みのContextを選択すると、Environments Variablesの項目が現れます。 ここで、Context固有の環境変数を設定できます。

以下は変数に関する注意点です。

  • GOOGLE_PROJECT_NUMBERはプロジェクト番号です。IDの方ではありません。
  • WIPはWorkload Identity Providerの略です。
  • 筆者はGOOGLE_SERVICE_ACCOUNT_EMAILに設定する値を間違えて時間を溶かしました。5

なお、CircleCIのContextに関して、詳しい情報は公式ドキュメントをご覧ください。

まとめ

本ブログでは、タイトルにある「CircleCIからOIDCを用いて安全にGoogle Cloudにアクセスする」という目的を達成するにあたって、差し当たり必要となるサンプルコードや具体的なアクションを紹介いたしました。

固定のアクセスキーを使っても当然Google Cloudのリソースにはアクセスできるので、どうしても地味な仕事に見えてしまいます。 その割に、インターネット上に分散した情報を探索・整理・検証する労力は想定以上に大きかったので、この記事を読む人がその労力から解放されたら幸いです。

繰り返しになりますが、サンプルコードの詳しい解説はカヤックSRE連載2月号のエントリに譲ります。お楽しみに!

更新: SRE連載2月号が出ました。ぜひご覧ください!!

techblog.kayac.com

なお、カヤックに入社すると、ブログの公開を待たず、常に最新の知見にアクセスできます!

hubspot.kayac.com


  1. 未対応の方は早急に対応されることを強くお勧めいたします。当該インシデントに関する公式レポートをご参照ください。
  2. 公式ドキュメントも同様で、Google Cloud対応方法について誤植と見られる記載があります。現在問い合わせ中です。
  3. 公式さん、見ていらっしゃったら是非お願いします!
  4. とは言っても、Contextの命名によって回避が困難な技術的制約が生じる訳ではございません。ご自身で納得できる命名があれば、そちらをご採用ください。
  5. 「いや筆者のミスは知らんがな!w」と思う気持ちは分かりますが、沼にハマっている時にTwitter検索で同様のツイートを発見したおかげで気づくことができました。どうやら人類はService Accountのemail情報を間違えるようです。