From 36626c8c848624d4ffcf7d0055b56b955ce49ccc Mon Sep 17 00:00:00 2001 From: Bohdan Yurov Date: Tue, 10 Sep 2019 19:02:18 +0300 Subject: [PATCH] Fixes #11: Cloud Function for automatic folder inclusion https://github.com/terraform-google-modules/terraform-google-vpc-service-controls/issues/11 https://github.com/terraform-google-modules/terraform-google-vpc-service-controls/issues/14 Added example with cloud function that automatically runs TF when project is created/moved/deleted. --- .gitignore | 5 ++ Makefile | 6 -- examples/automatic_folder/.gitignore | 4 + examples/automatic_folder/README.md | 52 +++++++++++++ examples/automatic_folder/main.py | 77 +++++++++++++++++++ examples/automatic_folder/main.tf | 77 +++++++++++++++++++ examples/automatic_folder/outputs.tf | 25 ++++++ examples/automatic_folder/provider.tf.dist | 15 ++++ examples/automatic_folder/variables.tf | 56 ++++++++++++++ examples/automatic_folder/versions.tf | 19 +++++ examples/automatic_folder/watcher.tf | 76 ++++++++++++++++++ examples/simple_example/README.md | 2 +- .../simple_example_access_level/README.md | 2 +- modules/access_level/README.md | 6 +- modules/bridge_service_perimeter/README.md | 2 +- modules/regular_service_perimeter/README.md | 2 +- test/ci_integration.sh | 1 - 17 files changed, 413 insertions(+), 14 deletions(-) create mode 100644 examples/automatic_folder/.gitignore create mode 100644 examples/automatic_folder/README.md create mode 100644 examples/automatic_folder/main.py create mode 100644 examples/automatic_folder/main.tf create mode 100644 examples/automatic_folder/outputs.tf create mode 100644 examples/automatic_folder/provider.tf.dist create mode 100644 examples/automatic_folder/variables.tf create mode 100644 examples/automatic_folder/versions.tf create mode 100644 examples/automatic_folder/watcher.tf diff --git a/.gitignore b/.gitignore index f0dc335..a1908bf 100644 --- a/.gitignore +++ b/.gitignore @@ -50,3 +50,8 @@ examples/simple_example_access_level/terraform.tfvars terraform.tfvars credentials.json + +examples/automatic_folder.zip + +node_modules +yarn.lock diff --git a/Makefile b/Makefile index 99b8967..d5f3c16 100644 --- a/Makefile +++ b/Makefile @@ -89,7 +89,6 @@ version: docker_run: docker run --rm -it \ -e PROJECT_ID \ - -e BUCKET_NAME \ -e SERVICE_ACCOUNT_JSON \ -e CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE=${CREDENTIALS_PATH} \ -e GOOGLE_APPLICATION_CREDENTIALS=${CREDENTIALS_PATH} \ @@ -101,7 +100,6 @@ docker_run: docker_create: docker run --rm -it \ -e PROJECT_ID \ - -e BUCKET_NAME \ -e SERVICE_ACCOUNT_JSON \ -e CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE=${CREDENTIALS_PATH} \ -e GOOGLE_APPLICATION_CREDENTIALS=${CREDENTIALS_PATH} \ @@ -113,7 +111,6 @@ docker_create: docker_converge: docker run --rm -it \ -e PROJECT_ID \ - -e BUCKET_NAME \ -e SERVICE_ACCOUNT_JSON \ -e CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE=${CREDENTIALS_PATH} \ -e GOOGLE_APPLICATION_CREDENTIALS=${CREDENTIALS_PATH} \ @@ -125,7 +122,6 @@ docker_converge: docker_verify: docker run --rm -it \ -e PROJECT_ID \ - -e BUCKET_NAME \ -e SERVICE_ACCOUNT_JSON \ -e CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE=${CREDENTIALS_PATH} \ -e GOOGLE_APPLICATION_CREDENTIALS=${CREDENTIALS_PATH} \ @@ -137,7 +133,6 @@ docker_verify: docker_destroy: docker run --rm -it \ -e PROJECT_ID \ - -e BUCKET_NAME \ -e SERVICE_ACCOUNT_JSON \ -e CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE=${CREDENTIALS_PATH} \ -e GOOGLE_APPLICATION_CREDENTIALS=${CREDENTIALS_PATH} \ @@ -149,7 +144,6 @@ docker_destroy: test_integration_docker: docker run --rm -it \ -e PROJECT_ID \ - -e BUCKET_NAME \ -e SERVICE_ACCOUNT_JSON \ -e CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE=${CREDENTIALS_PATH} \ -e GOOGLE_APPLICATION_CREDENTIALS=${CREDENTIALS_PATH} \ diff --git a/examples/automatic_folder/.gitignore b/examples/automatic_folder/.gitignore new file mode 100644 index 0000000..3571fb8 --- /dev/null +++ b/examples/automatic_folder/.gitignore @@ -0,0 +1,4 @@ +/provider.tf +/.terraform +tfplan +local.tfvars diff --git a/examples/automatic_folder/README.md b/examples/automatic_folder/README.md new file mode 100644 index 0000000..891027d --- /dev/null +++ b/examples/automatic_folder/README.md @@ -0,0 +1,52 @@ +# Automatic folder securing Example + +This example illustrates how to use the `vpc-service-controls` module to configure an org policy, an access level and a regular perimeter with projects inside a folder. + +# Requirements + +1. Make sure you've gone through the root [Requirement Section](../../README.md#requirements) on any project in your organization. +2. Updated `provider.tf.dist` with remote state configs. Copy `provider.tf.dist` to `provider.tf` changing variables for local running +3. Create `local.tfvars` file with required inputs, like this: +````hcl-terraform +project_id = "YOUR_PROJECT" +parent_id = "ORG_ID" +folder_id = "FOLDER_ID" +policy_name = "automatic_folder" +members = ["user:YOUR_NAME@google.com"] +region = "us-east1" +restricted_services = ["storage.googleapis.com"] +```` +4. Please note, that whole example folder is uploaded as Cloud Function root. Don't store credentials in it! +5. Add Cloud Function's SA to organization (Access Context Manager Admin), project IAM (Owner and Storage Object Admin) and watched folder (Logs Configuration Writer) +6. You might need to apply TF changes twice due to ACM race condition + + + + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|:----:|:-----:|:-----:| +| folder\_id | Folder ID to watch for projects. | string | n/a | yes | +| members | An allowed list of members \(users, service accounts\). The signed-in identity originating the request must be a part of one of the provided members. If not specified, a request may come from any user \(logged in/not logged in, etc.\). Formats: user:\{emailid\}, serviceAccount:\{emailid\} | list(string) | n/a | yes | +| parent\_id | The parent of this AccessPolicy in the Cloud Resource Hierarchy. As of now, only organization are accepted as parent \(ID\). | string | n/a | yes | +| perimeter\_name | Name of perimeter. | string | `"regular_perimeter"` | no | +| policy\_name | The policy's name. | string | n/a | yes | +| project\_id | The ID of the project to which resources will be applied. | string | n/a | yes | +| region | The region in which resources will be applied. | string | n/a | yes | +| restricted\_services | List of services to restrict. | list(string) | n/a | yes | + +## Outputs + +| Name | Description | +|------|-------------| +| policy\_name | Name of the parent policy | +| protected\_project\_ids | Project ids of the projects INSIDE the regular service perimeter | + + + +To provision this example, run the following from within this directory: +- `terraform init` to get the plugins +- `terraform plan` to see the infrastructure plan +- `terraform apply` to apply the infrastructure build +- `terraform destroy` to destroy the built infrastructure diff --git a/examples/automatic_folder/main.py b/examples/automatic_folder/main.py new file mode 100644 index 0000000..9fdc936 --- /dev/null +++ b/examples/automatic_folder/main.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- + +import os +import subprocess +import urllib.request +import os +from shutil import copytree, copyfile, ignore_patterns, rmtree + +# Version of Terraform that we're using +TERRAFORM_VERSION = '0.12.8' + +# Download URL for Terraform +TERRAFORM_DOWNLOAD_URL = ( + 'https://releases.hashicorp.com/terraform/%s/terraform_%s_linux_amd64.zip' + % (TERRAFORM_VERSION, TERRAFORM_VERSION)) + +# Paths where Terraform should be installed +TERRAFORM_DIR = os.path.join('/tmp', 'terraform_%s' % TERRAFORM_VERSION) +TERRAFORM_PATH = os.path.join(TERRAFORM_DIR, 'terraform') + +PROJECT_DIR = os.path.join('/tmp', 'project') + +def check_call(args, cwd=None, printOut=False): + """Wrapper for subprocess that checks if a process runs correctly, + and if not, prints stdout and stderr. + """ + proc = subprocess.Popen(args, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + cwd=cwd) + stdout, stderr = proc.communicate() + if proc.returncode != 0: + print(stdout) + print(stderr) + raise subprocess.CalledProcessError( + returncode=proc.returncode, + cmd=args) + if printOut: + print(stdout) + print(stderr) + + +def install_terraform(): + """Install Terraform.""" + if os.path.exists(TERRAFORM_PATH): + return + + urllib.request.urlretrieve(TERRAFORM_DOWNLOAD_URL, '/tmp/terraform.zip') + + check_call(['unzip', '-o', '/tmp/terraform.zip', '-d', TERRAFORM_DIR], '/tmp') + + check_call([TERRAFORM_PATH, '--version']) + + +def handler(event, context): + print(event) + + if os.path.exists(PROJECT_DIR): + rmtree(PROJECT_DIR) + + copytree('.', PROJECT_DIR, ignore=ignore_patterns('.terraform', 'provider.tf', 'credentials.json')) + copyfile('provider.tf.dist', os.path.join(PROJECT_DIR, 'provider.tf')) + + install_terraform() + + check_call([TERRAFORM_PATH, 'init'], + cwd=PROJECT_DIR, + printOut=True) + check_call([TERRAFORM_PATH, 'plan', '-no-color', '-var-file=local.tfvars', '-lock-timeout=300s', '-out', 'tfplan'], + cwd=PROJECT_DIR, + printOut=True) + check_call([TERRAFORM_PATH, 'apply', '-no-color', '-auto-approve', '-lock-timeout=300s', 'tfplan'], + cwd=PROJECT_DIR, + printOut=True) + check_call([TERRAFORM_PATH, 'output', '-json'], + cwd=PROJECT_DIR, + printOut=True) diff --git a/examples/automatic_folder/main.tf b/examples/automatic_folder/main.tf new file mode 100644 index 0000000..17e92a5 --- /dev/null +++ b/examples/automatic_folder/main.tf @@ -0,0 +1,77 @@ +/** + * Copyright 2019 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. + */ + +provider "archive" { + version = "~> 1.0" +} + +provider "random" { + version = "~> 2.0" +} + +provider "null" { + version = "~> 2.1" +} + +data "google_projects" "in_perimeter_folder" { + filter = "parent.id:${var.folder_id}" +} + +data "google_project" "in_perimeter_folder" { + count = length(data.google_projects.in_perimeter_folder.projects) + + project_id = data.google_projects.in_perimeter_folder.projects[count.index].project_id +} + +locals { + projects = compact(data.google_project.in_perimeter_folder.*.number) +} + +module "access_context_manager_policy" { + source = "terraform-google-modules/vpc-service-controls/google" + version = "1.0.0" + + parent_id = var.parent_id + policy_name = var.policy_name +} + +module "access_level_members" { + source = "terraform-google-modules/vpc-service-controls/google//modules/access_level" + version = "1.0.0" + + description = "${var.perimeter_name} Access Level" + policy = module.access_context_manager_policy.policy_id + name = "${var.perimeter_name}_members" + members = var.members +} + +module "service_perimeter" { + source = "terraform-google-modules/vpc-service-controls/google//modules/regular_service_perimeter" + version = "1.0.0" + + policy = module.access_context_manager_policy.policy_id + perimeter_name = var.perimeter_name + + description = "Perimeter ${var.perimeter_name}" + resources = local.projects + + access_levels = [module.access_level_members.name] + restricted_services = var.restricted_services + + shared_resources = { + all = local.projects + } +} diff --git a/examples/automatic_folder/outputs.tf b/examples/automatic_folder/outputs.tf new file mode 100644 index 0000000..1ecc208 --- /dev/null +++ b/examples/automatic_folder/outputs.tf @@ -0,0 +1,25 @@ +/** + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +output "policy_name" { + description = "Name of the parent policy" + value = var.policy_name +} + +output "protected_project_ids" { + description = "Project ids of the projects INSIDE the regular service perimeter" + value = local.projects +} diff --git a/examples/automatic_folder/provider.tf.dist b/examples/automatic_folder/provider.tf.dist new file mode 100644 index 0000000..b5f9304 --- /dev/null +++ b/examples/automatic_folder/provider.tf.dist @@ -0,0 +1,15 @@ +provider "google" { +// credentials = file("credentials.json") +// project = "YOUR_PROJECT" + region = "us-central1" +} + +terraform { + required_version = "~> 0.12.0" + + backend "gcs" { +// credentials = "credentials.json" + bucket = "YOUR_BUCKET" + prefix = "terraform/vpc-service-controls" + } +} diff --git a/examples/automatic_folder/variables.tf b/examples/automatic_folder/variables.tf new file mode 100644 index 0000000..557ef0d --- /dev/null +++ b/examples/automatic_folder/variables.tf @@ -0,0 +1,56 @@ +/** + * Copyright 2019 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 "project_id" { + type = string + description = "The ID of the project to which resources will be applied." +} + +variable "parent_id" { + description = "The parent of this AccessPolicy in the Cloud Resource Hierarchy. As of now, only organization are accepted as parent (ID)." + type = string +} + +variable "policy_name" { + description = "The policy's name." + type = string +} + +variable "members" { + description = "An allowed list of members (users, service accounts). The signed-in identity originating the request must be a part of one of the provided members. If not specified, a request may come from any user (logged in/not logged in, etc.). Formats: user:{emailid}, serviceAccount:{emailid}" + type = list(string) +} + +variable "folder_id" { + description = "Folder ID to watch for projects." + type = string +} + +variable "perimeter_name" { + description = "Name of perimeter." + type = string + default = "regular_perimeter" +} + +variable "restricted_services" { + description = "List of services to restrict." + type = list(string) +} + +variable "region" { + type = string + description = "The region in which resources will be applied." +} diff --git a/examples/automatic_folder/versions.tf b/examples/automatic_folder/versions.tf new file mode 100644 index 0000000..2970427 --- /dev/null +++ b/examples/automatic_folder/versions.tf @@ -0,0 +1,19 @@ +/** + * Copyright 2019 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. + */ + +terraform { + required_version = ">= 0.12" +} diff --git a/examples/automatic_folder/watcher.tf b/examples/automatic_folder/watcher.tf new file mode 100644 index 0000000..6cad747 --- /dev/null +++ b/examples/automatic_folder/watcher.tf @@ -0,0 +1,76 @@ +resource "random_pet" "main" { + separator = "-" +} + +module "event_folder_log_entry" { + source = "terraform-google-modules/event-function/google//modules/event-folder-log-entry" + version = "1.1.0" + + filter = <` | no | | allowed\_encryption\_statuses | Condition - A list of allowed encryptions statuses. An empty list allows all statuses. | list(string) | `` | no | | combining\_function | How the conditions list should be combined to determine if a request is granted this AccessLevel. If AND is used, each Condition must be satisfied for the AccessLevel to be applied. If OR is used, at least one Condition must be satisfied for the AccessLevel to be applied. | string | `"AND"` | no | -| description | Description of the access level | string | n/a | yes | -| ip\_subnetworks | Condition - A list of CIDR block IP subnetwork specification. May be IPv4 or IPv6. Note that for a CIDR IP address block, the specified IP address portion must be properly truncated (i.e. all the host bits must be zero) or the input is considered malformed. For example, "192.0.2.0/24" is accepted but "192.0.2.1/24" is not. Similarly, for IPv6, "2001:db8::/32" is accepted whereas "2001:db8::1/32" is not. The originating IP of a request must be in one of the listed subnets in order for this Condition to be true. If empty, all IP addresses are allowed. | list(string) | `` | no | -| members | Condition - An allowed list of members (users, service accounts). The signed-in identity originating the request must be a part of one of the provided members. If not specified, a request may come from any user (logged in/not logged in, etc.). Formats: user:{emailid}, serviceAccount:{emailid} | list(string) | `` | no | +| description | Description of the access level | string | `""` | no | +| ip\_subnetworks | Condition - A list of CIDR block IP subnetwork specification. May be IPv4 or IPv6. Note that for a CIDR IP address block, the specified IP address portion must be properly truncated \(i.e. all the host bits must be zero\) or the input is considered malformed. For example, "192.0.2.0/24" is accepted but "192.0.2.1/24" is not. Similarly, for IPv6, "2001:db8::/32" is accepted whereas "2001:db8::1/32" is not. The originating IP of a request must be in one of the listed subnets in order for this Condition to be true. If empty, all IP addresses are allowed. | list(string) | `` | no | +| members | Condition - An allowed list of members \(users, service accounts\). The signed-in identity originating the request must be a part of one of the provided members. If not specified, a request may come from any user \(logged in/not logged in, etc.\). Formats: user:\{emailid\}, serviceAccount:\{emailid\} | list(string) | `` | no | | minimum\_version | The minimum allowed OS version. If not set, any version of this OS satisfies the constraint. Format: "major.minor.patch" such as "10.5.301", "9.2.1". | string | `""` | no | | name | Description of the AccessLevel and its use. Does not affect behavior. | string | n/a | yes | | negate | Whether to negate the Condition. If true, the Condition becomes a NAND over its non-empty fields, each field must be false for the Condition overall to be satisfied. | bool | `"false"` | no | diff --git a/modules/bridge_service_perimeter/README.md b/modules/bridge_service_perimeter/README.md index a0de402..07c3f76 100644 --- a/modules/bridge_service_perimeter/README.md +++ b/modules/bridge_service_perimeter/README.md @@ -60,7 +60,7 @@ module "regular_service_perimeter_2" { | Name | Description | Type | Default | Required | |------|-------------|:----:|:-----:|:-----:| -| description | Description of the bridge perimeter | string | n/a | yes | +| description | Description of the bridge perimeter | string | `""` | no | | perimeter\_name | Name of the perimeter. Should be one unified string. Must only be letters, numbers and underscores | string | n/a | yes | | policy | Name of the parent policy | string | n/a | yes | | resources | A list of GCP resources that are inside of the service perimeter. Currently only projects are allowed. | list(string) | n/a | yes | diff --git a/modules/regular_service_perimeter/README.md b/modules/regular_service_perimeter/README.md index 0001daa..adf91b7 100644 --- a/modules/regular_service_perimeter/README.md +++ b/modules/regular_service_perimeter/README.md @@ -34,7 +34,7 @@ module "regular_service_perimeter_1" { | Name | Description | Type | Default | Required | |------|-------------|:----:|:-----:|:-----:| -| access\_levels | A list of AccessLevel resource names that allow resources within the ServicePerimeter to be accessed from the internet. AccessLevels listed must be in the same policy as this ServicePerimeter. Referencing a nonexistent AccessLevel is a syntax error. If no AccessLevel names are listed, resources within the perimeter can only be accessed via GCP calls with request origins within the perimeter. Example: 'accessPolicies/MY_POLICY/accessLevels/MY_LEVEL'. For Service Perimeter Bridge, must be empty. | list(string) | `` | no | +| access\_levels | A list of AccessLevel resource names that allow resources within the ServicePerimeter to be accessed from the internet. AccessLevels listed must be in the same policy as this ServicePerimeter. Referencing a nonexistent AccessLevel is a syntax error. If no AccessLevel names are listed, resources within the perimeter can only be accessed via GCP calls with request origins within the perimeter. Example: 'accessPolicies/MY\_POLICY/accessLevels/MY\_LEVEL'. For Service Perimeter Bridge, must be empty. | list(string) | `` | no | | description | Description of the regular perimeter | string | n/a | yes | | perimeter\_name | Name of the perimeter. Should be one unified string. Must only be letters, numbers and underscores | string | n/a | yes | | policy | Name of the parent policy | string | n/a | yes | diff --git a/test/ci_integration.sh b/test/ci_integration.sh index cb06dbe..170dbda 100755 --- a/test/ci_integration.sh +++ b/test/ci_integration.sh @@ -38,7 +38,6 @@ setup_environment() { # Terraform variables export TF_VAR_project_id="$PROJECT_ID" - export TF_VAR_bucket_name="$BUCKET_NAME" } main() {