diff --git a/README.md b/README.md index 86c52c7632..4aea05a5a7 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ Currently available modules: - **compute** - [VM/VM group](./modules/compute-vm), [MIG](./modules/compute-mig), [COS container](./modules/cloud-config-container/cos-generic-metadata/) (coredns, mysql, onprem, squid), [GKE cluster](./modules/gke-cluster-standard), [GKE hub](./modules/gke-hub), [GKE nodepool](./modules/gke-nodepool), [GCVE private cloud](./modules/gcve-private-cloud) - **data** - [Analytics Hub](./modules/analytics-hub), [BigQuery dataset](./modules/bigquery-dataset), [Bigtable instance](./modules/bigtable-instance), [Dataplex](./modules/dataplex), [Dataplex DataScan](./modules/dataplex-datascan), [Cloud SQL instance](./modules/cloudsql-instance), [Spanner instance](./modules/spanner-instance), [Firestore](./modules/firestore), [Data Catalog Policy Tag](./modules/data-catalog-policy-tag), [Data Catalog Tag](./modules/data-catalog-tag), [Data Catalog Tag Template](./modules/data-catalog-tag-template), [Datafusion](./modules/datafusion), [Dataproc](./modules/dataproc), [GCS](./modules/gcs), [Pub/Sub](./modules/pubsub), [Dataform Repository](./modules/dataform-repository/) - **development** - [API Gateway](./modules/api-gateway), [Apigee](./modules/apigee), [Artifact Registry](./modules/artifact-registry), [Container Registry](./modules/container-registry), [Cloud Source Repository](./modules/source-repository), [Workstation cluster](./modules/workstation-cluster) -- **security** - [Binauthz](./modules/binauthz/), [KMS](./modules/kms), [SecretManager](./modules/secret-manager), [VPC Service Control](./modules/vpc-sc), [Certificate Manager](./modules/certificate-manager/) +- **security** - [Binauthz](./modules/binauthz/), [Certificate Authority Service (CAS)](./certificate-authority-service), [KMS](./modules/kms), [SecretManager](./modules/secret-manager), [VPC Service Control](./modules/vpc-sc), [Certificate Manager](./modules/certificate-manager/) - **serverless** - [Cloud Function v1](./modules/cloud-function-v1), [Cloud Function v2](./modules/cloud-function-v2), [Cloud Run](./modules/cloud-run), [Cloud Run v2](./modules/cloud-run-v2) For more information and usage examples see each module's README file. diff --git a/modules/README.md b/modules/README.md index 8f21a90ca4..7fc97d6b86 100644 --- a/modules/README.md +++ b/modules/README.md @@ -109,6 +109,7 @@ These modules are used in the examples included in this repository. If you are u ## Security - [Binauthz](./binauthz/) +- [Certificate Authority Service (CAS)](./certificate-authority-service) - [KMS](./kms) - [SecretManager](./secret-manager) - [VPC Service Control](./vpc-sc) diff --git a/modules/certificate-authority-service/README.md b/modules/certificate-authority-service/README.md new file mode 100644 index 0000000000..85074033ba --- /dev/null +++ b/modules/certificate-authority-service/README.md @@ -0,0 +1,125 @@ +# Certificate Authority Service (CAS) + +The module allows you to create one or more CAs and an optional CA pool. + + +- [Examples](#examples) + - [Basic CA infrastructure](#basic-ca-infrastructure) + - [Create custom CAs](#create-custom-cas) + - [Reference an existing CA pool](#reference-an-existing-ca-pool) + - [IAM](#iam) +- [Variables](#variables) +- [Outputs](#outputs) + + +## Examples + +### Basic CA infrastructure + +This is enough to create a test CA pool and a self-signed root CA. + +```hcl +module "cas" { + source = "./fabric/modules/certificate-authority-service" + project_id = var.project_id + location = "europe-west1" + ca_pool_config = { + name = "test-cas" + } +} +# tftest modules=1 resources=2 inventory=basic.yaml +``` + +### Create custom CAs + +You can create multiple, custom CAs. + +```hcl +module "cas" { + source = "./fabric/modules/certificate-authority-service" + project_id = var.project_id + location = "europe-west1" + ca_pool_config = { + name = "test-cas" + } + ca_configs = { + root_ca_1 = { + key_spec_algorithm = "RSA_PKCS1_4096_SHA256" + key_usage = { + client_auth = true + server_auth = true + } + } + root_ca_2 = { + subject = { + common_name = "test2.example.com" + organization = "Example" + } + } + } +} +# tftest modules=1 resources=3 inventory=custom_cas.yaml +``` + +### Reference an existing CA pool + +```hcl +module "cas" { + source = "./fabric/modules/certificate-authority-service" + project_id = var.project_id + location = "europe-west1" + ca_pool_config = { + ca_pool_id = var.ca_pool_id + } +} +# tftest modules=1 resources=1 inventory=existing_ca.yaml +``` + +### IAM + +You can assign authoritative and addittive IAM roles to identities on the CA pool, using the usual fabric interface (`iam`, `iam_bindings`, `iam_binding_addittive`, `iam_by_principals`). + +```hcl +module "cas" { + source = "./fabric/modules/certificate-authority-service" + project_id = var.project_id + location = "europe-west1" + ca_pool_config = { + name = "test-cas" + } + iam = { + "roles/privateca.certificateManager" = [ + var.service_account.iam_email + ] + } + iam_bindings_additive = { + cert-manager = { + member = "group:${var.group_email}" + role = "roles/privateca.certificateManager" + } + } +} +# tftest modules=1 resources=4 inventory=iam.yaml +``` + +## Variables + +| name | description | type | required | default | +|---|---|:---:|:---:|:---:| +| [ca_pool_config](variables.tf#L116) | The CA pool config. If you pass ca_pool_id, an existing pool is used. | object({…}) | ✓ | | +| [location](variables.tf#L140) | The location of the CAs. | string | ✓ | | +| [project_id](variables.tf#L145) | Project id. | string | ✓ | | +| [ca_configs](variables.tf#L17) | The CA configurations. | map(object({…})) | | {…} | +| [iam](variables-iam.tf#L17) | IAM bindings in {ROLE => [MEMBERS]} format. | map(list(string)) | | {} | +| [iam_bindings](variables-iam.tf#L24) | Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary. | map(object({…})) | | {} | +| [iam_bindings_additive](variables-iam.tf#L39) | Individual additive IAM bindings. Keys are arbitrary. | map(object({…})) | | {} | +| [iam_by_principals](variables-iam.tf#L54) | Authoritative IAM binding in {PRINCIPAL => [ROLES]} format. Principals need to be statically defined to avoid cycle errors. Merged internally with the `iam` variable. | map(list(string)) | | {} | + +## Outputs + +| name | description | sensitive | +|---|---|:---:| +| [ca_ids](outputs.tf#L17) | The CA ids. | | +| [ca_pool_id](outputs.tf#L25) | The CA pool id. | | +| [cas](outputs.tf#L30) | The CAs. | | + diff --git a/modules/certificate-authority-service/iam.tf b/modules/certificate-authority-service/iam.tf new file mode 100644 index 0000000000..d1b3cf4acc --- /dev/null +++ b/modules/certificate-authority-service/iam.tf @@ -0,0 +1,71 @@ +/** + * 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. + */ + +# tfdoc:file:description IAM bindings. + +locals { + _iam_principal_roles = distinct(flatten(values(var.iam_by_principals))) + _iam_principals = { + for r in local._iam_principal_roles : r => [ + for k, v in var.iam_by_principals : + k if try(index(v, r), null) != null + ] + } + iam = { + for role in distinct(concat(keys(var.iam), keys(local._iam_principals))) : + role => concat( + try(var.iam[role], []), + try(local._iam_principals[role], []) + ) + } +} + +resource "google_privateca_ca_pool_iam_binding" "authoritative" { + for_each = local.iam + ca_pool = local.ca_pool_id + role = each.key + members = each.value +} + +resource "google_privateca_ca_pool_iam_binding" "bindings" { + for_each = var.iam_bindings + ca_pool = local.ca_pool_id + role = each.value.role + members = each.value.members + dynamic "condition" { + for_each = each.value.condition == null ? [] : [""] + content { + expression = each.value.condition.expression + title = each.value.condition.title + description = each.value.condition.description + } + } +} + +resource "google_privateca_ca_pool_iam_member" "bindings" { + for_each = var.iam_bindings_additive + ca_pool = local.ca_pool_id + role = each.value.role + member = each.value.member + dynamic "condition" { + for_each = each.value.condition == null ? [] : [""] + content { + expression = each.value.condition.expression + title = each.value.condition.title + description = each.value.condition.description + } + } +} diff --git a/modules/certificate-authority-service/main.tf b/modules/certificate-authority-service/main.tf new file mode 100644 index 0000000000..66d0872423 --- /dev/null +++ b/modules/certificate-authority-service/main.tf @@ -0,0 +1,104 @@ +/** + * 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 { + ca_pool_id = coalesce( + var.ca_pool_config.ca_pool_id == null, + try(google_privateca_ca_pool.ca_pool[0].name, null) + ) +} +resource "google_privateca_ca_pool" "ca_pool" { + count = var.ca_pool_config.ca_pool_id == null ? 1 : 0 + name = var.ca_pool_config.name + project = var.project_id + location = "europe-west8" + tier = "DEVOPS" +} + +resource "google_privateca_certificate_authority" "cas" { + for_each = var.ca_configs + pool = local.ca_pool_id + certificate_authority_id = each.key + project = var.project_id + location = var.location + type = each.value.type + deletion_protection = each.value.deletion_protection + lifetime = each.value.lifetime + pem_ca_certificate = each.value.pem_ca_certificate + ignore_active_certificates_on_deletion = each.value.ignore_active_certificates_on_deletion + skip_grace_period = each.value.skip_grace_period + gcs_bucket = each.value.gcs_bucket + labels = each.value.labels + + config { + subject_config { + subject { + common_name = each.value.subject.common_name + country_code = each.value.subject.country_code + organizational_unit = each.value.subject.organizational_unit + locality = each.value.subject.locality + organization = each.value.subject.organization + province = each.value.subject.province + street_address = each.value.subject.street_address + postal_code = each.value.subject.postal_code + } + subject_alt_name { + dns_names = each.value.subject_alt_name.dns_names + email_addresses = each.value.subject_alt_name.email_addresses + ip_addresses = each.value.subject_alt_name.ip_addresses + uris = each.value.subject_alt_name.uris + } + } + x509_config { + ca_options { + is_ca = each.value.is_ca + } + key_usage { + base_key_usage { + cert_sign = each.value.key_usage.cert_sign + content_commitment = each.value.key_usage.content_commitment + crl_sign = each.value.key_usage.crl_sign + data_encipherment = each.value.key_usage.data_encipherment + decipher_only = each.value.key_usage.decipher_only + digital_signature = each.value.key_usage.digital_signature + encipher_only = each.value.key_usage.encipher_only + key_agreement = each.value.key_usage.key_agreement + key_encipherment = each.value.key_usage.key_encipherment + } + extended_key_usage { + client_auth = each.value.key_usage.client_auth + code_signing = each.value.key_usage.code_signing + email_protection = each.value.key_usage.email_protection + ocsp_signing = each.value.key_usage.ocsp_signing + server_auth = each.value.key_usage.server_auth + time_stamping = each.value.key_usage.time_stamping + } + } + } + } + + key_spec { + algorithm = each.value.key_spec.algorithm + cloud_kms_key_version = each.value.key_spec.kms_key_id + } + + subordinate_config { + certificate_authority = each.value.subordinate_config.root_ca_id + pem_issuer_chain { + pem_certificates = each.value.subordinate_config.pem_issuer_certificates + } + } +} diff --git a/modules/certificate_authority_service/outputs.tf b/modules/certificate-authority-service/outputs.tf similarity index 94% rename from modules/certificate_authority_service/outputs.tf rename to modules/certificate-authority-service/outputs.tf index fb1c62703a..7912d2a083 100644 --- a/modules/certificate_authority_service/outputs.tf +++ b/modules/certificate-authority-service/outputs.tf @@ -14,11 +14,6 @@ * limitations under the License. */ -output "ca_pool_id" { - description = "The CA pool id." - value = google_privateca_ca_pool.ca_pool.id -} - output "ca_ids" { description = "The CA ids." value = { @@ -27,6 +22,11 @@ output "ca_ids" { } } +output "ca_pool_id" { + description = "The CA pool id." + value = local.ca_pool_id +} + output "cas" { description = "The CAs." value = { diff --git a/modules/certificate-authority-service/variables-iam.tf b/modules/certificate-authority-service/variables-iam.tf new file mode 100644 index 0000000000..4299d45540 --- /dev/null +++ b/modules/certificate-authority-service/variables-iam.tf @@ -0,0 +1,59 @@ +/** + * 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 "iam" { + description = "IAM bindings in {ROLE => [MEMBERS]} format." + type = map(list(string)) + default = {} + nullable = false +} + +variable "iam_bindings" { + description = "Authoritative IAM bindings in {KEY => {role = ROLE, members = [], condition = {}}}. Keys are arbitrary." + type = map(object({ + members = list(string) + role = string + condition = optional(object({ + expression = string + title = string + description = optional(string) + })) + })) + nullable = false + default = {} +} + +variable "iam_bindings_additive" { + description = "Individual additive IAM bindings. Keys are arbitrary." + type = map(object({ + member = string + role = string + condition = optional(object({ + expression = string + title = string + description = optional(string) + })) + })) + nullable = false + default = {} +} + +variable "iam_by_principals" { + description = "Authoritative IAM binding in {PRINCIPAL => [ROLES]} format. Principals need to be statically defined to avoid cycle errors. Merged internally with the `iam` variable." + type = map(list(string)) + default = {} + nullable = false +} diff --git a/modules/certificate-authority-service/variables.tf b/modules/certificate-authority-service/variables.tf new file mode 100644 index 0000000000..92e23374cf --- /dev/null +++ b/modules/certificate-authority-service/variables.tf @@ -0,0 +1,148 @@ +/** + * 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 "ca_configs" { + description = "The CA configurations." + type = map(object({ + deletion_protection = optional(string, true) + type = optional(string, "SELF_SIGNED") + is_ca = optional(bool, true) + lifetime = optional(string, null) + pem_ca_certificate = optional(string, null) + ignore_active_certificates_on_deletion = optional(bool, false) + skip_grace_period = optional(bool, true) + labels = optional(map(string), {}) + gcs_bucket = optional(string, null) + key_spec = optional(object({ + algorithm = optional(string, "RSA_PKCS1_2048_SHA256") + kms_key_id = optional(string, null) + }), {}) + key_usage = optional(object({ + cert_sign = optional(bool, true) + client_auth = optional(bool, false) + code_signing = optional(bool, false) + content_commitment = optional(bool, false) + crl_sign = optional(bool, true) + data_encipherment = optional(bool, false) + decipher_only = optional(bool, false) + digital_signature = optional(bool, false) + email_protection = optional(bool, false) + encipher_only = optional(bool, false) + key_agreement = optional(bool, false) + key_encipherment = optional(bool, true) + ocsp_signing = optional(bool, false) + server_auth = optional(bool, true) + time_stamping = optional(bool, false) + }), {}) + subject = optional(object({ + common_name = string + organization = string + country_code = optional(string) + locality = optional(string) + organizational_unit = optional(string) + postal_code = optional(string) + province = optional(string) + street_address = optional(string) + }), { + common_name = "test.example.com" + organization = "Test Example" + }) + subject_alt_name = optional(object({ + dns_names = optional(list(string), []) + email_addresses = optional(list(string), []) + ip_addresses = optional(list(string), []) + uris = optional(list(string), []) + }), {}) + subordinate_config = optional(object({ + root_ca_id = optional(string) + pem_issuer_certificates = optional(list(string)) + }), {}) + })) + nullable = false + default = { + test-ca = {} + } + validation { + condition = ( + length([ + for _, v in var.ca_configs + : v.type + if v.type != null + && !contains(["SELF_SIGNED", "SUBORDINATE"], v.type) + ]) == 0 + ) + error_message = "Type can only be `SELF_SIGNED` or `SUBORDINATE`." + } + validation { + condition = ( + length([ + for _, v in var.ca_configs + : v.key_spec.algorithm + if v.key_spec.algorithm != null + && !contains([ + "EC_P256_SHA256", + "EC_P384_SHA384", + "RSA_PSS_2048_SHA256", + "RSA_PSS_3072_SHA256", + "RSA_PSS_4096_SHA256", + "RSA_PKCS1_2048_SHA256", + "RSA_PKCS1_3072_SHA256", + "RSA_PKCS1_4096_SHA256", + "SIGN_HASH_ALGORITHM_UNSPECIFIED" + ], v.key_spec.algorithm) + ]) == 0 + ) + error_message = <