VPC Service Controls
This stage sets up VPC Service Controls (VPC-SC) for the whole organization and is a thing FAST-compliant wrapper on the VPC-SC module, with some minimal defaults.
- Design overview and choices
- Context expansion
- How to run this stage
- Customizations
- Notes
- Files
- Variables
- Outputs
Design overview and choices
The approach to VPC-SC design implemented in this stage aims at providing the simplest possible configuration that can be set to enforced mode, so as to provide immediate protection while minimizing operational complexity (which can be very high for VPC-SC).
Single perimeter with built-in extensibility
This stage uses a single VPC-SC perimeter by default, which is enough to provide protection against data exfiltration and use of credentials from outside of established boundaries, while minimizing operational toil.
The perimeter is set to dry-run mode by default, but the suggestion is to switch to enforced mode immediately after defining the initial set of access level and ingress/egress policies. This prevents the common situation where a complex design is deployed in dry-run mode, and then never enforced as the burden of addressing all violations is too high. A simpler design like the one presented here that employs very coarse access levels can be enforced quickly, and then refined iteratively as operations are streamlined and familiarity with VPC-SC quirks increases.
The stage is designed to allow defining additional perimeters via the perimeters variable, with a few caveats:
- there's no support for perimeter bridges, if those are needed they need to be integrated via code (which is easy enough to do anyway)
- only one set of discovered resources is supported and made available via the
$resource_sets:discovered_projectscontext expansion - the factory files for access levels and ingress/egress policies use the same folder regardless of perimeter, but their inclusion in a perimeter is controlled by the perimeter-level
access_levels,ingress_policies, andegress_policiesattributes.
Factories for VPC-SC configuration
Restricted services, access levels, ingress and egress policies and perimeters can all be configured via YAML-based files, which allow intuitive editing and minimize the complexity of running operations.
The default setup only contains a single access level and an initial list of restricted services in the datasets/classic/access-levels folder and the datasets/classic/restricted-services.yaml file.
To configure ingress and egress policies simply add ingress_policies and/or egress_policies folders under data, or point the factories to your own folders by changing the factories_config variable values. For more details on how the factories_config interface works, refer to the documentation for stage 0.
Examples on how to write access level and policy YAML files are provided further down in this document.
Default geo-based access level
The way we set up the perimeter for broad access is via a single geo-based access level, which is configured to allow access from one or more countries and deny all other traffic coming from outside the perimeter.
The datasets/classic/access-levels/geo.yaml file serves as an example and should be edited to contain the countries you need (or replaced/removed for more granular configuration).
conditions:
- regions:
# replace the following country codes with those you need
- ES
- IT
More access levels can be of course added to better tailor the configuration to specific needs. Some use cases are addressed further down in this document.
Ingress policy for organization-level log sinks
An ingress policy that allows ingress to the perimeter for identities used in organization-level sinks is automatically created, but needs to be explicitly referenced in the perimeter via the $identity_sets:logging_identities context expansion.
This only supports sinks defined in the bootstrap stage, but it can easily be used as a reference for different, specific needs (or replaced with a policy leveraging Asset Inventory for automatic inclusion of folder-level sinks).
Perimeter membership
The set of resources protected by each perimeter can be defined in two main ways:
- central and authoritative, where protected resources are only defined in this stage
- delegated and additive, where perimeter is defined in this stage and resources are added separately (e.g. by a project factory)
The first approach is more secure as it does not require granting editing permission to other actors, but it's also operationally heavier as it requires adding projects to the perimeter right after creation, before many operations can be run. For example, Shared VPC attachment for a service project cannot happen until the project is in the same perimeter as its host project. The main advantage of this approach is being able to leverage the resource discovery features provided by this stage.
The second approach is more flexible, but requires delegating a measure of control over perimeters to other actors, and losing control over perimeter membership which stops being enforced by Terraform.
When using the second approach, after applying this stage, provide perimeter information in your defaults.yaml file, for example:
projects:
overrides:
vpc_sc:
perimeter_name: accessPolicies/12345/servicePerimeters/default
And then apply 0-org-setup stage again. For later stages (such as networking, project factory), add the perimeter in a similar way, but there you can use context to provide perimeter:
projects:
overrides:
vpc_sc:
perimeter_name: default
Caution
Do not add any resources to the perimeter in this stage when using the second approach. Any resources added in this stage will not be properly removed from perimeter, if the
terraform applyis also changing the perimeter definition.
Resource discovery
If the first approach is desired in combination with resource discovery, you can simply tweak exclusions via the resource_discovery variable as the feature is enabled by default.
Discovered resources are made available via the $resource_sets:discovered_projects context expansion, which is already part of the definition of the default perimeter.
This approach is suitable for simple requirements, and provided out of the box in this stage's default configuration.
Manual resource membership
When resource discovery is not used, resources can be added to perimeters via a combination of:
- explicit project numbers inclusion
- individual project expansion via the
$project_numbers:xxxcontext namespace - set-based project expansion via the
$resource_sets:xxxcontext namespace
A selected number of auto-generated context expansions are generated by this stage, and described in the Context section below.This is what each of three methods above looks like in a perimeter definition.
spec:
resources:
# explicit reference
- projects/123456
# individual expansion
- $project_numbers:iac-0
# set expansion
- $resource_sets:org_setup_projects
When partial control is delegated to external actors, the perimeter needs to be configured to ignore changes to member resources so as not to trigger permadiffs between different stages. This is achieved via the ignore_resource_changes perimeter attribute.
ignore_resource_changes: true
use_explicit_dry_run_spec: true
spec:
# spec definition
Enforced vs dry-run perimeters
As mentioned above, the default configuration uses a single perimeter configured in dry-run mode. A dry-run mode perimeter has the following format.
# datasets/xxx/perimeters/myperimeter.yaml
use_explicit_dry_run_spec: true
spec:
# perimeter definition here
A perimeter in enforced mode uses status instead of spec.
# datasets/xxx/perimeters/myperimeter.yaml
use_explicit_dry_run_spec: true
status:
# perimeter definition here
If the dry-run and enforced configurations are different, define both explicitly in separate spec and status blocks, and set the use_explicit_dry_run_spec to true.
Context expansion
In much the same way as other FAST stages and underlying modules, context expansion is supported here in two ways:
- static values can be defined in the stage's
defaults.yamlfile orcontextvariable - specific values derived by other FAST stages are automatically added to the relevant context namespaces
These are the available context namespaces in this stage.
| context | type |
|---|---|
iam_principals |
map(string) |
identity_sets |
map(map(string)) |
project_numbers |
map(string) |
resource_sets |
map(map(string)) |
service_sets |
map(map(string)) |
storage_buckets |
map(string) |
The following sub-sections illustrate how both work.
Static contexts
Static contexts can be defined via the defaults.yaml file or the context variable, and are internally merged together and with the stage's built-in contexts.
This is an example of defining a context via defaults.yaml.
# datasets/xxx/defaults.yaml
context:
iam_principals:
me: user:foo@example.com
resource_sets:
my_projects:
- projects/12345
- projects/67890
These can then be reused in YAML definitions like shown below.
# datasets/xxx/perimeters/myperimeter.yaml
use_explicit_dry_run_spec: true
status:
resources:
- $resource_sets:my_projects
# datasets/xxx/access-levels/identity_me.yaml
conditions:
- members:
- $iam_principals:me
Stage-generated contexts
When this stage is used as part of a FAST installation, it consumes data made available from other stages and makes it available as context expansions. The resource discovery feature when enabled also add its own context expansion.
$iam_principals:service_accounts/xxx
service accounts created in the 0-org-setup stage$identity_sets:logging_identities
set of identities used by the log sinks created in the 0-org-setup stage$project_numbers:xxx
project numbers for projects created in the 0-org-setup stage$resource_sets:discovered_projects
set of projects numbers for discovered projects (when discovery is enabled)$resource_sets:org_setup_projects
set of projects numbers for projects created in the 0-org-setup stage$service_sets:restricted_services
set of services defined in the services factory file$storage_buckets:xxx
storage buckets created in the 0-org-setup stage
How to run this stage
This stage is meant to be executed after the bootstrap stage has run, as it leverages the automation service account and bucket created there. It does not depend from any other stage and no other stage requires it, so it can be run in any order or even skipped entirely.
It's of course possible to run this stage in isolation, but that's outside the scope of this document, and you would need to refer to the code for the previous stage for the environment requirements.
Provider and Terraform variables
As all other FAST stages, the mechanism used to pass variable values and pre-built provider files from one stage to the next is also leveraged here.
The commands to link or copy the provider and terraform variable files can be get from the fast-links.sh script in the FAST stages folder, passing it a single argument with the local output files folder or GCS output bucket. The following examples demonstrate both cases, and the resulting commands that then need to be copy/pasted and run.
../fast-links.sh ~/fast-config
# File linking commands for vpc service controls stage
# provider file
ln -s ~/fast-config/fast-test-00/providers/1-vpcsc-providers.tf ./
# input files from other stages
ln -s ~/fast-config/fast-test-00/tfvars/0-globals.auto.tfvars.json ./
ln -s ~/fast-config/fast-test-00/tfvars/0-org-setup.auto.tfvars.json ./
# conventional place for stage tfvars (manually created)
ln -s ~/fast-config/fast-test-00/1-vpcsc.auto.tfvars ./
# the outputs bucket name is in the stage 0 outputs and tfvars file
../fast-links.sh gs://xxx-prod-iac-core-outputs-0
# File linking commands for vpc service controls stage
# provider file
gcloud storage cp gs://xxx-prod-iac-core-outputs-0/providers/1-vpcsc-providers.tf ./
# input files from other stages
gcloud storage cp gs://xxx-prod-iac-core-outputs-0/tfvars/0-globals.auto.tfvars.json ./
gcloud storage cp gs://xxx-prod-iac-core-outputs-0/tfvars/0-org-setup.auto.tfvars.json ./
# conventional place for stage tfvars (manually created)
gcloud storage cp gs://xxx-prod-iac-core-outputs-0/1-vpcsc.auto.tfvars ./
Impersonating the automation service account
The preconfigured provider file uses impersonation to run with this stage's automation service account's credentials. The gcp-devops and organization-admins groups have the necessary IAM bindings in place to do that, so make sure the current user is a member of one of those groups.
Variable configuration
Variables in this stage -- like most other FAST stages -- are broadly divided into three separate sets:
- variables which refer to global values for the whole organization (org id, billing account id, prefix, etc.), which are pre-populated via the
0-globals.auto.tfvars.jsonfile linked or copied above - variables which refer to resources managed by previous stages, which are prepopulated here via the
0-org-setup.auto.tfvars.jsonfile linked or copied above - and finally variables that optionally control this stage's behaviour and customizations, that you are expected to configure in a custom
terraform.tfvarsfile
The latter set is explained in the Customization sections below, and the full list can be found in the Variables table at the bottom of this document.
Note that the outputs_location variable is disabled by default, you need to explicitly set it in your terraform.tfvars file if you want output files to be generated by this stage. This is an example of doing it:
outputs_location = "~/fast-config"
Running the stage
Once provider and variable values are in place and the correct user is configured, the stage can be run:
terraform init
terraform apply
Customizations
The stage is a thin wrapper that implements a single-perimeter design via the vpc-sc module, and most of the customizations available in the module are also available here. A few examples are provided below for ease of reference.
Access policy
The stage creates the org-level access policy by default. A pre-existing policy can instead be used by populating the access_policy variable with the policy id. In tenant-level mode this is done automatically by the FAST input/output files mechanism.
Perimeters
The default perimeter is defined in the perimeters/default.yaml file within each dataset. The file can be easily customized and used as a basis to create additional perimeters.
# yaml-language-server: $schema=../../../schemas/perimeter.schema.json
use_explicit_dry_run_spec: true
# default to dr-run
spec:
access_levels:
- geo
# include discovered projects
resources:
- $resource_sets:discovered_projects
ingress_policies:
- fast-org-log-sinks
# protect the full list of services
restricted_services:
- $service_sets:restricted_services
Access levels
Access levels can be defined via tfvars or factory files using the same mechanism in the underlying vpc-sc module.
These are example access levels defined via tfvars:
access_levels = {
a1 = {
conditions = [
{ members = ["user:user1@example.com"] }
]
}
a2 = {
combining_function = "OR"
conditions = [
{ regions = ["IT", "FR"] },
{ ip_subnetworks = ["101.101.101.0/24"] }
]
}
}
And the same defined instead via factory files.
# datasets/classic/access-levels/a1.yaml
conditions:
- members:
- "user:user1@example.com"
# datasets/classic/access-levels/a2.yaml
combining_function: OR
conditions:
- regions:
- ES
- IT
- ip_subnetworks:
- 8.8.0.0/16
Remember that in order for them to deployed, access levels need to be referenced by name in the perimeter definition.
Ingress/egress policies
Like access levels, ingress and egress policies can be defined via tfvars or factory files using the same mechanism in the underlying vpc-sc module.
This is an example ingress policy defined in yaml:
# datasets/classic/ingress-policies/sa-tf-test.yaml
from:
access_levels:
- "*"
identities:
- serviceAccount:test-tf@myproject.iam.gserviceaccount.com
to:
operations:
- service_name: compute.googleapis.com
method_selectors:
- ProjectsService.Get
- RegionsService.Get
resources:
- "*"
And this is an example egress policy:
# datasets/classic/egress-policies/gcs-sa-foo.yaml
from:
identities:
- serviceAccount:foo@myproject.iam.gserviceaccount.com
to:
operations:
- method_selectors:
- "*"
service_name: storage.googleapis.com
resources:
- projects/123456789
Remember that in order for them to deployed, ingress/egress policies need to be referenced by name in the perimeter definition.
Notes
Some references that might be useful in setting up this stage:
Files
| name | description | modules | resources |
|---|---|---|---|
| main.tf | Module-level locals and resources. | projects-data-source · vpc-sc |
|
| outputs.tf | Module outputs. | google_storage_bucket_object · local_file |
|
| variables-fast.tf | None | ||
| variables.tf | Module variables. |
Variables
| name | description | type | required | default | producer |
|---|---|---|---|---|---|
| organization | Organization details. | object({…}) |
✓ | 0-org-setup |
|
| access_levels | Access level definitions. | map(object({…})) |
{} |
||
| access_policy | Access policy id (used for tenant-level VPC-SC configurations). | number |
null |
||
| context | External context used in replacements. | object({…}) |
{} |
||
| egress_policies | Egress policy definitions that can be referenced in perimeters. | map(object({…})) |
{} |
||
| factories_config | Paths to folders that enable factory functionality. | object({…}) |
{} |
||
| iam_principals | Org-level IAM principals. | map(string) |
{} |
0-org-setup |
|
| ingress_policies | Ingress policy definitions that can be referenced in perimeters. | map(object({…})) |
{} |
||
| logging_sinks | Log sinks for the organization. | map(object({…})) |
{} |
0-org-setup |
|
| perimeters | Perimeter definitions. | map(object({…})) |
{} |
||
| project_ids | Project IDs. | map(string) |
{} |
0-org-setup |
|
| project_numbers | Project numbers. | map(number) |
{} |
0-org-setup |
|
| resource_discovery | Automatic discovery of perimeter projects. | object({…}) |
{} |
||
| root_node | Root node for the hierarchy, if running in tenant mode. | string |
null |
0-org-setup |
|
| service_accounts | Org-level service accounts. | map(string) |
{} |
0-org-setup |
|
| storage_buckets | Storage buckets created in the bootstrap stage. | map(string) |
{} |
0-org-setup |
Outputs
| name | description | sensitive | consumers |
|---|---|---|---|
| tfvars | Terraform variable files for the following stages. | ✓ | |
| vpc_sc_perimeter_default | Raw default perimeter resource. | ✓ |