diff --git a/examples/autokey_example/README.md b/examples/autokey_example/README.md new file mode 100644 index 0000000..ddd2966 --- /dev/null +++ b/examples/autokey_example/README.md @@ -0,0 +1,28 @@ +# Autokey Example + +This example illustrates how to use the `autokey` kms submodule for [KMS Autokey](https://cloud.google.com/kms/docs/autokey-overview) feature. + + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| autokey\_resource\_project\_id | The ID of the project for Autokey to be used (e.g: a storage project which expects to use Autokey as CMEK). | `string` | n/a | yes | +| folder\_id | The Autokey folder number used by Autokey config resource. Required when using Autokey. | `string` | n/a | yes | +| project\_id | The ID of the project in which to provision Autokey resources (autokey keyring and keyHandle keys). | `string` | n/a | yes | + +## Outputs + +| Name | Description | +|------|-------------| +| autokey\_config\_id | An Autokey configuration identifier. | +| autokey\_keyhandles | A map of KeyHandles created. | +| autokey\_project\_id | Project used for autokey. | + + + +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/autokey_example/main.tf b/examples/autokey_example/main.tf new file mode 100644 index 0000000..41d522c --- /dev/null +++ b/examples/autokey_example/main.tf @@ -0,0 +1,43 @@ +/** + * Copyright 2024 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 "autokey" { + source = "terraform-google-modules/kms/google//modules/autokey" + + project_id = var.project_id + autokey_folder_number = var.folder_id + autokey_handles = { + storage_bucket = { + name = "bucket-key-handle", + project = var.autokey_resource_project_id, + resource_type_selector = "storage.googleapis.com/Bucket", + location = "us-central1" + } + compute_disk = { + name = "disk-key-handle", + project = var.autokey_resource_project_id, + resource_type_selector = "compute.googleapis.com/Disk", + location = "us-central1" + } + bigquery_dataset = { + name = "dataset-key-handle", + project = var.autokey_resource_project_id, + resource_type_selector = "bigquery.googleapis.com/Dataset", + location = "us-central1" + } + } +} + diff --git a/examples/autokey_example/outputs.tf b/examples/autokey_example/outputs.tf new file mode 100644 index 0000000..45f188b --- /dev/null +++ b/examples/autokey_example/outputs.tf @@ -0,0 +1,30 @@ +/** + * Copyright 2024 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 "autokey_config_id" { + description = "An Autokey configuration identifier." + value = module.autokey.autokey_config_id != null ? module.autokey.autokey_config_id : "" +} + +output "autokey_keyhandles" { + description = "A map of KeyHandles created." + value = module.autokey.autokey_keyhandles != null ? module.autokey.autokey_keyhandles : {} +} + +output "autokey_project_id" { + description = "Project used for autokey." + value = var.project_id +} diff --git a/examples/autokey_example/variables.tf b/examples/autokey_example/variables.tf new file mode 100644 index 0000000..35a0fa5 --- /dev/null +++ b/examples/autokey_example/variables.tf @@ -0,0 +1,31 @@ +/** + * Copyright 2024 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" { + description = "The ID of the project in which to provision Autokey resources (autokey keyring and keyHandle keys)." + type = string +} + +variable "autokey_resource_project_id" { + description = "The ID of the project for Autokey to be used (e.g: a storage project which expects to use Autokey as CMEK)." + type = string +} + +variable "folder_id" { + type = string + description = "The Autokey folder number used by Autokey config resource. Required when using Autokey." +} + diff --git a/main.tf b/main.tf index 22e3c64..84c72b9 100644 --- a/main.tf +++ b/main.tf @@ -92,4 +92,3 @@ resource "google_kms_crypto_key_iam_binding" "encrypters" { crypto_key_id = local.keys_by_name[element(var.set_encrypters_for, count.index)] members = compact(split(",", var.encrypters[count.index])) } - diff --git a/modules/autokey/README.md b/modules/autokey/README.md new file mode 100644 index 0000000..d9fdf59 --- /dev/null +++ b/modules/autokey/README.md @@ -0,0 +1,21 @@ +# Autokey submodule + +This is a submodule built to make [KMS Autokey](https://cloud.google.com/kms/docs/autokey-overview) feature simple to be used. This submodule will create the [Autokey Config](https://cloud.google.com/kms/docs/enable-autokey#enable-autokey-folder) for an existing folder where you want to enable Autokey, set up the Cloud KMS [service agent](https://cloud.google.com/kms/docs/enable-autokey#autokey-service-agent) on an existing key project and create [Key Handles](https://cloud.google.com/kms/docs/resource-hierarchy#key_handles) for existing resource projects. + + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| autokey\_folder\_number | The Autokey folder number used by Autokey config resource. Required when using Autokey. | `string` | n/a | yes | +| autokey\_handles | (Optional) A KeyHandle is a resource used by Autokey to auto-provision CryptoKeys for CMEK for a particular service.
- name: The resource name for the KeyHandle.
- resource\_type\_selector: Indicates the resource type that the resulting CryptoKey is meant to protect, in the following format: {SERVICE}.googleapis.com/{TYPE}. For example, storage.googleapis.com/Bucket. All Cloud KMS Autokey compatible services available at https://cloud.google.com/kms/docs/autokey-overview#compatible-services.
- location: The location for the KeyHandle. A full list of valid locations can be found by running gcloud kms locations list.
- project: The ID of the project in which the resource belongs. If it is not provided, the provider project is used. |
map(object({
name = string
resource_type_selector = string
location = string
project = string
}))
| `null` | no | +| project\_id | Project id where the Autokey configuration and KeyHandles will be created. | `string` | n/a | yes | + +## Outputs + +| Name | Description | +|------|-------------| +| autokey\_config\_id | An Autokey configuration identifier. | +| autokey\_keyhandles | A map of KeyHandles created. | + + diff --git a/modules/autokey/iam.tf b/modules/autokey/iam.tf new file mode 100644 index 0000000..5d762b0 --- /dev/null +++ b/modules/autokey/iam.tf @@ -0,0 +1,55 @@ +/** + * Copyright 2024 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. + */ + +data "google_project" "kms_project" { + project_id = var.project_id +} + +#Create KMS Service Agent +resource "google_project_service_identity" "kms_service_agent" { + count = local.create_autokey_key_handles ? 1 : 0 + provider = google-beta + + service = "cloudkms.googleapis.com" + project = data.google_project.kms_project.number +} + +# Wait delay after creating service agent. +resource "time_sleep" "wait_service_agent" { + count = local.create_autokey_key_handles ? 1 : 0 + + create_duration = "10s" + depends_on = [google_project_service_identity.kms_service_agent] +} + +#Grant the KMS Service Agent the Cloud KMS Admin role +resource "google_project_iam_member" "autokey_project_admin" { + count = local.create_autokey_key_handles ? 1 : 0 + provider = google-beta + + project = var.project_id + role = "roles/cloudkms.admin" + member = "serviceAccount:service-${data.google_project.kms_project.number}@gcp-sa-cloudkms.iam.gserviceaccount.com" + depends_on = [time_sleep.wait_service_agent] +} + +# Wait delay after granting IAM permissions +resource "time_sleep" "wait_srv_acc_permissions" { + count = local.create_autokey_key_handles ? 1 : 0 + + create_duration = "10s" + depends_on = [google_project_iam_member.autokey_project_admin] +} diff --git a/modules/autokey/main.tf b/modules/autokey/main.tf new file mode 100644 index 0000000..9a60255 --- /dev/null +++ b/modules/autokey/main.tf @@ -0,0 +1,47 @@ +/** + * Copyright 2024 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 { + create_autokey_key_handles = var.autokey_folder_number != null && var.autokey_handles != null +} + +resource "google_kms_autokey_config" "primary" { + count = var.autokey_folder_number != null ? 1 : 0 + provider = google-beta + + folder = var.autokey_folder_number + key_project = "projects/${var.project_id}" +} + +resource "random_string" "suffix" { + count = local.create_autokey_key_handles ? 1 : 0 + + length = 4 + special = false + upper = false +} + +resource "google_kms_key_handle" "primary" { + for_each = local.create_autokey_key_handles ? var.autokey_handles : tomap({}) + provider = google-beta + + project = each.value.project + name = "${each.value.name}-${random_string.suffix[0].result}" + location = each.value.location + resource_type_selector = each.value.resource_type_selector + + depends_on = [time_sleep.wait_srv_acc_permissions] +} diff --git a/modules/autokey/outputs.tf b/modules/autokey/outputs.tf new file mode 100644 index 0000000..1d1ffec --- /dev/null +++ b/modules/autokey/outputs.tf @@ -0,0 +1,25 @@ +/** + * Copyright 2024 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 "autokey_config_id" { + description = "An Autokey configuration identifier." + value = var.autokey_folder_number != null ? google_kms_autokey_config.primary[0].id : "" +} + +output "autokey_keyhandles" { + description = "A map of KeyHandles created." + value = local.create_autokey_key_handles ? google_kms_key_handle.primary : {} +} diff --git a/modules/autokey/variables.tf b/modules/autokey/variables.tf new file mode 100644 index 0000000..0fb78ad --- /dev/null +++ b/modules/autokey/variables.tf @@ -0,0 +1,42 @@ +/** + * Copyright 2024 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" { + description = "Project id where the Autokey configuration and KeyHandles will be created." + type = string +} + +variable "autokey_folder_number" { + type = string + description = "The Autokey folder number used by Autokey config resource. Required when using Autokey." +} + +variable "autokey_handles" { + type = map(object({ + name = string + resource_type_selector = string + location = string + project = string + })) + description = <<-EOF + (Optional) A KeyHandle is a resource used by Autokey to auto-provision CryptoKeys for CMEK for a particular service. + - name: The resource name for the KeyHandle. + - resource_type_selector: Indicates the resource type that the resulting CryptoKey is meant to protect, in the following format: {SERVICE}.googleapis.com/{TYPE}. For example, storage.googleapis.com/Bucket. All Cloud KMS Autokey compatible services available at https://cloud.google.com/kms/docs/autokey-overview#compatible-services. + - location: The location for the KeyHandle. A full list of valid locations can be found by running gcloud kms locations list. + - project: The ID of the project in which the resource belongs. If it is not provided, the provider project is used. + EOF + default = null +} diff --git a/modules/autokey/versions.tf b/modules/autokey/versions.tf new file mode 100644 index 0000000..6bcc625 --- /dev/null +++ b/modules/autokey/versions.tf @@ -0,0 +1,46 @@ +/** + * Copyright 2024 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.13" + required_providers { + + google = { + source = "hashicorp/google" + version = ">= 5.31.0" + } + google-beta = { + source = "hashicorp/google-beta" + version = ">= 5.31.0" + } + time = { + source = "hashicorp/time" + version = ">= 0.12.0" + } + random = { + source = "hashicorp/random" + version = ">= 3.6.2" + } + } + + provider_meta "google" { + module_name = "blueprints/terraform/terraform-google-kms:autokey/v3.0.0" + } + provider_meta "google-beta" { + module_name = "blueprints/terraform/terraform-google-kms:autokey/v3.0.0" + } + +} diff --git a/outputs.tf b/outputs.tf index 838fa4a..8815505 100644 --- a/outputs.tf +++ b/outputs.tf @@ -61,4 +61,3 @@ output "keyring_name" { google_kms_crypto_key_iam_binding.encrypters, ] } - diff --git a/test/integration/autokey_example/autokey_example_test.go b/test/integration/autokey_example/autokey_example_test.go new file mode 100755 index 0000000..b793bda --- /dev/null +++ b/test/integration/autokey_example/autokey_example_test.go @@ -0,0 +1,84 @@ +// Copyright 2024 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. + +package autokey_example + +import ( + "context" + "fmt" + "io" + "regexp" + "testing" + + "github.com/GoogleCloudPlatform/cloud-foundation-toolkit/infra/blueprint-test/pkg/gcloud" + "github.com/GoogleCloudPlatform/cloud-foundation-toolkit/infra/blueprint-test/pkg/tft" + "github.com/GoogleCloudPlatform/cloud-foundation-toolkit/infra/blueprint-test/pkg/utils" + "github.com/stretchr/testify/assert" + "golang.org/x/oauth2/google" +) + +func validateKeyHandleVersion(input string, projectId string, autokeyResource string) bool { + pattern := fmt.Sprintf(`^projects/%s/locations/us-central1/keyRings/autokey/cryptoKeys/%s-(bigquery-dataset|compute-disk|storage-bucket)-.*?/cryptoKeyVersions/1$`, projectId, autokeyResource) + regex := regexp.MustCompile(pattern) + return regex.MatchString(input) +} + +func TestAutokeyExample(t *testing.T) { + bpt := tft.NewTFBlueprintTest(t) + bpt.DefineVerify(func(assert *assert.Assertions) { + bpt.DefaultVerify(assert) + + projectId := bpt.GetStringOutput("autokey_project_id") + autokeyConfig := bpt.GetStringOutput("autokey_config_id") + autokeyResourceProjectNumber := bpt.GetTFSetupJsonOutput("autokey_resource_project_number") + + // Autokey config doesn't have a gcloud command yet. That's why we need to hit the API. + autokeyConfigUrl := fmt.Sprintf("https://cloudkms.googleapis.com/v1/%s", autokeyConfig) + + httpClient, err := google.DefaultClient(context.Background(), "https://www.googleapis.com/auth/cloud-platform") + + if err != nil { + t.Fatal(err.Error()) + } + + resp, err := httpClient.Get(autokeyConfigUrl) + if err != nil { + t.Fatal(err.Error()) + } + + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + t.Fatal(err.Error()) + } + + result := utils.ParseJSONResult(t, string(body)) + + // Asserting if Autokey configuration was created + autokeyConfigProject := result.Get("keyProject").String() + assert.Equal(autokeyConfigProject, fmt.Sprintf("projects/%s", projectId), "autokey expected for project %s", projectId) + + // Asserting if Autokey keyring was created + op := gcloud.Runf(t, "--project=%s kms keyrings list --location us-central1 --filter name:autokey", projectId).Array()[0].Get("name") + assert.Contains(op.String(), fmt.Sprintf("projects/%s/locations/us-central1/keyRings/autokey", projectId), "Contains Autokey KeyRing") + + // Asserting if Autokey keyHandles were created + op1 := gcloud.Runf(t, "kms keys list --project=%s --keyring autokey --location us-central1", projectId).Array() + for _, element := range op1 { + assert.True(validateKeyHandleVersion(element.Get("primary").Map()["name"].Str, projectId, autokeyResourceProjectNumber.Str), "Contains KeyHandles") + } + }) + + bpt.Test() +} diff --git a/test/setup/iam.tf b/test/setup/iam.tf index 54caf5f..a0d1b7f 100644 --- a/test/setup/iam.tf +++ b/test/setup/iam.tf @@ -29,12 +29,12 @@ resource "google_service_account" "int_test" { display_name = "kms-int-test" } -resource "google_project_iam_member" "int_test" { +resource "google_folder_iam_member" "int_test" { count = length(local.int_required_roles) - project = module.project_ci_kms.project_id - role = local.int_required_roles[count.index] - member = "serviceAccount:${google_service_account.int_test.email}" + folder = google_folder.test_folder.folder_id + role = local.int_required_roles[count.index] + member = "serviceAccount:${google_service_account.int_test.email}" } resource "google_service_account_key" "int_test" { diff --git a/test/setup/main.tf b/test/setup/main.tf index b78315b..5c479d8 100644 --- a/test/setup/main.tf +++ b/test/setup/main.tf @@ -14,6 +14,17 @@ * limitations under the License. */ +resource "random_string" "suffix" { + length = 6 + special = false + upper = false +} + +resource "google_folder" "test_folder" { + display_name = "test_kms_fldr_${random_string.suffix.result}" + parent = "folders/${var.folder_id}" +} + module "project_ci_kms" { source = "terraform-google-modules/project-factory/google" version = "~> 16.0" @@ -21,11 +32,35 @@ module "project_ci_kms" { name = "ci-kms-module" random_project_id = "true" org_id = var.org_id - folder_id = var.folder_id + folder_id = google_folder.test_folder.folder_id billing_account = var.billing_account activate_apis = [ "cloudkms.googleapis.com", - "serviceusage.googleapis.com" + "serviceusage.googleapis.com", + "cloudresourcemanager.googleapis.com", + ] + + activate_api_identities = [{ + api = "cloudkms.googleapis.com" + roles = [ + "roles/cloudkms.admin" + ] + }] +} + +module "autokey_resource_project" { + source = "terraform-google-modules/project-factory/google" + version = "~> 15.0" + + name = "autokey-resource" + random_project_id = "true" + org_id = var.org_id + folder_id = google_folder.test_folder.folder_id + billing_account = var.billing_account + + activate_apis = [ + "serviceusage.googleapis.com", + "cloudresourcemanager.googleapis.com" ] } diff --git a/test/setup/outputs.tf b/test/setup/outputs.tf index 8553dd7..63f42bc 100644 --- a/test/setup/outputs.tf +++ b/test/setup/outputs.tf @@ -18,7 +18,19 @@ output "project_id" { value = module.project_ci_kms.project_id } +output "autokey_resource_project_id" { + value = module.autokey_resource_project.project_id +} + +output "autokey_resource_project_number" { + value = module.autokey_resource_project.project_number +} + output "sa_key" { value = google_service_account_key.int_test.private_key sensitive = true } + +output "folder_id" { + value = split("/", google_folder.test_folder.id)[1] +} diff --git a/test/setup/versions.tf b/test/setup/versions.tf index 919dfb5..37e995e 100644 --- a/test/setup/versions.tf +++ b/test/setup/versions.tf @@ -18,7 +18,12 @@ terraform { required_version = ">= 0.13" required_providers { google = { - source = "hashicorp/google" + source = "hashicorp/google" + version = ">= 5.31.0" + } + google-beta = { + source = "hashicorp/google-beta" + version = ">= 5.31.0" } } } diff --git a/versions.tf b/versions.tf index 5ef2034..209676b 100644 --- a/versions.tf +++ b/versions.tf @@ -20,7 +20,7 @@ terraform { google = { source = "hashicorp/google" - version = ">= 5.23.0, < 7" + version = ">= 5.31.0, < 7" } }