501 lines
16 KiB
Markdown
501 lines
16 KiB
Markdown
# 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).
|
|
|
|
<!-- BEGIN TOC -->
|
|
- [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)
|
|
<!-- END TOC -->
|
|
|
|
## 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.
|
|
|
|
<!-- TFDOC OPTS files:1 -->
|
|
<!-- BEGIN TFDOC -->
|
|
## Files
|
|
|
|
| name | description | resources |
|
|
|---|---|---|
|
|
| [access-levels.tf](./access-levels.tf) | Access level resources. | <code>google_access_context_manager_access_level</code> |
|
|
| [factory.tf](./factory.tf) | None | |
|
|
| [iam.tf](./iam.tf) | IAM bindings | <code>google_access_context_manager_access_policy_iam_binding</code> · <code>google_access_context_manager_access_policy_iam_member</code> |
|
|
| [main.tf](./main.tf) | Module-level locals and resources. | <code>google_access_context_manager_access_policy</code> |
|
|
| [outputs.tf](./outputs.tf) | Module outputs. | |
|
|
| [perimeters-additive.tf](./perimeters-additive.tf) | Regular service perimeter resources which ignore resource changes. | <code>google_access_context_manager_service_perimeter</code> |
|
|
| [perimeters.tf](./perimeters.tf) | Regular service perimeter resources. | <code>google_access_context_manager_service_perimeter</code> |
|
|
| [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. | <code>string</code> | ✓ | |
|
|
| [access_levels](variables.tf#L17) | Access level definitions. | <code>map(object({…}))</code> | | <code>{}</code> |
|
|
| [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. | <code>object({…})</code> | | <code>null</code> |
|
|
| [context](variables.tf#L83) | External context used in replacements. | <code>object({…})</code> | | <code>{}</code> |
|
|
| [egress_policies](variables.tf#L98) | Egress policy definitions that can be referenced in perimeters. | <code>map(object({…}))</code> | | <code>{}</code> |
|
|
| [factories_config](variables.tf#L141) | Paths to folders that enable factory functionality. | <code>object({…})</code> | | <code>{}</code> |
|
|
| [iam](variables.tf#L153) | IAM bindings in {ROLE => [MEMBERS]} format. | <code>map(list(string))</code> | | <code>{}</code> |
|
|
| [iam_bindings](variables.tf#L159) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | <code>map(object({…}))</code> | | <code>{}</code> |
|
|
| [iam_bindings_additive](variables.tf#L174) | Individual additive IAM bindings. Keys are arbitrary. | <code>map(object({…}))</code> | | <code>{}</code> |
|
|
| [ingress_policies](variables.tf#L189) | Ingress policy definitions that can be referenced in perimeters. | <code>map(object({…}))</code> | | <code>{}</code> |
|
|
| [perimeters](variables.tf#L231) | Regular service perimeters. | <code>map(object({…}))</code> | | <code>{}</code> |
|
|
| [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. | <code>string</code> | | <code>null</code> |
|
|
|
|
## 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. | |
|
|
<!-- END TFDOC -->
|
|
## 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
|
|
```
|