diff --git a/gcp/modules/tiles_tlog/global/network.tf b/gcp/modules/tiles_tlog/global/network.tf new file mode 100644 index 00000000..364edcd1 --- /dev/null +++ b/gcp/modules/tiles_tlog/global/network.tf @@ -0,0 +1,230 @@ +/** + * 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 "shared" { + source = "../shared" + + project_id = var.project_id + dns_subdomain_name = var.dns_subdomain_name + service_health_check_path = var.service_health_check_path + max_req_content_length = var.max_req_content_length + max_req_content_length_description = var.max_req_content_length_description + enable_healthcheck_logging = var.enable_healthcheck_logging + http_grpc_qpm_rate_limit = var.http_grpc_qpm_rate_limit + enable_adaptive_protection = var.enable_adaptive_protection + create_grpc_health_check = var.grpc_write_path == "" ? false : true +} + +locals { + hostname = trimsuffix("${var.dns_subdomain_name}.${var.dns_domain_name}", ".") + prefix = replace(var.dns_subdomain_name, ".", "-") +} + +resource "google_dns_record_set" "A_tlog" { + name = "${var.dns_subdomain_name}.${var.dns_domain_name}" + type = "A" + ttl = 60 + + project = var.project_id + managed_zone = var.dns_zone_name + + rrdatas = [google_compute_global_address.global_lb_ipv4.address] +} + +resource "google_certificate_manager_dns_authorization" "tlog_auth" { + name = "${local.prefix}-dns-auth" + domain = local.hostname +} + +resource "google_dns_record_set" "CNAME_auth_tlog" { + project = var.project_id + name = google_certificate_manager_dns_authorization.tlog_auth.dns_resource_record[0].name + type = google_certificate_manager_dns_authorization.tlog_auth.dns_resource_record[0].type + ttl = 60 + managed_zone = var.dns_zone_name + rrdatas = [google_certificate_manager_dns_authorization.tlog_auth.dns_resource_record[0].data] +} + +resource "google_compute_global_address" "global_lb_ipv4" { + name = "${local.prefix}-global-ext-lb" + address_type = "EXTERNAL" + project = var.project_id +} + +locals { + http_neg_map = { for neg in var.active_http_negs : "${neg.name}-${neg.zone}" => neg } + grpc_neg_map = { for neg in var.active_grpc_negs : "${neg.name}-${neg.zone}" => neg } +} + +data "google_compute_network_endpoint_group" "k8s_http_neg" { + for_each = local.http_neg_map + + name = each.value.name + project = var.project_id + zone = each.value.zone +} + +data "google_compute_network_endpoint_group" "k8s_grpc_neg" { + for_each = local.grpc_neg_map + + name = each.value.name + project = var.project_id + zone = each.value.zone +} + +resource "google_compute_backend_service" "k8s_http_backend_service" { + name = "${local.prefix}-global-k8s-neg-backend-service" + project = var.project_id + + load_balancing_scheme = "EXTERNAL_MANAGED" + port_name = "http" + protocol = "HTTP" + + connection_draining_timeout_sec = 15 + health_checks = [module.shared.http_health_check_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 + } + } + + security_policy = module.shared.security_policy_id + + log_config { + enable = var.enable_backend_service_logging + } +} + +resource "google_compute_backend_service" "k8s_grpc_backend_service" { + count = var.grpc_write_path == "" ? 0 : 1 + name = "${local.prefix}-global-k8s-grpc-neg-backend-service" + project = var.project_id + + load_balancing_scheme = "EXTERNAL_MANAGED" + port_name = "grpc" + protocol = "HTTP2" + + connection_draining_timeout_sec = 15 + health_checks = module.shared.grpc_health_check_id != "" ? [module.shared.grpc_health_check_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 + } + } + + security_policy = module.shared.security_policy_id + + log_config { + enable = var.enable_backend_service_logging + } +} + +resource "google_compute_url_map" "url_map" { + name = "${local.prefix}-global-lb" + project = var.project_id + + default_service = google_compute_backend_service.k8s_http_backend_service.id + + host_rule { + hosts = [local.hostname] + path_matcher = "global" + } + + path_matcher { + name = "global" + default_service = google_compute_backend_service.k8s_http_backend_service.id + dynamic "route_rules" { + for_each = length(var.active_http_negs) > 0 ? [1] : [] + + content { + priority = 1 + service = google_compute_backend_service.k8s_http_backend_service.id + match_rules { + path_template_match = var.http_write_path + } + match_rules { + full_path_match = "/healthz" + } + } + } + dynamic "route_rules" { + for_each = length(var.active_grpc_negs) > 0 && var.grpc_write_path != "" ? [1] : [] + + content { + priority = 2 + service = google_compute_backend_service.k8s_grpc_backend_service[0].id + match_rules { + path_template_match = var.grpc_write_path + } + } + } + } +} + +resource "google_certificate_manager_certificate" "ssl_certificate" { + name = "${local.prefix}-global-ssl-cert" + project = var.project_id + + managed { + domains = [local.hostname] + dns_authorizations = [ + google_certificate_manager_dns_authorization.tlog_auth.id + ] + } +} + +resource "google_certificate_manager_certificate_map" "tlog_certificate_map" { + name = "${local.prefix}-cert-map" +} + +resource "google_certificate_manager_certificate_map_entry" "tlog_certificate_map_entry" { + name = "${local.prefix}-cert-map-entry" + map = google_certificate_manager_certificate_map.tlog_certificate_map.name + certificates = [google_certificate_manager_certificate.ssl_certificate.id] + hostname = local.hostname +} + +resource "google_compute_target_https_proxy" "lb_proxy" { + name = "${local.prefix}-global-https-proxy" + project = var.project_id + + url_map = google_compute_url_map.url_map.id + + ssl_policy = module.shared.ssl_policy_id + certificate_map = "//certificatemanager.googleapis.com/${google_certificate_manager_certificate_map.tlog_certificate_map.id}" +} + +resource "google_compute_global_forwarding_rule" "https_forwarding_rule" { + name = "${local.prefix}-global-https-forwarding-rule" + project = var.project_id + + ip_address = google_compute_global_address.global_lb_ipv4.address + target = google_compute_target_https_proxy.lb_proxy.id + port_range = "443" + load_balancing_scheme = "EXTERNAL_MANAGED" +} diff --git a/gcp/modules/tiles_tlog/global/outputs.tf b/gcp/modules/tiles_tlog/global/outputs.tf new file mode 100644 index 00000000..82389e00 --- /dev/null +++ b/gcp/modules/tiles_tlog/global/outputs.tf @@ -0,0 +1,35 @@ +/** + * 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. + */ + +output "http_health_check_id" { + value = module.shared.http_health_check_id +} + +output "grpc_health_check_id" { + value = module.shared.grpc_health_check_id +} + +output "security_policy_id" { + value = module.shared.security_policy_id +} + +output "bucket_security_policy_id" { + value = module.shared.bucket_security_policy_id +} + +output "ssl_policy_id" { + value = module.shared.ssl_policy_id +} diff --git a/gcp/modules/tiles_tlog/global/variables.tf b/gcp/modules/tiles_tlog/global/variables.tf new file mode 100644 index 00000000..4d3a3a36 --- /dev/null +++ b/gcp/modules/tiles_tlog/global/variables.tf @@ -0,0 +1,116 @@ +/** + * 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 "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 "dns_subdomain_name" { + description = "Subdomain name for the service, e.g. 'v2.rekor' or 'tessera.ct'" + type = string +} + +variable "active_http_negs" { + type = list(object({ + name = string + zone = string + })) + description = "List of objects containing the names and zones of active HTTP NEGs across all shards and regions to route write traffic to." + default = [] +} + +variable "active_grpc_negs" { + type = list(object({ + name = string + zone = string + })) + description = "List of objects containing the names and zones of active gRPC NEGs across all shards and regions to route write traffic to." + default = [] +} + +variable "backend_service_max_rps" { + description = "Max requests per second that a single backend instance can handle." + type = number + default = 5 +} + +variable "enable_backend_service_logging" { + description = "Whether to enable logging for the HTTP backend service." + type = bool + default = true +} + +variable "http_write_path" { + description = "The path for write requests on HTTP" + type = string +} + +variable "grpc_write_path" { + description = "The path for write requests on GRPC" + type = string + default = "" +} + +variable "service_health_check_path" { + description = "HTTP URL request path for the service health check" + type = string + default = "/healthz" +} + +variable "max_req_content_length" { + description = "maximum request content length in bytes for the write path" + type = number + default = 8388608 // 8 MB +} + +variable "max_req_content_length_description" { + description = "maximum request content length, used only for security policy description" + type = string + default = "8MB" +} + +variable "enable_healthcheck_logging" { + description = "whether to enable logging for the HTTP and gRPC health checks" + type = bool + default = true +} + +variable "http_grpc_qpm_rate_limit" { + description = "count of write requests per minute allowed to HTTP and gRPC backends" + type = number + default = 600 // 10 QPS +} + +variable "enable_adaptive_protection" { + description = "whether to enable layer 7 DDoS adaptive protection" + type = bool + default = true +} diff --git a/gcp/modules/tiles_tlog/global/versions.tf b/gcp/modules/tiles_tlog/global/versions.tf new file mode 100644 index 00000000..0e1d908b --- /dev/null +++ b/gcp/modules/tiles_tlog/global/versions.tf @@ -0,0 +1,24 @@ +/** + * 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_providers { + google = { + source = "hashicorp/google" + version = ">= 5.5.0" + } + } +} diff --git a/gcp/modules/tiles_tlog/network.tf b/gcp/modules/tiles_tlog/network.tf index 84ff4d77..4a8a7604 100644 --- a/gcp/modules/tiles_tlog/network.tf +++ b/gcp/modules/tiles_tlog/network.tf @@ -18,8 +18,60 @@ locals { cluster_network_tag = var.cluster_network_tag != "" ? var.cluster_network_tag : "gke-${var.cluster_name}" } +module "shared" { + source = "./shared" + count = var.single_region ? 1 : 0 + + project_id = var.project_id + dns_subdomain_name = var.dns_subdomain_name + service_health_check_path = var.service_health_check_path + max_req_content_length = var.max_req_content_length + max_req_content_length_description = var.max_req_content_length_description + enable_healthcheck_logging = var.enable_healthcheck_logging + http_grpc_qpm_rate_limit = var.http_grpc_qpm_rate_limit + enable_adaptive_protection = var.enable_adaptive_protection + create_grpc_health_check = var.network_endpoint_group_grpc_name_suffix != "" + freeze_shard = var.freeze_shard + + http_health_check_name = "${var.shard_name}-${var.dns_subdomain_name}-http-health-check" + grpc_health_check_name = "${var.shard_name}-${var.dns_subdomain_name}-grpc-health-check" + security_policy_name = "${var.shard_name}-${var.dns_subdomain_name}-k8s-http-grpc-security-policy" + bucket_security_policy_name = "${var.shard_name}-${var.dns_subdomain_name}-bucket-security-policy" + ssl_policy_name = "${var.shard_name}-${var.dns_subdomain_name}-ssl-policy" +} +moved { + from = google_compute_health_check.http_health_check + to = module.shared[0].google_compute_health_check.http_health_check +} +moved { + from = google_compute_health_check.grpc_health_check + to = module.shared[0].google_compute_health_check.grpc_health_check +} +moved { + from = google_compute_security_policy.k8s_http_grpc_security_policy_renamed + to = module.shared[0].google_compute_security_policy.k8s_http_grpc_security_policy_renamed +} +moved { + from = google_compute_security_policy.bucket_security_policy_renamed + to = module.shared[0].google_compute_security_policy.bucket_security_policy_renamed +} +moved { + from = google_compute_ssl_policy.ssl_policy + to = module.shared[0].google_compute_ssl_policy.ssl_policy +} + +locals { + http_health_check_id = var.single_region && !var.freeze_shard ? module.shared[0].http_health_check_id : var.http_health_check_id + grpc_health_check_id = var.single_region && !var.freeze_shard && var.network_endpoint_group_grpc_name_suffix != "" ? module.shared[0].grpc_health_check_id : var.grpc_health_check_id + security_policy_id = var.single_region && !var.freeze_shard ? module.shared[0].security_policy_id : var.security_policy_id + bucket_security_policy_id = var.single_region ? module.shared[0].bucket_security_policy_id : var.bucket_security_policy_id + ssl_policy_id = var.single_region ? module.shared[0].ssl_policy_id : var.ssl_policy_id + hostname = trimsuffix("${var.shard_name}.${var.dns_subdomain_name}.${var.dns_domain_name}", ".") + prefix = replace("${var.shard_name}-${var.dns_subdomain_name}", ".", "-") +} + resource "google_compute_global_address" "gce_lb_ipv4" { - name = "${var.shard_name}-${var.dns_subdomain_name}-${var.cluster_name}-gce-ext-lb" + name = "${local.prefix}-${var.cluster_name}-gce-ext-lb" address_type = "EXTERNAL" project = var.project_id @@ -60,45 +112,6 @@ resource "google_compute_firewall" "backend_service_health_check" { } } -resource "google_compute_health_check" "http_health_check" { - count = var.freeze_shard ? 0 : 1 - name = "${var.shard_name}-${var.dns_subdomain_name}-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 = var.service_health_check_path - port_specification = "USE_SERVING_PORT" - } - - log_config { - enable = var.enable_healthcheck_logging - } -} - -resource "google_compute_health_check" "grpc_health_check" { - count = var.freeze_shard || var.network_endpoint_group_grpc_name_suffix == "" ? 0 : 1 - name = "${var.shard_name}-${var.dns_subdomain_name}-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 = var.freeze_shard ? [] : toset(var.network_endpoint_group_zones) @@ -115,74 +128,9 @@ data "google_compute_network_endpoint_group" "k8s_grpc_neg" { zone = each.key } -resource "google_compute_security_policy" "k8s_http_grpc_security_policy_renamed" { - count = var.freeze_shard ? 0 : 1 - name = "${var.shard_name}-${var.dns_subdomain_name}-k8s-http-grpc-security-policy" - project = var.project_id - type = "CLOUD_ARMOR" - - rule { - action = "deny(502)" - priority = "1" - - match { - expr { - expression = "int(request.headers['content-length']) > ${var.max_req_content_length}" - } - } - description = "Block all incoming write requests > ${var.max_req_content_length_description}" - } - - rule { - action = "throttle" - priority = "10" - match { - versioned_expr = "SRC_IPS_V1" - config { - src_ip_ranges = ["*"] - } - } - rate_limit_options { - enforce_on_key = "IP" - conform_action = "allow" - exceed_action = "deny(429)" - rate_limit_threshold { - count = var.http_grpc_qpm_rate_limit - interval_sec = "60" - } - } - description = "Rate limit all HTTP write traffic by client IP" - } - - rule { - action = "allow" - priority = "2147483647" - match { - versioned_expr = "SRC_IPS_V1" - config { - src_ip_ranges = ["*"] - } - } - description = "default rule" - } - - advanced_options_config { - json_parsing = "STANDARD" - json_custom_config { - content_types = ["application/json"] - } - } - - adaptive_protection_config { - layer_7_ddos_defense_config { - enable = var.enable_adaptive_protection - } - } -} - resource "google_compute_backend_service" "k8s_http_backend_service" { count = var.freeze_shard ? 0 : 1 - name = "${var.shard_name}-${var.dns_subdomain_name}-k8s-neg-backend-service" + name = "${local.prefix}-k8s-neg-backend-service" project = var.project_id load_balancing_scheme = "EXTERNAL_MANAGED" @@ -190,7 +138,7 @@ resource "google_compute_backend_service" "k8s_http_backend_service" { protocol = "HTTP" connection_draining_timeout_sec = 15 - health_checks = [google_compute_health_check.http_health_check[count.index].id] + health_checks = [local.http_health_check_id] dynamic "backend" { for_each = data.google_compute_network_endpoint_group.k8s_http_neg @@ -203,9 +151,7 @@ resource "google_compute_backend_service" "k8s_http_backend_service" { } } - depends_on = [google_compute_security_policy.k8s_http_grpc_security_policy_renamed[0]] - - security_policy = google_compute_security_policy.k8s_http_grpc_security_policy_renamed[0].self_link + security_policy = local.security_policy_id log_config { enable = var.enable_backend_service_logging @@ -214,7 +160,7 @@ resource "google_compute_backend_service" "k8s_http_backend_service" { resource "google_compute_backend_service" "k8s_grpc_backend_service" { count = var.freeze_shard || var.network_endpoint_group_grpc_name_suffix == "" ? 0 : 1 - name = "${var.shard_name}-${var.dns_subdomain_name}-k8s-grpc-neg-backend-service" + name = "${local.prefix}-k8s-grpc-neg-backend-service" project = var.project_id load_balancing_scheme = "EXTERNAL_MANAGED" @@ -222,7 +168,7 @@ resource "google_compute_backend_service" "k8s_grpc_backend_service" { protocol = "HTTP2" connection_draining_timeout_sec = 15 - health_checks = [google_compute_health_check.grpc_health_check[count.index].id] + health_checks = [local.grpc_health_check_id] dynamic "backend" { for_each = data.google_compute_network_endpoint_group.k8s_grpc_neg @@ -235,9 +181,7 @@ resource "google_compute_backend_service" "k8s_grpc_backend_service" { } } - depends_on = [google_compute_security_policy.k8s_http_grpc_security_policy_renamed[0]] - - security_policy = google_compute_security_policy.k8s_http_grpc_security_policy_renamed[0].self_link + security_policy = local.security_policy_id log_config { enable = var.enable_backend_service_logging @@ -248,7 +192,7 @@ resource "google_compute_backend_bucket" "tessera_backend_bucket" { name = "${var.shard_name}-${var.bucket_name_suffix}" project = var.project_id - depends_on = [google_storage_bucket.tessera_store, google_compute_security_policy.bucket_security_policy_renamed] + depends_on = [google_storage_bucket.tessera_store] bucket_name = google_storage_bucket.tessera_store.name @@ -257,43 +201,21 @@ resource "google_compute_backend_bucket" "tessera_backend_bucket" { cache_mode = "USE_ORIGIN_HEADERS" } - edge_security_policy = google_compute_security_policy.bucket_security_policy_renamed.self_link + edge_security_policy = local.bucket_security_policy_id lifecycle { prevent_destroy = true } } -resource "google_compute_security_policy" "bucket_security_policy_renamed" { - name = "${var.shard_name}-${var.dns_subdomain_name}-bucket-security-policy" - project = var.project_id - type = "CLOUD_ARMOR_EDGE" - - rule { - action = "allow" - priority = "2147483647" - match { - versioned_expr = "SRC_IPS_V1" - config { - src_ip_ranges = ["*"] - } - } - description = "default rule" - } -} - -locals { - hostname = var.dns_domain_name == "" ? "*" : trimsuffix("${var.shard_name}.${var.dns_subdomain_name}.${var.dns_domain_name}", ".") -} - resource "google_compute_url_map" "url_map" { - name = "${var.shard_name}-${var.dns_subdomain_name}-lb" + name = "${local.prefix}-lb" project = var.project_id default_service = google_compute_backend_bucket.tessera_backend_bucket.id host_rule { - hosts = [local.hostname] + hosts = var.dns_domain_name == "" ? ["*"] : [local.hostname] path_matcher = var.shard_name } @@ -350,25 +272,13 @@ resource "google_compute_url_map" "url_map" { // 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" { - name = "${var.shard_name}-${var.dns_subdomain_name}-ssl-policy" - project = var.project_id - - profile = "MODERN" - min_tls_version = "TLS_1_2" - - lifecycle { - prevent_destroy = true - } -} - resource "google_compute_managed_ssl_certificate" "ssl_certificate" { count = var.dns_domain_name == "" ? 0 : 1 # Domain validation certificates can only be used if you have a registered domain name - name = "${var.shard_name}-${var.dns_subdomain_name}-ssl-cert" + name = "${local.prefix}-ssl-cert" project = var.project_id managed { - domains = ["${var.shard_name}.${var.dns_subdomain_name}.${var.dns_domain_name}"] + domains = [local.hostname] } lifecycle { @@ -378,7 +288,7 @@ resource "google_compute_managed_ssl_certificate" "ssl_certificate" { resource "google_compute_target_http_proxy" "lb_proxy" { count = var.dns_domain_name == "" ? 1 : 0 - name = "${var.shard_name}-${var.dns_subdomain_name}-http-proxy" + name = "${local.prefix}-http-proxy" project = var.project_id url_map = google_compute_url_map.url_map.id @@ -389,13 +299,13 @@ resource "google_compute_target_http_proxy" "lb_proxy" { } resource "google_compute_target_https_proxy" "lb_proxy" { count = var.dns_domain_name == "" ? 0 : 1 - name = "${var.shard_name}-${var.dns_subdomain_name}-https-proxy" + name = "${local.prefix}-https-proxy" project = var.project_id url_map = google_compute_url_map.url_map.id ssl_certificates = [google_compute_managed_ssl_certificate.ssl_certificate[count.index].id] - ssl_policy = google_compute_ssl_policy.ssl_policy.id + ssl_policy = local.ssl_policy_id lifecycle { prevent_destroy = true @@ -404,7 +314,7 @@ resource "google_compute_target_https_proxy" "lb_proxy" { resource "google_compute_global_forwarding_rule" "http_forwarding_rule" { count = var.dns_domain_name == "" ? 1 : 0 - name = "${var.shard_name}-${var.dns_subdomain_name}-http-forwarding-rule" + name = "${local.prefix}-http-forwarding-rule" project = var.project_id ip_address = google_compute_global_address.gce_lb_ipv4.address @@ -419,7 +329,7 @@ resource "google_compute_global_forwarding_rule" "http_forwarding_rule" { resource "google_compute_global_forwarding_rule" "https_forwarding_rule" { count = var.dns_domain_name == "" ? 0 : 1 - name = "${var.shard_name}-${var.dns_subdomain_name}-https-forwarding-rule" + name = "${local.prefix}-https-forwarding-rule" project = var.project_id ip_address = google_compute_global_address.gce_lb_ipv4.address diff --git a/gcp/modules/tiles_tlog/outputs.tf b/gcp/modules/tiles_tlog/outputs.tf new file mode 100644 index 00000000..aa69dc7c --- /dev/null +++ b/gcp/modules/tiles_tlog/outputs.tf @@ -0,0 +1,25 @@ +/** + * 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. + */ + +output "http_neg_name" { + description = "Name of the HTTP Network Endpoint Group" + value = "${var.shard_name}-${var.network_endpoint_group_http_name_suffix}" +} + +output "grpc_neg_name" { + description = "Name of the gRPC Network Endpoint Group" + value = var.network_endpoint_group_grpc_name_suffix != "" ? "${var.shard_name}-${var.network_endpoint_group_grpc_name_suffix}" : "" +} diff --git a/gcp/modules/tiles_tlog/secret.tf b/gcp/modules/tiles_tlog/secret.tf index 0b38659e..e4087be8 100644 --- a/gcp/modules/tiles_tlog/secret.tf +++ b/gcp/modules/tiles_tlog/secret.tf @@ -18,7 +18,7 @@ resource "google_secret_manager_secret" "private-key" { count = var.enable_secrets ? 1 : 0 project = var.project_id - secret_id = "${var.shard_name}-${var.dns_subdomain_name}-private" + secret_id = "${local.prefix}-private" replication { auto {} @@ -30,7 +30,7 @@ resource "google_secret_manager_secret" "public-key" { count = var.enable_secrets ? 1 : 0 project = var.project_id - secret_id = "${var.shard_name}-${var.dns_subdomain_name}-public" + secret_id = "${local.prefix}-public" replication { auto {} diff --git a/gcp/modules/tiles_tlog/shared/network.tf b/gcp/modules/tiles_tlog/shared/network.tf new file mode 100644 index 00000000..fa00c016 --- /dev/null +++ b/gcp/modules/tiles_tlog/shared/network.tf @@ -0,0 +1,149 @@ +/** + * 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. + */ + +locals { + prefix = replace(var.dns_subdomain_name, ".", "-") +} + +resource "google_compute_health_check" "http_health_check" { + count = var.freeze_shard ? 0 : 1 + name = var.http_health_check_name != "" ? var.http_health_check_name : "${local.prefix}-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 = var.service_health_check_path + port_specification = "USE_SERVING_PORT" + } + + log_config { + enable = var.enable_healthcheck_logging + } +} + +resource "google_compute_health_check" "grpc_health_check" { + count = var.freeze_shard || !var.create_grpc_health_check ? 0 : 1 + name = var.grpc_health_check_name != "" ? var.grpc_health_check_name : "${local.prefix}-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 + } +} + +resource "google_compute_security_policy" "k8s_http_grpc_security_policy" { + count = var.freeze_shard ? 0 : 1 + name = var.security_policy_name != "" ? var.security_policy_name : "${local.prefix}-k8s-http-grpc-security-policy" + project = var.project_id + type = "CLOUD_ARMOR" + + rule { + action = "deny(502)" + priority = "1" + + match { + expr { + expression = "int(request.headers['content-length']) > ${var.max_req_content_length}" + } + } + description = "Block all incoming write requests > ${var.max_req_content_length_description}" + } + + rule { + action = "throttle" + priority = "10" + match { + versioned_expr = "SRC_IPS_V1" + config { + src_ip_ranges = ["*"] + } + } + rate_limit_options { + enforce_on_key = "IP" + conform_action = "allow" + exceed_action = "deny(429)" + rate_limit_threshold { + count = var.http_grpc_qpm_rate_limit + interval_sec = "60" + } + } + description = "Rate limit all HTTP write traffic by client IP" + } + + rule { + action = "allow" + priority = "2147483647" + match { + versioned_expr = "SRC_IPS_V1" + config { + src_ip_ranges = ["*"] + } + } + description = "default rule" + } + + advanced_options_config { + json_parsing = "STANDARD" + json_custom_config { + content_types = ["application/json"] + } + } + + adaptive_protection_config { + layer_7_ddos_defense_config { + enable = var.enable_adaptive_protection + } + } +} + +resource "google_compute_security_policy" "bucket_security_policy" { + name = var.bucket_security_policy_name != "" ? var.bucket_security_policy_name : "${local.prefix}-bucket-security-policy" + project = var.project_id + type = "CLOUD_ARMOR_EDGE" + + rule { + action = "allow" + priority = "2147483647" + match { + versioned_expr = "SRC_IPS_V1" + config { + src_ip_ranges = ["*"] + } + } + description = "default rule" + } +} + +resource "google_compute_ssl_policy" "ssl_policy" { + name = var.ssl_policy_name != "" ? var.ssl_policy_name : "${local.prefix}-ssl-policy" + project = var.project_id + + profile = "MODERN" + min_tls_version = "TLS_1_2" +} diff --git a/gcp/modules/tiles_tlog/shared/outputs.tf b/gcp/modules/tiles_tlog/shared/outputs.tf new file mode 100644 index 00000000..24d5bff4 --- /dev/null +++ b/gcp/modules/tiles_tlog/shared/outputs.tf @@ -0,0 +1,35 @@ +/** + * 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. + */ + +output "http_health_check_id" { + value = var.freeze_shard ? "" : google_compute_health_check.http_health_check[0].id +} + +output "grpc_health_check_id" { + value = var.freeze_shard || !var.create_grpc_health_check ? "" : google_compute_health_check.grpc_health_check[0].id +} + +output "security_policy_id" { + value = var.freeze_shard ? "" : google_compute_security_policy.k8s_http_grpc_security_policy[0].self_link +} + +output "bucket_security_policy_id" { + value = google_compute_security_policy.bucket_security_policy.self_link +} + +output "ssl_policy_id" { + value = google_compute_ssl_policy.ssl_policy.id +} diff --git a/gcp/modules/tiles_tlog/shared/variables.tf b/gcp/modules/tiles_tlog/shared/variables.tf new file mode 100644 index 00000000..49f510e5 --- /dev/null +++ b/gcp/modules/tiles_tlog/shared/variables.tf @@ -0,0 +1,107 @@ +/** + * 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 "dns_subdomain_name" { + description = "DNS subdomain name" + type = string +} + +variable "service_health_check_path" { + description = "HTTP URL request path for the service health check" + type = string + default = "/healthz" +} + +variable "max_req_content_length" { + description = "maximum request content length in bytes for the write path" + type = number + default = 8388608 // 8 MB +} + +variable "max_req_content_length_description" { + description = "maximum request content length, used only for security policy description" + type = string + default = "8MB" +} + +variable "enable_healthcheck_logging" { + description = "whether to enable logging for the HTTP and gRPC health checks" + type = bool + default = true +} + +variable "http_grpc_qpm_rate_limit" { + description = "count of write requests per minute allowed to HTTP and gRPC backends" + type = number + default = 600 // 10 QPS +} + +variable "enable_adaptive_protection" { + description = "whether to enable layer 7 DDoS adaptive protection" + type = bool + default = true +} + +variable "create_grpc_health_check" { + description = "whether to create the gRPC health check" + type = bool + default = true +} + +variable "freeze_shard" { + description = "whether the shard is frozen. Compute resources will be omitted." + type = bool + default = false +} + +variable "http_health_check_name" { + description = "name of the HTTP health check" + type = string + default = "" +} + +variable "grpc_health_check_name" { + description = "name of the gRPC health check" + type = string + default = "" +} + +variable "security_policy_name" { + description = "name of the backend service security policy" + type = string + default = "" +} + +variable "bucket_security_policy_name" { + description = "name of the bucket security policy" + type = string + default = "" +} + +variable "ssl_policy_name" { + description = "name of the SSL policy" + type = string + default = "" +} diff --git a/gcp/modules/tiles_tlog/shared/versions.tf b/gcp/modules/tiles_tlog/shared/versions.tf new file mode 100644 index 00000000..0e1d908b --- /dev/null +++ b/gcp/modules/tiles_tlog/shared/versions.tf @@ -0,0 +1,24 @@ +/** + * 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_providers { + google = { + source = "hashicorp/google" + version = ">= 5.5.0" + } + } +} diff --git a/gcp/modules/tiles_tlog/variables.tf b/gcp/modules/tiles_tlog/variables.tf index 3f411a62..9c366b0b 100644 --- a/gcp/modules/tiles_tlog/variables.tf +++ b/gcp/modules/tiles_tlog/variables.tf @@ -32,6 +32,12 @@ variable "project_number" { } } +variable "single_region" { + description = "Whether this module instance is only deployed in one region." + type = bool + default = true +} + variable "region" { description = "GCP region" type = string @@ -288,3 +294,33 @@ variable "enable_secrets" { type = bool default = false } + +variable "http_health_check_id" { + description = "ID of the shared HTTP health check" + type = string + default = "" +} + +variable "grpc_health_check_id" { + description = "ID of the shared gRPC health check" + type = string + default = "" +} + +variable "security_policy_id" { + description = "ID of the shared Cloud Armor security policy" + type = string + default = "" +} + +variable "bucket_security_policy_id" { + description = "ID of the shared Cloud Armor security policy for the bucket" + type = string + default = "" +} + +variable "ssl_policy_id" { + description = "ID of the shared SSL policy" + type = string + default = "" +}