Cloud Spannerで実現する「きめ細かなアクセス制御」– アソビューのDB権限新フローへの移行運用

こんにちは、アソビューSREチームの長友です。

今回はCloud Spannerのfine-grained access control(きめ細かなアクセス制御、以下FGAC)を使って、「ユーザー×DB」から「ユーザー×DB×テーブル」の粒度でより細かく権限制御を行うフローを構築したお話をします。

弊社でのSpanner導入ついてはこちらの記事こちらのスライドで紹介しているので、今回はより実践的な「運用の中でどう権限管理を工夫したか」にフォーカスしていきたいと思います。

DB権限新フローについて

FGACについて説明する前に、背景となったDB権限新フローについて簡単に説明しておきます。

従来の課題

DB権限の新フロー移行前の課題としては以下のようなものが挙げられます。

  • 従来のスキーマ・データベース単位で閲覧・編集権限が付与されており、機密情報が格納されたテーブルへのアクセス制限ができていない
  • データベースあたりロールが10個以上存在する場合もあり管理コストが高い
  • ユーザーによっては編集権限が常時付与された状態で運用時の安全性が低い

このようなセキュリティ面・運用課題面を解決するために新フローの構築を行うことにしました。

新フローの方式

上に述べた従来の課題を解決するために以下のような方式を検討しました。

1.ユーザーの職務権限によってデフォルトロールを定義し、機密情報へのアクセスを制限

まず、開発者とマネージャーの2つのロールを職務に応じて定義し、これらをデフォルトのロールとして適用しました。
これらのロールはテーブルごとに権限を与えるかが決まっており、一律機密情報へのアクセスは制限することにしました。

ロール名 説明
開発者ロール 機密情報以外のテーブルのSELECT権限
マネージャーロール 機密情報以外のテーブルのSELECT・UPDATE権限(緊急対応に備えてUPDATEまで付与)

2. データパッチや調査時は一時的に権限拡張を行う

データパッチを行う際や、範囲を拡大した調査を行う際にはデフォルトロールでは権限が足りない状況が生まれます。
こういった際には新たに閲覧・編集したいテーブルの申請を上げ、それが承認されることで一時的に権限が拡張される仕組みを利用してもらうことにしました。
この一時的な拡張についてはArgo WorkflowsのWorkflowTemplateを利用し、Workflowの手動実行による一時的な権限拡張と、CronWorkflowによるリセットを行う方式を採用しています。

次に示すのがArgo Workflows UIとなります。今回はこのワークフローでMySQL、 PostgreSQL、 Cloud Spannerの3種類のDBの一時的な権限付与を一括で行えるようにしています。

Argo Workflowsにおける一時的な権限付与のUI

実現するための処理の全体構成

以下が今回の新フローを構成する処理群となります。バッチ処理やUIトリガーの処理をArgo Workflowsのバッチ環境上でデータベースに対してDDLなどを発行するシェルを動かすことで実現しています。

新フローを構成する処理群

移行までの流れ

移行までの流れは以下となります。

  1. 技術検証
  2. 設計
  3. 実装
  4. 動作検証
  5. 承認フローの更新
  6. 全体周知
  7. 本番トライアル期間
  8. リリース

特に移行に伴い開発組織のデータパッチ時の運用も変更となるため、以下の2点が非常に重要となりました。

  • 開発全体から新フローに対する理解を得ること
  • 新フローが現実的に運用として回るのか

そこで以下の施策を実施することによって、開発者体験の可能な限りの維持と新フローが両立する状態を目指していきました。

  • 動作確認時に運用視点に立った細かいUX改善の繰り返し
  • 各ユースケースを想定したアナウンスドキュメントの作成・周知会の実施
  • トライアル期間の実施

Cloud Spannerにおける実現方法について

続いて本題となるFGACについて見ていきたいと思います。

fine-grained access control(きめ細かなアクセス制御)について

FGACについての概要と導入までの手引きは公式ドキュメントに記載されているので、詳細はそちらに譲って、ざっくりとした特徴を以下に示したいと思います。

  • テーブル、カラム、ビュー毎といった細かいレベルでCRUDを制御した権限構成が可能になること
  • FGACの権限構成はDDLで作成されるデータベースロールで管理されること
  • 上記のデータベースロールをIAMプリンシパル(ユーザーやサービスアカウント)に対し、roles/spanner.databaseRoleUserというIAMロール経由でデータベースごとに付与すること
  • データベースロールは実際のデータベースクライアント利用時にユーザーが1つ選択することで適用されること。(同時に1つのみ)
  • IAMポリシーレベルでの制御が優先されること。そのため、閲覧・編集に関わるIAMポリシーが付与されていない状態で初めてFGACでの制御が可能になること

DDLで作成されるロールによって権限が管理されている点はMySQL、PostgreSQLと一緒ですが、ユーザーへの付与やIAMとの絡みは仕様の部分になるかと思います。

次にIAMロールベースのSpannerへのアクセス管理からFGACへの一般的な移行手順を以下に示しておきます。

  1. roles/spanner.fineGrainedAccessUser の付与
  2. DBロール作成 → テーブル/スキーマ単位の権限定義
  3. roles/spanner.databaseRoleUser を条件付きで付与
  4. 実質的な切り替えトリガー ~ IAMレベルでの参照・編集権限を剥奪

1. roles/spanner.fineGrainedAccessUser の付与

FGACの仕組みを利用できるようにするためのIAMロールです。こちらは対象プリンシパルに一回付与すれば問題ありません。

2. DBロール作成 → テーブル/スキーマ単位の権限定義

細かい構文の違いはありそうですが、DDLで構成する点は他のDBMSと一緒です。
ここでは基本的な構文のみざっと紹介します。

CREATE ROLE DEFAULT_ROLE_SELECT; -- DEFAULT_ROLE_SELECTというロールを作成
GRANT USAGE ON SCHEMA schemapiyo TO ROLE DEFAULT_ROLE_SELECT; -- schemapiyoの利用権限付与
GRANT SELECT ON ALL TABLES IN SCHEMA schemapiyo TO ROLE DEFAULT_ROLE_SELECT; -- schemapiyoの全テーブルのSELECT権限を付与
REVOKE SELECT ON TABLE SecretTables FROM ROLE DEFAULT_ROLE_SELECT; -- 指定テーブルからSELECT権限剥奪
GRANT ROLE DEFAULT_ROLE_SELECT TO ROLE OTHER_ROLE; -- 別のロールを継承
DROP ROLE DEFAULT_ROLE_SELECT; -- ロール削除

3. roles/spanner.databaseRoleUser を条件付きで付与

データベースロールはroles/spanner.databaseRoleUserのconditionとして表現されます。そのため、以下のような構文で付与・剥奪する必要があります。

付与
gcloud spanner databases add-iam-policy-binding databasefoo \
  --project=test-project \
  --instance=instance-id \
  --role=roles/spanner.databaseRoleUser \
  --member=user:test.user@hoge.co.jp \
  --condition='expression=(resource.type == "spanner.googleapis.com/DatabaseRole" && resource.name.endsWith("/DEFAULT_ROLE_SELECT")),title=test,description=test'
剥奪
gcloud spanner databases remove-iam-policy-binding databasefoo \
  --project=test-project \
  --instance=instance-id \
  --member=user:test.user@hoge.co.jp \
  --role=roles/spanner.databaseRoleUser \
  --all

4. 実質的な切り替えトリガー ~ IAMレベルでの参照・編集権限を剥奪

公式ドキュメントにもある通り、FGACより上位レイヤーの IAM ロールによる制御が優先されます。言い換えると、IAM ロールで閲覧や編集が許可されている場合、FGAC の制御は上書きされて効力を持ちません。
この仕様を逆手に取ることで、移行時の切り替えトリガーとして利用できます。具体的には、まずユーザーに FGAC のデータベースロールを適用した状態を用意し、その後で Spanner の閲覧・編集に関わる IAM ロールを外すことで、IAM ロール制御から FGAC 制御へスムーズに切り替えられます。必要に応じて一部ユーザーだけ IAM ロールを外すこともできるため、動作検証にも活用できました。

実装上のポイント

次に実装・移行を進める上でのポイントを挙げていきます。

FGACによる権限拡張の実現方法

FGACによる権限拡張に際しては以下の点を考慮する必要がありました。

  • データベースロールは実際のデータベースクライアント利用時にユーザーが1つ選択することで適用されること。(同時に1つのみ)
  • その際、どのロールを選択すれば良いか一瞥で判別できること
  • ユーザーが一時的な権限拡張を期待する場合、デフォルトロールの権限は維持したまま、特定のテーブルへの権限が追加される形が望ましいこと
  • 一時的な権限拡張にかかる時間は可能な限り短い方が良いこと

これらを満たすために以下のような構成を採用しました。ポイントとしては一意になる命名規則やデフォルトロールの継承を利用する点です。

一時ロールの作成と削除

これらのバッチ上の処理はシェルからgcloudコマンドを実行することで実現しています。
シェルでロジックの絡む複数の処理を実装するのは骨が折れますが、AI コーディングツールのCursorの力で人力より遥かに高速かつ高精度で実装することができました。

ロール継承を用いてシステムロールは付与しておく

システムロールを考慮することも運用まで見据えて重要なポイントでした。
システムロールとはこちらで説明されているものです。以下に簡単に示します。

ロール名 説明
public 既定で全FGACユーザーが所属、初期は権限なし(付与すると全ロールに波及)
spanner_info_reader INFORMATION_SCHEMA(PGはinformation_schema)への読み取り権限を保有
spanner_sys_reader SPANNER_SYSスキーマへの読み取り権限を保有。このロールを持っていることによってFGAC化でもQuery Insightsやロック統計情報の閲覧が可能になる。

特に3つ目のspanner_sys_readerロールが重要です。
Cloud Spannerを利用するプロダクトチームでは定常的にQuery Insightsロック統計情報を閲覧してクエリの改善を図っています。
この運用のFGAC下でも可能にするためにはpublicロールにspanner_sys_readerロールを継承させる、もしくは生成するデータベースロールに継承させてこのシステムロールが常に開発ユーザーに付与された状態を保っておく必要があります。
弊社では後者の方法で実現しています。

FGAC単独での権限制御のための拒否ポリシーによるIAMポリシーの制御

先ほども説明した通り、SpannerではFGACより上位レイヤーでのIAMロールによる制御が優先されますが、ここが少し課題となりました。

本来、Spannerで FGAC を活用するにはユーザーごとに最小限の IAM ロールを付与するのが理想ですが、 現状の一部ではroles/viewer のような基本ロールや事前定義ロールを用い、複数の Google Cloud リソースに一括で権限付与を行っているケースがありました。

最小限の権限構成にするには、上述のケースを含めた全ての権限とユーザーの棚卸しを行い、最小権限まで移行する必要があります。しかし、その影響範囲や対応のスコープを考慮すると今回やり切ることは現実的ではありませんでした。

そこで、登場したのが拒否ポリシーです。 拒否ポリシーとはIAMプリンシパルから指定のIAMポリシーを剥奪する仕組みです。これによりユーザーが特定のリソースに対して行うことができる操作を明示的に拒否することができます。

こちらを利用し以下の構成で権限の拒否を行うことで、FGACでの制御に移行することができました。

対象: 現在Spannerに対して強い権限を持つユーザーの内、SREチーム以外のもの全員
拒否ポリシー: Cloud Spannerに関連するFGACで最低限必要なIAMポリシー以外の全て

Terraformで記述すると以下のようになります。
ポイントとしては拒否対象プリンシパル、拒否ポリシーの他に拒否の例外プリンシパル、例外ポリシーも設定していることです。

resource "google_iam_deny_policy" "spanner_deny_policy" {
  name         = "spanner-deny-policy-production"
  parent       = urlencode("cloudresourcemanager.googleapis.com/projects/${local.project_id}")
  display_name = "Spanner操作の拒否ポリシー"

  rules {
    description = "きめ細かい制御に移行するための拒否ポリシー"
    deny_rule {
      denied_principals = [
        // 拒否対象プリンシパル
        "principalSet://goog/group/hoge@foo.co.jp",
        "principal://goog/subject/piyo@foo.co.jp",
      ]
      exception_principals = [
        // 拒否の例外対象プリンシパル
        "principalSet://goog/group/exception.target@foo.co.jp",
      ]
      // spanner関連のポリシーを一旦全て拒否
      denied_permissions = [
        "spanner.googleapis.com/backupOperations.*",
        "spanner.googleapis.com/backupSchedules.*",
        "spanner.googleapis.com/backups.*",
        "spanner.googleapis.com/databaseOperations.*",
        "spanner.googleapis.com/databases.*",
        "spanner.googleapis.com/instanceConfigOperations.*",
        "spanner.googleapis.com/instanceConfigs.*",
        "spanner.googleapis.com/instanceOperations.*",
        "spanner.googleapis.com/instancePartitionOperations.*",
        "spanner.googleapis.com/instancePartitions.*",
        "spanner.googleapis.com/instances.*",
        "spanner.googleapis.com/sessions.*"
      ]
      // spanner関連のポリシーの中でも最低限必要なものだけexception_permissionでpick
      exception_permissions = [
        // 以下はroles/spanner.viewerの権限
        "monitoring.googleapis.com/timeSeries.list",
        "cloudresourcemanager.googleapis.com/projects.get",
        "spanner.googleapis.com/databases.list",
        "spanner.googleapis.com/instanceConfigs.get",
        "spanner.googleapis.com/instanceConfigs.list",
        "spanner.googleapis.com/instancePartitions.get",
        "spanner.googleapis.com/instancePartitions.list",
        "spanner.googleapis.com/instances.get",
        "spanner.googleapis.com/instances.list",
        "spanner.googleapis.com/instances.listEffectiveTags",
        "spanner.googleapis.com/instances.listTagBindings",
        // 以下はroles/spanner.fineGrainedAccessUserの権限
        "spanner.googleapis.com/databaseRoles.list",
        "spanner.googleapis.com/databases.useRoleBasedAccess",
        // コンソールを利用する上で無いと困る権限を追加
        "spanner.googleapis.com/databases.getDdl",
      ]
    }
  }
}

最後に

以上、DB権限新フローとそこで用いたFGACの利用についてのご紹介でした。
この新フローに移行して数ヶ月経ちますが、現在安定して運用を回せている状態となっています。
一方で利用チームからの要望として、カラムレベルでの制御まで実現されると普段の権限拡張頻度が減ることが期待できる点などが挙がっているため、今後タイミングを見て検討していきたいと考えています。

アソビューのSREチームは開発チームの開発体験・速度向上を目指した施策、開発体験を損なわない守りの施策の実現を常に目指しています。我々と一緒に働くエンジニアも募集していますので、興味のある方はぜひお気軽にエントリーください!

www.asoview.co.jp