Encryption for GCP

Make encrypted storage the default for all your GCP services

Encrypting cloud resources is a priority for security teams, but it oftentimes goes overlooked. Encryption should be the default for many use cases, and as easy as clicking a button.

In this tutorial, we’ll walk through how you can get started with encrypting common GCP cloud resources at rest. Then we’ll complete a complex encryption use case: encrypting data in transit via https. We’ll do all of this with Resourcely Blueprintsand Guardrails.

Getting started

Log in or sign up for an account

Navigate to https://portal.resourcely.io and sign up for a free account.

(Optional) Set up your environment

Resourcely generates configuration and enforces policies as part of your CI. While you can use Resourcely standalone, to make the most out of it you will want to integrate it with your version control, CI, and Terraform runner.

To set up your environment, follow the Quickstart.

Creating crypto keys

Encryption on GCP hinges on crypto keys: tools that can be used to encrypt and decrypt cloud resources. Keys are managed with GCP KMS, which allows users to easily create and manage keys with features like automatic rotation, permissions, grouping, and more.

Here's a simple example of creating a crypto key with Terraform:

resource "google_kms_key_ring" "keyring" {
  name     = "keyring-example"
  location = "global"
}

resource "google_kms_crypto_key" "example-key" {
  name            = "crypto-key-example"
  key_ring        = google_kms_key_ring.keyring.id
  purpose         = "ENCRYPT_DECRYPT"
  rotation_period = "7776000s"
}

While this example is straightforward, there are drawbacks. A developer may...

  • not know what any of these parameters mean

  • may enter values that are dangerous or incorrect

  • omit values that are important to their organization

We can turn this into a Resourcely Blueprint by adding tags. This will create a guided UI a developer can use to confidently deploy infrastructure. Here is a Resourcely Blueprint for a crypto key, and the associated UI that is generated:

This Blueprint has two primary sections: Frontmatter (where variables are defined and augmented with guidance), and the primary resources section (Terraform, but with variables added to it).

---
constants:
  __name: "{{ name }}_{{ __guid }}"
variables:
  name:
    desc: |
      The name of the resource being created, used for both the Key Ring and the Crypto Key.
      - Must be unique within the GCP project
      - Avoid using sensitive or identifiable information
    required: true
    suggest: "my-kms-key"
    group: General
  key_ring_id:
    desc: |
      The **ID of the Key Ring** to hold the encryption keys.
      - Must be unique within the specified location
      - For more information, see the [Terraform docs](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/kms_key_ring)
    suggest: "my-key-ring"
    required: true
    group: KMS Configuration
  location:
    desc: |
      The **GCP location** for the Key Ring and Crypto Key.
      - Default: `us-central1`
      - Possible values: `us-central1`, `us-east1`, `europe-west1`, `asia-east1`, ...
      - For best practices, see [GCP Regions and Zones](https://cloud.google.com/about/locations)
    suggest: "us-central1"
    required: true
    group: KMS Configuration
  rotation_period:
    desc: |
      The **rotation period** for the encryption key.
      - Default: `30d` (30 days)
      - Specify in the format `Xd`, e.g., `90d` for 90 days
      - Learn more about key rotation [here](https://cloud.google.com/kms/docs/key-rotation)
    suggest: "30d"
    required: true
    group: KMS Configuration
---

resource "google_kms_key_ring" "{{ __name }}" {
  name     = "{{ name }}"
  location = "{{ location }}"
}

resource "google_kms_crypto_key" "{{ __name }}" {
  name            = "{{ name }}"
  key_ring        = google_kms_key_ring.{{ __name }}.id
  purpose         = "ENCRYPT_DECRYPT"
  rotation_period = "{{ rotation_period }}"
}

Navigate to Foundry in Resourcely, paste the above code, and navigate to the Developer Experience tab to see the form that is created.

Attaching crypto keys to cloud resources

Now that we have given users the ability to create crypto keys, let's make it easy for them to use keys and encrypt their services.

In Terraform, you might use code that looks like this to encrypt your resources:

# Create a Crypto Key
resource "google_kms_crypto_key" "sql_crypto_key" {
  name            = "sql-crypto-key"
  key_ring        = google_kms_key_ring.sql_key_ring.id
  rotation_period = "100000s"       # Optional: Set key rotation period
  
  version_template {
    algorithm = "GOOGLE_SYMMETRIC_ENCRYPTION"
  }
}

# Provision a Cloud SQL Database Instance with encryption
resource "google_sql_database_instance" "default" {
  name             = "my-sql-instance"
  database_version = "POSTGRES_13"
  region           = "us-central1"

  settings {
    tier = "db-f1-micro"

    # Attaching a crypto key for encryption
    disk_encryption_configuration {
      kms_key_name = google_kms_crypto_key.sql_crypto_key.id
    }

This leaves too many surface areas for encryption failure:

  • Choosing the wrong alogrithm

  • Improper naming

  • Too long of a rotation period

  • Using the wrong crypto key ID

In our Blueprint code, notice our frontmatter tags:

  • Description

  • Default

  • Suggest

  • Required

  • Group

These tags embed context into our form that helps users avoid issues and choose the right configuration.

Let's now create Blueprints for our cloud resources that we want to encrypt:

Compute

With Google Compute Instances, you would normally link to a crypto key by referencing its ID:

What if you want to give users a simple list to choose from of possible crypto keys, with some guidance for what to pick? You can do this with the links_to tag!

Start out by creating this Google Compute Instance Blueprint in Resourcely Foundry:

Compute Instance Blueprint
---
constants:
  __name: "{{ name }}_{{ __guid }}"
variables:
  name:
    desc: |
      The **name of the Compute Engine instance**.
      - Used as the unique identifier for the instance
      - See the [Terraform docs](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/compute_instance) for details
    required: true
    suggest: "encrypted-instance"
    group: General
  machine_type:
    desc: |
      The **machine type** for the Compute Engine instance.
      - Default: `e2-medium`
      - Possible values: `e2-medium`, `e2-standard-2`, `n1-standard-1`, `e2-highmem-2`, `n2-standard-2`, ...
      - Check [GCP machine types](https://cloud.google.com/compute/docs/machine-types) for more options
    required: true
    suggest: "e2-medium"
    group: Compute Configuration
  disk_encryption_key:
    desc: |
      The **KMS key** used to encrypt the disk.
      - Must be a valid Crypto Key resource
      - [Learn more about encrypted disks](https://cloud.google.com/compute/docs/disks/customer-supplied-encryption)
    required: true
    links_to: resource.google_kms_crypto_key.id
    group: Compute Configuration
---

resource "google_compute_instance" "{{ __name }}" {
  name         = "{{ name }}"
  machine_type = "{{ machine_type }}"
  zone         = "us-central1-a"

  boot_disk {
    initialize_params {
      image = "projects/debian-cloud/global/images/family/debian-11"
    }
    kms_key_self_link = "{{ disk_encryption_key }}"
  }
}
```

Notice the links_to tag usage:

  disk_encryption_key:
    desc: |
      The **KMS key** used to encrypt the disk.
      - Must be a valid Crypto Key resource
      - [Learn more about encrypted disks](https://cloud.google.com/compute/docs/disks/customer-supplied-encryption)
    required: true
    links_to: resource.google_kms_crypto_key.id
    group: Compute Configuration

This manifests itself in the Blueprint UI with a dropdown box. Publish the Blueprint you just pasted, and create a PR in Resourcely with your crypto key Blueprint and this Compute Instance Blueprints.

When you do so, observe the disk encryption key field. It lists crypto keys that are being created by the user, or any existing crypto keys that you want to make available:

Using links_to makes it simple for developers to link resources, making encryption easy to achieve: no looking up ARNs or IDs.

Requiring encryption

Now that you've created paved roads for creating encrypted resources by default, let's build some more restrictions into our input fields and some checks when CI runs. We'll do this with Resourcely Guardrails.

Navigate to Foundry and click "Author a Guardrail". We'll start with a simple Guardrail that makes sure we have a crypto key on our Cloud SQL databases.

GUARDRAIL "Require encryption on Cloud SQL databases"
  WHEN google_sql_database_instance
    REQUIRE encryption_key_name EXISTS

Paste the above example into the Foundry IDE and publish it. Then, create a Blueprint in Foundry with for a Cloud SQL database that allows for a crypto key to be attached:

Cloud SQL Blueprint
---
constants:
  __name: "{{ name }}_{{ __guid }}"
variables:
  name:
    desc: |
      The **name of the Cloud SQL instance**.
      - Must be unique within the project
      - See [Cloud SQL Documentation](https://cloud.google.com/sql/docs/introduction) for instance naming rules
    required: true
    suggest: "encrypted-sql-instance"
    group: General
  database_version:
    desc: |
      The **version of the Cloud SQL database**.
      - Default: `MYSQL_8_0`
      - Possible values: `MYSQL_5_7`, `MYSQL_8_0`, `POSTGRES_13`, `POSTGRES_14`, ...
      - See [Supported Database Versions](https://cloud.google.com/sql/docs/mysql/supported-versions)
    required: true
    suggest: "MYSQL_8_0"
    group: SQL Configuration
  encryption_key:
    desc: |
      The **KMS key** used to encrypt the database instance.
      - Must link to a valid Crypto Key resource
      - Learn more about [Cloud SQL Encryption](https://cloud.google.com/sql/docs/mysql/data-encryption)
    required: true
    links_to: resource.google_kms_crypto_key.id
    group: SQL Configuration
---

resource "google_sql_database_instance" "{{ __name }}" {
  name             = "{{ name }}"
  database_version = "{{ database_version }}"
  region           = "us-central1"

  encryption_key_name = "{{ encryption_key }}"
}

```

Notice that our Guardrail manifests itself on the Cloud SQL Blueprint form (see the Developer Experience tab):

If the field is left empty, this Guardrail will be triggered. The user must unlock the Guardrail with the Proceed button, acknowledging they aren't attaching a crypto key. In this case, the resulting PR will be blocked until the appropriate team can review it.

Guardrails are a great way to require or limit configuration settings that your team cares about (like encryption) without blocking the developers following your guidance.

Encryption in transit

Suppose that you want to make it easy for developers to set up https support for their applications. Creating a certificate and connecting it to a backend service is not a straightforward exercise. There are many tricky parameters:

  • Host rules and path matching

  • Certificate setup and lifecycle rules

  • URL mapping and connecting to the backend service

Resourcely's QoL features for Blueprints are a perfect candidate for streamlining encryption in transit:

  • Easy, automated linking means that users never have to make their own references

  • Default values mean developers don't have to bother with fields they shouldn't need to know about

    • i.e. paths = ["/*"]

  • Markdown-based description make for robust instructions that keep developers on track

To implement encryption in transit using Resourcely, simply use the following Blueprint:

Encrypted Load Balancer Blueprint
---
constants:
  __name: "{{ name }}_{{ __guid }}"
variables:
  name:
    desc: |
      The **base name** for the resources to be created.
      - Used for all related resources such as certificates, proxies, and backend services
      - Must be unique within the GCP project
    required: true
    suggest: "secure-data-in-transit"
    group: General
  certificate_path:
    desc: |
      The **path to the SSL certificate file** for the self-managed SSL certificate.
      - PEM-formatted certificate
    required: true
    suggest: "/path/to/certificate.crt"
    group: SSL Configuration
  private_key_path:
    desc: |
      The **path to the private key file** for the self-managed SSL certificate.
      - PEM-formatted private key
    required: true
    suggest: "/path/to/private.key"
    group: SSL Configuration
  host_names:
    desc: |
      A list of hostnames for the URL map's host rules.
      - Must be valid DNS hostnames
    required: true
    suggest: ["domain.com","example.com"]
    group: URL Configuration
  backend_service_name:
    desc: |
      The name of the backend service.
      - Must be unique within the GCP project
    required: true
    suggest: "backend-service"
    links_to: resource.google_compute_backend_service.id
    group: Backend Configuration
  health_check_name:
    desc: |
      The name of the HTTP health check resource.
    required: true
    suggest: "http-health-check"
    links_to: resource.google_compute_http_health_check.id
    group: Health Check Configuration
  check_interval_sec:
    desc: "Interval in seconds for the health check to run"
    required: true
    suggest: 1
    group: Health Check Configuration
  timeout_sec:
    desc: "Timeout in seconds for the health check to wait for a response"
    required: true
    suggest: 10
    group: Health Check Configuration
---

# SSL Certificate
resource "google_compute_ssl_certificate" "{{ __name }}" {
  name_prefix = "{{ name }}-cert-"
  private_key = file("{{ private_key_path }}")
  certificate = file("{{ certificate_path }}")

  lifecycle {
    create_before_destroy = true
  }
}

# Target HTTPS Proxy
resource "google_compute_target_https_proxy" "{{ __name }}" {
  name             = "{{ name }}-proxy"
  url_map          = google_compute_url_map.{{ __name }}.id
  ssl_certificates = [google_compute_ssl_certificate.{{ __name }}.id]
}

# URL Map
resource "google_compute_url_map" "{{ __name }}" {
  name        = "{{ name }}-url-map"
  description = "URL map for {{ name }}"

  default_service = google_compute_backend_service.{{ backend_service_name }}.id

  host_rule {
    hosts        = {{ host_names }}
    path_matcher = "allpaths"
  }

  path_matcher {
    name            = "allpaths"
    default_service = google_compute_backend_service.{{ backend_service_name }}.id

    path_rule {
      paths   = ["/*"]
      service = google_compute_backend_service.{{ backend_service_name }}.id
    }
  }
}

# Backend Service
resource "google_compute_backend_service" "{{ backend_service_name }}" {
  name        = "{{ backend_service_name }}"
  port_name   = "http"
  protocol    = "HTTP"
  timeout_sec = 10

  health_checks = [google_compute_http_health_check.{{ health_check_name }}.id]
}

# HTTP Health Check
resource "google_compute_http_health_check" "{{ health_check_name }}" {
  name               = "{{ health_check_name }}"
  request_path       = "/"
  check_interval_sec = "{{ check_interval_sec }}"
  timeout_sec        = "{{ timeout_sec }}"
}

Paste it into Foundry, publish, and explore the UI.

Descriptions

See the desc tags in frontmatter, and note how links can be included - giving users the ability to explore documentation if they want.

Host Names

Note the host_names variables and see how they allow for multiple domains to be included in the UI. Resourcely automatically detects the hosts parameter in Terraform expects a list and adjusts accordingly.

Suggestions and defaults

Adding suggestions and defaults helps users know what they should pick, especially if they aren't familiar

Publish your Blueprint and use it as-is to facilitate encrypted in transit web traffic for your applications!

Conclusion

Encryption doesn't need to be an optional nice-to-have. You can make it easy to add encryption to your resources with Blueprints, and enforce encryption with Guardrails.

We have pre-built encryption Blueprints and Guardrails available for:

  • Crypto keys and key rings

  • Compute Instances

  • Blob Storage

  • Pub/Sub Topics

  • Cloud SQL

  • BigQuery

  • Cloud Functions

You can find them in our GitHub repo of use case examples! Simply copy the code into Foundry, and publish to get started.

Last updated