Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement function for managing VPC Service Controls #16

Merged
merged 17 commits into from
Sep 23, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,8 @@ examples/simple_example_access_level/terraform.tfvars
terraform.tfvars

credentials.json

examples/automatic_folder.zip

node_modules
yarn.lock
6 changes: 0 additions & 6 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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} \
Expand All @@ -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} \
Expand All @@ -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} \
Expand All @@ -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} \
Expand All @@ -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} \
Expand All @@ -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} \
Expand Down
4 changes: 4 additions & 0 deletions examples/automatic_folder/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/provider.tf
/.terraform
tfplan
local.tfvars
125 changes: 125 additions & 0 deletions examples/automatic_folder/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# Automatically Secured Folder

This example illustrates how to use Terraform and Cloud Functions to secure all projects within a folder via VPC service Controls.

Terraform is used to set up a new service perimeter and to deploy a Cloud Function which monitors that folder via Stackdriver and Cloud Pub/Sub. When the function notices a new project is added to the folder, it executes Terraform to add the new project to the associated perimeter. Similarly, the function automatically removes projects from the perimeter if they are moved out of the folder.

![Diagram](./diagram.png)

## Set up

**Please note, the whole example folder is uploaded as a Cloud Function. Do not store credentials in it.**

1. [Authenticate](https://www.terraform.io/docs/providers/google/provider_reference.html#credentials-1) to Terraform using either your user account or an exported Service Account key.

```bash
export GOOGLE_APPLICATION_CREDENTIALS=~/mykey.json
```

You will need these roles:

- Access Context Manager Admin (`roles/accesscontextmanager.policyAdmin`)
- Editor on the watched project (`roles/editor`)
- Logs Configuration Writer on the watched folder (`roles/logging.configWriter`)

2. Choose or create a project for hosting the VPC Service Controls manager.

```bash
gcloud config set project YOUR_PROJECT
```

3. Activate the required APIs on your management project:
- cloudfunctions.googleapis.com
- accesscontextmanager.googleapis.com
- cloudresourcemanager.googleapis.com

```bash
gcloud config set project YOUR_PROJECT
gcloud services enable cloudresourcemanager.googleapis.com
gcloud services enable cloudfunctions.googleapis.com
gcloud services enable accesscontextmanager.googleapis.com
```

3. Create a Google Cloud Storage bucket to hold Terraform state.

```sh
gsutil mb -p YOUR_PROJECT gs://YOUR_BUCKET_NAME
```

4. Copy `backend.tf.sample` to `backend.tf` and change the bucket to match your own on line 5.

```sh
cp backend.tf.sample backend.tf
```

3. Create a local `terraform.tfvars` file with required inputs, like this:

```tf
project_id = "YOUR_PROJECT"
org_id = "ORG_ID"
folder_id = "FOLDER_ID"
policy_name = "automatic_folder"
members = ["user:[email protected]"]
region = "us-east1"
restricted_services = ["storage.googleapis.com"]
```

4. Run `terraform apply` to create the perimeter and watching function.

5. Grant the Cloud Function's SA access to your organization and management project. It needs these roles:

- Access Context Manager Admin (`roles/accesscontextmanager.policyAdmin`)
- Editor on the watched project (`roles/editor`)
- Logs Configuration Writer on the watched folder (`roles/logging.configWriter`)

```bash
SA_ID=$(terraform output function_service_account)
ORG_ID=$(terraform output organization_id)
PROJECT_ID=$(terraform output project_id)
FOLDER_ID=$(terraform output folder_id)
gcloud organizations add-iam-policy-binding \
"${ORG_ID}" \
--member="serviceAccount:${SA_ID}" \
--role="roles/accesscontextmanager.policyAdmin"
gcloud projects add-iam-policy-binding \
"${PROJECT_ID}" \
--member="serviceAccount:${SA_ID}" \
--role="roles/editor"
gcloud resource-manager folders add-iam-policy-binding \
"${FOLDER_ID}" \
--member="serviceAccount:${SA_ID}" \
--role="roles/logging.configWriter"
```

6. Test the function by creating a project in the protected folder (or moving an existing project in).

## Limitations
1. The Cloud Function used to manage the perimeter must be hosted in a project **outside** the perimeter. This is because Cloud Functions do not yet support VPC Service Controls.
2. Nested folders are not supported. Only projects directly contained within your perimeter folder will be added.

<!-- BEGINNING OF PRE-COMMIT-TERRAFORM DOCS HOOK -->
## 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 |
| org\_id | The parent organization ID of this AccessPolicy in the Cloud Resource Hierarchy. | 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 host the watcher function. | 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 |
|------|-------------|
| folder\_id | The ID of the watched folder. |
| function\_service\_account | Email of the watcher function's Service Account |
| organization\_id | Organization ID hosting the perimeter |
| policy\_name | Name of the parent policy |
| project\_id | The ID of the project hosting the watcher function. |
| protected\_project\_ids | Project ids of the projects INSIDE the regular service perimeter |

<!-- END OF PRE-COMMIT-TERRAFORM DOCS HOOK -->
8 changes: 8 additions & 0 deletions examples/automatic_folder/backend.tf.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
terraform {
required_version = "~> 0.12.0"

backend "gcs" {
bucket = "<YOUR_BUCKET_NAME>"
prefix = "terraform/vpc-service-controls"
}
}
Binary file added examples/automatic_folder/diagram.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
79 changes: 79 additions & 0 deletions examples/automatic_folder/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# -*- 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
PLATFORM = 'linux'
TERRAFORM_DOWNLOAD_URL = (
'https://releases.hashicorp.com/terraform/%s/terraform_%s_%s_amd64.zip'
% (TERRAFORM_VERSION, TERRAFORM_VERSION, PLATFORM))

# 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.decode())
print(stderr.decode())
raise subprocess.CalledProcessError(
returncode=proc.returncode,
cmd=args)
if printOut:
print(stdout.decode())
print(stderr.decode())


def install_terraform():
"""Install Terraform."""
if os.path.exists(TERRAFORM_PATH):
return

print(TERRAFORM_PATH)

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', 'credentials.json'))

install_terraform()

check_call([TERRAFORM_PATH, 'init'],
cwd=PROJECT_DIR,
printOut=True)
check_call([TERRAFORM_PATH, 'apply',
'-target=module.service_perimeter', '-no-color',
'-auto-approve', '-lock=false',
'-lock-timeout=300s'],
cwd=PROJECT_DIR,
printOut=True)
check_call([TERRAFORM_PATH, 'output', '-json'],
cwd=PROJECT_DIR,
printOut=True)
79 changes: 79 additions & 0 deletions examples/automatic_folder/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/**
* 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)
parent_id = var.org_id
watcher_name = replace("${var.policy_name}-manager", "_", "-")
}

module "access_context_manager_policy" {
source = "terraform-google-modules/vpc-service-controls/google"
version = "1.0.1"

parent_id = local.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.1"

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.1"

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
}
}
Loading