<-- mermaid -->

GCPのIAMを整理し始めています(後編: Just-In-Time Accessを実現する)

前回のまとめ

みなさんこんにちは。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_idprojectは変数で外部から渡せるようにします。 サービスアカウントにはjit-accessが一時的なIAMバインディングを作成するためのiam.securityAdmincloudasset.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_idclient_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の整理により、オペレーションミスによるインシデントやトイルの削減を図ることができるので、是非取り組んでみてください。

Page top