🦁

ローカルからCloud SQLへつなぐときに1Passwordで保護されたサービスアカウントキーを使う

に公開

以下のようなコマンドでローカルマシンから Google Cloud の Cloud SQL へ接続できるようにします。

op run --no-masking --env-file "sql-proxy/.env.op" -- \
  docker compose -f sql-proxy/docker-compose.yml \
  --env-file "sql-proxy/.env.op" up 

1Password CLI が利用できる環境を前提としています。

https://developer.1password.com/docs/cli/get-started/

どうした?

検証目的で本番とは別の Google Cloud Cloud SQL インスタンスを用意し、DBデータを確認するケースはよくあるでしょう。このとき、ローカルマシンからつなげると便利です。私が開発するアプリケーションでも専用の Google Cloud 検証環境を用意し、Cloud SQL インスタンスをたてています。

サービスアカウントキーを使うが…

ローカルマシンから Google Cloud にある Cloud SQL へどうつなぐかというと、Cloud SQL Auth Proxy なるものを使うわけです。

https://docs.cloud.google.com/sql/docs/mysql/sql-proxy?hl=ja

私の理解で仕組みを喋ります:
ローカルマシンとサーバー側ともにProxyが存在していて、Proxy間の接続が確立するとセキュアトンネルがつながります。この状態になると、マシンのDBクライアントからは、さもローカルネットワーク(127.0.0.1)にあるデータベースへつなぎに行くような構成をとれるという仕組みです。

サーバー側のProxyは Cloud SQL に統合・隠蔽されているためとくに私たち開発者が意識することはありません。考えるべきはProxyクライアントと認証です。認証にはサービスアカウントの利用が紹介されていました。

https://docs.cloud.google.com/sql/docs/sqlserver/connect-docker?hl=ja

Cloud SQL Auth Proxy Docker イメージをローカルマシン上(Compute Engine インスタンスではなく)で実行している場合や、Compute Engine インスタンスに適切なスコープがない場合は、Google Cloud Platform サービス アカウントを作成します。

ということで、専用のサービスアカウントキーを生成して得られた資格情報を Proxy クライアント(Docker)に渡して接続していました。アプリケーションリポジトリで以下のようなディレクトリを用意します。

⏺ credentials/              #.gitignore
    └── gcp-key.test.json

⏺ sql-proxy/
    └── docker-compose.yml

docker-compose.ymlの中身は以下です。

docker-compose.yml
services:
  cloudsql-test:
    image: "gcr.io/cloud-sql-connectors/cloud-sql-proxy:2.19.0"
    container_name: cloudsql-test
    command:
      [
        "--address",
        "0.0.0.0",
        "myapp-test:us-central1:myapp-db-test"
      ]
    volumes:
      - type: bind
        source: /credentials/gcp-key.test.json
        target: /.config/gcloud/gcp-key.test.json
      GOOGLE_APPLICATION_CREDENTIALS: /.config/gcloud/gcp-key.test.json
    ports:
      - "65432:5432"

要するにローカルマシンにある資格情報をマウントして Cloud SQL Auth Proxy のDockerコンテナを起動しています。

ところがこの方法では、サービスアカウントキーを利用するリスクがそのまま浮き彫りになります:

  • 昨今話題のサプライチェーン攻撃からの防御を考えると、ローカルマシンの機密ファイルは平文保存を避けたい
  • .gitignoreに含まれているとはいえ、Dockerコンテナのビルド、Google Cloud のソースコードデプロイなど常にキーファイルが同梱されるリスクがつきまとう

https://zenn.dev/google_cloud_jp/articles/30b86208a94f03

そこで以下2点、対策を考えることにしました。

  • そもそもローカルに平文ファイルが置かれないようにする
  • 万が一漏れ出しても被害が少なく済むよう、最小権限の原則を適用する

1Password CLI による サブプロセスへのシークレット共有

すでにいくつか試されている実績がありますが、1Password CLIを使うことでサブプロセスにだけ環境変数経由でシークレット(今回の場合はサービスアカウントキー)を渡すことが可能です。

https://zenn.dev/mafron/articles/bb177d20925ed7

ざっくりと以下のような流れで利用できます:

  1. 1Password にシークレット情報を保存する
  2. 1の参照リンクが取得できるのでそれをファイル内で環境変数のかわりにセットする
  3. op run --env-file で環境変数が含まれるファイルを指定すると当該環境変数が平文化されサブプロセスから利用できる
  4. サブプロセスでは平文化された環境変数が利用できる

これを今回のケースに当てはめていきます。

サービスアカウントキーを1Passwordで読み出す

いまのところ op run でサブプロセスに渡せるのは環境変数のみのようでした。オンメモリでセキュアにわたすことを考えると妥当かと思います。ということで、まずはファイルで渡していたサービスアカウントキーを環境変数で渡さねばなりません。

https://github.com/GoogleCloudPlatform/cloud-sql-proxy/blob/f6747f93af92d235b449851599d1ef31642cfa11/docs/cmd/cloud-sql-proxy.md?plain=1#L257

GitHubの説明をみていると以下のオプションを見つけました。

-j, --json-credentials string  Use service account key JSON as a source of IAM credentials.

つまり、サービスアカウントキーをファイルではなく直接JSON文字列で1Passwordに保存し、環境変数でそのJSON文字列を Cloud SQL Auth Proxy にわたす方針です。

専用のサービスアカウントを作成する

新しくサービスアカウントを作成し、JSONキーファイルを作成しました。このサービスアカウントの権限は Cloud SQL クライアントのみです。

1Password にJSONを保存する

1Passwordで新しいパスワードを作成し、ダウンロードしたJSONキーファイルの中身をそのままpasswordとして保存します。

作成したパスワードは、Copy Secret Referenceで参照をコピーできます。

"op://Me/gcp-key/test-json" のような値になります。そしてこれこそが、op runしたときに実体に置き換えられる対象になります。

環境設定ファイルを用意する

以下のようにして .op.env を用意します。ファイル名は任意でOKです。

⏺ sql-proxy/
    ├── .op.env
    └── docker-compose.yml
.op.env
# コピーした 1Password の Secret Reference を値にする
GCP_CREDENTIALS_JSON="op://1234567890/gcp-key.json/test_json"

docker-compose.yml で環境変数を利用するように

環境変数からJSON文字列(サービスアカウントキー)を受け取るように書き換えます。

docker-compose.yml
# integration環境のDBにアクセスするプロキシ
# ドキュメントの infra/cloud-sql-proxy.md を参照

services:
  cloudsql:
    image: "gcr.io/cloud-sql-connectors/cloud-sql-proxy:2.19.0"
    container_name: cloudsql
    command:
      [
        "--address",
        "0.0.0.0",
        "--json-credentials",
        "${GCP_CREDENTIALS_JSON}",
        "myapp-test:us-central1:myapp-db-test",
      ]
    ports:
      - "65432:5432"

ボリュームのマウントがなくなり、コマンドで環境変数を渡していることがわかります。あとは、op run で動かし、GCP_CREDENTIALS_JSONに値が入るようにします。冒頭のコマンドに戻ります。

op run --no-masking --env-file "sql-proxy/.env.op" -- \
  docker compose -f sql-proxy/docker-compose.yml \
  --env-file "sql-proxy/.env.op" up 
  • op run --no-masking --env-file "sql-proxy/.env.op": .env.op を平文化対象として指定し、そこに指定されている1Passwordのパスを読み取って環境変数にセットします。--no-maskingについて、デフォルトではサブプロセスの出力からこの環境変数が出力されないようマスキングしてくれるようですが、今回は認証に使うだけで標準出力へ出力する予定はないため--no-maskingとしています。
  • docker compose -f sql-proxy/docker-compose.yml --env-file "sql-proxy/.env.op": --env-file で環境変数として展開するファイルを指定できます。

実行すると、1Passwordの機能で認証が求められます(わたしの場合は指紋認証でした)。

1Passwordの認証がパスすると、コマンドが実行されます。


無事につながりました。あとはローカルのDBへ接続する感覚で DBクライアントを設定すれば Google Cloud 上のDBにクエリが打てます。

チーム開発で便利

サービスアカウントキーを発行するという、すこし時代に逆行するプロセスを踏みますが、1Passwordで保護することで一気に便利になります。この方法の利点はふたつあります:

  1. .op.env はリポジトリにプッシュしてもよい
  2. 1Password のチーム共通Vaultに置けば、同じ参照パスで利用できる

つまり、.op.envを開発者個人が管理する必要がない、ということです。入場対応としては 1Passwordの Vault に参加してもらうだけでよくなります。

余談: gcloud auth application-default login は安全なのか?

Cloud SQL Auth Proxy に安全に接続することを考えた時、ADC+サービスアカウント認証も考えました。

https://docs.cloud.google.com/docs/authentication/set-up-adc-local-dev-environment?hl=ja#service-account

試したところ、ローカルに生成されるADCクレデンシャルを使ってたしかに認証できます。しかしローカルに資格情報を置くということは、窃取されるリスクがある?という点が疑問に思いました。さらに、--impersonate-service-account(権限借用)を使用する場合は目的に応じて都度サービスアカウントを切り替えるのか?開発効率という点でそれは許容できるか?結局強めの権限のサービスアカウントをつくることにならないか?といったことを考えていました。

この部分の疑問はまだ解消できていません。いまのところ、「ローカルに資格情報のファイルを置かない」というポイントを重視し、極力 gcloud auth application-default login は使わない方針としています。困ったらまた考えます。みなさまのご意見もお待ちしております。

おわりに

チームの誰かがサービスアカウントキーを1Passwordで管理する手間が発生しますが、秘匿化された環境変数ファイルなのでリポジトリにPUSHできる点、それにより入場対応が楽になる点が嬉しいポイントでしたので、紹介しました。ひとつのオプションとしてご参考にしていただけると嬉しいです。

Discussion