diff --git a/gcp/modules/fulcio/global/network.tf b/gcp/modules/fulcio/global/network.tf new file mode 100644 index 0000000..0228733 --- /dev/null +++ b/gcp/modules/fulcio/global/network.tf @@ -0,0 +1,358 @@ +/** + * Copyright 2026 The Sigstore Authors + * + * 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. + */ + +########################### PER-SERVICE ############################ +# One per service globally, whether single region or multi-region. # +#################################################################### + +locals { + hostname = trimsuffix("fulcio.${var.dns_domain_name}", ".") +} + +resource "google_dns_record_set" "A_fulcio" { + count = var.manage_dns_a_record ? 1 : 0 + + name = "fulcio.${var.dns_domain_name}" + type = "A" + ttl = 60 + + project = var.project_id + + managed_zone = var.dns_zone_name + rrdatas = [google_compute_global_address.gce_lb_ipv4.address] +} + +resource "google_certificate_manager_dns_authorization" "fulcio_auth" { + count = var.single_region ? 0 : 1 + + name = "fulcio-dns-auth" + domain = local.hostname +} + +resource "google_dns_record_set" "CNAME_auth_fulcio" { + count = var.single_region ? 0 : 1 + + project = var.project_id + + name = google_certificate_manager_dns_authorization.fulcio_auth[count.index].dns_resource_record[0].name + type = google_certificate_manager_dns_authorization.fulcio_auth[count.index].dns_resource_record[0].type + ttl = 60 + + managed_zone = var.dns_zone_name + rrdatas = [google_certificate_manager_dns_authorization.fulcio_auth[count.index].dns_resource_record[0].data] +} + +resource "google_compute_global_address" "gce_lb_ipv4" { + name = var.lb_address_name == "" ? format("fulcio-%s-gce-ext-lb", var.cluster_name) : var.lb_address_name + address_type = "EXTERNAL" + project = var.project_id +} + +resource "google_compute_security_policy" "http_security_policy" { + count = var.enable_cloud_armor ? 1 : 0 + + name = var.cloud_armor_policy_name + project = var.project_id + type = "CLOUD_ARMOR" + + dynamic "rule" { + for_each = var.cloud_armor_rules + content { + action = rule.value.action + priority = rule.value.priority + match { + versioned_expr = rule.value.match.versioned_expr + dynamic "config" { + for_each = rule.value.match.config != null ? [rule.value.match.config] : [] + content { + src_ip_ranges = config.value.src_ip_ranges + } + } + dynamic "expr" { + for_each = rule.value.match.expr != null ? [rule.value.match.expr] : [] + content { + expression = expr.value.expression + } + } + } + + dynamic "rate_limit_options" { + for_each = rule.value.rate_limit_options != null ? [rule.value.rate_limit_options] : [] + content { + enforce_on_key = rate_limit_options.value.enforce_on_key + conform_action = rate_limit_options.value.conform_action + exceed_action = rate_limit_options.value.exceed_action + rate_limit_threshold { + count = rate_limit_options.value.qpm_rate_limit + interval_sec = rate_limit_options.value.interval_sec + } + } + } + + dynamic "redirect_options" { + for_each = rule.value.redirect_options != null ? [rule.value.redirect_options] : [] + content { + type = redirect_options.value.type + target = redirect_options.value.target + } + } + + description = rule.value.description + } + + } + + rule { + action = "allow" + priority = "2147483647" + match { + versioned_expr = "SRC_IPS_V1" + config { + src_ip_ranges = ["*"] + } + } + description = "default rule" + } + + advanced_options_config { + json_parsing = "STANDARD" + } + + adaptive_protection_config { + layer_7_ddos_defense_config { + enable = var.enable_adaptive_protection + } + } +} + +resource "google_compute_ssl_policy" "ssl_policy" { + count = var.enable_ssl_policy ? 1 : 0 + + name = var.ssl_policy_name + project = var.project_id + + profile = "MODERN" + min_tls_version = "TLS_1_2" +} + +####################### PER-SERVICE MULTIREGION ######################## +# One per service globally, only if using a multi-region configuration # +# rather than K8s-Ingress-driven load balancing. # +######################################################################## + +resource "google_compute_health_check" "http_health_check" { + count = var.single_region ? 0 : 1 + + name = "fulcio-http-health-check" + project = var.project_id + + timeout_sec = 5 + check_interval_sec = 5 + healthy_threshold = 2 + unhealthy_threshold = 2 + + http_health_check { + request_path = "/healthz" + port_specification = "USE_SERVING_PORT" + } + + log_config { + enable = var.enable_healthcheck_logging + } +} + +resource "google_compute_health_check" "grpc_health_check" { + count = var.single_region ? 0 : 1 + + name = "fulcio-grpc-health-check" + project = var.project_id + + timeout_sec = 5 + check_interval_sec = 5 + healthy_threshold = 2 + unhealthy_threshold = 2 + + tcp_health_check { + port_specification = "USE_SERVING_PORT" + } + + log_config { + enable = var.enable_healthcheck_logging + } +} + +data "google_compute_network_endpoint_group" "k8s_http_neg" { + for_each = toset(var.network_endpoint_group_zones) + + name = var.network_endpoint_group_name + project = var.project_id + zone = each.key +} + +data "google_compute_network_endpoint_group" "k8s_grpc_neg" { + for_each = toset(var.network_endpoint_group_zones) + + name = var.network_endpoint_group_name_grpc + project = var.project_id + zone = each.key +} + +resource "google_compute_backend_service" "http_backend_service" { + count = var.single_region ? 0 : 1 + + name = "fulcio-http-backend" + project = var.project_id + + load_balancing_scheme = "EXTERNAL_MANAGED" + port_name = "http" + protocol = "HTTP" + + connection_draining_timeout_sec = 15 + health_checks = [google_compute_health_check.http_health_check[count.index].id] + + dynamic "backend" { + for_each = data.google_compute_network_endpoint_group.k8s_http_neg + iterator = neg + content { + group = neg.value.id + balancing_mode = "RATE" + max_rate_per_endpoint = var.backend_service_max_rps + } + } + + depends_on = [google_compute_security_policy.http_security_policy] + security_policy = length(google_compute_security_policy.http_security_policy) > 0 ? google_compute_security_policy.http_security_policy[0].self_link : "" + + log_config { + enable = var.enable_backend_service_logging + } +} + +resource "google_compute_backend_service" "grpc_backend_service" { + count = var.single_region ? 0 : 1 + + name = "fulcio-grpc-backend" + project = var.project_id + + load_balancing_scheme = "EXTERNAL_MANAGED" + port_name = "grpc" + protocol = "HTTP2" + + connection_draining_timeout_sec = 15 + health_checks = [google_compute_health_check.grpc_health_check[count.index].id] + + dynamic "backend" { + for_each = data.google_compute_network_endpoint_group.k8s_grpc_neg + iterator = neg + content { + group = neg.value.id + balancing_mode = "RATE" + max_rate_per_endpoint = var.backend_service_max_rps + } + } + + depends_on = [google_compute_security_policy.http_security_policy] + security_policy = length(google_compute_security_policy.http_security_policy) > 0 ? google_compute_security_policy.http_security_policy[0].self_link : "" + + log_config { + enable = var.enable_backend_service_logging + } +} + +resource "google_compute_url_map" "url_map" { + count = var.single_region ? 0 : 1 + + name = "fulcio-lb" + project = var.project_id + + default_service = google_compute_backend_service.http_backend_service[count.index].id + + host_rule { + hosts = var.dns_domain_name == "" ? ["*"] : [local.hostname] + path_matcher = "fulcio" + } + + path_matcher { + name = "fulcio" + default_service = google_compute_backend_service.http_backend_service[count.index].id + path_rule { + paths = ["/*"] + service = google_compute_backend_service.http_backend_service[count.index].id + } + path_rule { + paths = ["/dev.sigstore.fulcio.v2.CA"] + service = google_compute_backend_service.grpc_backend_service[count.index].id + } + path_rule { + paths = ["/dev.sigstore.fulcio.v2.CA/*"] + service = google_compute_backend_service.grpc_backend_service[count.index].id + } + } +} + +resource "google_certificate_manager_certificate" "ssl_certificate" { + count = var.single_region ? 0 : 1 + + name = "fulcio-ssl-cert" + project = var.project_id + + managed { + domains = [local.hostname] + dns_authorizations = [ + google_certificate_manager_dns_authorization.fulcio_auth[count.index].id + ] + } +} + +resource "google_certificate_manager_certificate_map" "fulcio_certificate_map" { + count = var.single_region ? 0 : 1 + + name = "fulcio-cert-map" +} + +resource "google_certificate_manager_certificate_map_entry" "fulcio_certificate_map_entry" { + count = var.single_region ? 0 : 1 + + name = "fulcio-cert-map-entry" + map = google_certificate_manager_certificate_map.fulcio_certificate_map[count.index].name + certificates = [google_certificate_manager_certificate.ssl_certificate[count.index].id] + hostname = local.hostname +} + +resource "google_compute_target_https_proxy" "lb_proxy" { + count = var.single_region ? 0 : 1 + + name = "fulcio-https-proxy" + project = var.project_id + + url_map = google_compute_url_map.url_map[count.index].id + + ssl_policy = google_compute_ssl_policy.ssl_policy[count.index].id + certificate_map = "//certificatemanager.googleapis.com/${google_certificate_manager_certificate_map.fulcio_certificate_map[count.index].id}" +} + +resource "google_compute_global_forwarding_rule" "https_forwarding_rule" { + count = var.single_region ? 0 : 1 + + name = "fulcio-https-forwarding-rule" + project = var.project_id + + ip_address = google_compute_global_address.gce_lb_ipv4.address + target = google_compute_target_https_proxy.lb_proxy[count.index].id + port_range = "443" + load_balancing_scheme = "EXTERNAL_MANAGED" + ip_protocol = "TCP" +} diff --git a/gcp/modules/fulcio/global/variables.tf b/gcp/modules/fulcio/global/variables.tf new file mode 100644 index 0000000..6eae45c --- /dev/null +++ b/gcp/modules/fulcio/global/variables.tf @@ -0,0 +1,171 @@ +/** + * Copyright 2026 The Sigstore Authors + * + * 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" { + type = string + default = "" + validation { + condition = length(var.project_id) > 0 + error_message = "Must specify project_id variable." + } +} + +variable "single_region" { + description = "Whether this module instance is only deployed in one region, and therefore in charge of managing its own IP address and DNS record but not other load balancer resources." + type = bool + default = false +} + +variable "lb_address_name" { + description = "Name of the global address of the load balancer. If not specified, defaults to 'fulcio-CLUSTER_NAME-gce-ext-lb'." + type = string + default = "" +} + +variable "cluster_name" { + description = "The name to give the new Kubernetes cluster." + type = string + default = "" +} + +variable "dns_zone_name" { + description = "Name of DNS Zone object in Google Cloud DNS" + type = string +} + +variable "dns_domain_name" { + description = "Name of DNS domain name in Google Cloud DNS" + type = string +} + +variable "manage_dns_a_record" { + description = "Whether this module is in charge of managing the DNS A record. This is to enable transitioning from having DNS managed in a single region to managing the same record globally for all regions." + type = bool + default = true +} + +variable "enable_cloud_armor" { + description = "Whether to create a Cloud Armor security policy." + type = bool + default = false +} + +variable "cloud_armor_policy_name" { + description = "Name of the Cloud Armor policy." + type = string + default = "fulcio-service-security-policy" +} + +variable "cloud_armor_rules" { + description = "Cloud Armor security policy rules." + type = list(object({ + action = string + priority = number + description = optional(string) + + match = object({ + versioned_expr = optional(string) + + config = optional(object({ + src_ip_ranges = list(string) + })) + + expr = optional(object({ + expression = string + })) + }) + + rate_limit_options = optional(object({ + enforce_on_key = string + conform_action = string + exceed_action = string + qpm_rate_limit = number + interval_sec = number + })) + + redirect_options = optional(object({ + type = string + target = string + })) + })) + default = [] +} + +variable "enable_adaptive_protection" { + description = "Whether to enable layer 7 DDoS adaptive protection in Cloud Armor." + type = bool + default = true +} + +variable "enable_ssl_policy" { + description = "Whether to create a SSL policy." + type = bool + default = false +} + +variable "ssl_policy_name" { + description = "Name of the SSL policy." + type = string + default = "fulcio-ingress-ssl-policy" +} + +variable "http_service_port" { + description = "The internal HTTP port for the service pod" + type = string + default = "5555" +} + +variable "grpc_service_port" { + description = "The internal HTTP port for the service pod" + type = string + default = "5554" +} + +variable "enable_healthcheck_logging" { + description = "Whether to enable logging for the HTTP health check" + type = bool + default = true +} + +variable "network_endpoint_group_zones" { + type = list(string) + description = "zones where the NEGs live. NEGs will not exist until the Kubernetes service they belong to exists and creates them. This value must be set to empty if NEGs are not expected to exist yet, and then can later be updated." + default = [] +} + +variable "network_endpoint_group_name" { + description = "Name of the NEG that will be created for the HTTP service by the Fulcio Kubernetes service." + type = string + default = "" +} + +variable "network_endpoint_group_name_grpc" { + description = "Name of the NEG that will be created for the gRPC service by the Fulcio Kubernetes service." + type = string + default = "" +} + +variable "backend_service_max_rps" { + description = "Max requests per second that a single backend instance can handle." + type = number + default = 100 +} + +variable "enable_backend_service_logging" { + description = "Whether to enable logging for the HTTP backend service." + type = bool + default = true +} diff --git a/gcp/modules/fulcio/global/versions.tf b/gcp/modules/fulcio/global/versions.tf new file mode 100644 index 0000000..371c313 --- /dev/null +++ b/gcp/modules/fulcio/global/versions.tf @@ -0,0 +1,26 @@ +/** + * Copyright 2026 The Sigstore Authors + * + * 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 = "1.14.2" + + required_providers { + google = { + version = "7.21.0" + source = "hashicorp/google" + } + } +} diff --git a/gcp/modules/fulcio/network.tf b/gcp/modules/fulcio/network.tf index a68ef61..080f9bf 100644 --- a/gcp/modules/fulcio/network.tf +++ b/gcp/modules/fulcio/network.tf @@ -14,107 +14,39 @@ * limitations under the License. */ -resource "google_dns_record_set" "A_fulcio" { - count = var.dns_domain_name == "" ? 0 : 1 - name = "fulcio.${var.dns_domain_name}" - type = "A" - ttl = 60 +module "global" { + count = var.single_region ? 1 : 0 - project = var.project_id - managed_zone = var.dns_zone_name + source = "./global" - rrdatas = [google_compute_global_address.gce_lb_ipv4.address] -} - -// Create a static global IP for the external IPV4 GCE L7 load balancer -resource "google_compute_global_address" "gce_lb_ipv4" { - name = format("fulcio-%s-gce-ext-lb", var.cluster_name) - address_type = "EXTERNAL" - project = var.project_id -} - -resource "google_compute_security_policy" "http_security_policy" { - count = var.enable_cloud_armor ? 1 : 0 - - name = "fulcio-service-security-policy" - project = var.project_id - type = "CLOUD_ARMOR" - - dynamic "rule" { - for_each = var.cloud_armor_rules - content { - action = rule.value.action - priority = rule.value.priority - match { - versioned_expr = rule.value.match.versioned_expr - dynamic "config" { - for_each = rule.value.match.config != null ? [rule.value.match.config] : [] - content { - src_ip_ranges = config.value.src_ip_ranges - } - } - dynamic "expr" { - for_each = rule.value.match.expr != null ? [rule.value.match.expr] : [] - content { - expression = expr.value.expression - } - } - } - - dynamic "rate_limit_options" { - for_each = rule.value.rate_limit_options != null ? [rule.value.rate_limit_options] : [] - content { - enforce_on_key = rate_limit_options.value.enforce_on_key - conform_action = rate_limit_options.value.conform_action - exceed_action = rate_limit_options.value.exceed_action - rate_limit_threshold { - count = rate_limit_options.value.qpm_rate_limit - interval_sec = rate_limit_options.value.interval_sec - } - } - } + project_id = var.project_id - dynamic "redirect_options" { - for_each = rule.value.redirect_options != null ? [rule.value.redirect_options] : [] - content { - type = redirect_options.value.type - target = redirect_options.value.target - } - } + single_region = true + manage_dns_a_record = var.manage_dns_a_record - description = rule.value.description - } + dns_zone_name = var.dns_zone_name + dns_domain_name = var.dns_domain_name - } + cluster_name = var.cluster_name - rule { - action = "allow" - priority = "2147483647" - match { - versioned_expr = "SRC_IPS_V1" - config { - src_ip_ranges = ["*"] - } - } - description = "default rule" - } - - advanced_options_config { - json_parsing = "STANDARD" - } - - adaptive_protection_config { - layer_7_ddos_defense_config { - enable = var.enable_adaptive_protection - } - } + enable_cloud_armor = var.enable_cloud_armor + cloud_armor_rules = var.cloud_armor_rules + enable_adaptive_protection = var.enable_adaptive_protection + enable_ssl_policy = var.enable_ssl_policy } - -resource "google_compute_ssl_policy" "ssl_policy" { - count = var.enable_ssl_policy ? 1 : 0 - name = "fulcio-ingress-ssl-policy" - project = var.project_id - - profile = "MODERN" - min_tls_version = "TLS_1_2" +moved { + from = google_dns_record_set.A_fulcio + to = module.global[0].google_dns_record_set.A_fulcio +} +moved { + from = google_compute_global_address.gce_lb_ipv4 + to = module.global[0].google_compute_global_address.gce_lb_ipv4 +} +moved { + from = google_compute_security_policy.http_security_policy + to = module.global[0].google_compute_security_policy.http_security_policy +} +moved { + from = google_compute_ssl_policy.ssl_policy + to = module.global[0].google_compute_ssl_policy.ssl_policy } diff --git a/gcp/modules/fulcio/variables.tf b/gcp/modules/fulcio/variables.tf index 6f92258..3147e5a 100644 --- a/gcp/modules/fulcio/variables.tf +++ b/gcp/modules/fulcio/variables.tf @@ -28,6 +28,12 @@ variable "region" { description = "GCP region" } +variable "single_region" { + description = "Whether this module instance is only deployed in one region, and therefore in charge of managing its own IP address and DNS record but not other load balancer resources." + type = bool + default = true +} + variable "cluster_name" { description = "The name to give the new Kubernetes cluster." type = string @@ -92,6 +98,12 @@ variable "dns_domain_name" { type = string } +variable "manage_dns_a_record" { + description = "Whether this module is in charge of managing the DNS A record. This is to enable transitioning from having DNS managed in a single region to managing the same record globally for all regions." + type = bool + default = true +} + // Network variable "enable_cloud_armor" { description = "Whether to create a Cloud Armor security policy." diff --git a/gcp/modules/sigstore/sigstore.tf b/gcp/modules/sigstore/sigstore.tf index b808afb..ffa7911 100644 --- a/gcp/modules/sigstore/sigstore.tf +++ b/gcp/modules/sigstore/sigstore.tf @@ -307,6 +307,10 @@ module "fulcio" { enable_cloud_armor = var.fulcio_enable_cloud_armor enable_ssl_policy = var.fulcio_enable_ssl_policy + // Load balancing + single_region = var.single_region + manage_dns_a_record = var.fulcio_manage_dns_a_record + depends_on = [ module.gke-cluster, module.network, @@ -340,6 +344,10 @@ module "timestamp" { enable_cloud_armor = var.timestamp_enable_cloud_armor enable_ssl_policy = var.timestamp_enable_ssl_policy + // Load balancing + single_region = var.single_region + manage_dns_a_record = var.timestamp_manage_dns_a_record + depends_on = [ module.gke-cluster, module.network, diff --git a/gcp/modules/sigstore/variables.tf b/gcp/modules/sigstore/variables.tf index 693f541..21b4f78 100644 --- a/gcp/modules/sigstore/variables.tf +++ b/gcp/modules/sigstore/variables.tf @@ -52,6 +52,16 @@ variable "dns_domain_name" { default = "" } +/********************************/ +/********* LOAD BALANCER ********/ +/********************************/ + +variable "single_region" { + description = "Whether this module instance is only deployed in one region, and therefore in charge of managing its own IP address and DNS record but not other load balancer resources." + type = bool + default = true +} + /********************************/ /************ BASTION ***********/ /********************************/ @@ -540,6 +550,12 @@ variable "fulcio_enable_ssl_policy" { default = false } +variable "fulcio_manage_dns_a_record" { + description = "Whether this module is in charge of managing the DNS A record for Fulcio. This is to enable transitioning from having DNS managed in a single region to managing the same record globally for all regions." + type = bool + default = true +} + /********************************/ /*********** REKOR v1 ***********/ /********************************/ @@ -670,6 +686,12 @@ variable "timestamp_enable_ssl_policy" { default = false } +variable "timestamp_manage_dns_a_record" { + description = "Whether this module is in charge of managing the DNS A record for TSA. This is to enable transitioning from having DNS managed in a single region to managing the same record globally for all regions." + type = bool + default = true +} + /********************************/ /************* CTLOG ************/ /********************************/ diff --git a/gcp/modules/sigstore_global/global.tf b/gcp/modules/sigstore_global/global.tf new file mode 100644 index 0000000..104a7a1 --- /dev/null +++ b/gcp/modules/sigstore_global/global.tf @@ -0,0 +1,74 @@ +/** + * Copyright 2026 The Sigstore Authors + * + * 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 "project_roles" { + source = "../project_roles" + project_id = var.project_id + iam_members_to_roles = var.iam_members_to_roles +} + +module "timestamp" { + source = "../timestamp/global" + + project_id = var.project_id + + lb_address_name = "timestamp-global-ext-lb" + + dns_zone_name = var.dns_zone_name + dns_domain_name = var.dns_domain_name + manage_dns_a_record = var.timestamp_manage_dns_a_record + + enable_cloud_armor = true + cloud_armor_policy_name = "timestamp-service-security-policy-global" + cloud_armor_rules = var.timestamp_cloud_armor_rules + enable_adaptive_protection = true + enable_ssl_policy = true + ssl_policy_name = "timestamp-ingress-ssl-policy-global" + + network_endpoint_group_zones = var.network_endpoint_group_zones + network_endpoint_group_name = var.timestamp_network_endpoint_group_name + backend_service_max_rps = var.timestamp_backend_service_max_rps + + enable_healthcheck_logging = var.enable_loadbalancer_logging + enable_backend_service_logging = var.enable_loadbalancer_logging +} + +module "fulcio" { + source = "../fulcio/global" + + project_id = var.project_id + + lb_address_name = "fulcio-global-ext-lb" + + dns_zone_name = var.dns_zone_name + dns_domain_name = var.dns_domain_name + manage_dns_a_record = var.fulcio_manage_dns_a_record + + enable_cloud_armor = true + cloud_armor_policy_name = "fulcio-service-security-policy-global" + cloud_armor_rules = var.fulcio_cloud_armor_rules + enable_adaptive_protection = true + enable_ssl_policy = true + ssl_policy_name = "fulcio-ingress-ssl-policy-global" + + network_endpoint_group_zones = var.network_endpoint_group_zones + network_endpoint_group_name = var.fulcio_network_endpoint_group_name + network_endpoint_group_name_grpc = var.fulcio_network_endpoint_group_name_grpc + backend_service_max_rps = var.fulcio_backend_service_max_rps + + enable_healthcheck_logging = var.enable_loadbalancer_logging + enable_backend_service_logging = var.enable_loadbalancer_logging +} diff --git a/gcp/modules/sigstore_global/variables.tf b/gcp/modules/sigstore_global/variables.tf new file mode 100644 index 0000000..fdf709c --- /dev/null +++ b/gcp/modules/sigstore_global/variables.tf @@ -0,0 +1,164 @@ +/** + * Copyright 2026 The Sigstore Authors + * + * 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" { + type = string + default = "" + validation { + condition = length(var.project_id) > 0 + error_message = "Must specify project_id variable." + } +} + +variable "iam_members_to_roles" { + description = "Map of IAM member (e.g. group:foo@sigstore.dev) to a set of IAM roles (e.g. roles/viewer)" + type = map(set(string)) + default = {} +} + +variable "dns_zone_name" { + description = "Name of DNS Zone object in Google Cloud DNS" + type = string +} + +variable "dns_domain_name" { + description = "Name of DNS domain name in Google Cloud DNS" + type = string +} + +variable "enable_loadbalancer_logging" { + description = "Whether to enable logging for the HTTP health checks and backend services for Fulcio, TSA, Dex" + type = bool + default = true +} + +variable "network_endpoint_group_zones" { + type = list(string) + description = "zones where the NEGs live. NEGs will not exist until the Kubernetes service they belong to exists and creates them. This value must be set to empty if NEGs are not expected to exist yet, and then can later be updated." + default = [] +} + +variable "timestamp_network_endpoint_group_name" { + description = "Name of the NEG that will be created for the HTTP service by the timestamp Kubernetes service." + type = string + default = "" +} + +variable "timestamp_backend_service_max_rps" { + description = "Max requests per second that a single TSA backend instance can handle." + type = number + default = 100 +} + +variable "timestamp_cloud_armor_rules" { + description = "Cloud Armor security policy rules for TSA." + type = list(object({ + action = string + priority = number + description = optional(string) + + match = object({ + versioned_expr = optional(string) + + config = optional(object({ + src_ip_ranges = list(string) + })) + + expr = optional(object({ + expression = string + })) + }) + + rate_limit_options = optional(object({ + enforce_on_key = string + conform_action = string + exceed_action = string + qpm_rate_limit = number + interval_sec = number + })) + + redirect_options = optional(object({ + type = string + target = string + })) + })) + default = [] +} + +variable "timestamp_manage_dns_a_record" { + description = "Whether this module is in charge of managing the DNS A record for TSA. This is to enable transitioning from having DNS managed in a single region to managing the same record globally for all regions." + type = bool + default = true +} + +variable "fulcio_network_endpoint_group_name" { + description = "Name of the NEG that will be created for the HTTP service by the Fulcio Kubernetes service." + type = string + default = "" +} + +variable "fulcio_network_endpoint_group_name_grpc" { + description = "Name of the NEG that will be created for the gRPC service by the Fulcio Kubernetes service." + type = string + default = "" +} + +variable "fulcio_backend_service_max_rps" { + description = "Max requests per second that a single Fulcio backend instance can handle." + type = number + default = 100 +} + +variable "fulcio_cloud_armor_rules" { + description = "Cloud Armor security policy rules for Fulcio." + type = list(object({ + action = string + priority = number + description = optional(string) + + match = object({ + versioned_expr = optional(string) + + config = optional(object({ + src_ip_ranges = list(string) + })) + + expr = optional(object({ + expression = string + })) + }) + + rate_limit_options = optional(object({ + enforce_on_key = string + conform_action = string + exceed_action = string + qpm_rate_limit = number + interval_sec = number + })) + + redirect_options = optional(object({ + type = string + target = string + })) + })) + default = [] +} + +variable "fulcio_manage_dns_a_record" { + description = "Whether this module is in charge of managing the DNS A record for Fulcio. This is to enable transitioning from having DNS managed in a single region to managing the same record globally for all regions." + type = bool + default = true +} diff --git a/gcp/modules/sigstore_global/versions.tf b/gcp/modules/sigstore_global/versions.tf new file mode 100644 index 0000000..371c313 --- /dev/null +++ b/gcp/modules/sigstore_global/versions.tf @@ -0,0 +1,26 @@ +/** + * Copyright 2026 The Sigstore Authors + * + * 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 = "1.14.2" + + required_providers { + google = { + version = "7.21.0" + source = "hashicorp/google" + } + } +} diff --git a/gcp/modules/timestamp/global/network.tf b/gcp/modules/timestamp/global/network.tf new file mode 100644 index 0000000..3807ee2 --- /dev/null +++ b/gcp/modules/timestamp/global/network.tf @@ -0,0 +1,281 @@ +/** + * Copyright 2026 The Sigstore Authors + * + * 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. + */ + +########################### PER-SERVICE ############################ +# One per service globally, whether single region or multi-region. # +#################################################################### + +locals { + hostname = trimsuffix("timestamp.${var.dns_domain_name}", ".") +} + +resource "google_dns_record_set" "A_timestamp" { + count = var.manage_dns_a_record ? 1 : 0 + + name = "timestamp.${var.dns_domain_name}" + type = "A" + ttl = 60 + + project = var.project_id + + managed_zone = var.dns_zone_name + rrdatas = [google_compute_global_address.gce_lb_ipv4.address] +} + +resource "google_certificate_manager_dns_authorization" "timestamp_auth" { + count = var.single_region ? 0 : 1 + + name = "timestamp-dns-auth" + domain = local.hostname +} + +resource "google_dns_record_set" "CNAME_auth_timestamp" { + count = var.single_region ? 0 : 1 + + project = var.project_id + + name = google_certificate_manager_dns_authorization.timestamp_auth[count.index].dns_resource_record[0].name + type = google_certificate_manager_dns_authorization.timestamp_auth[count.index].dns_resource_record[0].type + ttl = 60 + + managed_zone = var.dns_zone_name + rrdatas = [google_certificate_manager_dns_authorization.timestamp_auth[count.index].dns_resource_record[0].data] +} + +resource "google_compute_global_address" "gce_lb_ipv4" { + name = var.lb_address_name == "" ? format("timestamp-%s-gce-ext-lb", var.cluster_name) : var.lb_address_name + address_type = "EXTERNAL" + project = var.project_id +} + +resource "google_compute_security_policy" "http_security_policy" { + count = var.enable_cloud_armor ? 1 : 0 + + name = var.cloud_armor_policy_name + project = var.project_id + type = "CLOUD_ARMOR" + + dynamic "rule" { + for_each = var.cloud_armor_rules + content { + action = rule.value.action + priority = rule.value.priority + match { + versioned_expr = rule.value.match.versioned_expr + dynamic "config" { + for_each = rule.value.match.config != null ? [rule.value.match.config] : [] + content { + src_ip_ranges = config.value.src_ip_ranges + } + } + dynamic "expr" { + for_each = rule.value.match.expr != null ? [rule.value.match.expr] : [] + content { + expression = expr.value.expression + } + } + } + + dynamic "rate_limit_options" { + for_each = rule.value.rate_limit_options != null ? [rule.value.rate_limit_options] : [] + content { + enforce_on_key = rate_limit_options.value.enforce_on_key + conform_action = rate_limit_options.value.conform_action + exceed_action = rate_limit_options.value.exceed_action + rate_limit_threshold { + count = rate_limit_options.value.qpm_rate_limit + interval_sec = rate_limit_options.value.interval_sec + } + } + } + + dynamic "redirect_options" { + for_each = rule.value.redirect_options != null ? [rule.value.redirect_options] : [] + content { + type = redirect_options.value.type + target = redirect_options.value.target + } + } + + description = rule.value.description + } + + } + + rule { + action = "allow" + priority = "2147483647" + match { + versioned_expr = "SRC_IPS_V1" + config { + src_ip_ranges = ["*"] + } + } + description = "default rule" + } + + advanced_options_config { + json_parsing = "STANDARD" + } + + adaptive_protection_config { + layer_7_ddos_defense_config { + enable = var.enable_adaptive_protection + } + } +} + +// Create HTTPS certificate and load balancer configuration if managing DNS, otherwise use HTTP. +// Production instances should ALWAYS set a domain name and use HTTPS. Developers may choose to use HTTP for simpler ephemeral deployments. + +resource "google_compute_ssl_policy" "ssl_policy" { + count = var.enable_ssl_policy ? 1 : 0 + + name = var.ssl_policy_name + project = var.project_id + + profile = "MODERN" + min_tls_version = "TLS_1_2" +} + +####################### PER-SERVICE MULTIREGION ######################## +# One per service globally, only if using a multi-region configuration # +# rather than K8s-Ingress-driven load balancing. # +######################################################################## + +resource "google_compute_health_check" "http_health_check" { + count = var.single_region ? 0 : 1 + + name = "timestamp-http-health-check" + project = var.project_id + + timeout_sec = 5 + check_interval_sec = 5 + healthy_threshold = 2 + unhealthy_threshold = 2 + + http_health_check { + request_path = "/ping" + port_specification = "USE_SERVING_PORT" + } + + log_config { + enable = var.enable_healthcheck_logging + } +} + +data "google_compute_network_endpoint_group" "k8s_http_neg" { + for_each = toset(var.network_endpoint_group_zones) + + name = var.network_endpoint_group_name + project = var.project_id + zone = each.key +} + +resource "google_compute_backend_service" "http_backend_service" { + count = var.single_region ? 0 : 1 + + name = "timestamp-http-backend" + project = var.project_id + + load_balancing_scheme = "EXTERNAL_MANAGED" + port_name = "http" + protocol = "HTTP" + + connection_draining_timeout_sec = 15 + health_checks = [google_compute_health_check.http_health_check[count.index].id] + + dynamic "backend" { + for_each = data.google_compute_network_endpoint_group.k8s_http_neg + iterator = neg + content { + group = neg.value.id + balancing_mode = "RATE" + max_rate_per_endpoint = var.backend_service_max_rps + } + } + + depends_on = [google_compute_security_policy.http_security_policy] + security_policy = length(google_compute_security_policy.http_security_policy) > 0 ? google_compute_security_policy.http_security_policy[0].self_link : "" + + log_config { + enable = var.enable_backend_service_logging + } +} + +resource "google_compute_url_map" "url_map" { + count = var.single_region ? 0 : 1 + + name = "timestamp-lb" + project = var.project_id + + default_service = google_compute_backend_service.http_backend_service[count.index].id +} + +resource "google_certificate_manager_certificate" "ssl_certificate" { + count = var.single_region ? 0 : 1 + + name = "timestamp-ssl-cert" + project = var.project_id + + managed { + domains = [local.hostname] + dns_authorizations = [ + google_certificate_manager_dns_authorization.timestamp_auth[count.index].id + ] + } +} + +resource "google_certificate_manager_certificate_map" "timestamp_certificate_map" { + count = var.single_region ? 0 : 1 + + name = "timestamp-cert-map" +} + +resource "google_certificate_manager_certificate_map_entry" "timestamp_certificate_map_entry" { + count = var.single_region ? 0 : 1 + + name = "timestamp-cert-map-entry" + map = google_certificate_manager_certificate_map.timestamp_certificate_map[count.index].name + certificates = [google_certificate_manager_certificate.ssl_certificate[count.index].id] + hostname = local.hostname +} + +resource "google_compute_target_https_proxy" "lb_proxy" { + count = var.single_region ? 0 : 1 + + name = "timestamp-https-proxy" + project = var.project_id + + url_map = google_compute_url_map.url_map[count.index].id + + ssl_policy = google_compute_ssl_policy.ssl_policy[count.index].id + certificate_map = "//certificatemanager.googleapis.com/${google_certificate_manager_certificate_map.timestamp_certificate_map[count.index].id}" +} + + +resource "google_compute_global_forwarding_rule" "https_forwarding_rule" { + count = var.single_region ? 0 : 1 + + name = "timestamp-https-forwarding-rule" + project = var.project_id + + ip_address = google_compute_global_address.gce_lb_ipv4.address + target = google_compute_target_https_proxy.lb_proxy[count.index].id + port_range = "443" + load_balancing_scheme = "EXTERNAL_MANAGED" + ip_protocol = "TCP" +} diff --git a/gcp/modules/timestamp/global/variables.tf b/gcp/modules/timestamp/global/variables.tf new file mode 100644 index 0000000..c9d711e --- /dev/null +++ b/gcp/modules/timestamp/global/variables.tf @@ -0,0 +1,159 @@ +/** + * Copyright 2026 The Sigstore Authors + * + * 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" { + type = string + default = "" + validation { + condition = length(var.project_id) > 0 + error_message = "Must specify project_id variable." + } +} + +variable "single_region" { + description = "Whether this module instance is only deployed in one region, and therefore in charge of managing its own IP address and DNS record but not other load balancer resources." + type = bool + default = false +} + +variable "lb_address_name" { + description = "Name of the global address of the load balancer. If not specified, defaults to 'timestamp-CLUSTER_NAME-gce-ext-lb'." + type = string + default = "" +} + +variable "cluster_name" { + description = "The name to give the new Kubernetes cluster." + type = string + default = "" +} + +variable "dns_zone_name" { + description = "Name of DNS Zone object in Google Cloud DNS" + type = string +} + +variable "dns_domain_name" { + description = "Name of DNS domain name in Google Cloud DNS" + type = string +} + +variable "manage_dns_a_record" { + description = "Whether this module is in charge of managing the DNS A record. This is to enable transitioning from having DNS managed in a single region to managing the same record globally for all regions." + type = bool + default = true +} + +variable "enable_cloud_armor" { + description = "Whether to create a Cloud Armor security policy." + type = bool + default = false +} + +variable "cloud_armor_policy_name" { + description = "Name of the Cloud Armor policy." + type = string + default = "tsa-service-security-policy" +} + +variable "cloud_armor_rules" { + description = "Cloud Armor security policy rules." + type = list(object({ + action = string + priority = number + description = optional(string) + + match = object({ + versioned_expr = optional(string) + + config = optional(object({ + src_ip_ranges = list(string) + })) + + expr = optional(object({ + expression = string + })) + }) + + rate_limit_options = optional(object({ + enforce_on_key = string + conform_action = string + exceed_action = string + qpm_rate_limit = number + interval_sec = number + })) + + redirect_options = optional(object({ + type = string + target = string + })) + })) + default = [] +} + +variable "enable_adaptive_protection" { + description = "Whether to enable layer 7 DDoS adaptive protection in Cloud Armor." + type = bool + default = true +} + +variable "enable_ssl_policy" { + description = "Whether to create a SSL policy." + type = bool + default = false +} + +variable "ssl_policy_name" { + description = "Name of the SSL policy." + type = string + default = "tsa-ingress-ssl-policy" +} + +variable "network" { + description = "VPC network in which the GKE cluster lives" + type = string + default = "default" +} + +variable "enable_healthcheck_logging" { + description = "Whether to enable logging for the HTTP health check" + type = bool + default = true +} + +variable "network_endpoint_group_zones" { + type = list(string) + description = "zones where the NEGs live. NEGs will not exist until the Kubernetes service they belong to exists and creates them. This value must be set to empty if NEGs are not expected to exist yet, and then can later be updated." + default = [] +} + +variable "network_endpoint_group_name" { + description = "Name of the NEG that will be created for the HTTP service by the timestamp Kubernetes service." + type = string + default = "" +} + +variable "backend_service_max_rps" { + description = "Max requests per second that a single backend instance can handle." + type = number + default = 100 +} + +variable "enable_backend_service_logging" { + description = "Whether to enable logging for the HTTP backend service." + type = bool + default = true +} diff --git a/gcp/modules/timestamp/global/versions.tf b/gcp/modules/timestamp/global/versions.tf new file mode 100644 index 0000000..371c313 --- /dev/null +++ b/gcp/modules/timestamp/global/versions.tf @@ -0,0 +1,26 @@ +/** + * Copyright 2026 The Sigstore Authors + * + * 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 = "1.14.2" + + required_providers { + google = { + version = "7.21.0" + source = "hashicorp/google" + } + } +} diff --git a/gcp/modules/timestamp/network.tf b/gcp/modules/timestamp/network.tf index 849cdfb..594af36 100644 --- a/gcp/modules/timestamp/network.tf +++ b/gcp/modules/timestamp/network.tf @@ -14,107 +14,39 @@ * limitations under the License. */ -resource "google_dns_record_set" "A_timestamp" { - count = var.dns_domain_name == "" ? 0 : 1 - name = "timestamp.${var.dns_domain_name}" - type = "A" - ttl = 60 +module "global" { + count = var.single_region ? 1 : 0 - project = var.project_id - managed_zone = var.dns_zone_name + source = "./global" - rrdatas = [google_compute_global_address.gce_lb_ipv4.address] -} - -// Create a static global IP for the external IPV4 GCE L7 load balancer -resource "google_compute_global_address" "gce_lb_ipv4" { - name = format("timestamp-%s-gce-ext-lb", var.cluster_name) - address_type = "EXTERNAL" - project = var.project_id -} - -resource "google_compute_security_policy" "http_security_policy" { - count = var.enable_cloud_armor ? 1 : 0 - - name = "tsa-service-security-policy" - project = var.project_id - type = "CLOUD_ARMOR" - - dynamic "rule" { - for_each = var.cloud_armor_rules - content { - action = rule.value.action - priority = rule.value.priority - match { - versioned_expr = rule.value.match.versioned_expr - dynamic "config" { - for_each = rule.value.match.config != null ? [rule.value.match.config] : [] - content { - src_ip_ranges = config.value.src_ip_ranges - } - } - dynamic "expr" { - for_each = rule.value.match.expr != null ? [rule.value.match.expr] : [] - content { - expression = expr.value.expression - } - } - } - - dynamic "rate_limit_options" { - for_each = rule.value.rate_limit_options != null ? [rule.value.rate_limit_options] : [] - content { - enforce_on_key = rate_limit_options.value.enforce_on_key - conform_action = rate_limit_options.value.conform_action - exceed_action = rate_limit_options.value.exceed_action - rate_limit_threshold { - count = rate_limit_options.value.qpm_rate_limit - interval_sec = rate_limit_options.value.interval_sec - } - } - } + project_id = var.project_id - dynamic "redirect_options" { - for_each = rule.value.redirect_options != null ? [rule.value.redirect_options] : [] - content { - type = redirect_options.value.type - target = redirect_options.value.target - } - } + single_region = true + manage_dns_a_record = var.manage_dns_a_record - description = rule.value.description - } + dns_zone_name = var.dns_zone_name + dns_domain_name = var.dns_domain_name - } + cluster_name = var.cluster_name - rule { - action = "allow" - priority = "2147483647" - match { - versioned_expr = "SRC_IPS_V1" - config { - src_ip_ranges = ["*"] - } - } - description = "default rule" - } - - advanced_options_config { - json_parsing = "STANDARD" - } - - adaptive_protection_config { - layer_7_ddos_defense_config { - enable = var.enable_adaptive_protection - } - } + enable_cloud_armor = var.enable_cloud_armor + cloud_armor_rules = var.cloud_armor_rules + enable_adaptive_protection = var.enable_adaptive_protection + enable_ssl_policy = var.enable_ssl_policy } - -resource "google_compute_ssl_policy" "ssl_policy" { - count = var.enable_ssl_policy ? 1 : 0 - name = "tsa-ingress-ssl-policy" - project = var.project_id - - profile = "MODERN" - min_tls_version = "TLS_1_2" +moved { + from = google_dns_record_set.A_timestamp + to = module.global[0].google_dns_record_set.A_timestamp +} +moved { + from = google_compute_global_address.gce_lb_ipv4 + to = module.global[0].google_compute_global_address.gce_lb_ipv4 +} +moved { + from = google_compute_security_policy.http_security_policy + to = module.global[0].google_compute_security_policy.http_security_policy +} +moved { + from = google_compute_ssl_policy.ssl_policy + to = module.global[0].google_compute_ssl_policy.ssl_policy } diff --git a/gcp/modules/timestamp/variables.tf b/gcp/modules/timestamp/variables.tf index 3730849..f3b2739 100644 --- a/gcp/modules/timestamp/variables.tf +++ b/gcp/modules/timestamp/variables.tf @@ -28,6 +28,12 @@ variable "region" { description = "GCP region" } +variable "single_region" { + description = "Whether this module instance is only deployed in one region, and therefore in charge of managing its own IP address and DNS record but not other load balancer resources." + type = bool + default = true +} + variable "cluster_name" { description = "The name to give the new Kubernetes cluster." type = string @@ -68,6 +74,12 @@ variable "dns_domain_name" { type = string } +variable "manage_dns_a_record" { + description = "Whether this module is in charge of managing the DNS A record. This is to enable transitioning from having DNS managed in a single region to managing the same record globally for all regions." + type = bool + default = true +} + // Network variable "enable_cloud_armor" { description = "Whether to create a Cloud Armor security policy."