Skip to content

fix: use dry run resource to add resources to the perimeter #204

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions build/int.cloudbuild.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,30 @@ steps:
- verify-scoped-example-with-egress-rule
name: 'gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS'
args: ['/bin/bash', '-c', 'cft test run TestScopedExampleWithEgressRule --stage destroy --verbose']
# scoped example with access level dry-run
- id: init-scoped-example-access-level-dry-run
waitFor:
- destroy-scoped-example-with-egress-rule
name: 'gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS'
args: ['/bin/bash', '-c', 'cft test run TestScopedExampleAccessLevelDryRun --stage init --verbose']

- id: apply-scoped-example-access-level-dry-run
waitFor:
- init-scoped-example-access-level-dry-run
name: 'gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS'
args: ['/bin/bash', '-c', 'cft test run TestScopedExampleAccessLevelDryRun --stage apply --verbose']

- id: verify-scoped-example-access-level-dry-run
waitFor:
- apply-scoped-example-access-level-dry-run
name: 'gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS'
args: ['/bin/bash', '-c', 'cft test run TestScopedExampleAccessLevelDryRun --stage verify --verbose']

- id: destroy-scoped-example-access-level-dry-run
waitFor:
- verify-scoped-example-access-level-dry-run
name: 'gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS'
args: ['/bin/bash', '-c', 'cft test run TestScopedExampleAccessLevelDryRun --stage destroy --verbose']
tags:
- 'ci'
- 'integration'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,17 @@ This example illustrates how to use the `vpc-service-controls` module to configu
| ip\_subnetworks | A list of CIDR block IP subnetwork specification. May be IPv4 or IPv6. Note that for a CIDR IP address block, the specified IP address portion must be properly truncated (i.e. all the host bits must be zero) or the input is considered malformed. For example, "192.0.2.0/24" is accepted but "192.0.2.1/24" is not. Similarly, for IPv6, "2001:db8::/32" is accepted whereas "2001:db8::1/32" is not. The originating IP of a request must be in one of the listed subnets in order for this Condition to be true. If empty, all IP addresses are allowed. | `list(string)` | n/a | yes |
| parent\_id | The parent of this AccessPolicy in the Cloud Resource Hierarchy. As of now, only organization are accepted as parent. | `string` | n/a | yes |
| policy\_name | The policy's name. | `string` | n/a | yes |
| protected\_project\_id | Project number of the project INSIDE the regular service perimeter. | `number` | n/a | yes |
| protected\_project\_number | Project number of the project INSIDE the regular service perimeter. | `number` | n/a | yes |
| scopes | Folder or project on which this policy is applicable. Format: 'folders/FOLDER\_ID' or 'projects/PROJECT\_NUMBER' | `list(string)` | `[]` | no |

## Outputs

| Name | Description |
|------|-------------|
| policy\_name | n/a |
| access\_levels\_dry\_run | Access Level in Dry\_run mode |
| policy\_id | Resource name of the AccessPolicy. |
| policy\_name | Name of the AccessPolicy. |
| protected\_project\_number | Project number of the project INSIDE the regular service perimeter |
| service\_perimeter\_name | Service perimeter name |

<!-- END OF PRE-COMMIT-TERRAFORM DOCS HOOK -->
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,16 @@

module "access_context_manager_policy" {
source = "terraform-google-modules/vpc-service-controls/google"
version = "~> 7.0"
version = "~> 7.1"

parent_id = var.parent_id
policy_name = var.policy_name
scopes = var.scopes
}

module "access_level_1" {
source = "terraform-google-modules/vpc-service-controls/google//modules/access_level"
version = "~> 7.0"
version = "~> 7.1"

policy = module.access_context_manager_policy.policy_id
name = "single_ip_policy"
Expand All @@ -34,33 +35,43 @@ module "access_level_1" {

module "access_level_2" {
source = "terraform-google-modules/vpc-service-controls/google//modules/access_level"
version = "~> 7.0"
version = "~> 7.1"

policy = module.access_context_manager_policy.policy_id
name = "single_ip_policy_dry_run"
ip_subnetworks = var.ip_subnetworks
description = "Some description"
}

resource "time_sleep" "wait_for_access_levels" {
create_duration = "90s"
destroy_duration = "90s"

depends_on = [
module.access_level_1,
module.access_level_2
]
}

module "regular_service_perimeter_1" {
source = "terraform-google-modules/vpc-service-controls/google//modules/regular_service_perimeter"
version = "~> 7.0"
version = "~> 7.1"

policy = module.access_context_manager_policy.policy_id
perimeter_name = "regular_perimeter_1"
perimeter_name = "regular_perimeter_1_dry_run"
description = "Some description"
resources = [var.protected_project_id]

resources = [var.protected_project_number]
restricted_services = ["bigquery.googleapis.com", "storage.googleapis.com"]
access_levels = [module.access_level_1.name]

access_levels = [module.access_level_1.name]


resources_dry_run = [var.protected_project_id]
resources_dry_run = [var.protected_project_number]
restricted_services_dry_run = ["storage.googleapis.com"]
access_levels_dry_run = [module.access_level_2.name]

shared_resources = {
all = [var.protected_project_id]
all = [var.protected_project_number]
}

depends_on = [time_sleep.wait_for_access_levels]
}
40 changes: 40 additions & 0 deletions examples/scoped_example_access_level_dry_run/outputs.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

output "policy_id" {
description = "Resource name of the AccessPolicy."
value = module.access_context_manager_policy.policy_id
}

output "policy_name" {
description = "Name of the AccessPolicy."
value = var.policy_name
}

output "protected_project_number" {
description = "Project number of the project INSIDE the regular service perimeter"
value = var.protected_project_number
}

output "access_levels_dry_run" {
description = "Access Level in Dry_run mode"
value = module.access_level_2.name
}

output "service_perimeter_name" {
description = "Service perimeter name"
value = module.regular_service_perimeter_1.perimeter_name
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ variable "policy_name" {
type = string
}

variable "protected_project_id" {
variable "protected_project_number" {
description = "Project number of the project INSIDE the regular service perimeter."
type = number
}
Expand All @@ -33,3 +33,9 @@ variable "ip_subnetworks" {
description = "A list of CIDR block IP subnetwork specification. May be IPv4 or IPv6. Note that for a CIDR IP address block, the specified IP address portion must be properly truncated (i.e. all the host bits must be zero) or the input is considered malformed. For example, \"192.0.2.0/24\" is accepted but \"192.0.2.1/24\" is not. Similarly, for IPv6, \"2001:db8::/32\" is accepted whereas \"2001:db8::1/32\" is not. The originating IP of a request must be in one of the listed subnets in order for this Condition to be true. If empty, all IP addresses are allowed."
type = list(string)
}

variable "scopes" {
description = "Folder or project on which this policy is applicable. Format: 'folders/FOLDER_ID' or 'projects/PROJECT_NUMBER'"
type = list(string)
default = []
}
1 change: 1 addition & 0 deletions modules/regular_service_perimeter/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ module "regular_service_perimeter_1" {
| perimeter\_name | Name of the perimeter. Should be one unified string. Must only be letters, numbers and underscores | `string` | n/a | yes |
| policy | Name of the parent policy | `string` | n/a | yes |
| resource\_keys | A list of keys to use for the Terraform state. The order should correspond to var.resources and the keys must not be dynamically computed. If `null`, var.resources will be used as keys. | `list(string)` | `null` | no |
| resource\_keys\_dry\_run | A list of keys to use for the Terraform state. The order should correspond to var.resources\_dry\_run and the keys must not be dynamically computed. If `null`, var.resources\_dry\_run will be used as keys. | `list(string)` | `null` | no |
| resources | A list of GCP resources that are inside of the service perimeter. Currently only projects and VPC networks are allowed. | `list(string)` | `[]` | no |
| resources\_dry\_run | (Dry-run) A list of GCP resources that are inside of the service perimeter. Currently only projects and VPC networks are allowed. If set, a dry-run policy will be set. | `list(string)` | `[]` | no |
| restricted\_services | GCP services that are subject to the Service Perimeter restrictions. Must contain a list of services. For example, if storage.googleapis.com is specified, access to the storage buckets inside the perimeter must meet the perimeter's access restrictions. | `list(string)` | `[]` | no |
Expand Down
14 changes: 13 additions & 1 deletion modules/regular_service_perimeter/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ resource "google_access_context_manager_service_perimeter" "regular_service_peri
for_each = local.dry_run ? ["dry-run"] : []
content {
restricted_services = var.restricted_services_dry_run
resources = [for item in var.resources_dry_run : can(regex("global/networks", item)) ? format("//compute.googleapis.com/%s", item) : format("projects/%s", item)]
access_levels = formatlist(
"accessPolicies/${var.policy}/accessLevels/%s",
var.access_levels_dry_run
Expand Down Expand Up @@ -86,10 +85,23 @@ locals {
for rk in local.resource_keys :
rk => var.resources[index(local.resource_keys, rk)]
}

#dry-run
resource_keys_dry_run = var.resource_keys_dry_run != null ? var.resource_keys_dry_run : var.resources_dry_run
resources_dry_run = {
for rk in local.resource_keys_dry_run :
rk => var.resources_dry_run[index(local.resource_keys_dry_run, rk)]
}
}

resource "google_access_context_manager_service_perimeter_resource" "service_perimeter_resource" {
for_each = local.resources
perimeter_name = google_access_context_manager_service_perimeter.regular_service_perimeter.name
resource = can(regex("global/networks", each.value)) ? "//compute.googleapis.com/${each.value}" : "projects/${each.value}"
}

resource "google_access_context_manager_service_perimeter_dry_run_resource" "dry_run_service_perimeter_resource" {
for_each = local.resources_dry_run
perimeter_name = google_access_context_manager_service_perimeter.regular_service_perimeter.name
resource = can(regex("global/networks", each.value)) ? "//compute.googleapis.com/${each.value}" : "projects/${each.value}"
}
12 changes: 7 additions & 5 deletions modules/regular_service_perimeter/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,12 @@ variable "resources_dry_run" {
default = []
}

variable "resource_keys_dry_run" {
description = "A list of keys to use for the Terraform state. The order should correspond to var.resources_dry_run and the keys must not be dynamically computed. If `null`, var.resources_dry_run will be used as keys."
type = list(string)
default = null
}

variable "access_levels_dry_run" {
description = "(Dry-run) A list of AccessLevel resource names that allow resources within the ServicePerimeter to be accessed from the internet. AccessLevels listed must be in the same policy as this ServicePerimeter. Referencing a nonexistent AccessLevel is a syntax error. If no AccessLevel names are listed, resources within the perimeter can only be accessed via GCP calls with request origins within the perimeter. Example: 'accessPolicies/MY_POLICY/accessLevels/MY_LEVEL'. For Service Perimeter Bridge, must be empty. If set, a dry-run policy will be set."
type = list(string)
Expand All @@ -77,7 +83,6 @@ variable "shared_resources" {
default = { all = [] }
}

## Have to solve it like this don't want use optional flag because is still experimental
variable "egress_policies" {
description = "A list of all [egress policies](https://cloud.google.com/vpc-service-controls/docs/ingress-egress-rules#egress-rules-reference), each list object has a `from` and `to` value that describes egress_from and egress_to.\n\nExample: `[{ from={ identities=[], identity_type=\"ID_TYPE\" }, to={ resources=[], operations={ \"SRV_NAME\"={ OP_TYPE=[] }}}}]`\n\nValid Values:\n`ID_TYPE` = `null` or `IDENTITY_TYPE_UNSPECIFIED` (only allow indentities from list); `ANY_IDENTITY`; `ANY_USER_ACCOUNT`; `ANY_SERVICE_ACCOUNT`\n`SRV_NAME` = \"`*`\" (allow all services) or [Specific Services](https://cloud.google.com/vpc-service-controls/docs/supported-products#supported_products)\n`OP_TYPE` = [methods](https://cloud.google.com/vpc-service-controls/docs/supported-method-restrictions) or [permissions](https://cloud.google.com/vpc-service-controls/docs/supported-method-restrictions)"
type = list(object({
Expand All @@ -103,8 +108,7 @@ variable "egress_policies" {
default = []
}

## Have to solve it like this don't want use optional flag because is still experimental
variable "ingress_policies" { # TODO
variable "ingress_policies" {
description = "A list of all [ingress policies](https://cloud.google.com/vpc-service-controls/docs/ingress-egress-rules#ingress-rules-reference), each list object has a `from` and `to` value that describes ingress_from and ingress_to.\n\nExample: `[{ from={ sources={ resources=[], access_levels=[] }, identities=[], identity_type=\"ID_TYPE\" }, to={ resources=[], operations={ \"SRV_NAME\"={ OP_TYPE=[] }}}}]`\n\nValid Values:\n`ID_TYPE` = `null` or `IDENTITY_TYPE_UNSPECIFIED` (only allow indentities from list); `ANY_IDENTITY`; `ANY_USER_ACCOUNT`; `ANY_SERVICE_ACCOUNT`\n`SRV_NAME` = \"`*`\" (allow all services) or [Specific Services](https://cloud.google.com/vpc-service-controls/docs/supported-products#supported_products)\n`OP_TYPE` = [methods](https://cloud.google.com/vpc-service-controls/docs/supported-method-restrictions) or [permissions](https://cloud.google.com/vpc-service-controls/docs/supported-method-restrictions)"
type = list(object({
title = optional(string, null)
Expand All @@ -128,7 +132,6 @@ variable "ingress_policies" { # TODO
default = []
}

## Have to solve it like this don't want use optional flag because is still experimental
variable "egress_policies_dry_run" {
description = "A list of all [egress policies](https://cloud.google.com/vpc-service-controls/docs/ingress-egress-rules#egress-rules-reference), each list object has a `from` and `to` value that describes egress_from and egress_to. Use same formatting as `egress_policies`."
type = list(object({
Expand All @@ -154,7 +157,6 @@ variable "egress_policies_dry_run" {
default = []
}

## Have to solve it like this don't want use optional flag because is still experimental
variable "ingress_policies_dry_run" {
description = "A list of all [ingress policies](https://cloud.google.com/vpc-service-controls/docs/ingress-egress-rules#ingress-rules-reference), each list object has a `from` and `to` value that describes ingress_from and ingress_to. Use same formatting as `ingress_policies`."
type = list(object({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@

/**
* Copyright 2024 Google LLC
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -14,6 +15,19 @@
* limitations under the License.
*/

output "policy_name" {
value = var.policy_name
resource "random_string" "suffix" {
length = 6
special = false
upper = false
}

module "example" {
source = "../../../examples/scoped_example_access_level_dry_run"

parent_id = var.parent_id
policy_name = "int_test_vpc_al_dry_run_${random_string.suffix.result}"
scopes = var.scopes

protected_project_number = var.protected_project_ids["number"]
ip_subnetworks = ["192.0.2.0/24"]
}
40 changes: 40 additions & 0 deletions test/fixtures/scoped_example_access_level_dry_run/outputs.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

output "policy_id" {
description = "Resource name of the AccessPolicy."
value = module.example.policy_id
}

output "policy_name" {
description = "Name of the parent policy"
value = module.example.policy_name
}

output "protected_project_number" {
description = "Project number of the project INSIDE the regular service perimeter"
value = module.example.protected_project_number
}

output "service_perimeter_name" {
description = "Service perimeter name"
value = module.example.service_perimeter_name
}

output "access_levels_dry_run" {
description = "Access Level in Dry_run mode"
value = module.example.access_levels_dry_run
}
31 changes: 31 additions & 0 deletions test/fixtures/scoped_example_access_level_dry_run/variables.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

variable "parent_id" {
description = "The parent of this AccessPolicy in the Cloud Resource Hierarchy. As of now, only organization are accepted as parent."
type = string
}

variable "protected_project_ids" {
description = "Project id and number of the project INSIDE the regular service perimeter. This map variable expects an \"id\" for the project id and \"number\" key for the project number."
type = object({ id = string, number = number })
}

variable "scopes" {
description = "Folder or project on which this policy is applicable. Format: 'folders/FOLDER_ID' or 'projects/PROJECT_NUMBER'"
type = list(string)
default = []
}
Loading