diff --git a/fast/stages/1-vpcsc/README.md b/fast/stages/1-vpcsc/README.md
index cc348c903..bd15efb7c 100644
--- a/fast/stages/1-vpcsc/README.md
+++ b/fast/stages/1-vpcsc/README.md
@@ -303,15 +303,15 @@ Some references that might be useful in setting up this stage:
|---|---|:---:|:---:|:---:|:---:|
| [automation](variables-fast.tf#L17) | Automation resources created by the bootstrap stage. | object({…}) | ✓ | | 0-bootstrap |
| [organization](variables-fast.tf#L35) | Organization details. | object({…}) | ✓ | | 0-bootstrap |
-| [access_levels](variables.tf#L17) | Access level definitions. | map(object({…})) | | {} | |
-| [access_policy](variables.tf#L56) | Access policy id (used for tenant-level VPC-SC configurations). | number | | null | |
-| [egress_policies](variables.tf#L62) | Egress policy definitions that can be referenced in perimeters. | map(object({…})) | | {} | |
-| [factories_config](variables.tf#L93) | Paths to folders that enable factory functionality. | object({…}) | | {} | |
-| [ingress_policies](variables.tf#L104) | Ingress policy definitions that can be referenced in perimeters. | map(object({…})) | | {} | |
+| [access_levels](variables.tf#L17) | Access level definitions. | map(object({…})) | | {} | |
+| [access_policy](variables.tf#L67) | Access policy id (used for tenant-level VPC-SC configurations). | number | | null | |
+| [egress_policies](variables.tf#L73) | Egress policy definitions that can be referenced in perimeters. | map(object({…})) | | {} | |
+| [factories_config](variables.tf#L114) | Paths to folders that enable factory functionality. | object({…}) | | {} | |
+| [ingress_policies](variables.tf#L125) | Ingress policy definitions that can be referenced in perimeters. | map(object({…})) | | {} | |
| [logging](variables-fast.tf#L25) | Log writer identities for organization / folders. | object({…}) | | null | 0-bootstrap |
-| [outputs_location](variables.tf#L136) | Path where providers, tfvars files, and lists for the following stages are written. Leave empty to disable. | string | | null | |
-| [perimeters](variables.tf#L142) | Perimeter definitions. | map(object({…})) | | {…} | |
-| [resource_discovery](variables.tf#L165) | Automatic discovery of perimeter projects. | object({…}) | | {} | |
+| [outputs_location](variables.tf#L165) | Path where providers, tfvars files, and lists for the following stages are written. Leave empty to disable. | string | | null | |
+| [perimeters](variables.tf#L171) | Perimeter definitions. | map(object({…})) | | {…} | |
+| [resource_discovery](variables.tf#L194) | Automatic discovery of perimeter projects. | object({…}) | | {} | |
| [root_node](variables-fast.tf#L45) | Root node for the hierarchy, if running in tenant mode. | string | | null | 0-bootstrap |
## Outputs
diff --git a/fast/stages/1-vpcsc/variables.tf b/fast/stages/1-vpcsc/variables.tf
index ef8307158..ca91a66a7 100644
--- a/fast/stages/1-vpcsc/variables.tf
+++ b/fast/stages/1-vpcsc/variables.tf
@@ -1,5 +1,5 @@
/**
- * 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.
@@ -36,6 +36,7 @@ variable "access_levels" {
negate = optional(bool)
regions = optional(list(string), [])
required_access_levels = optional(list(string), [])
+ vpc_subnets = optional(map(list(string)), {})
})), [])
description = optional(string)
}))
@@ -51,6 +52,16 @@ variable "access_levels" {
])
error_message = "Invalid `combining_function` value (null, \"AND\", \"OR\" accepted)."
}
+ validation {
+ condition = alltrue([
+ for k, v in var.access_levels : alltrue([
+ for condition in v.conditions : alltrue([
+ for member in condition.members : can(regex("^(?:serviceAccount:|user:)", member))
+ ])
+ ])
+ ])
+ error_message = "Invalid `conditions[].members`. It needs to start with on of the prefixes: 'serviceAccount:' or 'user:'."
+ }
}
variable "access_policy" {
@@ -63,17 +74,19 @@ variable "egress_policies" {
description = "Egress policy definitions that can be referenced in perimeters."
type = map(object({
from = object({
+ access_levels = optional(list(string), [])
identity_type = optional(string)
identities = optional(list(string))
+ resources = optional(list(string), [])
})
to = object({
+ external_resources = optional(list(string))
operations = optional(list(object({
method_selectors = optional(list(string))
permission_selectors = optional(list(string))
service_name = string
})), [])
- resources = optional(list(string))
- resource_type_external = optional(bool, false)
+ resources = optional(list(string))
})
}))
default = {}
@@ -88,6 +101,14 @@ variable "egress_policies" {
])
error_message = "Invalid `from.identity_type` value in egress policy."
}
+ validation {
+ condition = alltrue([
+ for k, v in var.egress_policies : v.from.identities == null ? true : alltrue([
+ for identity in v.from.identities : can(regex("^(?:serviceAccount:|user:|group:|principal:|principalSet:)", identity))
+ ])
+ ])
+ error_message = "Invalid `from.identity`. It needs to start with on of the prefixes: 'serviceAccount:', 'user:', 'group:', 'principal:' or 'principalSet:."
+ }
}
variable "factories_config" {
@@ -131,6 +152,14 @@ variable "ingress_policies" {
])
error_message = "Invalid `from.identity_type` value in ingress policy."
}
+ validation {
+ condition = alltrue([
+ for k, v in var.ingress_policies : v.from.identities == null ? true : alltrue([
+ for identity in v.from.identities : can(regex("^(?:serviceAccount:|user:|group:|principal:|principalSet:)", identity))
+ ])
+ ])
+ error_message = "Invalid `from.identity`. It needs to start with on of the prefixes: 'serviceAccount:', 'user:', 'group:', 'principal:', 'principalSet:'."
+ }
}
variable "outputs_location" {
diff --git a/modules/vpc-sc/README.md b/modules/vpc-sc/README.md
index cafa4f627..44ede6b83 100644
--- a/modules/vpc-sc/README.md
+++ b/modules/vpc-sc/README.md
@@ -339,17 +339,17 @@ to:
| name | description | type | required | default |
|---|---|:---:|:---:|:---:|
-| [access_policy](variables.tf#L66) | 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#L71) | 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 |
-| [egress_policies](variables.tf#L81) | Egress policy definitions that can be referenced in perimeters. | map(object({…})) | | {} |
-| [factories_config](variables.tf#L120) | Paths to folders that enable factory functionality. | object({…}) | | {} |
-| [iam](variables.tf#L132) | IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} |
-| [iam_bindings](variables.tf#L138) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | map(object({…})) | | {} |
-| [iam_bindings_additive](variables.tf#L153) | Individual additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} |
-| [ingress_policies](variables.tf#L168) | Ingress policy definitions that can be referenced in perimeters. | map(object({…})) | | {} |
-| [service_perimeters_bridge](variables.tf#L208) | Bridge service perimeters. | map(object({…})) | | {} |
-| [service_perimeters_regular](variables.tf#L218) | Regular service perimeters. | map(object({…})) | | {} |
+| [access_policy](variables.tf#L67) | 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#L72) | 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 |
+| [egress_policies](variables.tf#L82) | Egress policy definitions that can be referenced in perimeters. | map(object({…})) | | {} |
+| [factories_config](variables.tf#L123) | Paths to folders that enable factory functionality. | object({…}) | | {} |
+| [iam](variables.tf#L135) | IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} |
+| [iam_bindings](variables.tf#L141) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | map(object({…})) | | {} |
+| [iam_bindings_additive](variables.tf#L156) | Individual additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} |
+| [ingress_policies](variables.tf#L171) | Ingress policy definitions that can be referenced in perimeters. | map(object({…})) | | {} |
+| [service_perimeters_bridge](variables.tf#L211) | Bridge service perimeters. | map(object({…})) | | {} |
+| [service_perimeters_regular](variables.tf#L221) | Regular service perimeters. | map(object({…})) | | {} |
## Outputs
@@ -363,7 +363,6 @@ to:
| [service_perimeters_bridge](outputs.tf#L47) | Bridge service perimeter resources. | |
| [service_perimeters_regular](outputs.tf#L52) | Regular service perimeter resources. | |
-
## Tests
```hcl
diff --git a/modules/vpc-sc/access-levels.tf b/modules/vpc-sc/access-levels.tf
index c00288e16..b173f0f60 100644
--- a/modules/vpc-sc/access-levels.tf
+++ b/modules/vpc-sc/access-levels.tf
@@ -1,5 +1,5 @@
/**
- * Copyright 2022 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.
@@ -72,8 +72,17 @@ resource "google_access_context_manager_access_level" "basic" {
}
}
+ dynamic "vpc_network_sources" {
+ for_each = c.value.vpc_subnets
+ iterator = vpc
+ content {
+ vpc_subnetwork {
+ network = vpc.key
+ vpc_ip_subnetworks = vpc.value
+ }
+ }
+ }
}
}
-
}
}
diff --git a/modules/vpc-sc/factory.tf b/modules/vpc-sc/factory.tf
index 4e9cdc740..eb4608364 100644
--- a/modules/vpc-sc/factory.tf
+++ b/modules/vpc-sc/factory.tf
@@ -1,5 +1,5 @@
/**
- * 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.
@@ -41,6 +41,7 @@ locals {
negate = null
regions = []
required_access_levels = []
+ vpc_subnets = {}
}, c)
]
}
@@ -48,10 +49,13 @@ locals {
egress_policies = {
for k, v in local._data.egress_policies : k => {
from = merge({
+ access_levels = []
identity_type = null
identities = []
+ resources = []
}, try(v.from, {}))
to = {
+ external_resources = try(v.to.external_resources, null)
operations = [
for o in try(v.to.operations, []) : merge({
method_selectors = []
@@ -59,8 +63,7 @@ locals {
service_name = null
}, o)
]
- resources = try(v.to.resources, [])
- resource_type_external = try(v.to.resource_type_external, false)
+ resources = try(v.to.resources, [])
}
}
}
diff --git a/modules/vpc-sc/schemas/access-level.schema.json b/modules/vpc-sc/schemas/access-level.schema.json
index 7945559cd..664f84bf3 100644
--- a/modules/vpc-sc/schemas/access-level.schema.json
+++ b/modules/vpc-sc/schemas/access-level.schema.json
@@ -92,6 +92,18 @@
"items": {
"type": "string"
}
+ },
+ "vpc_subnets": {
+ "type": "object",
+ "additionalProperties": false,
+ "patternProperties": {
+ "^//compute.googleapis.com/projects/[^/]+/global/networks/[^/]+$": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
}
}
}
diff --git a/modules/vpc-sc/schemas/egress-policy.schema.json b/modules/vpc-sc/schemas/egress-policy.schema.json
index c8e0931bd..316c9d588 100644
--- a/modules/vpc-sc/schemas/egress-policy.schema.json
+++ b/modules/vpc-sc/schemas/egress-policy.schema.json
@@ -12,6 +12,12 @@
"type": "object",
"additionalProperties": false,
"properties": {
+ "access_levels": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
"identity_type": {
"enum": [
"IDENTITY_TYPE_UNSPECIFIED",
@@ -27,6 +33,12 @@
"type": "string",
"pattern": "^(?:serviceAccount:|user:|group:|principal:)"
}
+ },
+ "resources": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
}
}
},
@@ -34,6 +46,12 @@
"type": "object",
"additionalProperties": false,
"properties": {
+ "external_resources": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
"operations": {
"type": "array",
"items": {
@@ -66,9 +84,6 @@
"items": {
"type": "string"
}
- },
- "resource_type_external": {
- "type": "boolean"
}
}
}
diff --git a/modules/vpc-sc/service-perimeters-regular.tf b/modules/vpc-sc/service-perimeters-regular.tf
index 3bc1744ff..970d49bc3 100644
--- a/modules/vpc-sc/service-perimeters-regular.tf
+++ b/modules/vpc-sc/service-perimeters-regular.tf
@@ -1,5 +1,5 @@
/**
- * Copyright 2022 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.
@@ -59,12 +59,35 @@ resource "google_access_context_manager_service_perimeter" "regular" {
content {
identity_type = policy.value.from.identity_type
identities = policy.value.from.identities
+ source_restriction = (
+ length(policy.value.from.access_levels) > 0 || length(policy.value.from.resources) > 0
+ ? "SOURCE_RESTRICTION_ENABLED"
+ : "SOURCE_RESTRICTION_DISABLED"
+ )
+ dynamic "sources" {
+ for_each = policy.value.from.access_levels
+ iterator = access_level
+ content {
+ access_level = try(
+ google_access_context_manager_access_level.basic[access_level.value].id,
+ access_level.value
+ )
+ }
+ }
+ dynamic "sources" {
+ for_each = policy.value.from.resources
+ iterator = resource
+ content {
+ resource = resource.value
+ }
+ }
}
}
dynamic "egress_to" {
for_each = policy.value.to == null ? [] : [""]
content {
- resources = policy.value.to.resources
+ external_resources = policy.value.to.external_resources
+ resources = policy.value.to.resources
dynamic "operations" {
for_each = toset(policy.value.to.operations)
iterator = o
@@ -183,12 +206,35 @@ resource "google_access_context_manager_service_perimeter" "regular" {
content {
identity_type = policy.value.from.identity_type
identities = policy.value.from.identities
+ source_restriction = (
+ length(policy.value.from.access_levels) > 0 || length(policy.value.from.resources) > 0
+ ? "SOURCE_RESTRICTION_ENABLED"
+ : "SOURCE_RESTRICTION_DISABLED"
+ )
+ dynamic "sources" {
+ for_each = policy.value.from.access_levels
+ iterator = access_level
+ content {
+ access_level = try(
+ google_access_context_manager_access_level.basic[access_level.value].id,
+ access_level.value
+ )
+ }
+ }
+ dynamic "sources" {
+ for_each = policy.value.from.resources
+ iterator = resource
+ content {
+ resource = resource.value
+ }
+ }
}
}
dynamic "egress_to" {
for_each = policy.value.to == null ? [] : [""]
content {
- resources = policy.value.to.resources
+ external_resources = policy.value.to.external_resources
+ resources = policy.value.to.resources
dynamic "operations" {
for_each = toset(policy.value.to.operations)
iterator = o
diff --git a/modules/vpc-sc/variables.tf b/modules/vpc-sc/variables.tf
index 7a1304921..baf08fadb 100644
--- a/modules/vpc-sc/variables.tf
+++ b/modules/vpc-sc/variables.tf
@@ -1,5 +1,5 @@
/**
- * Copyright 2022 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.
@@ -36,6 +36,7 @@ variable "access_levels" {
negate = optional(bool)
regions = optional(list(string), [])
required_access_levels = optional(list(string), [])
+ vpc_subnets = optional(map(list(string)), {})
})), [])
description = optional(string)
}))
@@ -82,17 +83,19 @@ variable "egress_policies" {
description = "Egress policy definitions that can be referenced in perimeters."
type = map(object({
from = object({
+ access_levels = optional(list(string), [])
identity_type = optional(string)
identities = optional(list(string))
+ resources = optional(list(string), [])
})
to = object({
+ external_resources = optional(list(string))
operations = optional(list(object({
method_selectors = optional(list(string))
permission_selectors = optional(list(string))
service_name = string
})), [])
- resources = optional(list(string))
- resource_type_external = optional(bool, false)
+ resources = optional(list(string))
})
}))
default = {}
@@ -110,10 +113,10 @@ variable "egress_policies" {
validation {
condition = alltrue([
for k, v in var.egress_policies : v.from.identities == null ? true : alltrue([
- for identity in v.from.identities : can(regex("^(?:serviceAccount:|user:|group:|principal:)", identity))
+ for identity in v.from.identities : can(regex("^(?:serviceAccount:|user:|group:|principal:|principalSet:)", identity))
])
])
- error_message = "Invalid `from.identity`. It needs to start with on of the prefixes: 'serviceAccount:', 'user:', 'group:' or 'principal:'."
+ error_message = "Invalid `from.identity`. It needs to start with on of the prefixes: 'serviceAccount:', 'user:', 'group:', 'principal:' or 'principalSet:."
}
}
@@ -198,10 +201,10 @@ variable "ingress_policies" {
validation {
condition = alltrue([
for k, v in var.ingress_policies : v.from.identities == null ? true : alltrue([
- for identity in v.from.identities : can(regex("^(?:serviceAccount:|user:|group:|principal:)", identity))
+ for identity in v.from.identities : can(regex("^(?:serviceAccount:|user:|group:|principal:|principalSet:)", identity))
])
])
- error_message = "Invalid `from.identity`. It needs to start with on of the prefixes: 'serviceAccount:', 'user:', 'group:' or 'principal:'."
+ error_message = "Invalid `from.identity`. It needs to start with on of the prefixes: 'serviceAccount:', 'user:', 'group:', 'principal:', 'principalSet:'."
}
}
diff --git a/tests/fast/stages/s1_vpcsc/simple.yaml b/tests/fast/stages/s1_vpcsc/simple.yaml
index 3350d3dc9..819c2b965 100644
--- a/tests/fast/stages/s1_vpcsc/simple.yaml
+++ b/tests/fast/stages/s1_vpcsc/simple.yaml
@@ -1,4 +1,4 @@
-# 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.
@@ -74,7 +74,7 @@ values:
- identities:
- user:user@fast.example.com
identity_type: null
- source_restriction: null
+ source_restriction: SOURCE_RESTRICTION_DISABLED
sources: []
egress_to:
- external_resources: null
diff --git a/tests/modules/vpc_sc/examples/factory.yaml b/tests/modules/vpc_sc/examples/factory.yaml
index 007eebf9c..6159adcd6 100644
--- a/tests/modules/vpc_sc/examples/factory.yaml
+++ b/tests/modules/vpc_sc/examples/factory.yaml
@@ -1,4 +1,4 @@
-# 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.
@@ -62,7 +62,7 @@ values:
- serviceAccount:bar@myproject.iam.gserviceaccount.com
- serviceAccount:foo@myproject.iam.gserviceaccount.com
identity_type: null
- source_restriction: null
+ source_restriction: 'SOURCE_RESTRICTION_DISABLED'
sources: []
egress_to:
- external_resources: null