前回のまとめ
みなさんこんにちは。ProductTeam SREのkterui9019です。
前回の記事では、無秩序に作られていたIAMの設定を見直し、Googleグループの作成と組織体系に沿ったGCPプロジェクトのフォルダ階層を設計しました。しかし、最小権限の原則に従って設計したため、「障害対応時だけ強い権限をもったIAMロールを使いたい」というニーズを満たすことができませんでした。
今回の後編では、その問題を解決するためにGoogleが公開しているOSSであるjit-accessの紹介と、terraformを使用した構築手順について解説します。
jit-accessとは
jit-accessは、Googleが提供するOSSであり、App EngineやCloud Runで動作するように設計されています。このツールは、障害発生時など一時的に特権アクセスを実行するための仕組みを提供します。特権アクセスが必要な場合でも、適切な制限と監査を実施しながら、必要なアクセス権限を一時的に付与することができます。
より詳しい説明やコンソールでの構築手順は公式ドキュメントがあるので一度目を通すことをおすすめします。
terraformでjit-accessを構築する
以下に、jit-accessを構築するためのterraform moduleを作成する手順を示します(最後にmoduleの全体を掲載するので、忙しい方はそちらを参照してください)。 基本的な構築手順は前述の公式ドキュメントに沿って進めていきます。なお、今回はCloudRunにデプロイしていきます。
なお、構成図に書き起こしてみると以下のようなイメージです。
1. jit-accessアプリケーション用のサービスアカウントを作成
まずアプリケーションが使用するサービスアカウントを作成します。service_account_id
とproject
は変数で外部から渡せるようにします。
サービスアカウントにはjit-accessが一時的なIAMバインディングを作成するためのiam.securityAdmin
とcloudasset.viewer
のロールを付与します。
resource "google_service_account" "default" { account_id = var.service_account_id display_name = "Just-In-Time Access" } resource "google_project_iam_member" "securityadmin" { project = var.project role = "roles/iam.securityAdmin" member = "serviceAccount:${google_service_account.default.email}" } resource "google_project_iam_member" "jitaccess-cloudasset" { project = var.project role = "roles/cloudasset.viewer" member = "serviceAccount:${google_service_account.default.email}" }
2. ネットワークリソースの作成
外からアプリケーションにアクセスするためのLB関連のリソースを作成します。
jit-accessはセキュリティを担保するためIdentity-Aware Proxy経由でのアクセスでのみ許可する設計となっているので、事前にIAPを有効にしてOAuth同意画面を構成してclient_id
とclient_secret
を外から渡せるようにしておく必要があります。(公式手順)
## NEG resource "google_compute_region_network_endpoint_group" "default" { provider = google-beta name = "${var.name}-neg" network_endpoint_type = "SERVERLESS" region = var.region cloud_run { service = var.name } } ## External IP resource "google_compute_global_address" "default" { name = "pub-${var.name}" } ## GCLB resource "google_compute_backend_service" "default" { name = "${var.name}-backend" protocol = "HTTP" port_name = "http" timeout_sec = 30 backend { group = google_compute_region_network_endpoint_group.default.id } iap { oauth2_client_id = var.oauth_client_id oauth2_client_secret = var.oauth_client_secret } } resource "google_compute_url_map" "default" { name = "${var.name}-urlmap" default_service = google_compute_backend_service.default.id } resource "google_compute_target_https_proxy" "default" { name = "${var.name}-https-proxy" url_map = google_compute_url_map.default.id ssl_certificates = [ var.ssl_certificate_id ] } resource "google_compute_global_forwarding_rule" "default" { name = "${var.name}-forwarding-rule" target = google_compute_target_https_proxy.default.id port_range = "443" ip_address = google_compute_global_address.default.address }
3. cloudrunの作成
次にjit-accessを動かすためのCloudRunを作成します。
ここでも外から渡すべき値は変数化していますが、特にjit-accessは環境変数でアプリケーションの動作を制御することができる(ドキュメント参照)ので、渡された変数を環境変数に設定するように作ります。IAP_BACKEND_SERVICE_ID
に関しては手順2で作成したIAPが適用されたbackend_serviceとの繋ぎ込みに必要な環境変数なのでこの場で設定してしまいます。
container_image
に関してはjit-accessのリポジトリにDockerfileがあるので、事前にビルドしてGCR等にPushしておきます。
## CloudRun resource "google_cloud_run_service" "default" { project = var.project name = var.name location = var.region template { spec { containers { image = var.container_image ports { container_port = var.container_port } env { name = "IAP_BACKEND_SERVICE_ID" value = google_compute_backend_service.default.generated_id } dynamic env { for_each = var.env_variables content { name = env.key value = env.value } } } service_account_name = google_service_account.default.email } } traffic { percent = 100 latest_revision = true } autogenerate_revision_name = true lifecycle { ignore_changes = [ status ] } }
4. モジュールの適用
ここまででjit-accessをデプロイするためのmoduleが完成しました。 変数を埋めてapplyすればアクセスできるようになるかと思いますが、作成したサービスアカウントにGoogle管理コンソール上でGoogle Groupの閲覧権限を付与する必要があります。(公式手順)
main.tf
## IAM resource "google_service_account" "default" { account_id = var.service_account_id display_name = "Just-In-Time Access" } resource "google_project_iam_member" "securityadmin" { project = var.project role = "roles/iam.securityAdmin" member = "serviceAccount:${google_service_account.default.email}" } resource "google_project_iam_member" "jitaccess-cloudasset" { project = var.project role = "roles/cloudasset.viewer" member = "serviceAccount:${google_service_account.default.email}" } ## CloudRun resource "google_cloud_run_service" "default" { project = var.project name = var.name location = var.region template { spec { containers { image = var.container_image ports { container_port = var.container_port } env { name = "IAP_BACKEND_SERVICE_ID" value = google_compute_backend_service.default.generated_id } dynamic env { for_each = var.env_variables content { name = env.key value = env.value } } } service_account_name = google_service_account.default.email } } traffic { percent = 100 latest_revision = true } autogenerate_revision_name = true lifecycle { ignore_changes = [ status ] } } ## NEG resource "google_compute_region_network_endpoint_group" "default" { provider = google-beta name = "${var.name}-neg" network_endpoint_type = "SERVERLESS" region = var.region cloud_run { service = var.name } } ## External IP resource "google_compute_global_address" "default" { name = "pub-${var.name}" } ## GCLB resource "google_compute_backend_service" "default" { name = "${var.name}-backend" protocol = "HTTP" port_name = "http" timeout_sec = 30 backend { group = google_compute_region_network_endpoint_group.default.id } iap { oauth2_client_id = var.oauth_client_id oauth2_client_secret = var.oauth_client_secret } } resource "google_compute_url_map" "default" { name = "${var.name}-urlmap" default_service = google_compute_backend_service.default.id } resource "google_compute_target_https_proxy" "default" { name = "${var.name}-https-proxy" url_map = google_compute_url_map.default.id ssl_certificates = [ var.ssl_certificate_id ] } resource "google_compute_global_forwarding_rule" "default" { name = "${var.name}-forwarding-rule" target = google_compute_target_https_proxy.default.id port_range = "443" ip_address = google_compute_global_address.default.address }
variable.tf
variable "name" { type = string } variable "project" { type = string } variable "region" { type = string } variable "service_account_id" { type = string } variable "container_image" { type = string } variable "container_port" { type = number } variable "ssl_certificate_id" { type = string } variable "oauth_client_id" { type = string } variable "oauth_client_secret" { type = string } variable "env_variables" { type = map(string) }
5. IAM Conditionの追加
jit-accessで一時的に付与させたいIAMロールには事前に条件付きロールバインディングでメンバーにバインドしておく必要があります。この作業を忘れるとjit-accessの画面で付与できるロールがない旨エラーが出るので忘れずにリソース作成をしておきます。今回は分けましたが、もちろんこのリソースも変数を取るようにして上記moduleに組み込んでもいいかと思います。
resource "google_project_iam_member" "eligible_jit_access" { project = google_project.initial_prd.project_id role = "<role>" member = "<member>" condition { title = "Eligible for JIT access" expression = "has({}.jitAccessConstraint)" } }
以上でjit-accessの構築が完了しました。
jit-accessに権限昇格のリクエストがあれば「誰が、何のロールをリクエストしたか」という内容のアプリケーションログに出力されるので、適宜CloudMonitoring等でアラート上げるといった工夫をすると良いかと思います。 また今回は紹介しませんでしたが、昇格前に第三者による承認を必須とするユースケース(Multi Party Approval)も用意されているのでチェックしてみてください。
まとめ
今回の後編では、前回の記事で整理した永続アクセス権に加えて、一時的な特権アクセス権を整理する方法について解説しました。jit-accessを取り入れることで、間違って本番リソースを削除してしまうなどのインシデントを防ぎつつ、柔軟に特権アクセスを実行することができるようになります。
これら最小権限の原則に則ったIAMの整理により、オペレーションミスによるインシデントやトイルの削減を図ることができるので、是非取り組んでみてください。