😽

VPC-SC の構築に関わる様々な設定を Terraform で書きながら整理する

2024/06/06に公開

はじめに

こんにちは。クラウドエース SRE 部の工藤です。
VPC Service Controls(以下、VPC-SC) を作成しようとすると Access Context Manager、アクセス ポリシー、アクセスレベル、サービス境界、上り(内向き)/ 下り(外向き)ポリシーなど様々な設定が出現してきます。
こんなにたくさん出てくると何がどう関係しているのか混乱しませんか?
僕はかなり混乱しました。
なので、今回はそんな VPC-SC に関わる設定たちを Terraform を使って作成しながら整理したいと思います。

VPC-SC の基本

上述のように、VPC-SC を構築する過程では色々な設定が関わってきます。
各設定の関係を整理する前に、そもそも何ができることがゴールなのか、全体像を認識しておく必要があるかと思います。

前提として Google Cloud では各サービスのリソースを操作する際に Application Programming Interface(以下、API)を呼び出します。
その API の呼び出しを保護し、Google Cloud サービスのリソースを意図しない操作から守るためのセキュリティ機能が VPC-SC です。
API の呼び出しを保護するために必要なのがサービス境界であり、境界によって、API の呼び出し元を制御することができます。

つまり、VPC-SC を設定するということは境界を作成し、API の呼び出し元を制御することで、リソースを意図しない操作から守ることと言えます。
これが VPC-SC の最も基本的な要素です。

https://cloud.google.com/vpc-service-controls/docs/overview?hl=ja

境界によって指定したリソースへのアクセスは拒否されてしまいます。
システム構成上、すべてのアクセスを拒否してしまうと問題が発生する可能性が高く、運用する上でも必要最低限はアクセスを許可する必要があるかと思います。
その際に、設定するものとしてアクセスレベルや上り(内向き)/ 下り(外向き)ポリシーが存在しています。
アクセスレベルや上り(内向き)/ 下り(外向き)ポリシーを設定することで、アクセスを許可することができます。(それぞれの違いは後述)

つまり、アクセスを拒否するサービス境界に対して、許可を行うアクセスレベルや上り(内向き)/ 下り(外向き)ポリシーがあるということです。
基本的にはこれらのリソースを組み合わせることで VPC-SC が機能します。


VPC-SC 概念図

また、それぞれの詳細については、以下の弊社エンジニアによるブログ記事にまとめられております。
こちらもご参照いただくことで、各設定に対する理解が深まるかと思います。

https://zenn.dev/cloud_ace/articles/6a26443b8e7bb4

Access Context Manager

Terraform での作成に入る前に、Access Context Manager についてまとめていきます。
Access Context Manager はリクエストのコンテキストを認識し、分類するサービスであり、Google Cloud のプロジェクトとリソースへのアクセスに対して細かい制御を定義することができます。

https://cloud.google.com/access-context-manager/docs/overview?hl=ja

Access Context Manager におけるアクセス制御の定義のため、アクセス ポリシーやアクセスレベル、サービス境界を作成します。
つまり、アクセス ポリシー、アクセスレベル、サービス境界は Access Context Manager のリソースです。
Access Context Manager というサービスの中でこれらのリソースが作成されるという関係となります。
これを基盤とし、VPC-SC というサービスが構成されているといえます。

これらの関係性についてもう少し深掘ると

これらのリソースを操作する際には accesscontextmanager.googleapis.com という API を使用します。
Access Context Manager の API を使用して、アクセス ポリシー、アクセスレベル、サービス境界といったリソースの作成などを行います。
このことからも関係性を理解いただけるのではないかなと思います。

アクセスポリシーを作成する

アクセスポリシー

では、Terraform で各リソースを作成していきます。
まずはアクセス ポリシーからです。
アクセス ポリシーはアクセスレベルやサービス境界のコンテナであり、組織全体に適用されます。
つまり、アクセス ポリシーの配下にアクセスレベルやサービス境界が作成されるという構成になります。

組織レベルのアクセス ポリシーは 1 つの組織に対して 1 つのみ作成することができます。
そのため、すでに組織にアクセス ポリシーが存在している場合、作成することができないので注意が必要です。
例えば、コンソール上でアクセスレベルを作成すると、同時に default policy が作成される仕様になっています。

アクセス ポリシー自体の作成はしていないのに、既に存在しているなどということもあるので、Terraform でアクセス ポリシーを作成する前に一度確認しておくと良いかと思います。

https://cloud.google.com/access-context-manager/docs/overview?hl=ja#access-policies

https://cloud.google.com/access-context-manager/docs/create-access-policy?hl=ja

Terraform でアクセス ポリシーを設定する

vpcsc.tf
resource "google_access_context_manager_access_policy" "access_policy" {
  parent = "organizations/{organization_id}"
  title  = "demo policy"
}

Terraform でアクセス ポリシーを作成するには、google_access_context_manager_access_policy リソースを使用します。

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

parent
組織 ID を指定します。
アクセス ポリシーは組織レベルでの作成となるため、親として組織を指定します。

title
適切なポリシー名を設定します。

必須となる設定はこの 2 つです。
アクセス ポリシーはあくまでコンテナという役割なので、設定すべき項目は多くないです。

アクセスレベルを作成する

アクセスレベル

次に、アクセスレベルを作成していきます。
アクセスレベルはリクエストの属性に基づきリソースへのアクセスを許可することができます。

https://cloud.google.com/access-context-manager/docs/overview?hl=ja#access-levels

アクセスレベルでサポートされている属性は以下の通りです。

  • IP アドレス
  • リージョン
  • プリンシパル(ユーザ アカウント / サービス アカウント)
  • アクセスレベルの依存関係
  • デバイス ポリシー

https://cloud.google.com/access-context-manager/docs/access-level-attributes?hl=ja

これらの属性を条件に設定することで、アクセスレベルでフィルタリングされ、一致したリクエストはアクセスが許可されるようになります。

また、アクセスレベルの制御は外からの通信である上り(内向き)のアクセスのみを許可することができます。
外へと向かう下り(外向き)の通信を許可する場合は、後述の 下り(外向き)ポリシーで設定する必要があります。

Terraform でアクセスレベルを設定する

今回は特定の IP アドレスを許可するアクセスレベルを作成していきます。

vpcsc.tf
resource "google_access_context_manager_access_level" "access_level" {
  parent = "accessPolicies/${google_access_context_manager_access_policy.access_policy.name}"
  name   = "accessPolicies/${google_access_context_manager_access_policy.access_policy.name}/accessLevels/acl_demo"
  title  = "acl_demo"
  basic {
    conditions {
      ip_subnetworks = [
        "xx.xx.xx.xx/32",
        "yy.yy.yy.yy/32"
        ]
    }
  }
}

Terraform でアクセスレベルを作成するには google_access_context_manager_access_level リソースを使用します。

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

parent
親リソースとしてアクセス ポリシーを指定します。
上述のようにアクセス ポリシーの配下に作成されるため、アクセスレベルの親はアクセス ポリシーになります。

name
アクセスレベルのリソース名を設定します。
ここでの形式は accessPolicies/{policy_id}/accessLevels/{short_name} のように完全パスで構成する必要があります。

title
アクセスレベルのタイトルとして省略した名前を設定します。
ここで設定した名前がコンソール上で表示されます。

conditions ブロック
このブロック内でアクセスレベルの条件を設定していきます。

  • ip_subnetworks
    アクセスを許可する実際の IP アドレスを設定します。

サービス境界を作成する

サービス境界

サービス境界を作成していきます。
サービス境界は指定したプロジェクト内のリソースを保護します。

サービス境界を作成することで、プロジェクト内のリソースに対するアクセスを制限することができます。
これはユーザーからのアクセスだけではなく、リソース同士の通信でも同様となります。
境界外のリソースが境界内リソースの API を呼び出すような構成や、境界内のリソースが境界外のリソースにアクセスするような通信は拒否されます。

一方で、境界内のリソース同士であれば基本的に自由に通信することができます。

https://cloud.google.com/vpc-service-controls/docs/service-perimeters?hl=ja

自動適用モードとドライラン モード

サービス境界は自動適用モードとドライラン モードの2つで構成することができます。

  • 自動適用モード
    • 実際に境界が環境に適用されるモードです。このモードで作成すると実際に通信が拒否され、リソースにアクセスできなくなります。
  • ドライラン モード
    • 環境に境界を設定しますが、通信は拒否されず、アクセスすることができるモードです。許可されていない通信からのアクセスがあった場合はログに記録されます。

ドライラン モードは、境界の構成をテストすることや、ログを見ながら許可する通信を特定し、アクセスレベルや上り(内向き)/ 下り(外向き)ポリシーの設定を追加するなどの用途で使用します。

https://cloud.google.com/vpc-service-controls/docs/dry-run-mode?hl=ja

Terraform でサービス境界を設定する

今回はドライラン モードで境界を作成していきます。

vpcsc.tf
resource "google_access_context_manager_service_perimeter" "service_perimeter" {
// 以下3つはアクセスレベルと同様に設定する
  parent = "accessPolicies/${google_access_context_manager_access_policy.access_policy.name}"
  name   = "accessPolicies/${google_access_context_manager_access_policy.access_policy.name}/servicePerimeters/demo_perimeter"
  title  = "demo_perimeter"
  
  spec {
    resources = [
        "projects/1111111",
        "projects/2222222"
        ]
    restricted_services = [
        "storage.googleapis.com",
        "bigquery.googleapis.com"
        ]
    vpc_accessible_services {
      enable_restriction = false
      allowed_services   = []
    }
    access_levels       = [google_access_context_manager_access_level.access_level.name]
  }

  use_explicit_dry_run_spec = true
}

Terraform でサービス境界を作成するには google_access_context_manager_service_perimeter リソースを使用します。

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

ドライラン モードの境界を作成する場合は spec ブロック以下で境界の詳細な設定をしていきます。自動適用モードの場合は status ブロックです。

resources
境界内に含めるプロジェクトを指定します。
projects/{project_number} という形式で指定します。

restricted_services
境界内で保護するリソースを指定します。
ここで指定したリソースへのアクセスが制限されるようになります。
今回は Cloud Storage と BigQuery を指定しています。
これで、上記 resources で指定したプロジェクト内の、Cloud Storage と BigQuery へのアクセスを制限する設定になります。

vpc_accessible_services ブロック
このブロック内で境界内の通信に関する設定を行います。

  • enable_ristriction
    境界内の API 呼び出しなどを allowedService で指定されたリストに制限するかどうかを決めます。
    今回はそれらのアクセスを制限はしないため false にします。

  • allowedService
    上記の enable_ristriction が true の場合、制限する API をリストに列挙します。
    今回のように false の場合は空にします。

access_levels
境界に紐づけるアクセスレベルを指定します。
ここで指定したアクセスレベルの条件に一致するリクエストは境界へのアクセスが許可されます。
先に記述していた、アクセスレベルを指定します。

use_explict_dry_run_spec
spec ブロックの設定を適用するために ture にする必要があります。
通常、自動適用モードのサービス境界の作成時でも、暗黙的にドライラン モードのサービス境界が作成される仕様になっています。
このフラグを設定することで暗黙的な作成を制御し、明示的にドライラン モードの境界のみを作成することができます。

上り(内向き)/ 下り(外向き)ポリシーを作成する

上り(内向き)/ 下り(外向き)ポリシー

最後に上り(内向き)/ 下り(外向き)ポリシーを追加していきます。
上り(内向き)/ 下り(外向き)ポリシーを設定することでサービス境界で保護されたリソースへのアクセスを許可します。

特定のプロジェクトや 特定の API に対してのみアクセスを許可する設定ができ、アクセスレベルよりも細かい制御が可能です。

上り(内向き)は境界外のクライアントから境界内リソースへのアクセスを許可し、下り(外向き)は境界内リソースから境界外リソースのアクセスを許可します。
この 2 つのポリシーはサービス境界に紐づく形で作成されます。
そのため、Terraform においてもサービス境界の作成とともに条件を設定していきます。

https://cloud.google.com/vpc-service-controls/docs/ingress-egress-rules?hl=ja

Terraform で上り(内向き)/ 下り(外向き)ポリシーを設定する

上り(内向き)ポリシー

境界外プロジェクトのサービス アカウントから Cloud Storage へのアクセスを許可する上り(内向き)ポリシーを作成していきます。

ingress
ingress_policies {
    ingress_from {
        identity_type = "IDENTITY_TYPE_UNSPECIFIED"
        identities = [
          "serviceAccount:aaa@bbb.gserviceaccount.com"
        ]
        sources {
            resource = "projects/3333333"
        }
    }
    ingress_to {
        resources = ["projects/1111111"]
        operations {
            service_name = "storage.googleapis.com"
            method_selectors {
                method = "*"
          }
        }
    }
}

上り(内向き)ポリシーは ingress_policies ブロック以下で設定します。

https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/access_context_manager_service_perimeter#nested_ingress_policies

ingress_from ブロック
このブロックの中でアクセス元の API クライアントの条件を設定をします。
条件に一致した境界外からのアクセスは許可されます。

  • identity_type
    アクセスを許可する ID のタイプを指定します。
    ID のタイプとしては IDENTITY_TYPE_UNSPECIFIED、ANY_IDENTITY、ANY_USER_ACCOUNT、ANY_SERVICE_ACCOUNT を指定することができます。
    今回のように特定のサービス アカウントなどを許可したい場合は IDENTITY_TYPE_UNSPECIFIED を指定します。

https://cloud.google.com/vpc-service-controls/docs/ingress-egress-rules?hl=ja#ingress-rules-reference

https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/access_context_manager_service_perimeter#identity_type

  • identities
    アクセスを許可する特定の ID を指定します。
    今回はサービス アカウントなので serviceAccount:{emailid} という形式で指定します。

  • sources ブロック
    このブロック以下でアクセスを許可するソースを指定します。

    • resources
      アクセスを許可する境界外プロジェクトを指定します。

ingress_to ブロック
アクセスを許可する境界内のリソースを指定し、リソースに対しクライアントがどのようなオペレーションを行えるか設定します。

  • resources
    アクセス可能な境界内のプロジェクトを指定します。

  • operations ブロック
    境界内で可能な操作を設定します。

    • service_name
      操作するサービスを指定します。
      今回は Cloud Storage を操作したいので、"storage.googleapi.com"を指定します。
    • method_selectors ブロック
      • method
        制限する API のメソッドを指定します。
        すべてのメソッドを許可するため、* を指定しています。

下り(外向き)ポリシー

境界内の全てのリソースから境界外のプロジェクトの BigQuery へのアクセスを許可します。

egress
egress_policies {
    egress_from {
        identity_type = "ANY_IDENTITY"
    }
    egress_to {
        resources = [
            "projects/3333333"
        ]
        operations {
            service_name = "bigquery.googleapis.com"
            method_selectors {
                permission = "bigquery.tables.getData"
            }
        }
    }
}

下り(外向き)ポリシーは egress_policies ブロック以下で設定していきます。
設定する項目は ingress_policies とほとんど変わりません。
ただ、通信の方向が変わるのでその点を注意する必要があります。

https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/access_context_manager_service_perimeter#nested_egress_policies

egress_from ブロック
このブロック内で境界内のアクセス元の条件を設定します。
どのような条件であれば境界内から外へのアクセスを許可するのか設定します。

  • identity_type
    今回は ANY_IDENTITY ですべての ID を許可します。

egress_to ブロック
このブロック内で境界外のどのリソースに対して、どのようなオペレーションを許可するか設定します。

  • resources
    宛先となる境界外のプロジェクトを指定します。

  • operations ブロック
    宛先の境界外リソースに対して可能な操作を設定します。

    • service_name
      境界外の BigQuery を操作したいので bigquery.googleapis.com を指定します。
  • method_selectors ブロック

    • permission
      境界外の BigQuery に対して bigquery.tables.getData という権限を必要とするオペレーションのみ許可します。
      このように許可したいオペレーションが権限の場合は permission で指定する必要があります。

以下、google_access_context_manager_service_perimeter リソース内で記述したコードです。

上り(内向き)/ 下り(外向き)ポリシーの Terraform
vpcsc.tf
resource "google_access_context_manager_service_perimeter" "service_perimeter" {
  parent = "accessPolicies/${google_access_context_manager_access_policy.access_policy.name}"
  name   = "accessPolicies/${google_access_context_manager_access_policy.access_policy.name}/servicePerimeters/demo_perimeter"
  title  = "demo_perimeter"
  
  spec {
    resources = [
        "projects/1111111",
        "projects/2222222"
        ]
    restricted_services = [
        "storage.googleapis.com",
        "bigquery.googleapis.com"
        ]
    vpc_accessible_services {
      enable_restriction = false
      allowed_services   = []
    }
    access_levels       = [google_access_context_manager_access_level.access_level.name]
    
    ingress_policies {
        ingress_from {
            identity_type = "IDENTITY_TYPE_UNSPECIFIED"
            identities = [
                "serviceAccount:aaa@bbb.gserviceaccount.com"
            ]
            sources {
                resource = "projects/3333333"
            }
        }
        ingress_to {
            resources = ["projects/1111111"]
            operations {
                service_name = "storage.googleapis.com"
                method_selectors {
                    method = "*"
                }
            }
        }
    }
    egress_policies {
        egress_from {
            identity_type = "ANY_IDENTITY"
        }
        egress_to {
            resources = [
                "projects/3333333"
            ]
            operations {
                service_name = "bigquery.googleapis.com"
                method_selectors {
                    permission = "bigquery.tables.getData"
                }
            }
        }
    }
  }

  use_explicit_dry_run_spec = true
}

動作確認

VPC-SC の構築に必要な Terraform コードは以上です。
terraform apply して、コンソール画面でどうのように構成されるか確認してみましょう。

コンソールで「VPC Service Controls」のページを開きます。
アクセス ポリシーが以下のように、画面上部に表示されます。

今回はドライラン モードで境界を作成しているので、ドライラン モードのタブに切り替えると境界が表示されます。
作成した demo_primeter をクリックすると境界の詳細が表示されます。

境界の詳細画面では以下のように、ドライラン構成の欄に各設定が表示されます。
適用済みの構成には自動適用モードで境界を適用した際に設定が表示されるものとなっています。

アクセスレベルは「Access Context Manager」のページで以下のように確認できます。

このように、VPC-SCの各設定を確認することができます。

では、次にアクセスがどのように拒否されるか確認していきましょう。
アクセスレベルで許可していない IP アドレスを使って、境界内の BigQuery にてデータセットを作成してみると、以下のようにログ エクスプローラにエラーが出力されます。

前述の通り、ドライランモードでは実際にアクセスが拒否されません。
IAM 権限があればリソースの操作は可能となっていますが、アクセスレベルや上り(内向き)/ 下り(外向き)ポリシーで許可していない通信による操作は上記画像のように、エラーとして出力されます。許可設定がされている通信であればエラーは出力されません。

ログの詳細を開くと、アクセス元の IP アドレスやプリンシパル、使用した API 、通信の向きなどが表示されているので、そのログを確認し、システム上必要な通信であれば許可設定を追加していくことで、最終的な本適用に向けて、VPC-SC を設計することができます。

また、今回はドライラン モードで境界を作成しましたが、自動適用モードで VPC-SC を実際に有効にし、上記と同様の操作をしてみると、以下のように、データセットの作成が VPC-SC によってブロックされます。

そもそもリソースの画面自体表示させたくないといった場合には IAM の権限を制限する必要があります。VPC-SC のみでは画面自体は表示されるので認識しておくとよいかと思います。
VPC-SC と IAM を併用することでセキュリティをより強固にすることができます。

補足

BigQuery の場合、上記のような許可設定のない通信では、データセットなどが表示されないようになっています。

関係性をまとめる

VPC-SC に関わるリソースを図にまとめると以下のようになります。


VPC-SC リソース関係図

組織に対して、一つのアクセス ポリシーが存在していて、コンテナであるアクセス ポリシーの下にアクセスレベルとサービス境界が作成されます。
アクセスレベルはサービス境界に対して適用させることで機能します。
そして、これら 3 つは Access Context Manager のリソースです。
また、細かい穴あけが可能な上り(内向き)/ 下り(外向き)ポリシーはサービス境界に紐づく形で作成されます。
VPC-SC の作成で登場するリソースはこのような関係となっています。

最後にここまで書いたコードを以下で一つにまとめます。

VPC-SC Terraformコード
vpcsc.tf

resource "google_access_context_manager_access_policy" "access_policy" {
  parent = "organizations/{organization_id}"
  title  = "demo policy"
}

resource "google_access_context_manager_access_level" "access_level" {
  parent = "accessPolicies/${google_access_context_manager_access_policy.access_policy.name}"
  name   = "accessPolicies/${google_access_context_manager_access_policy.access_policy.name}/accessLevels/acl_demo"
  title  = "acl_demo"
  basic {
    conditions {
      ip_subnetworks = [
        "xx.xx.xx.xx/32",
        "yy.yy.yy.yy/32"
        ]
    }
  }
}

resource "google_access_context_manager_service_perimeter" "service_perimeter" {
  parent = "accessPolicies/${google_access_context_manager_access_policy.access_policy.name}"
  name   = "accessPolicies/${google_access_context_manager_access_policy.access_policy.name}/servicePerimeters/demo_perimeter"
  title  = "demo_perimeter"
  
  spec {
    resources = [
        "projects/1111111",
        "projects/2222222"
        ]
    restricted_services = [
        "storage.googleapis.com",
        "bigquery.googleapis.com"
        ]
    vpc_accessible_services {
      enable_restriction = false
      allowed_services   = []
    }
    access_levels       = [google_access_context_manager_access_level.access_level.name]
    ingress_policies {
        ingress_from {
            identity_type = "IDENTITY_TYPE_UNSPECIFIED"
            identities = [
                "serviceAccount:aaa@bbb.gserviceaccount.com"
            ]
            sources {
                resource = "projects/3333333"
            }
        }
        ingress_to {
            resources = ["projects/1111111"]
            operations {
                service_name = "storage.googleapis.com"
                method_selectors {
                    method = "*"
                }
            }
        }
    }
    egress_policies {
        egress_from {
            identity_type = "ANY_IDENTITY"
        }
        egress_to {
            resources = [
                "projects/3333333"
            ]
            operations {
                service_name = "bigquery.googleapis.com"
                method_selectors {
                    permission = "bigquery.tables.getData"
                }
            }
        }
    }
  }

  use_explicit_dry_run_spec = true
}

終わりに

Terraform で VPC-SC を作成しながら、関わるリソースについて解説してきました。
本記事で VPC-SC に関わるリソースの関係性や Terraform での作成方法を理解する一助になれば幸いです。
最後までご覧いただき、ありがとうございました。

Discussion