stage 02-security

This commit is contained in:
Ludovico Magnocavallo 2022-01-17 10:30:26 +01:00
parent ea2d739456
commit 34e845fcdd
9 changed files with 977 additions and 0 deletions

View File

@ -0,0 +1,319 @@
# Shared security resources
This stage sets up security resources and configurations which impact the whole org, or are shared across the hierarchy to other projects and teams.
Its design is fairly general, and provides a reference example for [Cloud KMS](https://cloud.google.com/security-key-management) and a [VPC Service Controls](https://cloud.google.com/vpc-service-controls) configuration that sets up three perimeters (landing, development, production), the related bridge perimeters, and provides variables to configure their resources, access levels, and directional policies.
Expanding this stage to include other security-related services like Secret Manager, is fairly simple by using the provided implementation for Cloud KMS, and leveraging the broad permissions on the top-level Security folder of the automation service account used.
The following diagram illustrates the high level design of created resources, and a schema of the VPC SC design, which can be adapted to specific requirements via variables:
![Security diagram](diagram.png)
## Design overview and choices
Project-level security resources are grouped into two separate projects, one per environment. This matches requirements we frequently observe in real life, and provides enough separation without needlessly complicating operations.
Cloud KMS is configured and designed mainly to encrypt GCP resources with a [Customer-managed encryption key](https://cloud.google.com/kms/docs/cmek) but it may be used to create cryptokeys used to [encrypt application data](https://cloud.google.com/kms/docs/encrypting-application-data) too.
IAM for management-related operations is already assigned at the folder level to the security team by the previous stage, but more granularity can of course be added here at the project level, to grant control of separate services across environments to different actors.
### Cloud KMS
A reference Cloud KMS implementation is part of this stage, to provide a simple way of managing centralized keys, that are then shared and consumed widely across the org to enable customer-managed encryption. The implementation is also easy to clone and modify to support other services like Secret Manager.
The Cloud KMS configuration allows defining keys by name (typically matching the downstream service that uses them) in different locations, either based on a common default or a per-key setting, and then takes care internally of provisioning the relevant keyrings and creating keys in the appropriate location.
IAM roles on keys can of course be configured at the logical level for all locations where a logical key is created, and their management can also be delegated via [delegated role grants](https://cloud.google.com/iam/docs/setting-limits-on-granting-roles) exposed through a simple variable, to allow other identities to set IAM policies on keys. This is particularly useful in setups like project factories, making it possible to configure IAM bindings during project creation for team groups or service agent accounts (compute, storage, etc.).
### VPC Service Controls
This stage also provisions the VPC Service Controls configuration on demand for the whole organization, implementing the straightforward design illustrated above:
- one perimeter for each environment
- one perimeter for centralized services and the landing VPC
- bridge perimeters to connect the landing perimeter to each environment
The VPC SC configuration is set to dry-run mode, but switching to enforced mode is a simple operation involving the modification of a few lines of code highlighted by ad-hoc comments. Variables are designed to enable easy centralized management of VPC Service Controls, including access levels and [ingress/egress rules](https://cloud.google.com/vpc-service-controls/docs/ingress-egress-rules) as described below.
Some care needs to be taken with project membership in perimeters, which can only be implemented here instead of being delegated (all or partially) to different stages, until the [Google Provider feature request](https://github.com/hashicorp/terraform-provider-google/issues/7270) that allows using project-level association for both enforced and dry-run modes is implemented.
## How to run this stage
This stage is meant to be executed after the [resouce management](../01-resman) stage has run, as it leverages the folder and automation resources created there. The relevant user groups must also exist, but that's one of the requirements for the previous stages too, so if you ran those successfully, you're good to go.
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 bootstrap stage for the actual roles needed.
Before running this stage, you need to make sure you have the correct credentials and permissions, and localize variables by assigning values that match your configuration.
### Providers configuration
The default way of making sure you have the right permissions, is to use the identity of the service account pre-created for this stage during bootstrap, and that you are a member of the group that can impersonate it via provider-level configuration (`gcp-devops` or `organization-admins`).
To simplify setup, the previous stage pre-configures a valid providers file in its output, and optionally writes it to a local file if the `outputs_location` variable is set to a valid path.
If you have set a valid value for `outputs_location` in the resource management stage, simply link the relevant `providers.tf` file from this stage's folder in the path you specified:
```bash
# `outputs_location` is set to `../../configs/example`
ln -s ../../configs/example/02-security/providers.tf
```
If you have not configured `outputs_location` in resource management, you can derive the providers file from that stage's outputs:
```bash
cd ../01-resman
terraform output -json providers | jq -r '.["02-security"]' \
> ../02-security/providers.tf
```
### Variable configuration
There are two broad sets of variables you will need to fill in:
- variables shared by other stages (org id, billing account id, etc.), or derived from a resource managed by a different stage (folder id, automation project id, etc.)
- variables specific to resources managed by this stage
To avoid the tedious job of filling in the first group of variable with values derived from other stages' outputs, the same mechanism used above for the provider configuration can be used to leverage pre-configured `.tfvars` files.
If you configured a valid path for `outputs_location` in the previous stages, simply link the relevant `terraform-*.auto.tfvars.json` files from this stage's output folder (under the path you specified), where the `*` above is set to the name of the stage that produced it. For this stage, two `.tfvars` files are avalaible:
```bash
# `outputs_location` is set to `../../configs/example`
ln -s ../../configs/example/02-security/terraform-bootstrap.auto.tfvars.json
ln -s ../../configs/example/02-security/terraform-resman.auto.tfvars.json
```
A second set of variables is specific to this stage, they are all optional so if you need to customize them, create an extra `terraform.tfvars` file.
Refer to the [Variables](#variables) table at the bottom of this document, for a full list of variables, their origin (e.g. a stage or specific to this one), and descriptions explaining their meaning. The sections below also describe some of the possible customizations.
Once done, you can run this stage:
```bash
terraform init
terraform apply
```
## Customizations
### KMS keys
Cloud KMS configuration is split in two variables:
- `kms_defaults` configures the locations and rotation period, used for keys that don't specifically configure them
- `kms_keys` configures the actual keys to create, and also allows configuring their IAM bindings and labels, and overriding locations and rotation period. When configuring locations for a key, please take into account limitations each cloud product may have.
The additional `kms_restricted_admins` variable allows granting `roles/cloudkms.admin` to specified principals, restricted via [delegated role grants](https://cloud.google.com/iam/docs/setting-limits-on-granting-roles) so that it only allows granting the roles needed for encryption/decryption on keys. This allows safe delegation of key management to subsequent Terraform stages like the Project Factory, for example to grant usage access on relevant keys to the service agent accounts for compute, storage, etc.
To support these scenarios, key IAM bindings are configured by default to be additive, so as to enable other stages or Terraform configuration to safely co-manage bindings on the same keys. If this is not desired, follow the comments in the `core-dev.tf` and `core-prod.tf` files to switch to using authoritative bindings on keys.
An example on how to configure keys:
```hcl
# terraform.tfvars
kms_defaults = {
locations = ["europe-west1", "europe-west3", "global"]
rotation_period = "7776000s"
}
kms_keys = {
compute = {
iam = {
"roles/cloudkms.cryptoKeyEncrypterDecrypter" = [
"user:user1@example.com"
]
}
labels = { service = "compute" }
locations = null
rotation_period = null
}
storage = {
iam = null
labels = { service = "compute" }
locations = ["europe"]
rotation_period = null
}
}
```
The script will create one keyring for each location specified and create necessarily keys on each keyring.
### VPC Service Controls configuration
A set of variables allows configuring the VPC SC perimeters described above:
- `vpc_sc_perimeter_projects` configures project membership in the three regular perimeters
- `vpc_sc_access_levels` configures access levels, which can then be associated to perimeters by key using the `vpc_sc_perimeter_access_levels`
- `vpc_sc_egress_policies` configures directional egress policies, which can then be associated to perimeters by key using the `vpc_sc_perimeter_egress_policies`
- `vpc_sc_ingress_policies` configures directional ingress policies, which can then be associated to perimeters by key using the `vpc_sc_perimeter_ingress_policies`
This allows configuring VPC SC in a fairly flexible and concise way, without having to repeat similar definitions. Bridges perimeters configuration will be computed automatically to allow communication between regular perimeters: `landing <-> prod` and `landing <-> dev`.
#### Dry-run vs enforced
The VPC SC configuration is set up by default in dry-run mode, to allow easy experimentation, and detecting violations before enforcement. Once everything has been set up correctly, switching to enforced mode needs to be done in code, by swapping the contents of the `spec` and `status` attributes for perimeters in the `vpc-sc.tf` file. The effort involved is minimal (2 lines of code per perimeter), and comments help with identifying the correct lines.
#### Perimeter resources
Project are added to perimeters via the `vpc_sc_perimeter_projects`, and that's currently the only way of doing it without generating permadiffs or conflicts, since the only Terraform resource that works for both enforced and dry-run mode is authoritative (perimeter resources need to be managed in a single place).
Once the Google Terraform Provider [implements support for dry-run mode in the additive resource](https://github.com/hashicorp/terraform-provider-google/issues/7270), it will be possible to concurrently manage perimeter resources both here and in subsequent Terraform configurations, for example to allow the Project Factory to add a project to a perimeter during the creation process.
Bridge perimeters are auto-populated with all projects configured for the connected regular perimeters.
An example of adding projects to perimeters using project numbers:
```hcl
# terraform.tfvars
vpc_sc_perimeter_projects = {
dev = ["projects/12345678", "projects/12345679"]
landing = ["projects/12345670"]
prod = ["projects/12345674", "projects/12345675"]
}
```
#### Access levels
Below an example for an access level that allows unconditional ingress from a set of IP CIDR ranges can be configured once, and enabled on selected perimeters:
```hcl
# terraform.tfvars
vpc_sc_access_levels = {
on-prem = {
conditions = [{
ip_subnetworks = ["10.0.0.0/24", "10.0.0.1/24"],
combining_function = null, members = null, negate = null,
regions = null, required_access_levels = null
}]
}
}
vpc_sc_perimeter_access_levels = {
dev = null
landing = ["on-prem"]
prod = ["on-prem"]
}
```
#### Ingress and Egress policies
The same applies to Ingress and Egress policies, as shown in the examples below that reference the automation service account for this stage.
Below you can find an ingress policy configuration that allows applying Terraform from outside the perimeter, useful when bringing up this stage to avoid generating violations:
```hcl
# terraform.tfvars
vpc_sc_ingress_policies = {
iac = {
ingress_from = {
identities = [
"serviceAccount:xxx-prod-resman-security-0@xxx-prod-iac-core-0.iam.gserviceaccount.com"
]
source_access_levels = ["*"]
identity_type = null
source_resources = null
}
ingress_to = {
operations = [{ method_selectors = [], service_name = "*" }]
resources = ["*"]
}
}
}
vpc_sc_perimeter_ingress_policies = {
dev = ["iac"]
landing = ["iac"]
prod = ["iac"]
}
```
Below you can find an egress policy that allows writing Terraform state to the automation bucket, useful once Terraform starts running inside the perimeter in a pipeline:
```hcl
# terraform.tfvars
vpc_sc_egress_policies = {
iac-gcs = {
egress_from = {
identity_type = null
identities = [
"serviceAccount:xxx-prod-resman-security-0@xxx-prod-iac-core-0.iam.gserviceaccount.com"
]
}
egress_to = {
operations = [{
method_selectors = ["*"], service_name = "storage.googleapis.com"
}]
resources = ["projects/123456782"]
}
}
}
vpc_sc_perimeter_ingress_policies = {
dev = ["iac-gcs"]
landing = ["iac-gcs"]
prod = ["iac-gcs"]
}
```
## Notes
Some references that might be useful in setting up this stage:
- [VPC SC CSCC requirements](https://cloud.google.com/security-command-center/docs/troubleshooting).
<!-- BEGIN TFDOC -->
## Files
| name | description | modules | resources |
|---|---|---|---|
| [core-dev.tf](./core-dev.tf) | None | <code>kms</code> · <code>project</code> | <code>google_project_iam_member</code> |
| [core-prod.tf](./core-prod.tf) | None | <code>kms</code> · <code>project</code> | <code>google_project_iam_member</code> |
| [main.tf](./main.tf) | Module-level locals and resources. | | |
| [outputs.tf](./outputs.tf) | Module outputs. | | <code>local_file</code> |
| [variables.tf](./variables.tf) | Module variables. | | |
| [vpc-sc.tf](./vpc-sc.tf) | None | <code>vpc-sc</code> | |
## Variables
| name | description | type | required | default | producer |
|---|---|:---:|:---:|:---:|:---:|
| billing_account_id | Billing account id. | <code>string</code> | ✓ | | <code>bootstrap</code> |
| folder_id | Folder to be used for the networking resources in folders/nnnn format. | <code>string</code> | ✓ | | <code>resman</code> |
| organization | Organization details. | <code title="object&#40;&#123;&#10; domain &#61; string&#10; id &#61; number&#10; customer_id &#61; string&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | ✓ | | <code>bootstrap</code> |
| prefix | Prefix used for resources that need unique names. | <code>string</code> | ✓ | | |
| groups | Group names to grant organization-level permissions. | <code>map&#40;string&#41;</code> | | <code title="&#123;&#10; gcp-billing-admins &#61; &#34;gcp-billing-admins&#34;,&#10; gcp-devops &#61; &#34;gcp-devops&#34;,&#10; gcp-network-admins &#61; &#34;gcp-network-admins&#34;&#10; gcp-organization-admins &#61; &#34;gcp-organization-admins&#34;&#10; gcp-security-admins &#61; &#34;gcp-security-admins&#34;&#10; gcp-support &#61; &#34;gcp-support&#34;&#10;&#125;">&#123;&#8230;&#125;</code> | <code>bootstrap</code> |
| kms_defaults | Defaults used for KMS keys. | <code title="object&#40;&#123;&#10; locations &#61; list&#40;string&#41;&#10; rotation_period &#61; string&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code title="&#123;&#10; locations &#61; &#91;&#34;europe&#34;, &#34;europe-west1&#34;, &#34;europe-west3&#34;, &#34;global&#34;&#93;&#10; rotation_period &#61; &#34;7776000s&#34;&#10;&#125;">&#123;&#8230;&#125;</code> | |
| kms_keys | KMS keys to create, keyed by name. Null attributes will be interpolated with defaults. | <code title="map&#40;object&#40;&#123;&#10; iam &#61; map&#40;list&#40;string&#41;&#41;&#10; labels &#61; map&#40;string&#41;&#10; locations &#61; list&#40;string&#41;&#10; rotation_period &#61; string&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> | |
| kms_restricted_admins | Map of environment => [identities] who can assign the encrypt/decrypt roles on keys. | <code>map&#40;list&#40;string&#41;&#41;</code> | | <code>&#123;&#125;</code> | |
| outputs_location | Path where providers, tfvars files, and lists for the following stages are written. Leave empty to disable. | <code>string</code> | | <code>null</code> | |
| vpc_sc_access_levels | VPC SC access level definitions. | <code title="map&#40;object&#40;&#123;&#10; combining_function &#61; string&#10; conditions &#61; list&#40;object&#40;&#123;&#10; ip_subnetworks &#61; list&#40;string&#41;&#10; members &#61; list&#40;string&#41;&#10; negate &#61; bool&#10; regions &#61; list&#40;string&#41;&#10; required_access_levels &#61; list&#40;string&#41;&#10; &#125;&#41;&#41;&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> | |
| vpc_sc_egress_policies | VPC SC egress policy defnitions. | <code title="map&#40;object&#40;&#123;&#10; egress_from &#61; object&#40;&#123;&#10; identity_type &#61; string&#10; identities &#61; list&#40;string&#41;&#10; &#125;&#41;&#10; egress_to &#61; object&#40;&#123;&#10; operations &#61; list&#40;object&#40;&#123;&#10; method_selectors &#61; list&#40;string&#41;&#10; service_name &#61; string&#10; &#125;&#41;&#41;&#10; resources &#61; list&#40;string&#41;&#10; &#125;&#41;&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> | |
| vpc_sc_ingress_policies | VPC SC ingress policy defnitions. | <code title="map&#40;object&#40;&#123;&#10; ingress_from &#61; object&#40;&#123;&#10; identity_type &#61; string&#10; identities &#61; list&#40;string&#41;&#10; source_access_levels &#61; list&#40;string&#41;&#10; source_resources &#61; list&#40;string&#41;&#10; &#125;&#41;&#10; ingress_to &#61; object&#40;&#123;&#10; operations &#61; list&#40;object&#40;&#123;&#10; method_selectors &#61; list&#40;string&#41;&#10; service_name &#61; string&#10; &#125;&#41;&#41;&#10; resources &#61; list&#40;string&#41;&#10; &#125;&#41;&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> | |
| vpc_sc_perimeter_access_levels | VPC SC perimeter access_levels. | <code title="object&#40;&#123;&#10; dev &#61; list&#40;string&#41;&#10; landing &#61; list&#40;string&#41;&#10; prod &#61; list&#40;string&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>null</code> | |
| vpc_sc_perimeter_egress_policies | VPC SC egress policies per perimeter, values reference keys defined in the `vpc_sc_ingress_policies` variable. | <code title="object&#40;&#123;&#10; dev &#61; list&#40;string&#41;&#10; landing &#61; list&#40;string&#41;&#10; prod &#61; list&#40;string&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>null</code> | |
| vpc_sc_perimeter_ingress_policies | VPC SC ingress policies per perimeter, values reference keys defined in the `vpc_sc_ingress_policies` variable. | <code title="object&#40;&#123;&#10; dev &#61; list&#40;string&#41;&#10; landing &#61; list&#40;string&#41;&#10; prod &#61; list&#40;string&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>null</code> | |
| vpc_sc_perimeter_projects | VPC SC perimeter resources. | <code title="object&#40;&#123;&#10; dev &#61; list&#40;string&#41;&#10; landing &#61; list&#40;string&#41;&#10; prod &#61; list&#40;string&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>null</code> | |
## Outputs
| name | description | sensitive | consumers |
|---|---|:---:|---|
| stage_perimeter_projects | Security project numbers. They can be added to perimeter resources. | | |
<!-- END TFDOC -->

View File

@ -0,0 +1,64 @@
/**
* Copyright 2022 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.
*/
module "dev-sec-project" {
source = "github.com/terraform-google-modules/cloud-foundation-fabric//modules/project?ref=v12.0.0"
name = "dev-sec-core-0"
parent = var.folder_id
prefix = var.prefix
billing_account = var.billing_account_id
iam = {
"roles/cloudkms.viewer" = try(var.kms_restricted_admins.dev, [])
}
labels = { environment = "dev", team = "security" }
services = local.project_services
}
module "dev-sec-kms" {
for_each = toset(local.kms_locations)
source = "github.com/terraform-google-modules/cloud-foundation-fabric//modules/kms?ref=v12.0.0"
project_id = module.dev-sec-project.project_id
keyring = {
location = each.key
name = "dev-${each.key}"
}
# rename to `key_iam` to switch to authoritative bindings
key_iam_additive = {
for k, v in local.kms_locations_keys[each.key] : k => v.iam
}
keys = local.kms_locations_keys[each.key]
}
# TODO(ludo): add support for conditions to Fabric modules
resource "google_project_iam_member" "dev_key_admin_delegated" {
for_each = toset(try(var.kms_restricted_admins.dev, []))
project = module.dev-sec-project.project_id
role = "roles/cloudkms.admin"
member = each.key
condition {
title = "kms_sa_delegated_grants"
description = "Automation service account delegated grants"
expression = format(
"api.getAttribute('iam.googleapis.com/modifiedGrantsByRole', []).hasOnly([%s])",
join(",", formatlist("'%s'", [
"roles/cloudkms.cryptoKeyEncrypterDecrypter",
"roles/cloudkms.cryptoKeyEncrypterDecrypterViaDelegation"
]))
)
}
depends_on = [module.dev-sec-project]
}

View File

@ -0,0 +1,64 @@
/**
* Copyright 2022 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.
*/
module "prod-sec-project" {
source = "github.com/terraform-google-modules/cloud-foundation-fabric//modules/project?ref=v12.0.0"
name = "prod-sec-core-0"
parent = var.folder_id
prefix = var.prefix
billing_account = var.billing_account_id
iam = {
"roles/cloudkms.viewer" = try(var.kms_restricted_admins.prod, [])
}
labels = { environment = "prod", team = "security" }
services = local.project_services
}
module "prod-sec-kms" {
for_each = toset(local.kms_locations)
source = "github.com/terraform-google-modules/cloud-foundation-fabric//modules/kms?ref=v12.0.0"
project_id = module.prod-sec-project.project_id
keyring = {
location = each.key
name = "prod-${each.key}"
}
# rename to `key_iam` to switch to authoritative bindings
key_iam_additive = {
for k, v in local.kms_locations_keys[each.key] : k => v.iam
}
keys = local.kms_locations_keys[each.key]
}
# TODO(ludo): add support for conditions to Fabric modules
resource "google_project_iam_member" "prod_key_admin_delegated" {
for_each = toset(try(var.kms_restricted_admins.prod, []))
project = module.prod-sec-project.project_id
role = "roles/cloudkms.admin"
member = each.key
condition {
title = "kms_sa_delegated_grants"
description = "Automation service account delegated grants"
expression = format(
"api.getAttribute('iam.googleapis.com/modifiedGrantsByRole', []).hasOnly([%s])",
join(",", formatlist("'%s'", [
"roles/cloudkms.cryptoKeyEncrypterDecrypter",
"roles/cloudkms.cryptoKeyEncrypterDecrypterViaDelegation"
]))
)
}
depends_on = [module.prod-sec-project]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

View File

@ -0,0 +1,47 @@
/**
* Copyright 2022 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.
*/
locals {
kms_keys = {
for k, v in var.kms_keys : k => {
iam = coalesce(v.iam, {})
labels = coalesce(v.labels, {})
locations = (
v.locations == null
? var.kms_defaults.locations
: v.locations
)
rotation_period = (
v.rotation_period == null
? var.kms_defaults.rotation_period
: v.rotation_period
)
}
}
kms_locations = distinct(flatten([
for k, v in local.kms_keys : v.locations
]))
kms_locations_keys = {
for loc in local.kms_locations : loc => {
for k, v in local.kms_keys : k => v if contains(v.locations, loc)
}
}
project_services = [
"cloudkms.googleapis.com",
"secretmanager.googleapis.com",
"stackdriver.googleapis.com"
]
}

View File

@ -0,0 +1,43 @@
/**
* Copyright 2022 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.
*/
# optionally generate files for subsequent stages
resource "local_file" "dev_sec_kms" {
for_each = var.outputs_location == null ? {} : { 1 = 1 }
filename = "${var.outputs_location}/yamls/02-security-kms-dev-keys.yaml"
content = yamlencode({
for k, m in module.dev-sec-kms : k => m.key_ids
})
}
resource "local_file" "prod_sec_kms" {
for_each = var.outputs_location == null ? {} : { 1 = 1 }
filename = "${var.outputs_location}/yamls/02-security-kms-prod-keys.yaml"
content = yamlencode({
for k, m in module.prod-sec-kms : k => m.key_ids
})
}
# outputs
output "stage_perimeter_projects" {
description = "Security project numbers. They can be added to perimeter resources."
value = {
dev = ["projects/${module.dev-sec-project.number}"]
prod = ["projects/${module.prod-sec-project.number}"]
}
}

View File

@ -0,0 +1,185 @@
/**
* Copyright 2022 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 "billing_account_id" {
# tfdoc:variable:source bootstrap
description = "Billing account id."
type = string
}
variable "folder_id" {
# tfdoc:variable:source resman
description = "Folder to be used for the networking resources in folders/nnnn format."
type = string
}
variable "groups" {
# tfdoc:variable:source bootstrap
description = "Group names to grant organization-level permissions."
type = map(string)
# https://cloud.google.com/docs/enterprise/setup-checklist
default = {
gcp-billing-admins = "gcp-billing-admins",
gcp-devops = "gcp-devops",
gcp-network-admins = "gcp-network-admins"
gcp-organization-admins = "gcp-organization-admins"
gcp-security-admins = "gcp-security-admins"
gcp-support = "gcp-support"
}
}
variable "kms_defaults" {
description = "Defaults used for KMS keys."
type = object({
locations = list(string)
rotation_period = string
})
default = {
locations = ["europe", "europe-west1", "europe-west3", "global"]
rotation_period = "7776000s"
}
}
variable "kms_keys" {
description = "KMS keys to create, keyed by name. Null attributes will be interpolated with defaults."
type = map(object({
iam = map(list(string))
labels = map(string)
locations = list(string)
rotation_period = string
}))
default = {}
}
variable "kms_restricted_admins" {
description = "Map of environment => [identities] who can assign the encrypt/decrypt roles on keys."
type = map(list(string))
default = {}
}
variable "organization" {
# tfdoc:variable:source bootstrap
description = "Organization details."
type = object({
domain = string
id = number
customer_id = string
})
}
variable "outputs_location" {
description = "Path where providers, tfvars files, and lists for the following stages are written. Leave empty to disable."
type = string
default = null
}
variable "prefix" {
description = "Prefix used for resources that need unique names."
type = string
}
variable "vpc_sc_access_levels" {
description = "VPC SC access level definitions."
type = map(object({
combining_function = string
conditions = list(object({
ip_subnetworks = list(string)
members = list(string)
negate = bool
regions = list(string)
required_access_levels = list(string)
}))
}))
default = {}
}
variable "vpc_sc_egress_policies" {
description = "VPC SC egress policy defnitions."
type = map(object({
egress_from = object({
identity_type = string
identities = list(string)
})
egress_to = object({
operations = list(object({
method_selectors = list(string)
service_name = string
}))
resources = list(string)
})
}))
default = {}
}
variable "vpc_sc_ingress_policies" {
description = "VPC SC ingress policy defnitions."
type = map(object({
ingress_from = object({
identity_type = string
identities = list(string)
source_access_levels = list(string)
source_resources = list(string)
})
ingress_to = object({
operations = list(object({
method_selectors = list(string)
service_name = string
}))
resources = list(string)
})
}))
default = {}
}
variable "vpc_sc_perimeter_access_levels" {
description = "VPC SC perimeter access_levels."
type = object({
dev = list(string)
landing = list(string)
prod = list(string)
})
default = null
}
variable "vpc_sc_perimeter_egress_policies" {
description = "VPC SC egress policies per perimeter, values reference keys defined in the `vpc_sc_ingress_policies` variable."
type = object({
dev = list(string)
landing = list(string)
prod = list(string)
})
default = null
}
variable "vpc_sc_perimeter_ingress_policies" {
description = "VPC SC ingress policies per perimeter, values reference keys defined in the `vpc_sc_ingress_policies` variable."
type = object({
dev = list(string)
landing = list(string)
prod = list(string)
})
default = null
}
variable "vpc_sc_perimeter_projects" {
description = "VPC SC perimeter resources."
type = object({
dev = list(string)
landing = list(string)
prod = list(string)
})
default = null
}

View File

@ -0,0 +1,88 @@
# skip boilerplate check
- accessapproval.googleapis.com
- adsdatahub.googleapis.com
- aiplatform.googleapis.com
- alpha-documentai.googleapis.com
- apigee.googleapis.com
- apigeeconnect.googleapis.com
- artifactregistry.googleapis.com
- assuredworkloads.googleapis.com
- automl.googleapis.com
- bigquery.googleapis.com
- bigquerydatatransfer.googleapis.com
- bigtable.googleapis.com
- binaryauthorization.googleapis.com
- cloudasset.googleapis.com
- cloudbuild.googleapis.com
- cloudfunctions.googleapis.com
- cloudkms.googleapis.com
- cloudprofiler.googleapis.com
- cloudresourcemanager.googleapis.com
- cloudsearch.googleapis.com
- cloudtrace.googleapis.com
- composer.googleapis.com
- compute.googleapis.com
- connectgateway.googleapis.com
- contactcenterinsights.googleapis.com
- container.googleapis.com
- containeranalysis.googleapis.com
- containerregistry.googleapis.com
- containerthreatdetection.googleapis.com
- datacatalog.googleapis.com
- dataflow.googleapis.com
- datafusion.googleapis.com
- dataproc.googleapis.com
- datastream.googleapis.com
- dialogflow.googleapis.com
- dlp.googleapis.com
- dns.googleapis.com
- documentai.googleapis.com
- eventarc.googleapis.com
- file.googleapis.com
- gameservices.googleapis.com
- gkeconnect.googleapis.com
- gkehub.googleapis.com
- healthcare.googleapis.com
- iam.googleapis.com
- iaptunnel.googleapis.com
- language.googleapis.com
- lifesciences.googleapis.com
- logging.googleapis.com
- managedidentities.googleapis.com
- memcache.googleapis.com
- meshca.googleapis.com
- metastore.googleapis.com
- ml.googleapis.com
- monitoring.googleapis.com
- networkconnectivity.googleapis.com
- networkmanagement.googleapis.com
- networksecurity.googleapis.com
- networkservices.googleapis.com
- notebooks.googleapis.com
- opsconfigmonitoring.googleapis.com
- osconfig.googleapis.com
- oslogin.googleapis.com
- privateca.googleapis.com
- pubsub.googleapis.com
- pubsublite.googleapis.com
- recaptchaenterprise.googleapis.com
- recommender.googleapis.com
- redis.googleapis.com
- run.googleapis.com
- secretmanager.googleapis.com
- servicecontrol.googleapis.com
- servicedirectory.googleapis.com
- spanner.googleapis.com
- speakerid.googleapis.com
- speech.googleapis.com
- sqladmin.googleapis.com
- storage.googleapis.com
- storagetransfer.googleapis.com
- texttospeech.googleapis.com
- tpu.googleapis.com
- trafficdirector.googleapis.com
- transcoder.googleapis.com
- translate.googleapis.com
- videointelligence.googleapis.com
- vision.googleapis.com
- vpcaccess.googleapis.com

View File

@ -0,0 +1,167 @@
/**
* Copyright 2022 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.
*/
locals {
# compute the number of projects in each perimeter to detect which to create
vpc_sc_counts = {
for k in ["dev", "landing", "prod"] : k => length(
coalesce(try(var.vpc_sc_perimeter_projects[k], null), [])
)
}
# dereference perimeter egress policy names to the actual objects
vpc_sc_perimeter_egress_policies = {
for k, v in coalesce(var.vpc_sc_perimeter_egress_policies, {}) :
k => [
for i in coalesce(v, []) : var.vpc_sc_egress_policies[i]
if lookup(var.vpc_sc_egress_policies, i, null) != null
]
}
# dereference perimeter ingress policy names to the actual objects
vpc_sc_perimeter_ingress_policies = {
for k, v in coalesce(var.vpc_sc_perimeter_ingress_policies, {}) :
k => [
for i in coalesce(v, []) : var.vpc_sc_ingress_policies[i]
if lookup(var.vpc_sc_ingress_policies, i, null) != null
]
}
# get the list of restricted services from the yaml file
vpcsc_restricted_services = yamldecode(
file("${path.module}/vpc-sc-restricted-services.yaml")
)
}
module "vpc-sc" {
source = "github.com/terraform-google-modules/cloud-foundation-fabric//modules/vpc-sc?ref=ea17e65"
# only enable if we have projects defined for perimeters
count = anytrue([for k, v in local.vpc_sc_counts : v > 0]) ? 1 : 0
access_policy = null
access_policy_create = {
parent = "organizations/${var.organization.id}"
title = "default"
}
access_levels = coalesce(try(var.vpc_sc_access_levels, null), {})
# bridge type perimeters
service_perimeters_bridge = merge(
# landing to dev, only we have projects in landing and dev perimeters
local.vpc_sc_counts.landing * local.vpc_sc_counts.dev == 0 ? {} : {
landing_to_dev = {
status_resources = null
spec_resources = concat(
var.vpc_sc_perimeter_projects.landing,
var.vpc_sc_perimeter_projects.dev
)
use_explicit_dry_run_spec = true
}
},
# landing to prod, only we have projects in landing and prod perimeters
local.vpc_sc_counts.landing * local.vpc_sc_counts.prod == 0 ? {} : {
landing_to_prod = {
status_resources = null
spec_resources = concat(
var.vpc_sc_perimeter_projects.landing,
var.vpc_sc_perimeter_projects.prod
)
# set to null and switch spec and status above to enforce
use_explicit_dry_run_spec = true
}
}
)
# regular type perimeters
service_perimeters_regular = merge(
# dev if we have projects in var.vpc_sc_perimeter_projects.dev
local.vpc_sc_counts.dev == 0 ? {} : {
dev = {
spec = {
access_levels = coalesce(
try(var.vpc_sc_perimeter_access_levels.dev, null), []
)
resources = var.vpc_sc_perimeter_projects.dev
restricted_services = local.vpcsc_restricted_services
egress_policies = try(
local.vpc_sc_perimeter_egress_policies.dev, null
)
ingress_policies = try(
local.vpc_sc_perimeter_ingress_policies.dev, null
)
# replace with commented block to enable vpc restrictions
vpc_accessible_services = null
# vpc_accessible_services = {
# allowed_services = ["RESTRICTED-SERVICES"]
# enable_restriction = true
# }
}
status = null
# set to null and switch spec and status above to enforce
use_explicit_dry_run_spec = true
}
},
# prod if we have projects in var.vpc_sc_perimeter_projects.prod
local.vpc_sc_counts.prod == 0 ? {} : {
prod = {
spec = {
access_levels = coalesce(
try(var.vpc_sc_perimeter_access_levels.prod, null), []
)
# combine the security project, and any specified in the variable
resources = var.vpc_sc_perimeter_projects.prod
restricted_services = local.vpcsc_restricted_services
egress_policies = try(
local.vpc_sc_perimeter_egress_policies.prod, null
)
ingress_policies = try(
local.vpc_sc_perimeter_ingress_policies.prod, null
)
# replace with commented block to enable vpc restrictions
vpc_accessible_services = null
# vpc_accessible_services = {
# allowed_services = ["RESTRICTED-SERVICES"]
# enable_restriction = true
# }
}
status = null
# set to null and switch spec and status above to enforce
use_explicit_dry_run_spec = true
}
},
# prod if we have projects in var.vpc_sc_perimeter_projects.prod
local.vpc_sc_counts.landing == 0 ? {} : {
landing = {
spec = {
access_levels = coalesce(
try(var.vpc_sc_perimeter_access_levels.landing, null), []
)
resources = var.vpc_sc_perimeter_projects.landing
restricted_services = local.vpcsc_restricted_services
egress_policies = try(
local.vpc_sc_perimeter_egress_policies.landing, null
)
ingress_policies = try(
local.vpc_sc_perimeter_ingress_policies.landing, null
)
# replace with commented block to enable vpc restrictions
vpc_accessible_services = null
# vpc_accessible_services = {
# allowed_services = ["RESTRICTED-SERVICES"]
# enable_restriction = true
# }
}
status = null
# set to null and switch spec and status above to enforce
use_explicit_dry_run_spec = true
}
}
)
}