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 = <