# VPC Service Controls This module offers a unified interface to manage VPC Service Controls [Access Policy](https://cloud.google.com/access-context-manager/docs/create-access-policy), [Access Levels](https://cloud.google.com/access-context-manager/docs/manage-access-levels), and [Service Perimeters](https://cloud.google.com/vpc-service-controls/docs/service-perimeters). Given the complexity of the underlying resources, the module intentionally mimics their interfaces to make it easier to map their documentation onto its variables, and reduce the internal complexity. If you are using [Application Default Credentials](https://cloud.google.com/sdk/gcloud/reference/auth/application-default) with Terraform and run into permission issues, make sure to check out the recommended provider configuration in the [VPC SC resources documentation](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/access_context_manager_access_level). - [Examples](#examples) - [Access policy](#access-policy) - [Scoped policy](#scoped-policy) - [Access policy IAM](#access-policy-iam) - [Access levels](#access-levels) - [Perimeters](#perimeters) - [Automatic Project ID to Project Number Conversion](#automatic-project-id-to-project-number-conversion) - [Factories](#factories) - [Notes](#notes) - [Files](#files) - [Variables](#variables) - [Outputs](#outputs) - [Tests](#tests) ## Examples ### Access policy By default, the module is configured to use an existing policy, passed in by name in the `access_policy` variable: ```hcl module "test" { source = "./fabric/modules/vpc-sc" access_policy = "12345678" } # tftest modules=0 resources=0 ``` If you need the module to create the policy for you, use the `access_policy_create` variable, and set `access_policy` to `null`: ```hcl module "test" { source = "./fabric/modules/vpc-sc" access_policy = null access_policy_create = { parent = "organizations/123456" title = "vpcsc-policy" } } # tftest modules=1 resources=1 inventory=access-policy.yaml ``` #### Scoped policy If you need the module to create a scoped policy for you, specify 'scopes' of the policy in the `access_policy_create` variable: ```hcl module "test" { source = "./fabric/modules/vpc-sc" access_policy = null access_policy_create = { parent = "organizations/123456" title = "vpcsc-policy" scopes = ["folders/456789"] } } # tftest modules=1 resources=1 inventory=scoped-access-policy.yaml ``` #### Access policy IAM The usual IAM interface is also implemented here, and can be used with service accounts or user principals: ```hcl module "test" { source = "./fabric/modules/vpc-sc" access_policy = "12345678" iam = { "roles/accesscontextmanager.policyAdmin" = [ "user:foo@example.org" ] } } # tftest modules=1 resources=1 ``` ### Access levels As highlighted above, the `access_levels` type replicates the underlying resource structure. ```hcl module "test" { source = "./fabric/modules/vpc-sc" access_policy = "12345678" access_levels = { a1 = { conditions = [ { members = ["user:user1@example.com"] } ] } a2 = { combining_function = "OR" conditions = [ { regions = ["IT", "FR"] }, { ip_subnetworks = ["101.101.101.0/24"] } ] } } } # tftest modules=1 resources=2 inventory=access-levels.yaml ``` ### Perimeters Perimeters are defined via `perimeters` variable, or the dedicated factory. Perimeters by default manage all their attributes authoritatively. To have perimeter resources managed externally (e.g. from the project factory) set the perimeter-level attribute `ignore_resource_changes` at the perimeter level. ```hcl module "test" { source = "./fabric/modules/vpc-sc" access_policy = "12345678" access_levels = { a1 = { conditions = [ { members = ["user:user1@example.com"] } ] } a2 = { conditions = [ { members = ["user:user2@example.com"] } ] } } egress_policies = { # allow writing to external GCS bucket from a specific SA gcs-sa-foo = { from = { identities = [ "serviceAccount:foo@myproject.iam.gserviceaccount.com" ] } to = { operations = [{ method_selectors = ["*"] service_name = "storage.googleapis.com" }] resources = ["projects/123456789"] } } } ingress_policies = { # allow management from external automation SA sa-tf-test = { from = { identities = [ "serviceAccount:test-tf-0@myproject.iam.gserviceaccount.com", "serviceAccount:test-tf-1@myproject.iam.gserviceaccount.com" ] access_levels = ["$access_levels:a1"] } to = { operations = [{ service_name = "*" }] resources = ["*"] } } sa-roles = { from = { identities = [ "serviceAccount:test-tf-2@myproject.iam.gserviceaccount.com", ] access_levels = ["*"] } to = { operations = [{ service_name = "*" }] resources = ["*"] roles = ["roles/storage.objectViewer"] } } } perimeters = { r1 = { status = { access_levels = ["$access_levels:a1", "$access_levels:a2"] resources = ["projects/1111", "projects/2222"] restricted_services = ["storage.googleapis.com"] egress_policies = ["$egress_policies:gcs-sa-foo"] ingress_policies = [ "$ingress_policies:sa-tf-test", "$ingress_policies:sa-roles" ] vpc_accessible_services = { allowed_services = ["storage.googleapis.com"] enable_restriction = true } } } } } # tftest modules=1 resources=3 inventory=regular.yaml ``` ## Automatic Project ID to Project Number Conversion As a convenience, this module can optionally convert project IDs to project numbers. Set `var.project_id_search_scope` to a folder or organization ID to define the search scope. The caller must have `cloudasset.assets.searchAllResources` permission to perform the search. Roles like `roles/accesscontextmanager.policyAdmin`, `roles/cloudasset.viewer`, or `roles/viewer` grant this. ```hcl module "vpc-sc" { source = "./fabric/modules/vpc-sc" project_id_search_scope = var.org_id access_policy = "12345678" ingress_policies = { i1 = { from = { identities = [ "serviceAccount:foo@myproject.iam.gserviceaccount.com" ] resources = ["projects/my-source-project"] } to = { operations = [{ method_selectors = ["*"] service_name = "storage.googleapis.com" }] resources = ["projects/my-destionation-project"] } } } perimeters = { p = { spec = { ingress_policies = ["$ingress_policies:i1"] resources = ["projects/my-destionation-project"] } use_explicit_dry_run_spec = true } } } # tftest skip because uses data sources ``` ## Factories This module implements support for four distinct factories, used to create and manage perimeters, access levels, egress policies, and ingress policies via YAML files. JSON Schema files for each factory object are available in the [`schemas`](./schemas/) folder, and can be used to validate input YAML data with [`validate-yaml`](https://github.com/gerald1248/validate-yaml) or any of the available tools and libraries. 3Note that the factory configuration points to folders, where each file represents one resource. ```hcl module "test" { source = "./fabric/modules/vpc-sc" access_policy = "12345678" context = { resource_sets = { foo_projects = ["projects/321", "projects/654"] } } factories_config = { access_levels = "data/access-levels" egress_policies = "data/egress-policies" ingress_policies = "data/ingress-policies" perimeters = "data/perimeters" } } # tftest modules=1 resources=3 files=p1,a1,a2,e1,i1,i2 inventory=factory.yaml ``` ```yaml description: Main perimeter status: access_levels: - $access_levels:geo-it - $access_levels:identity-user1 resources: - projects/1111 - projects/2222 restricted_services: - storage.googleapis.com egress_policies: - $egress_policies:gcs-sa-foo ingress_policies: - $ingress_policies:sa-tf-test-geo - $ingress_policies:sa-tf-test vpc_accessible_services: allowed_services: - storage.googleapis.com enable_restriction: yes # tftest-file id=p1 path=data/perimeters/perimeter-north.yaml schema=perimeter.schema.json ``` ```yaml description: "Main perimeter" status: access_levels: - $access_levels:geo-it - $access_levels:identity-user1 resources: - projects/1111 - projects/2222 restricted_services: - storage.googleapis.com egress_policies: - $egress_policies:gcs-sa-foo ingress_policies: - $ingress_policies:sa-tf-test-geo - $ingress_policies:sa-tf-test vpc_accessible_services: allowed_services: - storage.googleapis.com enable_restriction: true # tftest-file id=p1 path=data/perimeters/perimeter-north.yaml schema=perimeter.schema.json ``` ```yaml conditions: - members: - user:user1@example.com # tftest-file id=a1 path=data/access-levels/identity-user1.yaml schema=access-level.schema.json ``` ```yaml conditions: - regions: - IT # tftest-file id=a2 path=data/access-levels/geo-it.yaml schema=access-level.schema.json ``` ```yaml from: identities: - serviceAccount:foo@myproject.iam.gserviceaccount.com - serviceAccount:bar@myproject.iam.gserviceaccount.com to: operations: - method_selectors: - "*" service_name: storage.googleapis.com resources: - projects/123456789 # tftest-file id=e1 path=data/egress-policies/gcs-sa-foo.yaml schema=egress-policy.schema.json ``` ```yaml from: access_levels: - "*" identities: - serviceAccount:test-tf-0@myproject.iam.gserviceaccount.com - serviceAccount:test-tf-1@myproject.iam.gserviceaccount.com to: operations: - service_name: compute.googleapis.com method_selectors: - ProjectsService.Get - RegionsService.Get resources: - "*" # tftest-file id=i1 path=data/ingress-policies/sa-tf-test.yaml schema=ingress-policy.schema.json ``` ```yaml from: access_levels: - $access_levels:geo-it identities: - serviceAccount:test-tf@myproject.iam.gserviceaccount.com to: operations: - service_name: "*" resources: - projects/1234567890 - $resource_sets:foo_projects # tftest-file id=i2 path=data/ingress-policies/sa-tf-test-geo.yaml schema=ingress-policy.schema.json ``` ## Notes - To remove an access level, first remove the binding between perimeter and the access level in `status` and/or `spec` without removing the access level itself. Once you have run `terraform apply`, you'll then be able to remove the access level and run `terraform apply` again. ## Files | name | description | resources | |---|---|---| | [access-levels.tf](./access-levels.tf) | Access level resources. | google_access_context_manager_access_level | | [factory.tf](./factory.tf) | None | | | [iam.tf](./iam.tf) | IAM bindings | google_access_context_manager_access_policy_iam_binding · google_access_context_manager_access_policy_iam_member | | [main.tf](./main.tf) | Module-level locals and resources. | google_access_context_manager_access_policy | | [outputs.tf](./outputs.tf) | Module outputs. | | | [perimeters-additive.tf](./perimeters-additive.tf) | Regular service perimeter resources which ignore resource changes. | google_access_context_manager_service_perimeter | | [perimeters.tf](./perimeters.tf) | Regular service perimeter resources. | google_access_context_manager_service_perimeter | | [variables.tf](./variables.tf) | Module variables. | | | [versions.tf](./versions.tf) | Version pins. | | ## Variables | name | description | type | required | default | |---|---|:---:|:---:|:---:| | [access_policy](variables.tf#L68) | Access Policy name, set to null if creating one. | string | ✓ | | | [access_levels](variables.tf#L17) | Access level definitions. | map(object({…})) | | {} | | [access_policy_create](variables.tf#L73) | Access Policy configuration, fill in to create. Parent is in 'organizations/123456' format, scopes are in 'folders/456789' or 'projects/project_id' format. | object({…}) | | null | | [context](variables.tf#L83) | External context used in replacements. | object({…}) | | {} | | [egress_policies](variables.tf#L98) | Egress policy definitions that can be referenced in perimeters. | map(object({…})) | | {} | | [factories_config](variables.tf#L141) | Paths to folders that enable factory functionality. | object({…}) | | {} | | [iam](variables.tf#L153) | IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | | [iam_bindings](variables.tf#L159) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | map(object({…})) | | {} | | [iam_bindings_additive](variables.tf#L174) | Individual additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} | | [ingress_policies](variables.tf#L189) | Ingress policy definitions that can be referenced in perimeters. | map(object({…})) | | {} | | [perimeters](variables.tf#L231) | Regular service perimeters. | map(object({…})) | | {} | | [project_id_search_scope](variables.tf#L265) | Set this to an organization or folder ID to use Cloud Asset Inventory to automatically translate project ids to numbers. | string | | null | ## Outputs | name | description | sensitive | |---|---|:---:| | [access_level_names](outputs.tf#L17) | Access level resources. | | | [access_levels](outputs.tf#L25) | Access level resources. | | | [access_policy](outputs.tf#L30) | Access policy resource, if autocreated. | | | [access_policy_name](outputs.tf#L37) | Access policy name. | | | [id](outputs.tf#L42) | Fully qualified access policy id. | | | [perimeters](outputs.tf#L47) | Regular service perimeter resources. | | ## Tests ```hcl module "test" { source = "./fabric/modules/vpc-sc" access_policy = "12345678" factories_config = { access_levels = "data/access-levels" egress_policies = "data/egress-policies" ingress_policies = "data/ingress-policies" } ingress_policies = { variable-policy = { from = { identities = [ "serviceAccount:sa-0@myproject.iam.gserviceaccount.com" ] access_levels = ["*"] } to = { operations = [{ service_name = "*" }] resources = ["*"] } } } perimeters = { default = { status = { access_levels = ["geo-it"] resources = ["projects/1111"] egress_policies = ["$egress_policies:factory-egress-policy"] ingress_policies = [ "$ingress_policies:variable-policy", "$ingress_policies:factory-ingress-policy" ] } } } } # tftest modules=1 resources=2 files=t1a1,t1i1,t1e1 ``` ```yaml conditions: - regions: - IT # tftest-file id=t1a1 path=data/access-levels/geo-it.yaml schema=access-level.schema.json ``` ```yaml from: access_levels: - geo-it identity_type: ANY_IDENTITY to: operations: - service_name: "*" resources: - projects/1234567890 # tftest-file id=t1i1 path=data/ingress-policies/factory-ingress-policy.yaml schema=ingress-policy.schema.json ``` ```yaml from: identity_type: ANY_IDENTITY to: operations: - service_name: "*" resources: - "*" # tftest-file id=t1e1 path=data/egress-policies/factory-egress-policy.yaml schema=egress-policy.schema.json ```