💽

【Cloud SQL】パスワード認証からIAM認証へ「安全に」移行する手順

に公開

この記事は株式会社ビットキー Advent Calendar 2025、22日目の記事です。
プラットフォーム開発部Cross Service Backendチーム(通称CSBチーム) の @daikideal が担当します。

https://qiita.com/advent-calendar/2025/bitkey_developer

はじめに

私たちのチームのプロダクトはGoogle Cloudでホスティングされており、データベースとしてCloud SQL for PostgreSQLを使用しています。

このCloud SQLへの接続をPostgreSQL組み込みのパスワード認証からIAM認証に切り替え、平時の運用ではパスワードを使わず済むようにしました。
稼働しているアプリケーションに影響を出さず、「安全に」IAM認証へ移行するために考えたことを共有します。

課題感

概ねどのコンポーネントも(開発者も)、Cloud SQLへの接続には以下の手順を踏んでいます。

  1. Secret Managerからパスワードを取得
  2. ユーザーとしてpostgresを指定し、パスワードを入力

当然のことかと思うかもしれませんが、改善の余地があります。

  • パスワード認証であること
  • postgresがCloud SQLインスタンス作成時のデフォルトユーザーであること

ここには「運用上の不便性」と「セキュリティ上の懸念」があります。

  • 運用上の不便性:
    • アプリケーションを構成するマイクロサービスからDBへの接続がパスワード認証であるため、各サービスにシークレットをマウントする必要があり、管理するリソースが増えている
    • Cloud SQL Studioからクエリする時、いちいちSecret Managerにパスワードを取得しに行かないといけなくて不便
    • パスワード認証だと、データにアクセスしたのが誰なのかをSecret ManagerのログとCloud SQLのログを見比べなければならず、監査のコストが高い
  • セキュリティ上の懸念:
    • アプリケーションがDBに対して、アプリケーションのユースケース以上の権限を持っている。万が一アプリケーションを攻撃されてDBへの接続情報が社外の人間に盗まれた時、被害範囲が大きくなる
    • スキーママイグレーションをGitHub Actionsで行なっており、Cloud SQLに接続する時、CI環境でSecret Managerからパスワードを取得する。このとき、開発者がマスキング処理をミスしたら漏洩する可能性がある

パスワードの管理が煩雑、かつパスワードを管理すること自体にもリスクがあるため、これを無くす方法を検討した結果、IAM認証を導入するのがいいだろうという結論になりました。

IAM認証対応の全体像

すでに運用中のCloud SQLインスタンスをIAM認証での接続に切り替え、これまでと同じように運用するためには以下のようなステップが必要です。

  1. Cloud SQLインスタンスのIAM認証を有効にする
  2. Cloud SQLインスタンスにIAMユーザーを追加する
  3. IAMユーザーに必要な権限を割り当てる
  4. 既存のオブジェクトのオーナーを変更する
  5. アプリケーションがIAM認証でコネクションを取得するよう修正する

Google Cloudの公式ドキュメントのこちらのページにもIAM認証を実現するための段取りが記載されておりますのでご参照ください。

https://cloud.google.com/sql/docs/postgres/iam-logins?hl=ja#before_you_begin

インフラ側の対応

ここでは下記に該当する箇所の対応を行います。

  1. Cloud SQLインスタンスのIAM認証を有効にする
  2. Cloud SQLインスタンスにIAMユーザーを追加する

Cloud SQLインスタンスのIAM認証を有効にする

まずはインスタンスがIAM認証を利用できる構成になっている必要があります。
Cloud SQLインスタンスのcloudsql.iam_authenticationフラグの値にonを設定してください。

フラグの変更にはインスタンスの再起動を伴うものもありますが、cloudsql.iam_authenticationの変更については再起動が不要なことを確認しています。

インスタンスのフラグを編集している画像

https://docs.cloud.google.com/sql/docs/postgres/create-edit-iam-instances?hl=ja

Cloud SQLインスタンスにIAMユーザーを追加する

続けて、IAM認証でログインしたいユーザーをCloud SQLインスタンスに登録します。
私たちのチームの場合、IAM認証でログインしたいのは3種類でした。

  1. アプリケーションのサービスアカウント
  2. スキーママイグレーション用のサービスアカウント
  3. CSBチーム開発者のIAMユーザー

開発者のIAMユーザーですが、弊社では情シスによってチームごとにGoogleグループとして管理されています。この場合、開発者のIAMユーザーのアドレスを個別に登録するのではなく、Googleグループのアドレスを登録するのが以下の点で便利です。

  • Googleグループを登録すれば、そのグループに所属する開発者のIAMユーザーは個別に登録しなくてもIAM認証でログインできるようになる
  • 登録したGoogleグループがpostgresのグループになるため、GRANTの割り当ても開発者個別ではなくグループに割り当てればよい(後述)

https://docs.cloud.google.com/sql/docs/postgres/iam-authentication?hl=ja#iam-group-auth

登録するIAMユーザーに必要な権限

登録したIAMユーザー・サービスアカウントがIAM認証でCloud SQLインスタンスにログインするためには以下の権限が付与されている必要があります。

  • cloudsql.instances.get
  • cloudsql.instances.connect
  • cloudsql.instances.login

IAMロールとしては roles/cloudsql.client, roles/cloudsql.instanceUser を付与しておけば十分です。 (参考: https://docs.cloud.google.com/sql/docs/postgres/iam-roles?hl=ja#roles)

https://docs.cloud.google.com/sql/docs/postgres/add-manage-iam-users?hl=ja

IAMユーザーの登録方法

登録方法ですが、Google CloudのコンソールならCloud SQLインスタンス詳細画面 > ユーザー > 「ユーザーアカウントを追加」で追加できます。

インスタンス詳細画面
インスタンスにサービスアカウントを登録

Terraformのリソースだとgoogle_sql_userになります。

https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/sql_user.html

接続の確認

ここまで完了した時点で、すでにIAM認証でのログイン自体は可能です。

cloud-sql-proxy経由でのpsqlだと以下のようになります。サービスアカウントは文字数の関係で、.gserviceaccount.comが自動でカットされて登録されています。

./cloud-sql-proxy --auto-iam-authn <CLOUD_SQL_INSTANCE_CONNECTION_NAME>
# IAMユーザーの場合 (john.doeは登録したGoogleグループのメンバー)
psql -h localhost -p 5432 -U john.doe@example.com -d myapp

# サービスアカウントの場合
psql -h localhost -p 5432 -U example-db-user@example-project.iam -d myapp

PostgreSQLの権限調整

ここでは下記に該当する箇所の対応を行います。

  1. IAMユーザーに必要な権限を割り当てる
  2. 既存のオブジェクトのオーナーを変更する

IAMユーザーに必要な権限を割り当てる

IAMユーザーはインスタンスに登録しただけではデータベース権限を一切持たないので、テーブルに対して何の操作も行えません。なので、IAMユーザーに対して、PostgreSQLのロールとして必要十分な権限を割り当てる必要があります。

まずは、管理するロールとそのロールに必要な権限を整理します。以下を前提とします。

ロール ユースケース 権限
アプリケーションのサービスアカウント アプリケーションからのCRUD操作 ・publicスキーマへのUSAGE
・publicスキーマ下に現存する全てのテーブルへのSELECT, INSERT, UPDATE, DELETE
・publicスキーマ下に今後作成される全てのテーブルへのSELECT, INSERT, UPDATE, DELETE
開発者 問い合わせ対応でのデータ確認/修正 (アプリケーションのサービスアカウントと同じ)
スキーママイグレーション用のサービスアカウント テーブル・ビュー・関数の作成/変更(GitHub Actionsから実行) ・databaseへのCREATE
・publicスキーマへのCREATE, USAGE
・publicスキーマ下に現存する全てのテーブル・ビュー・関数へのオーナー権限
Cloud SQLのデフォルトユーザー データベースの管理 (e.g. データベースの作成, ロールの作成や権限調整, ダンプとリストア, レプリケーション) ・CREATEDB, CREATEROLE属性
・スキーママイグレーション用ロールのメンバ資格

アプリケーションのサービスアカウント/開発者

割り当てる権限については、これらのロールはデータの読み書きがしたいだけなのでSELECT, INSERT, UPDATE, DELETEだけを割り当てれば十分です。

権限を割り当てる単位ですが、アプリケーションのサービスアカウントや開発者のIAMユーザーに個別に設定するよりも以下のようにした方がいいです。

  1. ロールグループを作成(開発者はGoogleグループをインスタンスに登録したので不要)
  2. メンバーをロールグループにアサイン
  3. ロールグループに権限を割り当てる

こうすれば、GRANT文やALTER DELFAULT PRIVILEGES文を、サービスアカウントや開発メンバーの増減に応じて追加で実行したり、逆に権限を剥奪する作業を忘れたりせずに済みます。

スキーママイグレーション用のサービスアカウント

前に述べた「オブジェクトを作成/変更するロールを1つ決め、それ以外のロールはオブジェクトの定義に干渉できなくする」運用を実現するために用意します。

私のチームのプロダクトでは、スキーママイグレーションの中でPostgreSQLの拡張機能をインストールしている箇所があります。拡張機能のインストールにはデータベースに対するCREATE権限が必要です。

テーブル・ビュー・関数はpublicスキーマ下に作成します。マイグレーション用ロールはpublicスキーマにアクセスしたうえでオブジェクトを作成できないといけないので、publicスキーマに対するUSAGEとCREATEの権限が必要になります。

https://www.postgresql.jp/document/16/html/ddl-priv.html#DDL-PRIV-CREATE
https://www.postgresql.jp/document/16/html/ddl-priv.html#DDL-PRIV-USAGE

後述しますが、オブジェクトの定義を変更できるのはそのオブジェクトのオーナーだけですので、マイグレーション用ロールを既存のオブジェクトのオーナーに設定し直す必要もあります。

Cloud SQLのデフォルトユーザー

従来も運用していたロールで、唯一PostgreSQL組み込みのパスワード認証のままにしています。

「権限を設定するためのロールはどうするんだ?」を考えた時に、このデフォルトユーザーを使うのがいいだろうという結論になりました。
より厳密にロールの権限を区切るなら"権限設定を行う専用のロール"を用意するべきなのですが、以下の理由でやめました。

  • "権限設定を行う専用のロール"を用意してCREATEROLE属性(だけ)を与えるためにさらに別のCREATEROLE属性持ちのロールが必要である
  • "権限設定を行う専用のロール"の構成はどこで管理するのかを決めないといけない (今回、権限設定はTerraformで行うことにしたが、Terraformの管理外になって一元管理できなくなりそう)

結局パスワード認証を完全には無くせないということになりますが、パスワードを取り扱うタイミングを、権限設定のような数ヶ月~1年に一回程度発生するデータベースの管理業務時だけに絞れるならリスクは少ないと思います。

また、このパスワードを自動でローテーションさせる仕組み[1]を構築してよりセキュアにすることも検討しています。アプリケーションがIAM認証になっているので、ローテーションによる顧客への影響はないものとして考えられます。

 
デフォルトユーザーにマイグレーション用ロールのメンバ資格を付与する理由は2つあります。

  1. 既存のオブジェクトのオーナー変更の前後でアプリケーション側の後方互換性を維持するため
  2. ALTER DEFAULT PRIVILEGES文でオブジェクトの作成主(前述でいう<role_a>)を指定する場合、オブジェクトの作成主本人かそのロールのメンバ資格を有するロールしか実行できないため

既存のオブジェクトのオーナー変更の前後でアプリケーション側の後方互換性を維持するため

  • 従来、デフォルトユーザーでスキーママイグレーションを行ってきた
  • 現在、アプリケーションはデフォルトユーザーでDBにログインししている

いまデフォルトユーザーがテーブルに読み書きを行えるのは自らがテーブルのオーナーだからです。
アプリケーション側がデフォルトユーザーでDB接続し続けている状態でテーブルのオーナーだけが変わると、アプリケーションがテーブルへの権限を失って読み書きができなくなり、不具合が発生します。

この インフラ側の対応→アプリケーション側の対応 の間で不具合を出さないためにマイグレーション用ロールのメンバ資格をデフォルトユーザーに与えることにしました。
この設定により、デフォルトユーザーはメンバ資格を通じてオブジェクトのオーナー権限を持つことになり、引き続きデフォルトユーザーはテーブルの読み書きを行えます。

https://www.postgresql.jp/document/16/html/role-membership.html

マイグレーション用ロールとデフォルトユーザーの継承関係

ALTER DEFAULT PRIVILEGES文でオブジェクトの作成主(前述でいう<role_a>)を指定する場合、オブジェクトの作成主本人かそのロールのメンバ資格を有するロールしか実行できないため

こちらはPostgreSQLのドキュメントに記載の通りです。

https://www.postgresql.jp/document/16/html/sql-alterdefaultprivileges.html#:~:text=ユーザ自身のデフォルト権限とユーザがメンバとして属するロールのデフォルト権限は変更できます

権限設定は全てデフォルトユーザーで行うので、例えば「マイグレーション用ロールが今後作成するテーブルに対し、アプリケーションのサービスアカウントはSELECT, INSERT, UPDATE, DELETEの権限を持つ」という設定をデフォルトユーザーの権限で実行できるためにも、マイグレーション用ロールのメンバ資格をデフォルトユーザーに与える必要があります。

メンバ資格を与えるには以下のようなGRANT文を実行します。

GRANT "migration-role@example-project.iam" TO "postgres";

権限のコード管理

権限の設定はTerraformのcyrilgdn/postgresqlプロバイダーを使用して行いました。
自前のSQLスクリプトを作成してバージョン管理するという方法も考えましたが、以下の理由で不採用でした。

  • スクリプト自体の動作担保をしなければならず、その検証が重い
  • スクリプトの認知負荷が高い
  • GRANT(権限の付与)の管理は簡単だが、REVOKE(権限の剥奪)の管理が難しい

このプロバイダーを使えば、「どのロールはどのオブジェクトに対してどんな権限を持つ」かを宣言的に記述することができ、リソースの定義の変更により権限の付与・剥奪をterraform apply一発で操作できます。

以下に、例としてアプリケーションのサービスアカウントの権限設定を行うサンプルコードを示します。参考情報として、各リソースがどういうSQLに相当するのかもコメントしておきます。

terraform {
  required_providers {
    postgresql = {
      source  = "cyrilgdn/postgresql"
      version = "1.26.0"
    }
  }
}

provider "postgresql" {
  scheme    = "gcppostgres"
  host      = var.cloud_sql_instance_conn_name
  port      = 5432
  username  = var.cloud_sql_default_user_name
  password  = var.cloud_sql_default_user_password
  superuser = false
}

# GRANT ${var.cloud_sql_default_user_name} TO ${var.cloud_sql_default_user_name};
resource "postgresql_grant_role" "default_role_inherits_schema_migrator_role" {
  role       = var.cloud_sql_default_user_name
  grant_role = var.schema_migrator_iam_user.name
}

# CREATE ROLE applications WITH NOLOGIN;
resource "postgresql_role" "applications_group" {
  name  = "applications"
  login = false
}

# GRANT applications TO "service-a@example-project.iam";
# GRANT applications TO "service-b@example-project.iam";
resource "postgresql_grant_role" "applications_group_memberships" {
  for_each = toset(["service-a@example-project.iam", "service-b@example-project.iam"])

  role       = each.value
  grant_role = postgresql_role.applications_group.name
}

# GRANT USAGE ON SCHEMA public TO applications;
resource "postgresql_grant" "applications_can_use_public_schema" {
  database    = "myapp"
  role        = postgresql_role.applications_group.name
  schema      = "public"
  object_type = "schema"
  privileges  = ["USAGE"]
}

# GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO applications;
resource "postgresql_grant" "applications_can_execute_crud_to_all_existing_tables_in_public_schema" {
  database    = "myapp"
  role        = postgresql_role.applications_group.name
  schema      = "public"
  object_type = "table"
  privileges  = ["SELECT", "INSERT", "UPDATE", "DELETE"]
}

# ALTER DEFAULT PRIVILEGES FOR ROLE %I IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO applications;
resource "postgresql_default_privileges" "applications_can_execute_crud_to_all_future_tables_in_public_schema" {
  database    = "myapp"
  role        = postgresql_role.applications_group.name
  schema      = "public"
  owner       = postgresql_grant_role.default_role_inherits_schema_migrator_role.grant_role
  object_type = "table"
  privileges  = ["SELECT", "INSERT", "UPDATE", "DELETE"]
}

既存のオブジェクトのオーナーを変更する

PostgreSQLの仕様として、オブジェクトはそのオーナーしか定義を変更することができません。これまでpostgresでスキーママイグレーションを行なっていたので、既存のオブジェクトのオーナーをスキーママイグレーション用のサービスアカウントに変更しなければALTER TABLEなどがエラーになってしまいます。

https://www.postgresql.jp/document/16/html/sql-altertable.html#:~:text=ALTER TABLEコマンドを使用するには、変更するテーブルを所有している必要があります。

Terraformではテーブル・ビュー・関数のオーナー変更に対応していなかったので、IAM認証に移行するタイミングでのみ実行する想定のスクリプトを書いて対応しました[2]。

DO $$
DECLARE
    permittee VARCHAR(128) := 'migration-role@example-project.iam';

    database_name VARCHAR(128);
    table_name VARCHAR(128);
    view_name VARCHAR(128);
    function RECORD;
BEGIN
    -- 既存のテーブルのオーナーを変更
    FOR table_name IN (SELECT tablename FROM pg_tables WHERE schemaname = 'public')
    LOOP
        EXECUTE format('ALTER TABLE public.%I OWNER TO %I', table_name, permittee);
    END LOOP;

    -- 既存のビューのオーナーを変更
    FOR view_name IN (SELECT viewname FROM pg_views WHERE schemaname = 'public')
    LOOP
        EXECUTE format('ALTER VIEW public.%I OWNER TO %I', view_name, permittee);
    END LOOP;

    -- 既存の関数のオーナーを変更 (ユーザー定義関数に絞る)
    FOR function IN (
        SELECT
            proname,
            pg_get_function_identity_arguments(pg_proc.oid) as proargs
        FROM
            pg_proc
            INNER JOIN pg_namespace
                ON pg_proc.pronamespace = pg_namespace.oid
        WHERE
            pg_namespace.nspname = 'public'
            -- NOTE: 既存のマイグレーションファイルを参考に、関数のソースコードに 'begin|BEGIN' または 'declare|DECLARE' が含まれているものをユーザー定義関数とみなした。
            AND (
                prosrc ILIKE '%begin%'
                OR prosrc ILIKE '%declare%'
            )
    )
    LOOP
        -- NOTE: 引数の型まで含んだ関数の完全なシグネチャを指定しないと意図通りの関数を更新できない可能性がある。
        EXECUTE format('ALTER FUNCTION public.%I(%s) OWNER TO %I', function.proname, function.proargs, permittee);
    END LOOP;
END
$$;

アプリケーション側の対応

ここでは下記に該当する箇所の対応を行います。

  1. アプリケーションがIAM認証でコネクションを取得するよう修正する

公式ドキュメントに書かれている通り、Cloud SQLコネクタを使います。
https://cloud.google.com/sql/docs/postgres/iam-logins?hl=ja#log-in-with-automatic

私が所属するチームのアプリケーションはGoで書かれており、ORMとしてGORM(v2)を使用しています。
GORMは使用するドライバーをカスタマイズすることができますので、初期化・登録したドライバーの名前がgorm.DialectorのConfig.DriverNameにセットされるように実装します。

https://pkg.go.dev/cloud.google.com/go/cloudsqlconn#section-readme:~:text=Using the dialer with database/sql

https://gorm.io/ja_JP/docs/connecting_to_the_database.html#ドライバーのカスタマイズ

また、ローカルではdocker-composeで起動したpostgresサーバーに接続したいので、メタデータサーバーを使って、Google Cloud上ならIAM認証、そうでないならパスワード認証になるよう制御しました。

上記を実装例としてコードに落とし込むと以下のようになります。

const cloudsqlPostgresDriverName = "cloudsql-postgres"

var DB *gorm.DB

func main() {
    if metadata.OnGCE() {
        cleanup, _ := pgxv5.RegisterDriver(
    		cloudsqlPostgresDriverName,
    		cloudsqlconn.WithIAMAuthN(),
    	)
        defer cleanup()
    }

    dsn := getDSN()

    DB, _ = gorm.Open(getDialector(dsn), &gorm.Config{})
}

func getDSN() string {
    if !metadata.OnGCE() {
        return "host=localhost port=5432 user=postgres dbname=myapp sslmode=false"
    }

    username, _ := metadata.Email("default")

    cloudsqlconnName := fmt.Sprintf(
        "%s:%s:%s",00
        os.Getenv("CLOUD_SQL_PROJECT_ID"),
        os.Getenv("CLOUD_SQL_REGION"),
        os.Getenv("CLOUD_SQL_PRIMARY_INSTANCE_ID"),
    )

    return fmt.Sprintf(
        "host=%s port=%s user=%s dbname=%s sslmode=%s",
        cloudsqlconnName,
        os.Getenv("CLOUD_SQL_PORT"),
        username,
        os.Getenv("CLOUD_SQL_DBNAME"),
        os.Getenv("CLOUD_SQL_CLIENT_SSLMODE"),
    )
}

func getDialector(dsn string) gorm.Dialector {
    if !metadata.OnGCE() {
        return postgres.Open(dsn)
    }

    return postgres.New(postgres.Config{
        DriverName: cloudsqlPostgresDriverName,
        DSN:        dsn,
    })
}

権限設定の完了後に上記のコードをデプロイすれば、アプリケーションがIAM認証でDBに接続するようになります。

おわりに

ここまで紹介した手順を完了しIAM認証への切り替えを行なった後、以下を確認できています。

  • アプリケーションの挙動に変化がないこと
  • 開発者がCloud SQL Studioでクエリできること
  • IAM認証でDB接続したプリンシパル名がCloud SQLのログに表示されること

この記事がDBのパスワード管理に課題を感じていたり、これからCloud SQLのIAM認証を導入しようと考えている方の参考になれば幸いです。

 
※本記事に記載されている会社名および製品・サービス名は、各社の商標または登録商標です。

脚注
  1. 参考: https://cloud.google.com/blog/ja/products/identity-security/how-to-use-google-clouds-automatic-password-rotation ↩︎

  2. REASSIGN OWNEDで一括で全てのオブジェクトのオーナーを変更することもできますが、効果範囲がpublicスキーマに閉じておらず、変更元がデフォルトユーザーの場合PostgreSQLの組み込みオブジェクトも対象にとってしまい、その影響が把握できなかったのでやめました。 (ref. https://www.postgresql.jp/document/16/html/sql-reassign-owned.html) ↩︎

Bitkey Developers

Discussion