diff --git a/blueprints/fedramp-high/beyondcorp/provider.tf b/blueprints/fedramp-high/beyondcorp/provider.tf index 9d85cb3aa..c773f1765 100644 --- a/blueprints/fedramp-high/beyondcorp/provider.tf +++ b/blueprints/fedramp-high/beyondcorp/provider.tf @@ -31,12 +31,14 @@ terraform { } provider "google" { + project = var.main_project_id region = var.region billing_project = var.main_project_id user_project_override = true } provider "google-beta" { + project = var.main_project_id region = var.region billing_project = var.main_project_id user_project_override = true diff --git a/blueprints/fedramp-high/beyondcorp/variables.tf b/blueprints/fedramp-high/beyondcorp/variables.tf index 9e6e9233d..5ea52de01 100644 --- a/blueprints/fedramp-high/beyondcorp/variables.tf +++ b/blueprints/fedramp-high/beyondcorp/variables.tf @@ -35,6 +35,7 @@ variable "oauth_client_id" { variable "oauth_client_secret" { description = "OAuth Client Secret for IAP." type = string + sensitive = true } variable "organization_id" { diff --git a/blueprints/fedramp-high/bigtable/main.tf b/blueprints/fedramp-high/bigtable/main.tf index d90016770..e308a6e39 100644 --- a/blueprints/fedramp-high/bigtable/main.tf +++ b/blueprints/fedramp-high/bigtable/main.tf @@ -12,7 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -data "google_project" "project" {} +data "google_project" "project" { + project_id = var.main_project_id +} resource "google_project_service" "bigtable_api" { project = var.main_project_id @@ -51,7 +53,7 @@ resource "google_kms_crypto_key_iam_member" "bigtable_sa_kms_access" { resource "google_kms_crypto_key_iam_member" "bigtable_agent_kms_access" { crypto_key_id = data.google_kms_crypto_key.default.id role = "roles/cloudkms.cryptoKeyEncrypterDecrypter" - member = "serviceAccount:service-${data.google_project.project.number}@gcp-sa-bigtable.iam.gserviceaccount.com" + member = google_project_service_identity.bigtable_sa.member } module "bigtable-instance" { @@ -68,5 +70,5 @@ module "bigtable-instance" { } } tables = var.table - deletion_protection = false + deletion_protection = var.deletion_protection } diff --git a/blueprints/fedramp-high/bigtable/terraform.tfvars.sample b/blueprints/fedramp-high/bigtable/terraform.tfvars.sample index 2854f4772..950eea75a 100644 --- a/blueprints/fedramp-high/bigtable/terraform.tfvars.sample +++ b/blueprints/fedramp-high/bigtable/terraform.tfvars.sample @@ -4,9 +4,9 @@ core_project_id = "xxxx-xxxx-xxxx-iac-core-0" bigtable_service_account_id = "bigtable-sa" instance_name = "bigtable-instance-00" cluster_id = "bigtable-cluster-00" -num_nodes = 3 -auto_delete = true -region = "us-east4" +num_nodes = 3 +deletion_protection = false +region = "us-east4" zone = "us-east4-a" kms_keyring_name = "xxxx-xxxx-keyring" diff --git a/blueprints/fedramp-high/bigtable/variables.tf b/blueprints/fedramp-high/bigtable/variables.tf index fad46aee2..498691b06 100644 --- a/blueprints/fedramp-high/bigtable/variables.tf +++ b/blueprints/fedramp-high/bigtable/variables.tf @@ -12,8 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -variable "auto_delete" { - description = "Persistent Disk auto delete options." +variable "deletion_protection" { + description = "Prevent Terraform from destroying the Bigtable instance." type = bool default = true } @@ -77,11 +77,7 @@ variable "table" { split_keys = optional(list(string)) column_families = map(object({})) })) - default = { - "Test" = { - column_families = {} - } - } + default = null } variable "zone" { diff --git a/blueprints/fedramp-high/cloud-armor/main.tf b/blueprints/fedramp-high/cloud-armor/main.tf index b180baec7..735754ed1 100644 --- a/blueprints/fedramp-high/cloud-armor/main.tf +++ b/blueprints/fedramp-high/cloud-armor/main.tf @@ -49,6 +49,8 @@ resource "google_compute_region_security_policy" "policy" { provider = google-beta for_each = local.policies + project = var.main_project_id + region = var.region name = each.key description = each.value.description type = "CLOUD_ARMOR" @@ -56,13 +58,14 @@ resource "google_compute_region_security_policy" "policy" { resource "google_compute_region_security_policy_rule" "policy_rule" { provider = google-beta - for_each = { for rule in local.indexed_rules : rule.priority => rule } + for_each = { for rule in local.indexed_rules : "${rule.policy}-${rule.priority}" => rule } depends_on = [google_compute_region_security_policy.policy] + project = var.main_project_id security_policy = each.value.policy region = each.value.region priority = each.value.priority - action = try(each.value.action, "allow") + action = try(each.value.action, "deny(403)") preview = try(each.value.preview, null) description = try(each.value.description, null) @@ -79,7 +82,7 @@ resource "google_compute_region_security_policy_rule" "policy_rule" { dynamic "config" { for_each = try(each.value.expression, null) != null ? [] : [1] content { - src_ip_ranges = ["*"] + src_ip_ranges = try(each.value.src_ip_ranges, ["*"]) } } } diff --git a/blueprints/fedramp-high/cloud-composer-environment/variables.tf b/blueprints/fedramp-high/cloud-composer-environment/variables.tf index f40713a6f..b125797f5 100644 --- a/blueprints/fedramp-high/cloud-composer-environment/variables.tf +++ b/blueprints/fedramp-high/cloud-composer-environment/variables.tf @@ -60,7 +60,7 @@ variable "sa_display_name" { variable "service_agent_version" { description = "Composer Service Agent version. This must correspond to Composer version." type = string - default = "roles/composer.ServiceAgentV2Ext" + default = "roles/composer.ServiceAgentV3Ext" } variable "subnetwork_name" { diff --git a/blueprints/fedramp-high/cloud-functions/main.tf b/blueprints/fedramp-high/cloud-functions/main.tf index e1bcc8b8d..48d525619 100644 --- a/blueprints/fedramp-high/cloud-functions/main.tf +++ b/blueprints/fedramp-high/cloud-functions/main.tf @@ -51,16 +51,17 @@ resource "google_project_iam_member" "cloud_invoker" { member = module.service-account-runner.iam_email } -resource "google_kms_crypto_key_iam_binding" "cloud_storage" { - crypto_key_id = var.kms_key_name - role = "roles/cloudkms.cryptoKeyEncrypterDecrypter" - members = [ +resource "google_kms_crypto_key_iam_member" "cloud_storage" { + for_each = toset([ "serviceAccount:service-${data.google_project.current.number}@gs-project-accounts.iam.gserviceaccount.com", "serviceAccount:service-${data.google_project.current.number}@compute-system.iam.gserviceaccount.com", "serviceAccount:service-${data.google_project.current.number}@gcf-admin-robot.iam.gserviceaccount.com", "serviceAccount:service-${data.google_project.current.number}@gcp-sa-artifactregistry.iam.gserviceaccount.com", "serviceAccount:service-${data.google_project.current.number}@serverless-robot-prod.iam.gserviceaccount.com" - ] + ]) + crypto_key_id = var.kms_key_name + role = "roles/cloudkms.cryptoKeyEncrypterDecrypter" + member = each.value } resource "google_project_iam_member" "artifactregistry_createOnPushWriter" { @@ -94,21 +95,35 @@ resource "null_resource" "cloud_function_deploy" { source_code_hash = sha256(join("", [for f in fileset("./src-code", "**") : file("./src-code/${f}")])) } provisioner "local-exec" { + environment = { + CF_NAME = var.function_name + CF_PROJECT = var.main_project_id + CF_REGION = var.region + CF_RUNTIME = var.function_runtime + CF_ENTRY_POINT = var.function_entry_point + CF_MEMORY = "${var.function_memory_mb}MB" + CF_TIMEOUT = "${var.function_timeout_seconds}s" + CF_MAX_INSTANCES = tostring(var.function_instance_count) + CF_DOCKER_REPOSITORY = module.registry-docker.id + CF_KMS_KEY = var.kms_key_name + CF_SERVICE_ACCOUNT = coalesce(var.service_account, module.service-account-runner.email) + } command = < { - res.send(`Hello, ${req.query.name || req.body.name || 'World'}!`); + res.set('Content-Type', 'text/plain'); + res.send(`Hello, ${req.query.name || (req.body && req.body.name) || 'World'}!`); }); \ No newline at end of file diff --git a/blueprints/fedramp-high/cloud-ids/variables.tf b/blueprints/fedramp-high/cloud-ids/variables.tf index 2eccdd933..fbeba3e84 100644 --- a/blueprints/fedramp-high/cloud-ids/variables.tf +++ b/blueprints/fedramp-high/cloud-ids/variables.tf @@ -59,7 +59,7 @@ variable "region" { variable "severity" { description = "Impact of an incident on a system." type = string - default = "MEDIUM" + default = "INFORMATIONAL" } variable "subnetwork_list" { diff --git a/blueprints/fedramp-high/cloud-run/main.tf b/blueprints/fedramp-high/cloud-run/main.tf index 9cade1cc1..6dc61c517 100644 --- a/blueprints/fedramp-high/cloud-run/main.tf +++ b/blueprints/fedramp-high/cloud-run/main.tf @@ -49,12 +49,6 @@ resource "google_project_iam_member" "cloud_run_permissions" { member = "serviceAccount:${google_service_account.cloud_run_service_account.email}" } -resource "google_kms_crypto_key_iam_member" "cloud_run_sa_kms_access" { - crypto_key_id = data.google_kms_crypto_key.default.id - role = "roles/cloudkms.cryptoKeyEncrypterDecrypter" - member = google_service_account.cloud_run_service_account.member -} - resource "google_kms_crypto_key_iam_member" "cloud_run_service_agent_kms_permissions" { crypto_key_id = data.google_kms_crypto_key.default.id role = "roles/cloudkms.cryptoKeyEncrypterDecrypter" @@ -91,5 +85,5 @@ module "cloud_run" { } } service_account = google_service_account.cloud_run_service_account.email - deletion_protection = false + deletion_protection = true } \ No newline at end of file diff --git a/blueprints/fedramp-high/cloud-run/variables.tf b/blueprints/fedramp-high/cloud-run/variables.tf index 7553695ee..f02d74131 100644 --- a/blueprints/fedramp-high/cloud-run/variables.tf +++ b/blueprints/fedramp-high/cloud-run/variables.tf @@ -27,8 +27,8 @@ variable "binary_authorization_policy" { type = string default = null validation { - condition = var.binary_authorization_mode != "policy" || (var.binary_authorization_mode == "policy" && var.binary_authorization_policy != null) - error_message = "If binary_authorization_mode is set to 'policy', then binary_authorization_policy must be specified." + condition = var.binary_authorization_policy == null || can(regex("^projects/[^/]+/policies/[^/]+$", var.binary_authorization_policy)) + error_message = "If specified, binary_authorization_policy must match projects/PROJECT_ID/policies/POLICY_ID." } } @@ -58,6 +58,7 @@ variable "env_vars" { description = "Environment variables for the Cloud Run service or job." type = map(string) default = {} + sensitive = true } variable "ingress" { diff --git a/blueprints/fedramp-high/cloud-scheduler-job/main.tf b/blueprints/fedramp-high/cloud-scheduler-job/main.tf index d5722a9cd..22ecc7e72 100644 --- a/blueprints/fedramp-high/cloud-scheduler-job/main.tf +++ b/blueprints/fedramp-high/cloud-scheduler-job/main.tf @@ -23,15 +23,13 @@ resource "google_project_service" "cloudscheduler_api" { } # Grant Pub/Sub service account permissions on the KMS key for CMEK of the Pub/Sub topic -resource "google_kms_crypto_key_iam_binding" "pubsub" { +resource "google_kms_crypto_key_iam_member" "pubsub" { count = var.kms_key_name != null ? 1 : 0 # This grants permission to the Pub/Sub service account to use the KMS key # for the *existing* Pub/Sub topic. crypto_key_id = var.kms_key_name # Full self-link of the existing KMS key role = "roles/cloudkms.cryptoKeyEncrypterDecrypter" - members = [ - "serviceAccount:service-${data.google_project.current.number}@gcp-sa-pubsub.iam.gserviceaccount.com" - ] + member = "serviceAccount:service-${data.google_project.current.number}@gcp-sa-pubsub.iam.gserviceaccount.com" # Depend on google_project_service_identity if we were creating it in this blueprint # but here, we just need to ensure the API is enabled. depends_on = [google_project_service.cloudscheduler_api] # Ensure API is enabled before IAM @@ -60,7 +58,7 @@ module "pubsub_job" { } depends_on = [ google_project_service.cloudscheduler_api, - google_kms_crypto_key_iam_binding.pubsub, # Ensure Pub/Sub SA has KMS permission if new topic is CMEK'd + google_kms_crypto_key_iam_member.pubsub, # Ensure Pub/Sub SA has KMS permission if new topic is CMEK'd ] } diff --git a/blueprints/fedramp-high/cloud-scheduler-job/terraform.tfvars.sample b/blueprints/fedramp-high/cloud-scheduler-job/terraform.tfvars.sample index 3884aba6a..8d73903ca 100644 --- a/blueprints/fedramp-high/cloud-scheduler-job/terraform.tfvars.sample +++ b/blueprints/fedramp-high/cloud-scheduler-job/terraform.tfvars.sample @@ -10,8 +10,8 @@ gcp_region = "us-east4" # The Google Cloud region where the Cloud Schedu topic_id = "projects/YOUR_PUBSUB_PROJECT_ID/topics/YOUR_PUBSUB_TOPIC_NAME" # Full resource path of the existing Pub/Sub topic. # Example: "projects/my-pubsub-project/topics/my-existing-topic" -data = base64encode("YOUR_MESSAGE_HERE") # Base64-encoded data to be sent as the Pub/Sub message payload. -# Example: base64encode("{\"key\": \"value\"}") or base64encode("My message") +data = "YOUR_MESSAGE_HERE" # Message payload to be base64-encoded and sent to the Pub/Sub topic. +# Example: "{\"key\": \"value\"}" or "My message" # --- Cloud KMS for Pub/Sub Topic Encryption (CMEK) --- # This blueprint assumes your Pub/Sub topic is already encrypted with CMEK. diff --git a/blueprints/fedramp-high/cloud-scheduler-job/variables.tf b/blueprints/fedramp-high/cloud-scheduler-job/variables.tf index 21e233050..4c4ffb40a 100644 --- a/blueprints/fedramp-high/cloud-scheduler-job/variables.tf +++ b/blueprints/fedramp-high/cloud-scheduler-job/variables.tf @@ -16,6 +16,7 @@ variable "data" { description = "The base64-encoded data to be sent as the Pub/Sub message payload." type = string default = null + sensitive = true } variable "description" { diff --git a/blueprints/fedramp-high/cloud-spanner/variables.tf b/blueprints/fedramp-high/cloud-spanner/variables.tf index be4b9faff..12a7f0a1c 100644 --- a/blueprints/fedramp-high/cloud-spanner/variables.tf +++ b/blueprints/fedramp-high/cloud-spanner/variables.tf @@ -26,6 +26,10 @@ variable "database_name" { variable "database_user" { description = "Database user or group. Must start with \"user:\" or \"group:\" or \"serviceAccount:\"." type = string + validation { + condition = can(regex("^(user|group|serviceAccount):", var.database_user)) + error_message = "database_user must start with user:, group:, or serviceAccount:." + } } variable "display_name" { diff --git a/blueprints/fedramp-high/cloud-translation/main.tf b/blueprints/fedramp-high/cloud-translation/main.tf index 01055407f..3002f87ea 100644 --- a/blueprints/fedramp-high/cloud-translation/main.tf +++ b/blueprints/fedramp-high/cloud-translation/main.tf @@ -58,6 +58,7 @@ module "workflows" { deletion_protection = var.deletion_protection description = "Translation LLM example workflow." file = var.file + kms_key_self_link = var.kms_key_self_link service_account = google_service_account.workflow_sa.email env_vars = { input_bucket = "${module.input_bucket.url}/*.txt" @@ -72,11 +73,8 @@ module "workflows" { "roles/serviceusage.serviceUsageConsumer" = [google_service_account.workflow_sa.member], "roles/storage.objectViewer" = [google_service_account.workflow_sa.member], "roles/storage.objectCreator" = [google_service_account.workflow_sa.member], - "roles/storage.objectUser" = [google_service_account.workflow_sa.member], "roles/storage.insightsCollectorService" = [google_service_account.workflow_sa.member], "roles/cloudtranslate.user" = [google_service_account.workflow_sa.member], - "roles/cloudtranslate.viewer" = [google_service_account.workflow_sa.member], - "roles/cloudtranslate.editor" = [google_service_account.workflow_sa.member], } depends_on = [ google_project_service.translate, diff --git a/blueprints/fedramp-high/cloud-translation/variables.tf b/blueprints/fedramp-high/cloud-translation/variables.tf index 0af22aa25..a577fcefd 100644 --- a/blueprints/fedramp-high/cloud-translation/variables.tf +++ b/blueprints/fedramp-high/cloud-translation/variables.tf @@ -24,6 +24,12 @@ variable "file" { default = "code/example.yaml" } +variable "kms_key_self_link" { + description = "The full self-link of the existing KMS key to use for Workflow encryption (CMEK)." + type = string + default = null +} + variable "main_project_id" { description = "The Google Project ID." type = string diff --git a/blueprints/fedramp-high/cloud-workstations/main.tf b/blueprints/fedramp-high/cloud-workstations/main.tf index c6933ed46..c14783e54 100644 --- a/blueprints/fedramp-high/cloud-workstations/main.tf +++ b/blueprints/fedramp-high/cloud-workstations/main.tf @@ -50,7 +50,9 @@ resource "google_project_service" "workstations" { disable_on_destroy = false } -data "google_project" "project" {} +data "google_project" "project" { + project_id = var.main_project_id +} module "workstations" { source = "../../../modules/workstation-cluster" @@ -58,9 +60,9 @@ module "workstations" { project_id = var.main_project_id location = var.region network_config = local.network_config - # private_cluster_config = { - # enable_private_endpoint = true - # } + private_cluster_config = { + enable_private_endpoint = true + } workstation_configs = { (var.config_id) = { container = var.image == null ? null : { diff --git a/blueprints/fedramp-high/cnap/access_policy.tf b/blueprints/fedramp-high/cnap/access_policy.tf index 7b0ebbb93..860dba95e 100644 --- a/blueprints/fedramp-high/cnap/access_policy.tf +++ b/blueprints/fedramp-high/cnap/access_policy.tf @@ -62,24 +62,24 @@ resource "google_access_context_manager_access_levels" "access-levels" { } } - # Access level for "moderate" service, including US Region, Time (7AM-9PM Monday-Friday) & Expiring Access by end of 2024. + # Access level for "moderate" service, including US Region & Time (7AM-9PM Monday-Friday). access_levels { name = "accessPolicies/${var.access_policy_number}/accessLevels/moderate_device" title = "Moderate Device Policy" basic { conditions { - required_access_levels = ["accessPolicies/${var.access_policy_number}/accessLevels/us", "accessPolicies/${var.access_policy_number}/accessLevels/time", "accessPolicies/${var.access_policy_number}/accessLevels/expire"] + required_access_levels = ["accessPolicies/${var.access_policy_number}/accessLevels/us", "accessPolicies/${var.access_policy_number}/accessLevels/time"] } } } - # Access level for "strict" service, including Mac/Windows OS, Encryption enabled, Corp owned device, Expiring Access by end of 2024, Time (7AM-9PM Monday-Friday), & US Region. + # Access level for "strict" service, including Mac/Windows OS, Encryption enabled, Corp owned device, Time (7AM-9PM Monday-Friday), & US Region. access_levels { name = "accessPolicies/${var.access_policy_number}/accessLevels/strict_device" title = "Strict Device Policy" basic { conditions { - required_access_levels = ["accessPolicies/${var.access_policy_number}/accessLevels/us", "accessPolicies/${var.access_policy_number}/accessLevels/time", "accessPolicies/${var.access_policy_number}/accessLevels/expire"] + required_access_levels = ["accessPolicies/${var.access_policy_number}/accessLevels/us", "accessPolicies/${var.access_policy_number}/accessLevels/time"] device_policy { require_screen_lock = true os_constraints { diff --git a/blueprints/fedramp-high/cnap/compute_engine.tf b/blueprints/fedramp-high/cnap/compute_engine.tf index 51155a7b9..6fe1d4904 100644 --- a/blueprints/fedramp-high/cnap/compute_engine.tf +++ b/blueprints/fedramp-high/cnap/compute_engine.tf @@ -82,7 +82,7 @@ resource "google_compute_region_instance_template" "cos-template" { # CIS Compliance Benchmark 4.11 confidential_instance_config { - enable_confidential_compute = false # This is rejecting my instance type, n2d-highcpu-2 which is supported. I think it's a bug + enable_confidential_compute = true } shielded_instance_config { enable_secure_boot = true @@ -155,7 +155,7 @@ module "kms" { iam = { "roles/cloudkms.cryptoKeyEncrypterDecrypter" = [ google_service_account.compute.member, - data.google_compute_default_service_account.default.member + "serviceAccount:service-${data.google_project.project.number}@compute-system.iam.gserviceaccount.com" ] } keyring = { diff --git a/blueprints/fedramp-high/cnap/org_policy.tf b/blueprints/fedramp-high/cnap/org_policy.tf index 26c65cfbc..8f188887a 100644 --- a/blueprints/fedramp-high/cnap/org_policy.tf +++ b/blueprints/fedramp-high/cnap/org_policy.tf @@ -16,7 +16,7 @@ resource "google_org_policy_policy" "allow_external_lb" { name = "projects/${data.google_project.landing_project.number}/policies/compute.restrictLoadBalancerCreationForTypes" parent = "projects/${data.google_project.landing_project.number}" spec { - inherit_from_parent = true + inherit_from_parent = false rules { values { diff --git a/blueprints/fedramp-high/cnap/terraform.tfvars.sample b/blueprints/fedramp-high/cnap/terraform.tfvars.sample index d99a16bc9..39cddc850 100644 --- a/blueprints/fedramp-high/cnap/terraform.tfvars.sample +++ b/blueprints/fedramp-high/cnap/terraform.tfvars.sample @@ -6,7 +6,7 @@ network_project_id = "xxxx-xxxx-net-host" prefix = "xxxx" region = "us-east4" default_backend = "easy" -oauth_brand_number = xxxxxxxxxxxx -access_policy_number = xxxxxxxxxxxx +oauth_brand_number = 123456789012 +access_policy_number = 123456789012 network_name = "xxxx-spoke-0" \ No newline at end of file diff --git a/blueprints/fedramp-high/datafusion/iam.tf b/blueprints/fedramp-high/datafusion/iam.tf index a71d6dd05..55a1ca66f 100644 --- a/blueprints/fedramp-high/datafusion/iam.tf +++ b/blueprints/fedramp-high/datafusion/iam.tf @@ -32,13 +32,6 @@ resource "google_project_iam_member" "datafusion_agent_network_user_network_proj depends_on = [time_sleep.datafusion_service_propagation] } -resource "google_project_iam_member" "datafusion_agent_spanner_viewer" { - project = var.main_project_id - role = "roles/spanner.viewer" - member = "serviceAccount:${google_project_service_identity.datafusion_agent.email}" - depends_on = [time_sleep.datafusion_service_propagation] -} - resource "google_kms_crypto_key_iam_member" "datafusion_agent_kms_access" { crypto_key_id = data.google_kms_crypto_key.default.id role = "roles/cloudkms.cryptoKeyEncrypterDecrypter" diff --git a/blueprints/fedramp-high/datafusion/main.tf b/blueprints/fedramp-high/datafusion/main.tf index 73433756f..62bf40a64 100644 --- a/blueprints/fedramp-high/datafusion/main.tf +++ b/blueprints/fedramp-high/datafusion/main.tf @@ -103,7 +103,6 @@ module "datafusion" { google_project_service.datafusion_apis, google_project_iam_member.datafusion_agent_network_user_main_project, google_project_iam_member.datafusion_agent_network_user_network_project, - google_project_iam_member.datafusion_agent_spanner_viewer, google_project_iam_member.datafusion_service_agent, google_project_iam_member.dataproc_service_agent, google_kms_crypto_key_iam_member.datafusion_agent_kms_access, diff --git a/blueprints/fedramp-high/dataproc-cluster/iam.tf b/blueprints/fedramp-high/dataproc-cluster/iam.tf index 3bd6f9534..3ccaac03c 100644 --- a/blueprints/fedramp-high/dataproc-cluster/iam.tf +++ b/blueprints/fedramp-high/dataproc-cluster/iam.tf @@ -37,13 +37,14 @@ resource "google_project_iam_member" "dataproc_compute_viewer" { member = "serviceAccount:service-${data.google_project.current.number}@dataproc-accounts.iam.gserviceaccount.com" } -resource "google_kms_crypto_key_iam_binding" "dataproc_kms" { +resource "google_kms_crypto_key_iam_member" "dataproc_kms" { + for_each = { + dataproc_vm = google_service_account.dataproc_vm.member + gcs_agent = "serviceAccount:service-${data.google_project.current.number}@gs-project-accounts.iam.gserviceaccount.com" + dataproc_agent = "serviceAccount:service-${data.google_project.current.number}@dataproc-accounts.iam.gserviceaccount.com" + compute_agent = "serviceAccount:service-${data.google_project.current.number}@compute-system.iam.gserviceaccount.com" + } crypto_key_id = data.google_kms_crypto_key.default.id role = "roles/cloudkms.cryptoKeyEncrypterDecrypter" - members = [ - google_service_account.dataproc_vm.member, - "serviceAccount:service-${data.google_project.current.number}@gs-project-accounts.iam.gserviceaccount.com", - "serviceAccount:service-${data.google_project.current.number}@dataproc-accounts.iam.gserviceaccount.com", - "serviceAccount:service-${data.google_project.current.number}@compute-system.iam.gserviceaccount.com" - ] + member = each.value } \ No newline at end of file diff --git a/blueprints/fedramp-high/dataproc-cluster/main.tf b/blueprints/fedramp-high/dataproc-cluster/main.tf index 36035e2bb..69ff9111f 100644 --- a/blueprints/fedramp-high/dataproc-cluster/main.tf +++ b/blueprints/fedramp-high/dataproc-cluster/main.tf @@ -77,7 +77,8 @@ resource "google_compute_firewall" "dataproc" { log_config { metadata = "INCLUDE_ALL_METADATA" } - source_ranges = ["10.128.0.0/9"] + target_tags = ["dataproc"] + source_ranges = [data.google_compute_subnetwork.subnetwork.ip_cidr_range] } module "gcs" { @@ -105,7 +106,7 @@ module "gcs" { force_destroy = true depends_on = [ - google_kms_crypto_key_iam_binding.dataproc_kms + google_kms_crypto_key_iam_member.dataproc_kms ] } @@ -133,6 +134,6 @@ module "dataproc_cluster" { } } depends_on = [ - google_kms_crypto_key_iam_binding.dataproc_kms + google_kms_crypto_key_iam_member.dataproc_kms ] } diff --git a/blueprints/fedramp-high/datastore/provider.tf b/blueprints/fedramp-high/datastore/provider.tf index 2014efec8..d6d75c1d3 100644 --- a/blueprints/fedramp-high/datastore/provider.tf +++ b/blueprints/fedramp-high/datastore/provider.tf @@ -20,7 +20,8 @@ terraform { version = ">= 3.53, < 6" } null = { - source = "hashicorp/null" + source = "hashicorp/null" + version = "~> 3.2" } } } diff --git a/blueprints/fedramp-high/document-ai/main.tf b/blueprints/fedramp-high/document-ai/main.tf index de2b2cd8d..df27256b4 100644 --- a/blueprints/fedramp-high/document-ai/main.tf +++ b/blueprints/fedramp-high/document-ai/main.tf @@ -23,7 +23,9 @@ resource "google_project_service" "documentai" { disable_on_destroy = false } -data "google_project" "project" {} +data "google_project" "project" { + project_id = var.main_project_id +} resource "google_document_ai_processor" "processor" { location = "us" @@ -94,7 +96,6 @@ module "workflows" { "roles/documentai.apiUser" = [google_service_account.workflow_sa.member], "roles/storage.objectViewer" = [google_service_account.workflow_sa.member], "roles/storage.objectCreator" = [google_service_account.workflow_sa.member], - "roles/storage.objectUser" = [google_service_account.workflow_sa.member], } depends_on = [ google_project_service.documentai, diff --git a/blueprints/fedramp-high/document-ai/terraform.tfvars.sample b/blueprints/fedramp-high/document-ai/terraform.tfvars.sample index 6eb0b7fa4..7818bb5c1 100644 --- a/blueprints/fedramp-high/document-ai/terraform.tfvars.sample +++ b/blueprints/fedramp-high/document-ai/terraform.tfvars.sample @@ -3,4 +3,5 @@ main_project_id = "xxxx-xxxx-xxxx-main-0" name = "document-ai-processor-name-00" type = "OCR_PROCESSOR" # https://cloud.google.com/document-ai/docs/processors-list (look for "Type in API" field) deletion_protection = false -region = "us-east4" \ No newline at end of file +region = "us-east4" +kms_key_name = "projects/xxxx-xxxx-xxxx-iac-core-0/locations/us-east4/keyRings/xxxx-xxxx-keyring/cryptoKeys/default" \ No newline at end of file diff --git a/blueprints/fedramp-high/firestore/main.tf b/blueprints/fedramp-high/firestore/main.tf index a42ac67c6..a91c04324 100644 --- a/blueprints/fedramp-high/firestore/main.tf +++ b/blueprints/fedramp-high/firestore/main.tf @@ -25,6 +25,7 @@ module "firestore" { location_id = var.region type = "FIRESTORE_NATIVE" deletion_policy = "DELETE" + kms_key_name = var.kms_key_name } backup_schedule = var.backup_schedule diff --git a/blueprints/fedramp-high/gemini-enterprise/analytics/Dockerfile b/blueprints/fedramp-high/gemini-enterprise/analytics/Dockerfile index 1af08c50f..f064b8126 100644 --- a/blueprints/fedramp-high/gemini-enterprise/analytics/Dockerfile +++ b/blueprints/fedramp-high/gemini-enterprise/analytics/Dockerfile @@ -7,6 +7,9 @@ RUN pip install --no-cache-dir -r requirements.txt COPY . . +RUN useradd --create-home --shell /usr/sbin/nologin appuser && chown -R appuser:appuser /app +USER appuser + # Expose port 8080 for Cloud Run EXPOSE 8080 diff --git a/blueprints/fedramp-high/gemini-enterprise/deploy.sh b/blueprints/fedramp-high/gemini-enterprise/deploy.sh index 3ab205ff2..4eb5b0929 100755 --- a/blueprints/fedramp-high/gemini-enterprise/deploy.sh +++ b/blueprints/fedramp-high/gemini-enterprise/deploy.sh @@ -2882,12 +2882,12 @@ update_app_compliance() { return 1 fi - CMD="gem4gov app update-compliance --project-id ${PROJECT_ID} --engine-id ${ENGINE_ID} --compliance-regime ${COMPLIANCE_REGIME}" + local cmd=(gem4gov app update-compliance --project-id "${PROJECT_ID}" --engine-id "${ENGINE_ID}" --compliance-regime "${COMPLIANCE_REGIME}") - echo "Running: $CMD" + echo "Running: ${cmd[*]}" export GOOGLE_CLOUD_PROJECT="${PROJECT_ID}" export GOOGLE_CLOUD_QUOTA_PROJECT="${PROJECT_ID}" - if ! $CMD; then + if ! "${cmd[@]}"; then echo -e "${RED}ERROR: Failed to update compliance regime.${NC}" return 1 fi @@ -3709,22 +3709,22 @@ deploy_analytics_dashboard() { # Deploy to Cloud Run echo "Deploying to Cloud Run..." - local deploy_cmd="gcloud run deploy gemini-analytics-dashboard \ - --image \"$image_name\" \ - --project \"$state_project_id\" \ - --region \"$region\" \ - --no-allow-unauthenticated \ - --binary-authorization=default \ - --ingress internal-and-cloud-load-balancing \ - --set-env-vars PROJECT_ID=\"$state_project_id\",DATASET_ID=\"$state_dataset_id\"" + local deploy_cmd=( + gcloud run deploy gemini-analytics-dashboard + --image "$image_name" + --project "$state_project_id" + --region "$region" + --no-allow-unauthenticated + --binary-authorization=default + --ingress internal-and-cloud-load-balancing + --set-env-vars "PROJECT_ID=${state_project_id},DATASET_ID=${state_dataset_id}" + ) - - if [[ -n "$state_sa_email" ]]; then - deploy_cmd="$deploy_cmd --service-account \"$state_sa_email\"" + deploy_cmd+=(--service-account "$state_sa_email") fi - if eval "$deploy_cmd"; then + if "${deploy_cmd[@]}"; then echo -e "${GREEN}Dashboard deployed successfully.${NC}" else echo -e "${RED}Failed to deploy dashboard.${NC}" diff --git a/blueprints/fedramp-high/gemini-enterprise/gem4gov-cli/data_stores.py b/blueprints/fedramp-high/gemini-enterprise/gem4gov-cli/data_stores.py index 87986aa0d..51815878f 100644 --- a/blueprints/fedramp-high/gemini-enterprise/gem4gov-cli/data_stores.py +++ b/blueprints/fedramp-high/gemini-enterprise/gem4gov-cli/data_stores.py @@ -17,12 +17,13 @@ from googleapiclient.errors import HttpError from googleapiclient.discovery import build from google.api_core.client_options import ClientOptions -import random +import secrets import string def generate_id(prefix): """Generates a random 6-character alphanumeric string.""" - return prefix + ''.join(random.choices(string.ascii_lowercase + string.digits, k=6)) + alphabet = string.ascii_lowercase + string.digits + return prefix + ''.join(secrets.choice(alphabet) for _ in range(6)) def validate_data_store(credentials, project_id, data_store_id): diff --git a/blueprints/fedramp-high/gemini-enterprise/gem4gov-cli/gem4gov.py b/blueprints/fedramp-high/gemini-enterprise/gem4gov-cli/gem4gov.py index b11324a22..3c33b1bd8 100644 --- a/blueprints/fedramp-high/gemini-enterprise/gem4gov-cli/gem4gov.py +++ b/blueprints/fedramp-high/gemini-enterprise/gem4gov-cli/gem4gov.py @@ -1315,7 +1315,7 @@ def configure_idp_for_widget(credentials, project_id, engine_id, workforce_pool_ # Use subprocess to run the curl command curl_command = [ 'curl', '-X', 'PATCH', - '-H', f"Authorization: Bearer {access_token}", + '-H', '@-', '-H', f"x-goog-user-project: {project_id}", '-H', "Content-Type: application/json", '-d', json.dumps(data), @@ -1326,7 +1326,7 @@ def configure_idp_for_widget(credentials, project_id, engine_id, workforce_pool_ # Retry logic for widget config availability max_retries = 5 for attempt in range(max_retries): - result = subprocess.run(curl_command, capture_output=True, text=True) + result = subprocess.run(curl_command, input=f"Authorization: Bearer {access_token}\n", capture_output=True, text=True) if result.returncode == 0 and "error" not in result.stdout.lower(): click.echo("Successfully configured identity provider for the search widget.") @@ -1371,7 +1371,7 @@ def disable_user_event_collection(credentials, project_id, engine_id): # Use subprocess to run the curl command curl_command = [ 'curl', '-X', 'PATCH', - '-H', f"Authorization: Bearer {access_token}", + '-H', '@-', '-H', f"x-goog-user-project: {project_id}", '-H', "Content-Type: application/json", '-d', json.dumps(data), @@ -1382,7 +1382,7 @@ def disable_user_event_collection(credentials, project_id, engine_id): # Retry logic for widget config availability max_retries = 5 for attempt in range(max_retries): - result = subprocess.run(curl_command, capture_output=True, text=True) + result = subprocess.run(curl_command, input=f"Authorization: Bearer {access_token}\n", capture_output=True, text=True) if result.returncode == 0 and "error" not in result.stdout.lower(): click.echo("Successfully disabled user event collection.") @@ -1470,7 +1470,7 @@ def configure_gemini_enterprise_for_fedramp_high(credentials, project_id, engine # Use subprocess to run the curl command curl_command = [ 'curl', '-X', 'PATCH', - '-H', f"Authorization: Bearer {access_token}", + '-H', '@-', '-H', f"x-goog-user-project: {project_id}", '-H', "Content-Type: application/json", '-d', json.dumps(assistant_patch_body), @@ -1478,7 +1478,7 @@ def configure_gemini_enterprise_for_fedramp_high(credentials, project_id, engine ] try: - result = subprocess.run(curl_command, capture_output=True, text=True) + result = subprocess.run(curl_command, input=f"Authorization: Bearer {access_token}\n", capture_output=True, text=True) if result.returncode == 0 and "error" not in result.stdout.lower(): click.echo(f"Default assistant for engine {engine_id} configured for FedRAMP High.") @@ -1581,7 +1581,7 @@ def configure_gemini_enterprise_for_il4(credentials, project_id, engine_id): # Use subprocess to run the curl command curl_command = [ 'curl', '-X', 'PATCH', - '-H', f"Authorization: Bearer {access_token}", + '-H', '@-', '-H', f"x-goog-user-project: {project_id}", '-H', "Content-Type: application/json", '-d', json.dumps(assistant_patch_body), @@ -1589,7 +1589,7 @@ def configure_gemini_enterprise_for_il4(credentials, project_id, engine_id): ] try: - result = subprocess.run(curl_command, capture_output=True, text=True) + result = subprocess.run(curl_command, input=f"Authorization: Bearer {access_token}\n", capture_output=True, text=True) if result.returncode == 0 and "error" not in result.stdout.lower(): click.echo(f"Default assistant for engine {engine_id} configured for IL4.") @@ -1692,7 +1692,7 @@ def configure_gemini_enterprise_for_il5(credentials, project_id, engine_id): # Use subprocess to run the curl command curl_command = [ 'curl', '-X', 'PATCH', - '-H', f"Authorization: Bearer {access_token}", + '-H', '@-', '-H', f"x-goog-user-project: {project_id}", '-H', "Content-Type: application/json", '-d', json.dumps(assistant_patch_body), @@ -1700,7 +1700,7 @@ def configure_gemini_enterprise_for_il5(credentials, project_id, engine_id): ] try: - result = subprocess.run(curl_command, capture_output=True, text=True) + result = subprocess.run(curl_command, input=f"Authorization: Bearer {access_token}\n", capture_output=True, text=True) if result.returncode == 0 and "error" not in result.stdout.lower(): click.echo(f"Default assistant for engine {engine_id} configured for IL5.") diff --git a/blueprints/fedramp-high/gemini-enterprise/gemini-stage-0/cloudarmor.tf b/blueprints/fedramp-high/gemini-enterprise/gemini-stage-0/cloudarmor.tf index 4bd820156..92b72eb2d 100644 --- a/blueprints/fedramp-high/gemini-enterprise/gemini-stage-0/cloudarmor.tf +++ b/blueprints/fedramp-high/gemini-enterprise/gemini-stage-0/cloudarmor.tf @@ -31,7 +31,7 @@ resource "google_compute_region_security_policy" "gemini_enterprise_policy" { dynamic "rules" { for_each = local.waf.basic_rules content { - action = "allow" + action = "deny(403)" priority = rules.value.priority preview = rules.value.preview match { diff --git a/blueprints/fedramp-high/gemini-enterprise/gemini-stage-0/discovery-engine.tf b/blueprints/fedramp-high/gemini-enterprise/gemini-stage-0/discovery-engine.tf index c6bed09aa..891e89042 100644 --- a/blueprints/fedramp-high/gemini-enterprise/gemini-stage-0/discovery-engine.tf +++ b/blueprints/fedramp-high/gemini-enterprise/gemini-stage-0/discovery-engine.tf @@ -62,7 +62,8 @@ resource "google_storage_bucket" "gemini_enterprise_gcs_bucket" { name = each.value.name location = var.geolocation uniform_bucket_level_access = true - force_destroy = true # Set to true only for non-production/demo + public_access_prevention = "enforced" + force_destroy = false dynamic "encryption" { for_each = local.cmek_key_id != null ? [1] : [] diff --git a/blueprints/fedramp-high/gemini-enterprise/gemini-stage-0/iam.tf b/blueprints/fedramp-high/gemini-enterprise/gemini-stage-0/iam.tf index fbb6989e3..5581e1da6 100644 --- a/blueprints/fedramp-high/gemini-enterprise/gemini-stage-0/iam.tf +++ b/blueprints/fedramp-high/gemini-enterprise/gemini-stage-0/iam.tf @@ -51,10 +51,3 @@ resource "google_project_iam_member" "users_discoveryengine_user" { role = "roles/discoveryengine.user" member = each.value } - -resource "google_project_iam_member" "users_serviceusage_consumer" { - for_each = toset(var.user_groups) - project = var.main_project_id - role = "roles/serviceusage.serviceUsageConsumer" - member = each.value -} diff --git a/blueprints/fedramp-high/gemini-enterprise/gemini-stage-0/load_balancer.tf b/blueprints/fedramp-high/gemini-enterprise/gemini-stage-0/load_balancer.tf index 8ff9923ea..4b12bb20a 100644 --- a/blueprints/fedramp-high/gemini-enterprise/gemini-stage-0/load_balancer.tf +++ b/blueprints/fedramp-high/gemini-enterprise/gemini-stage-0/load_balancer.tf @@ -45,7 +45,8 @@ resource "google_compute_region_backend_service" "gemini_enterprise_backend" { lifecycle { ignore_changes = [ - iap + iap[0].oauth2_client_id, + iap[0].oauth2_client_secret, ] } } diff --git a/blueprints/fedramp-high/gemini-enterprise/gemini-stage-0/network.tf b/blueprints/fedramp-high/gemini-enterprise/gemini-stage-0/network.tf index de13095fb..1aef8bb6d 100644 --- a/blueprints/fedramp-high/gemini-enterprise/gemini-stage-0/network.tf +++ b/blueprints/fedramp-high/gemini-enterprise/gemini-stage-0/network.tf @@ -44,6 +44,11 @@ resource "google_compute_subnetwork" "gemini_enterprise_vpc_subnet" { region = var.region network = google_compute_network.gemini_enterprise_vpc[0].id private_ip_google_access = true + log_config { + aggregation_interval = "INTERVAL_5_SEC" + flow_sampling = 1.0 + metadata = "INCLUDE_ALL_METADATA" + } } resource "google_compute_subnetwork" "gemini_enterprise_vpc_proxy_subnet" { diff --git a/blueprints/fedramp-high/gemini-enterprise/gemini-stage-0/terraform.tfvars.sample b/blueprints/fedramp-high/gemini-enterprise/gemini-stage-0/terraform.tfvars.sample index 8f78846d9..359738e09 100644 --- a/blueprints/fedramp-high/gemini-enterprise/gemini-stage-0/terraform.tfvars.sample +++ b/blueprints/fedramp-high/gemini-enterprise/gemini-stage-0/terraform.tfvars.sample @@ -15,6 +15,9 @@ main_project_id = "prefx-main-xxx-x" prefix = "prefx" environment = "dev" +tenant = "tnt1" +domain = "example.com" +terraform_state_bucket = "prefx-iac-core-state-0" # Get your organization ID via `gcloud organizations list` # Then run `gcloud access-context-manager policies list --organization [ORGANIZATION_ID]` @@ -105,8 +108,8 @@ gemini_apps = { "g4g-gem-ent-app-": { "display_name": "gemini app display name", "company_name": "company name", - "gcs_data_store_keys": ["gcs-data-store-name"], - "bq_data_store_keys": ["bigquery-data-store-name"], + "gcs_data_store_keys": ["company-docs"], + "bq_data_store_keys": ["internal-wiki"], "enable_agent_sharing": false, "enable_agent_sharing_without_approval": false, "enable_audit_logs": false, diff --git a/blueprints/fedramp-high/gemini-enterprise/gemini-stage-1/load_balancer.tf b/blueprints/fedramp-high/gemini-enterprise/gemini-stage-1/load_balancer.tf index cd5695adf..b9bb3f9d3 100644 --- a/blueprints/fedramp-high/gemini-enterprise/gemini-stage-1/load_balancer.tf +++ b/blueprints/fedramp-high/gemini-enterprise/gemini-stage-1/load_balancer.tf @@ -39,7 +39,7 @@ data "google_compute_region_backend_service" "gemini_enterprise_backend" { # Data source to get the network created in stage-0 or Shared VPC data "google_compute_network" "gemini_enterprise_vpc" { - count = data.terraform_remote_state.stage_0.outputs.deployment_type != "none" ? 1 : 0 + count = data.terraform_remote_state.stage_0.outputs.deployment_type != "none" ? 1 : 0 project = var.host_project_id != "" ? var.host_project_id : ( try(data.terraform_remote_state.stage_0.outputs.use_shared_vpc, false) ? data.terraform_remote_state.stage_0.outputs.network_project_id : data.terraform_remote_state.stage_0.outputs.main_project_id ) @@ -94,7 +94,7 @@ resource "google_compute_region_url_map" "gemini_enterprise_load_balancer" { route_action { url_rewrite { host_rewrite = "vertexaisearch.cloud.google.com" - path_prefix_rewrite = "/us/home/cid/${data.terraform_remote_state.stage_0.outputs.gemini_apps_widget_ids[route_rules.value]}?hl=en_US" + path_prefix_rewrite = "/us/home/cid/${data.terraform_remote_state.stage_0.outputs.gemini_apps_widget_ids[route_rules.value]}" } } } @@ -110,7 +110,7 @@ resource "google_compute_region_url_map" "gemini_enterprise_load_balancer" { route_action { url_rewrite { host_rewrite = "vertexaisearch.cloud.google.com" - path_prefix_rewrite = "/us/home/cid/${local.first_gemini_widget_id}?hl=en_US" + path_prefix_rewrite = "/us/home/cid/${local.first_gemini_widget_id}" } } } @@ -133,7 +133,7 @@ resource "google_compute_region_url_map" "gemini_enterprise_load_balancer" { host_redirect = "auth.cloud.google" path_redirect = "/signin/${data.terraform_remote_state.stage_0.outputs.acl_workforce_pool_name}/providers/${data.terraform_remote_state.stage_0.outputs.acl_workforce_provider_id}?continueUrl=https%3A%2F%2Fvertexaisearch.cloud.google%2Fus%2Fhome%2Fcid%2F${data.terraform_remote_state.stage_0.outputs.gemini_apps_widget_ids[route_rules.value]}&hl=en_US" redirect_response_code = "FOUND" - strip_query = false + strip_query = true https_redirect = true } } @@ -145,7 +145,7 @@ resource "google_compute_region_url_map" "gemini_enterprise_load_balancer" { host_redirect = "auth.cloud.google" path_redirect = "/signin/${data.terraform_remote_state.stage_0.outputs.acl_workforce_pool_name}/providers/${data.terraform_remote_state.stage_0.outputs.acl_workforce_provider_id}?continueUrl=https%3A%2F%2Fvertexaisearch.cloud.google%2Fus%2Fhome%2Fcid%2F${local.first_gemini_widget_id}&hl=en_US" redirect_response_code = "FOUND" - strip_query = false + strip_query = true } } } @@ -168,13 +168,13 @@ resource "google_certificate_manager_certificate" "gemini_enterprise_managed_cer # This resource creates the target HTTPS proxy for the load balancer. resource "google_compute_region_target_https_proxy" "gemini_enterprise_https_proxy" { - count = data.terraform_remote_state.stage_0.outputs.deployment_type != "none" ? 1 : 0 - project = data.terraform_remote_state.stage_0.outputs.main_project_id - name = "${data.terraform_remote_state.stage_0.outputs.prefix}-gemini-enterprise-https-proxy" - region = data.terraform_remote_state.stage_0.outputs.region - url_map = google_compute_region_url_map.gemini_enterprise_load_balancer[0].id - - ssl_certificates = var.cert_management_choice == "self_managed" ? [data.google_compute_region_ssl_certificate.gemini_enterprise_cert[0].self_link] : null + count = data.terraform_remote_state.stage_0.outputs.deployment_type != "none" ? 1 : 0 + project = data.terraform_remote_state.stage_0.outputs.main_project_id + name = "${data.terraform_remote_state.stage_0.outputs.prefix}-gemini-enterprise-https-proxy" + region = data.terraform_remote_state.stage_0.outputs.region + url_map = google_compute_region_url_map.gemini_enterprise_load_balancer[0].id + + ssl_certificates = var.cert_management_choice == "self_managed" ? [data.google_compute_region_ssl_certificate.gemini_enterprise_cert[0].self_link] : null certificate_manager_certificates = var.cert_management_choice == "google_managed" ? [google_certificate_manager_certificate.gemini_enterprise_managed_cert[0].id] : null } @@ -195,7 +195,7 @@ resource "google_compute_forwarding_rule" "gemini_enterprise_forwarding_rule" { # Data source to get the subnet created in stage-0 or Shared VPC data "google_compute_subnetwork" "gemini_enterprise_vpc_subnet" { - count = data.terraform_remote_state.stage_0.outputs.deployment_type == "internal" ? 1 : 0 + count = data.terraform_remote_state.stage_0.outputs.deployment_type == "internal" ? 1 : 0 project = var.host_project_id != "" ? var.host_project_id : ( try(data.terraform_remote_state.stage_0.outputs.use_shared_vpc, false) ? data.terraform_remote_state.stage_0.outputs.network_project_id : data.terraform_remote_state.stage_0.outputs.main_project_id ) diff --git a/blueprints/fedramp-high/gitlab/main.tf b/blueprints/fedramp-high/gitlab/main.tf index 676756036..d5724c4d0 100644 --- a/blueprints/fedramp-high/gitlab/main.tf +++ b/blueprints/fedramp-high/gitlab/main.tf @@ -12,7 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -data "google_project" "project" {} +data "google_project" "project" { + project_id = var.project_id +} resource "google_service_account" "gitlab-sa" { account_id = var.sa @@ -36,28 +38,28 @@ resource "google_project_iam_member" "gke_host_agent_use" { member = "serviceAccount:service-${data.google_project.project.number}@container-engine-robot.iam.gserviceaccount.com" # Use project where the GKE cluster is being created } -resource "google_project_iam_binding" "compute_agent_subnet_user" { +resource "google_project_iam_member" "compute_agent_subnet_user" { + for_each = { + compute_agent = "serviceAccount:service-${data.google_project.project.number}@compute-system.iam.gserviceaccount.com" + gke_agent = "serviceAccount:service-${data.google_project.project.number}@container-engine-robot.iam.gserviceaccount.com" + cloudservices = "serviceAccount:${data.google_project.project.number}@cloudservices.gserviceaccount.com" + gitlab_sa = "serviceAccount:${google_service_account.gitlab-sa.email}" + } project = var.net_project role = "roles/compute.networkUser" - members = [ - "serviceAccount:service-${data.google_project.project.number}@compute-system.iam.gserviceaccount.com", - "serviceAccount:service-${data.google_project.project.number}@container-engine-robot.iam.gserviceaccount.com", - "serviceAccount:${data.google_project.project.number}-compute@developer.gserviceaccount.com", - "serviceAccount:${data.google_project.project.number}@cloudservices.gserviceaccount.com", - "serviceAccount:${google_service_account.gitlab-sa.email}" - ] + member = each.value } -resource "google_kms_crypto_key_iam_binding" "compute_service_agent_kms_permissions" { +resource "google_kms_crypto_key_iam_member" "compute_service_agent_kms_permissions" { + for_each = { + compute_agent = "serviceAccount:service-${data.google_project.project.number}@compute-system.iam.gserviceaccount.com" + gke_agent = "serviceAccount:service-${data.google_project.project.number}@container-engine-robot.iam.gserviceaccount.com" + cloudservices = "serviceAccount:${data.google_project.project.number}@cloudservices.gserviceaccount.com" + gitlab_sa = "serviceAccount:${google_service_account.gitlab-sa.email}" + } crypto_key_id = var.kms_key role = "roles/cloudkms.cryptoKeyEncrypterDecrypter" - members = [ - "serviceAccount:service-${data.google_project.project.number}@compute-system.iam.gserviceaccount.com", - "serviceAccount:service-${data.google_project.project.number}@container-engine-robot.iam.gserviceaccount.com", - "serviceAccount:${data.google_project.project.number}-compute@developer.gserviceaccount.com", - "serviceAccount:${data.google_project.project.number}@cloudservices.gserviceaccount.com", - "serviceAccount:${google_service_account.gitlab-sa.email}" - ] + member = each.value } resource "google_compute_instance_group" "umig" { @@ -159,7 +161,7 @@ sudo EXTERNAL_URL="${var.gitlab_uri}" apt install gitlab-ee -y EOT } - depends_on = [google_kms_crypto_key_iam_binding.compute_service_agent_kms_permissions] + depends_on = [google_kms_crypto_key_iam_member.compute_service_agent_kms_permissions] } // This creates a self signed certificate @@ -253,7 +255,7 @@ module "cluster" { } } deletion_protection = false - depends_on = [google_kms_crypto_key_iam_binding.compute_service_agent_kms_permissions, google_project_iam_binding.compute_agent_subnet_user, google_project_iam_member.gke_host_agent_use, google_project_iam_member.gke_cluster_admin] + depends_on = [google_kms_crypto_key_iam_member.compute_service_agent_kms_permissions, google_project_iam_member.compute_agent_subnet_user, google_project_iam_member.gke_host_agent_use, google_project_iam_member.gke_cluster_admin] } module "gke_node_pool" { diff --git a/blueprints/fedramp-high/gitlab/provider.tf b/blueprints/fedramp-high/gitlab/provider.tf index 3624c92af..bb62fa53e 100644 --- a/blueprints/fedramp-high/gitlab/provider.tf +++ b/blueprints/fedramp-high/gitlab/provider.tf @@ -30,3 +30,8 @@ provider "google" { project = var.project_id region = var.region } + +provider "google-beta" { + project = var.project_id + region = var.region +} diff --git a/blueprints/fedramp-high/gitlab/terraform.tfvars.sample b/blueprints/fedramp-high/gitlab/terraform.tfvars.sample index a91c86560..22443305a 100644 --- a/blueprints/fedramp-high/gitlab/terraform.tfvars.sample +++ b/blueprints/fedramp-high/gitlab/terraform.tfvars.sample @@ -1,7 +1,7 @@ -gitlab_uri = "gitlab.mine.com" +gitlab_uri = "https://gitlab.mine.com" network = "projects//global/networks/" subnetwork = "projects//regions/us-east4/subnetworks/" -project_id = "" +project_id = "xxxx-xxxx-main-0" kms_key = "projects//locations/us-east4/keyRings/keyring/cryptoKeys/gcs" zone = "us-east4" net_project = "int-net-host" diff --git a/blueprints/fedramp-high/gitlab/variables.tf b/blueprints/fedramp-high/gitlab/variables.tf index c9ae3428a..4f19503f2 100644 --- a/blueprints/fedramp-high/gitlab/variables.tf +++ b/blueprints/fedramp-high/gitlab/variables.tf @@ -15,6 +15,10 @@ variable "gitlab_uri" { description = "The URL hostname that the gitlab instance will be attached to." type = string + validation { + condition = can(regex("^https?://[a-zA-Z0-9.-]+(:[0-9]+)?/?$", var.gitlab_uri)) + error_message = "gitlab_uri must be a valid HTTP/HTTPS URL without shell metacharacters." + } } variable "gke_initial_node_per_zone" { @@ -113,4 +117,8 @@ variable "gitlab_install_script_sha256" { description = "Expected SHA-256 hash for the GitLab package repository install script." type = string default = "47c124527729776870cf09cd6bd46a9b94f55d40c7545ca26640c75de86b560d" + validation { + condition = can(regex("^[a-fA-F0-9]{64}$", var.gitlab_install_script_sha256)) + error_message = "gitlab_install_script_sha256 must be a valid 64-character hexadecimal SHA-256 digest." + } } diff --git a/blueprints/fedramp-high/network-connectivity-center/main.tf b/blueprints/fedramp-high/network-connectivity-center/main.tf index 7d46185b9..2ec5ba54a 100644 --- a/blueprints/fedramp-high/network-connectivity-center/main.tf +++ b/blueprints/fedramp-high/network-connectivity-center/main.tf @@ -60,27 +60,18 @@ resource "google_network_connectivity_group" "default" { count = var.topology == "MESH" ? 1 : 0 hub = google_network_connectivity_hub.hub.id name = "default" - auto_accept { - auto_accept_projects = local.spoke_projects - } } resource "google_network_connectivity_group" "center" { count = var.topology == "STAR" ? 1 : 0 hub = google_network_connectivity_hub.hub.id name = "center" - auto_accept { - auto_accept_projects = [var.main_project_id] - } } resource "google_network_connectivity_group" "edge" { count = var.topology == "STAR" ? 1 : 0 hub = google_network_connectivity_hub.hub.id name = "edge" - auto_accept { - auto_accept_projects = local.spoke_projects - } } resource "google_network_connectivity_spoke" "spokes" { diff --git a/blueprints/fedramp-high/secret-manager/main.tf b/blueprints/fedramp-high/secret-manager/main.tf index 14be94705..a467a580d 100644 --- a/blueprints/fedramp-high/secret-manager/main.tf +++ b/blueprints/fedramp-high/secret-manager/main.tf @@ -24,8 +24,10 @@ locals { secrets = { for secret_id, secret_data in var.secrets : secret_id => { - locations = [secret_data.location] # Convert single string to list of strings - keys = { (secret_data.location) = secret_data.key } # Convert single string to map of strings + locations = [secret_data.location] # Convert single string to list of strings + keys = { (secret_data.location) = secret_data.key } # Convert single string to map of strings + expire_time = try(secret_data.expire_time, null) + version_destroy_ttl = try(secret_data.version_destroy_ttl, null) } } } diff --git a/blueprints/fedramp-high/vertex-mlops/terraform.tfvars.sample b/blueprints/fedramp-high/vertex-mlops/terraform.tfvars.sample index 265b1e2ad..854f93e7d 100644 --- a/blueprints/fedramp-high/vertex-mlops/terraform.tfvars.sample +++ b/blueprints/fedramp-high/vertex-mlops/terraform.tfvars.sample @@ -1,5 +1,5 @@ project_config = { - main_project_id = "xxxx-xxxx-main-0" + project_id = "xxxx-xxxx-main-0" } network_config = { @@ -14,7 +14,7 @@ prefix = "xxxx" region = "us-central" # us-central1 is recommended notebooks = { - "" = { + "notebook-00" = { type = "USER_MANAGED" } } diff --git a/blueprints/fedramp-high/vertex-mlops/variables.tf b/blueprints/fedramp-high/vertex-mlops/variables.tf index 1342ae0f2..21c885bdb 100644 --- a/blueprints/fedramp-high/vertex-mlops/variables.tf +++ b/blueprints/fedramp-high/vertex-mlops/variables.tf @@ -30,7 +30,7 @@ variable "dataset_name" { variable "deletion_protection" { description = "Prevent Terraform from destroying data storage resources (storage buckets, GKE clusters, CloudSQL instances) in this blueprint. When this field is set in Terraform state, a terraform destroy or terraform apply that would delete data storage resources will fail." type = bool - default = false + default = true nullable = false } @@ -60,7 +60,7 @@ variable "notebooks" { type = string machine_type = optional(string, "n1-standard-4") internal_ip_only = optional(bool, true) - idle_shutdown = optional(bool, false) + idle_shutdown = optional(bool, true) owner = optional(string) })) validation { @@ -73,6 +73,11 @@ variable "notebooks" { for k, v in var.notebooks : (v.type == "MANAGED" && try(v.owner != null, false) || v.type == "USER_MANAGED")]) error_message = "`owner` must be set for `MANAGED` instances." } + validation { + condition = alltrue([ + for k, v in var.notebooks : v.internal_ip_only == true]) + error_message = "All notebooks must have `internal_ip_only` set to `true`." + } } variable "prefix" { diff --git a/blueprints/fedramp-high/vertex-mlops/vertex.tf b/blueprints/fedramp-high/vertex-mlops/vertex.tf index 2876e3ef0..a8dc52e0f 100644 --- a/blueprints/fedramp-high/vertex-mlops/vertex.tf +++ b/blueprints/fedramp-high/vertex-mlops/vertex.tf @@ -31,6 +31,7 @@ resource "google_notebooks_runtime" "runtime" { } software_config { enable_health_monitoring = true + idle_shutdown = var.notebooks[each.key].idle_shutdown } virtual_machine { virtual_machine_config { @@ -44,6 +45,11 @@ resource "google_notebooks_runtime" "runtime" { kms_key = var.service_encryption_keys.notebooks } } + shielded_instance_config { + enable_secure_boot = true + enable_vtpm = true + enable_integrity_monitoring = true + } metadata = { notebook-disable-nbconvert = "false" notebook-disable-downloads = "true" @@ -82,6 +88,24 @@ resource "google_workbench_instance" "playground" { kms_key = var.service_encryption_keys.notebooks } + data_disks { + disk_size_gb = 100 + disk_type = "PD_STANDARD" + disk_encryption = var.service_encryption_keys.notebooks != null ? "CMEK" : null + kms_key = var.service_encryption_keys.notebooks + } + + shielded_instance_config { + enable_secure_boot = true + enable_vtpm = true + enable_integrity_monitoring = true + } + + metadata = { + notebook-disable-downloads = "true" + notebook-disable-root = "true" + } + network_interfaces { network = local.vpc subnet = local.subnet diff --git a/blueprints/fedramp-high/workflows/main.tf b/blueprints/fedramp-high/workflows/main.tf index 898dc8081..c3149b823 100644 --- a/blueprints/fedramp-high/workflows/main.tf +++ b/blueprints/fedramp-high/workflows/main.tf @@ -12,7 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -data "google_project" "current" {} +data "google_project" "current" { + project_id = var.main_project_id +} locals { # Determine the KMS key region: use kms_key_location if provided, otherwise default to the workflow's region @@ -54,9 +56,7 @@ resource "google_service_account" "workflow_sa" { resource "google_kms_crypto_key_iam_member" "workflows_agent_kms_access" { crypto_key_id = local.kms_key_self_link_calculated # Using the calculated local role = "roles/cloudkms.cryptoKeyEncrypterDecrypter" - # The Workflows service agent is typically in the format: - # service-${project_number}@gcp-sa-workflows.iam.gserviceaccount.com - member = "serviceAccount:service-${data.google_project.current.number}@gcp-sa-workflows.iam.gserviceaccount.com" + member = google_project_service_identity.workflows_si.member depends_on = [ google_project_service.workflows_api, diff --git a/blueprints/fedramp-high/workflows/outputs.tf b/blueprints/fedramp-high/workflows/outputs.tf index e1469f1fe..cb96bec09 100644 --- a/blueprints/fedramp-high/workflows/outputs.tf +++ b/blueprints/fedramp-high/workflows/outputs.tf @@ -20,5 +20,6 @@ output "service_account" { output "workflow" { description = "The newly created workflow." value = module.workflows.workflow + sensitive = true } diff --git a/blueprints/fedramp-high/workflows/variables.tf b/blueprints/fedramp-high/workflows/variables.tf index 111617315..d4d910825 100644 --- a/blueprints/fedramp-high/workflows/variables.tf +++ b/blueprints/fedramp-high/workflows/variables.tf @@ -28,6 +28,7 @@ variable "env_vars" { description = "Environment variables made available to your workflow execution." type = map(string) default = null + sensitive = true } variable "file" { @@ -43,9 +44,9 @@ variable "logging_level" { validation { # Check if the provided value is one of the allowed options - condition = contains(["CALL_LOG_LEVEL_UNSPECIFIED", "LOG_ALL_CALLS", "LOG_ERRORS_ONLY", "LOG_NONE"], var.logging_level) + condition = contains(["LOG_ALL_CALLS", "LOG_ERRORS_ONLY"], var.logging_level) # Provide a helpful error message if the condition is false - error_message = "Invalid value for var.logging_level. Must be one of: CALL_LOG_LEVEL_UNSPECIFIED, LOG_ALL_CALLS, LOG_ERRORS_ONLY, LOG_NONE." + error_message = "Invalid value for var.logging_level. Must be one of: LOG_ALL_CALLS, LOG_ERRORS_ONLY." } } diff --git a/blueprints/il5/artifact-registry/consumer.tf b/blueprints/il5/artifact-registry/consumer.tf index 02ab3842e..864d1f3a3 100644 --- a/blueprints/il5/artifact-registry/consumer.tf +++ b/blueprints/il5/artifact-registry/consumer.tf @@ -23,12 +23,6 @@ resource "google_project_iam_member" "consumer-readonly" { member = google_service_account.consumer.member } -resource "google_kms_crypto_key_iam_member" "consumer_sa_kms_access" { - crypto_key_id = data.google_kms_crypto_key.default.id - role = "roles/cloudkms.cryptoKeyEncrypterDecrypter" - member = google_service_account.consumer.member -} - resource "google_kms_crypto_key_iam_member" "compute_agent_kms_access" { crypto_key_id = data.google_kms_crypto_key.default.id role = "roles/cloudkms.cryptoKeyEncrypterDecrypter" diff --git a/blueprints/il5/artifact-registry/main.tf b/blueprints/il5/artifact-registry/main.tf index 6c74a0355..fd47e8314 100644 --- a/blueprints/il5/artifact-registry/main.tf +++ b/blueprints/il5/artifact-registry/main.tf @@ -54,6 +54,7 @@ resource "google_project_service" "api" { } resource "google_artifact_registry_repository" "yum-repos" { + project = data.google_project.project.project_id location = var.region for_each = local.repositories.yum repository_id = each.key @@ -77,6 +78,7 @@ resource "google_artifact_registry_repository" "yum-repos" { } resource "google_artifact_registry_repository" "docker-hub" { + project = data.google_project.project.project_id location = var.region repository_id = "docker-hub" description = "Pull through registry for Docker Hub" @@ -98,6 +100,7 @@ resource "google_artifact_registry_repository" "docker-hub" { } resource "google_artifact_registry_repository" "docker-repos" { + project = data.google_project.project.project_id location = var.region for_each = local.repositories.docker repository_id = each.key @@ -122,6 +125,7 @@ resource "google_artifact_registry_repository" "docker-repos" { resource "google_artifact_registry_repository" "docker-repos-developer" { for_each = var.developer_registries + project = data.google_project.project.project_id location = var.region repository_id = each.key description = "Developer repo for ${each.key}" diff --git a/blueprints/il5/bastion-pattern/main.tf b/blueprints/il5/bastion-pattern/main.tf index 1e0dc4c14..2222db06b 100644 --- a/blueprints/il5/bastion-pattern/main.tf +++ b/blueprints/il5/bastion-pattern/main.tf @@ -14,7 +14,9 @@ * limitations under the License. */ -data "google_project" "current" {} +data "google_project" "current" { + project_id = var.main_project_id +} data "google_compute_network" "network" { name = var.network_name @@ -66,7 +68,8 @@ module "bastion-vm" { confidential_compute = true # CIS Compliance Benchmark 4.11 - Must use compliant instance and image types metadata = { - block-project-ssh-keys = true # CIS Compliance Benchmark 4.3 + block-project-ssh-keys = true # CIS Compliance Benchmark 4.3 + enable-oslogin = "TRUE" # CIS Compliance Benchmark 4.4 } shielded_config = { @@ -104,9 +107,9 @@ module "bastion-vm" { attached_disks = [ { - auto_delete = true - size = 10 - name = var.disk_name + auto_delete = true + size = 10 + name = var.disk_name snapshot_schedule = ["daily-backup"] initialize_params = { image = var.image @@ -122,16 +125,10 @@ module "bastion-vm" { } } depends_on = [ - google_kms_crypto_key_iam_member.bastion_sa_kms_access + google_kms_crypto_key_iam_member.crypto_key ] } -resource "google_kms_crypto_key_iam_member" "bastion_sa_kms_access" { - crypto_key_id = data.google_kms_crypto_key.default.id - role = "roles/cloudkms.cryptoKeyEncrypterDecrypter" - member = google_service_account.compute.member -} - resource "google_kms_crypto_key_iam_member" "crypto_key" { crypto_key_id = data.google_kms_crypto_key.default.id role = "roles/cloudkms.cryptoKeyEncrypterDecrypter" diff --git a/blueprints/il5/bcap/main.tf b/blueprints/il5/bcap/main.tf index 5a273473a..1b6d12b83 100644 --- a/blueprints/il5/bcap/main.tf +++ b/blueprints/il5/bcap/main.tf @@ -47,6 +47,7 @@ resource "google_compute_network" "vpc_network" { project = var.hub_project_id name = var.network_name auto_create_subnetworks = false + routing_mode = "GLOBAL" mtu = 1460 description = "VPC Network for BCAP deployment" } @@ -67,7 +68,7 @@ resource "google_compute_subnetwork" "bcap_subnets" { for_each = var.subnet_enable_flow_logs ? [1] : [] content { aggregation_interval = "INTERVAL_5_SEC" - flow_sampling = 0.5 + flow_sampling = 1.0 metadata = "INCLUDE_ALL_METADATA" filter_expr = "true" } diff --git a/blueprints/il5/bcap/outputs.tf b/blueprints/il5/bcap/outputs.tf index 79737d431..41256e550 100644 --- a/blueprints/il5/bcap/outputs.tf +++ b/blueprints/il5/bcap/outputs.tf @@ -45,6 +45,7 @@ output "pairing_keys" { output "vlan_attachments" { description = "Details of the created VLAN attachments." value = google_compute_interconnect_attachment.attachments + sensitive = true } output "vpc_network" { diff --git a/blueprints/il5/bcap/terraform.tfvars.sample b/blueprints/il5/bcap/terraform.tfvars.sample index 56e3e2a56..676601d4a 100644 --- a/blueprints/il5/bcap/terraform.tfvars.sample +++ b/blueprints/il5/bcap/terraform.tfvars.sample @@ -8,7 +8,7 @@ region1 = "us-central1" region2 = "us-east4" # DoD Base CIDR Block (to be split into two /25s) - Replace with your DoD NIC assigned /24 CIDR -dod_base_cidr_block = "x.x.x.0/24" +dod_base_cidr_block = "10.0.0.0/24" # Subnet Configurations subnet_configs = { diff --git a/blueprints/il5/bcap/variables.tf b/blueprints/il5/bcap/variables.tf index 78b51bf6e..a812f3084 100644 --- a/blueprints/il5/bcap/variables.tf +++ b/blueprints/il5/bcap/variables.tf @@ -38,6 +38,12 @@ variable "attachment_configs" { ]) error_message = "The router_key must be either 'router1' or 'router2'." } + validation { + condition = length(distinct([ + for k, v in var.attachment_configs : "${v.router_key}-${v.edge_availability_domain}" + ])) == 4 + error_message = "The four attachments must cover all four unique (router_key, edge_availability_domain) combinations for 99.99% SLA." + } } variable "dod_base_cidr_block" { diff --git a/blueprints/il5/bigquery/main.tf b/blueprints/il5/bigquery/main.tf index cbcb4e755..c01d10000 100644 --- a/blueprints/il5/bigquery/main.tf +++ b/blueprints/il5/bigquery/main.tf @@ -15,6 +15,7 @@ */ data "google_bigquery_default_service_account" "bq_sa" { + project = var.main_project_id depends_on = [ google_project_service.bigquery_api ] diff --git a/blueprints/il5/bigquery/variables.tf b/blueprints/il5/bigquery/variables.tf index 208ac447d..2a36edfa5 100644 --- a/blueprints/il5/bigquery/variables.tf +++ b/blueprints/il5/bigquery/variables.tf @@ -52,7 +52,7 @@ variable "region" { variable "tables" { description = "BigQuery tables." type = map(object({ - deletion_protection = optional(bool) + deletion_protection = optional(bool, true) description = optional(string, "Terraform managed.") friendly_name = optional(string) labels = optional(map(string), {}) diff --git a/blueprints/il5/bq-project/main.tf b/blueprints/il5/bq-project/main.tf index 2cc567420..1369aca32 100644 --- a/blueprints/il5/bq-project/main.tf +++ b/blueprints/il5/bq-project/main.tf @@ -14,7 +14,9 @@ * limitations under the License. */ -data "google_bigquery_default_service_account" "bq_sa" {} +data "google_bigquery_default_service_account" "bq_sa" { + project = var.main_project_id +} module "bigquery-dataset" { source = "../../../modules/bigquery-dataset" diff --git a/blueprints/il5/bq-project/terraform.tfvars.sample b/blueprints/il5/bq-project/terraform.tfvars.sample index 91e01e9b0..bcb6bc13d 100644 --- a/blueprints/il5/bq-project/terraform.tfvars.sample +++ b/blueprints/il5/bq-project/terraform.tfvars.sample @@ -1,6 +1,6 @@ main_project_id = "xxxx-xxxx-xxxx-main-0" -dataset_id = "dataset-id-00" +dataset_id = "dataset_id_00" dataset_description = "dataset-description" tables = {} region = "us-east4" diff --git a/blueprints/il5/bq-project/variables.tf b/blueprints/il5/bq-project/variables.tf index cd763fee8..88c8e1355 100644 --- a/blueprints/il5/bq-project/variables.tf +++ b/blueprints/il5/bq-project/variables.tf @@ -62,7 +62,7 @@ variable "kms_key_names" { default = { "default" = { destroy_scheduled_duration = null - rotation_period = null + rotation_period = "7776000s" labels = null purpose = "ENCRYPT_DECRYPT" skip_initial_version_creation = false diff --git a/blueprints/il5/compute-engine/main.tf b/blueprints/il5/compute-engine/main.tf index 170182fa4..231815db8 100644 --- a/blueprints/il5/compute-engine/main.tf +++ b/blueprints/il5/compute-engine/main.tf @@ -13,7 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -data "google_project" "current" {} +data "google_project" "current" { + project_id = var.main_project_id +} data "google_compute_network" "network" { name = var.network_name @@ -42,12 +44,6 @@ resource "google_service_account" "compute" { project = var.main_project_id } -resource "google_kms_crypto_key_iam_member" "compute_sa_kms_access" { - crypto_key_id = data.google_kms_crypto_key.default.id - role = "roles/cloudkms.cryptoKeyEncrypterDecrypter" - member = google_service_account.compute.member -} - resource "google_kms_crypto_key_iam_member" "compute_agent_kms_access" { crypto_key_id = data.google_kms_crypto_key.default.id role = "roles/cloudkms.cryptoKeyEncrypterDecrypter" @@ -116,7 +112,6 @@ module "compute-engine-vm" { depends_on = [ google_service_account.compute, - google_kms_crypto_key_iam_member.compute_sa_kms_access, google_kms_crypto_key_iam_member.compute_agent_kms_access ] } diff --git a/blueprints/il5/dataflow/main.tf b/blueprints/il5/dataflow/main.tf index 41a4e59ea..8787b20cc 100644 --- a/blueprints/il5/dataflow/main.tf +++ b/blueprints/il5/dataflow/main.tf @@ -68,7 +68,8 @@ resource "google_compute_firewall" "dataflow" { log_config { metadata = "INCLUDE_ALL_METADATA" } - source_ranges = var.allowed_source_ranges + target_service_accounts = [google_service_account.dataflow_worker.email] + source_ranges = var.allowed_source_ranges } resource "google_compute_subnetwork_iam_member" "dataflow_sa_compute_network_user" { diff --git a/blueprints/il5/dataflow/provider.tf b/blueprints/il5/dataflow/provider.tf index 01e5d8abe..4b3f8df3f 100644 --- a/blueprints/il5/dataflow/provider.tf +++ b/blueprints/il5/dataflow/provider.tf @@ -30,4 +30,9 @@ terraform { provider "google" { project = var.main_project_id region = var.region +} + +provider "google-beta" { + project = var.main_project_id + region = var.region } \ No newline at end of file diff --git a/blueprints/il5/gke-hardened/main.tf b/blueprints/il5/gke-hardened/main.tf index 84f78619d..8910b9c8f 100644 --- a/blueprints/il5/gke-hardened/main.tf +++ b/blueprints/il5/gke-hardened/main.tf @@ -14,7 +14,9 @@ * limitations under the License. */ -data "google_project" "current" {} +data "google_project" "current" { + project_id = var.main_project_id +} # Only uncomment if no organization policies enforce the below # resource "google_compute_project_metadata" "default" { @@ -35,12 +37,6 @@ resource "google_service_account" "gatekeeper_sa" { display_name = "GSA for GKE Policy Controller/Gatekeeper" } -resource "google_project_iam_member" "gke_cluster_admin" { - project = data.google_project.current.project_id - role = "roles/container.developer" - member = "serviceAccount:${data.google_project.current.number}-compute@developer.gserviceaccount.com" -} - resource "google_project_iam_member" "gatekeeper_monitoring_writer" { project = var.main_project_id role = "roles/monitoring.metricWriter" @@ -102,7 +98,6 @@ module "kms" { iam = { "roles/cloudkms.cryptoKeyEncrypterDecrypter" = [ google_service_account.gke.member, - "serviceAccount:${data.google_project.current.number}-compute@developer.gserviceaccount.com", "serviceAccount:service-${data.google_project.current.number}@gs-project-accounts.iam.gserviceaccount.com", "serviceAccount:service-${data.google_project.current.number}@compute-system.iam.gserviceaccount.com" ] @@ -307,7 +302,7 @@ resource "google_compute_router_nat" "nat" { log_config { enable = true - filter = "ERRORS_ONLY" + filter = "ALL" } depends_on = [module.vpc] } \ No newline at end of file diff --git a/blueprints/il5/gke-hardened/terraform.tfvars.sample b/blueprints/il5/gke-hardened/terraform.tfvars.sample index 283a274dc..a10c165d9 100644 --- a/blueprints/il5/gke-hardened/terraform.tfvars.sample +++ b/blueprints/il5/gke-hardened/terraform.tfvars.sample @@ -1,9 +1,9 @@ -gatekeeper_sa = "gatekeeper-XXX-XXX-sa" +gatekeeper_sa = "gatekeeper-xxx-xxx-sa" gke_cluster_enable_private_endpoint = true gke_cluster_master_global_access = true -gke_cluster_name = "XXX-cluster" +gke_cluster_name = "xxx-cluster" gke_initial_node_per_zone = 1 -gke_nodepool_name = "XXX-nodepool" +gke_nodepool_name = "xxx-nodepool" gke_vpc_master_ipv4_cidr_block = "192.168.0.0/28" kms_keyring_name = { @@ -28,14 +28,14 @@ kms_key_names = { } } -local_admin_external_ip = ["X.X.X.X/32"] +local_admin_external_ip = ["10.0.0.1/32"] -main_project_id = "XXX-main-0" +main_project_id = "xxx-main-0" master_authorized_ranges_ip_ranges = "10.0.0.0/8" -nat_gateway_name = "XXX-nat-gateway" -nat_router_name = "XXX-nat-router" -network_name = "XXX-gke-vpc" +nat_gateway_name = "xxx-nat-gateway" +nat_router_name = "xxx-nat-router" +network_name = "xxx-gke-vpc" node_config_tags = ["node-config-gke", "intial-nodeconfig-gke"] node_disk_size_gb = 20 nodepool_node_count = { diff --git a/blueprints/il5/gke-hardened/variables.tf b/blueprints/il5/gke-hardened/variables.tf index 0feaf5876..132d723b6 100644 --- a/blueprints/il5/gke-hardened/variables.tf +++ b/blueprints/il5/gke-hardened/variables.tf @@ -63,7 +63,7 @@ variable "kms_key_names" { skip_initial_version_creation = optional(bool, false) version_template = optional(object({ algorithm = string - protection_level = optional(string, "SOFTWARE") + protection_level = optional(string, "HSM") })) iam = optional(map(list(string)), {}) iam_bindings = optional(map(object({ @@ -94,7 +94,7 @@ variable "kms_key_names" { } version_template = { algorithm = "GOOGLE_SYMMETRIC_ENCRYPTION" - protection_level = "SOFTWARE" + protection_level = "HSM" } lifecycle = { prevent_destroy = true diff --git a/blueprints/il5/gke/main.tf b/blueprints/il5/gke/main.tf index 04378f7f1..49296c300 100644 --- a/blueprints/il5/gke/main.tf +++ b/blueprints/il5/gke/main.tf @@ -121,7 +121,6 @@ data "google_kms_crypto_key" "existing_kms_key" { # --- IAM Bindings --- resource "google_kms_crypto_key_iam_member" "gke_kms_access" { for_each = { - custom_sa = "serviceAccount:${google_service_account.gke_cluster_sa.email}", gke_agent = "serviceAccount:${google_project_service_identity.container_engine_robot.email}" } crypto_key_id = data.google_kms_crypto_key.existing_kms_key.id @@ -169,6 +168,7 @@ resource "google_compute_firewall" "allow_gke_nodes_to_master" { protocol = "udp" ports = ["10250", "443"] } + target_service_accounts = [google_service_account.gke_cluster_sa.email] log_config { metadata = "INCLUDE_ALL_METADATA" } @@ -278,6 +278,7 @@ module "bastion_vm" { zone = var.bastion_vm_zone instance_type = var.bastion_vm_machine_type service_account = { + email = google_service_account.gke_cluster_sa.email scopes = ["cloud-platform"] } snapshot_schedules = { diff --git a/blueprints/il5/gke/terraform.tfvars.sample b/blueprints/il5/gke/terraform.tfvars.sample index 4b0026782..57563f93c 100644 --- a/blueprints/il5/gke/terraform.tfvars.sample +++ b/blueprints/il5/gke/terraform.tfvars.sample @@ -46,7 +46,7 @@ gke_cluster_master_global_access = false # that can access the GKE master endpoint. This should be scoped as tightly # as possible. master_authorized_ranges = { - "management-subnet" = "xxx.xxx.xxx.xxx/xx" + "management-subnet" = "10.0.0.0/24" } diff --git a/blueprints/il5/gke/variables.tf b/blueprints/il5/gke/variables.tf index 2b9679ea9..790733099 100644 --- a/blueprints/il5/gke/variables.tf +++ b/blueprints/il5/gke/variables.tf @@ -136,7 +136,7 @@ variable "node_config_tags" { variable "remove_default_node_pool" { description = "Set to true to remove the default node pool created with the cluster. Requires at least one other node pool to be created." type = bool - default = false + default = true } # --- Bastion Host Configuration --- diff --git a/blueprints/il5/postgresql/main.tf b/blueprints/il5/postgresql/main.tf index 653ba7793..e48e16b82 100644 --- a/blueprints/il5/postgresql/main.tf +++ b/blueprints/il5/postgresql/main.tf @@ -14,7 +14,9 @@ * limitations under the License. */ -data "google_project" "current" {} +data "google_project" "current" { + project_id = var.main_project_id +} data "google_compute_network" "network" { name = var.network_name diff --git a/blueprints/il5/private-service-connect/provider.tf b/blueprints/il5/private-service-connect/provider.tf index d4b6cc156..e9f3bd896 100644 --- a/blueprints/il5/private-service-connect/provider.tf +++ b/blueprints/il5/private-service-connect/provider.tf @@ -19,6 +19,10 @@ terraform { source = "hashicorp/google" version = ">= 6.21.0, < 7.0.0" # tftest } + google-beta = { + source = "hashicorp/google-beta" + version = ">= 6.21.0, < 7.0.0" # tftest + } } } diff --git a/blueprints/il5/private-service-connect/variables.tf b/blueprints/il5/private-service-connect/variables.tf index 522ef5113..58cf67f1a 100644 --- a/blueprints/il5/private-service-connect/variables.tf +++ b/blueprints/il5/private-service-connect/variables.tf @@ -54,5 +54,9 @@ variable "region" { variable "service" { description = "Target resource to receive the matched traffic. Only `all-apis` and `vpc-sc` are valid." type = string - default = "all-apis" + default = "vpc-sc" + validation { + condition = contains(["all-apis", "vpc-sc"], var.service) + error_message = "The service variable must be either all-apis or vpc-sc." + } } \ No newline at end of file diff --git a/blueprints/il5/pub-sub-project/main.tf b/blueprints/il5/pub-sub-project/main.tf index 5267e8229..6474de1f2 100644 --- a/blueprints/il5/pub-sub-project/main.tf +++ b/blueprints/il5/pub-sub-project/main.tf @@ -14,7 +14,9 @@ * limitations under the License. */ -data "google_project" "current" {} +data "google_project" "current" { + project_id = var.main_project_id +} # Explicitly enable the Pub/Sub API resource "google_project_service" "pubsub_api" { diff --git a/blueprints/il5/shielded-vm-project/main.tf b/blueprints/il5/shielded-vm-project/main.tf index 80887e5ca..992d620f4 100644 --- a/blueprints/il5/shielded-vm-project/main.tf +++ b/blueprints/il5/shielded-vm-project/main.tf @@ -127,5 +127,6 @@ resource "google_compute_firewall" "default" { log_config { metadata = "INCLUDE_ALL_METADATA" } - source_ranges = var.source_ranges_allowed + target_service_accounts = [google_service_account.compute.email] + source_ranges = var.source_ranges_allowed } \ No newline at end of file diff --git a/blueprints/stand-alone/vpc-peering-project/main.tf b/blueprints/stand-alone/vpc-peering-project/main.tf index d114d6c87..87f25a773 100644 --- a/blueprints/stand-alone/vpc-peering-project/main.tf +++ b/blueprints/stand-alone/vpc-peering-project/main.tf @@ -72,7 +72,7 @@ module "vpc_networks" { aggregation_interval = "INTERVAL_5_SEC" flow_sampling = 1.0 metadata = "INCLUDE_ALL_METADATA" - filter_expression = "false" + filter_expression = "true" } }, { @@ -85,7 +85,7 @@ module "vpc_networks" { aggregation_interval = "INTERVAL_5_SEC" flow_sampling = 1.0 metadata = "INCLUDE_ALL_METADATA" - filter_expression = "false" + filter_expression = "true" } }, { @@ -101,7 +101,7 @@ module "vpc_networks" { aggregation_interval = "INTERVAL_5_SEC" flow_sampling = 1.0 metadata = "INCLUDE_ALL_METADATA" - filter_expression = "false" + filter_expression = "true" } } ] diff --git a/blueprints/third-party-solutions/gitlab/gitlab.tf b/blueprints/third-party-solutions/gitlab/gitlab.tf index b21aeb8eb..3628ae31a 100644 --- a/blueprints/third-party-solutions/gitlab/gitlab.tf +++ b/blueprints/third-party-solutions/gitlab/gitlab.tf @@ -57,7 +57,6 @@ module "gitlab-sa" { (module.project.project_id) = [ "roles/logging.logWriter", "roles/monitoring.metricWriter", - "roles/storage.admin" ] } } @@ -65,7 +64,7 @@ module "gitlab-sa" { module "gitlab-instance" { source = "../../../modules/compute-vm" project_id = module.project.project_id - zone = var.gitlab_instance_config.zone + zone = coalesce(var.gitlab_instance_config.zone, "${var.region}-a") name = var.gitlab_instance_config.name instance_type = var.gitlab_instance_config.instance_type snapshot_schedules = { @@ -132,7 +131,7 @@ module "ilb" { } group_configs = { gitlab = { - zone = var.gitlab_instance_config.zone + zone = coalesce(var.gitlab_instance_config.zone, "${var.region}-a") instances = [ module.gitlab-instance.self_link ] diff --git a/blueprints/third-party-solutions/gitlab/main.tf b/blueprints/third-party-solutions/gitlab/main.tf index 054ea8fcf..991133b89 100644 --- a/blueprints/third-party-solutions/gitlab/main.tf +++ b/blueprints/third-party-solutions/gitlab/main.tf @@ -20,7 +20,7 @@ module "project" { billing_account = try(var.project_create.billing_account_id, null) prefix = var.project_create == null ? null : var.prefix name = var.project_id - project_create = var.project_create != null + project_reuse = var.project_create != null ? null : {} services = [ "compute.googleapis.com", "memcache.googleapis.com", diff --git a/blueprints/third-party-solutions/gitlab/services.tf b/blueprints/third-party-solutions/gitlab/services.tf index 54b09eb37..abdeee644 100644 --- a/blueprints/third-party-solutions/gitlab/services.tf +++ b/blueprints/third-party-solutions/gitlab/services.tf @@ -58,13 +58,15 @@ module "db" { # https://docs.gitlab.com/ee/install/requirements.html#redis resource "google_redis_instance" "cache" { - project = module.project.project_id - region = var.region - name = var.redis_config.name - tier = var.redis_config.tier - memory_size_gb = var.redis_config.memory_size_gb - authorized_network = var.network_config.network_self_link - connect_mode = "PRIVATE_SERVICE_ACCESS" + project = module.project.project_id + region = var.region + name = var.redis_config.name + tier = var.redis_config.tier + memory_size_gb = var.redis_config.memory_size_gb + authorized_network = var.network_config.network_self_link + connect_mode = "PRIVATE_SERVICE_ACCESS" + auth_enabled = true + transit_encryption_mode = "SERVER_AUTHENTICATION" redis_version = var.redis_config.version display_name = "Gitlab Redis Instance" diff --git a/blueprints/third-party-solutions/gitlab/ssl.tf b/blueprints/third-party-solutions/gitlab/ssl.tf index a1ae9a6cd..44df7c062 100644 --- a/blueprints/third-party-solutions/gitlab/ssl.tf +++ b/blueprints/third-party-solutions/gitlab/ssl.tf @@ -96,7 +96,7 @@ resource "tls_locally_signed_cert" "gitlab_server_singed_cert" { ca_private_key_pem = tls_private_key.gitlab_ca_private_key[0].private_key_pem ca_cert_pem = tls_self_signed_cert.gitlab_ca_cert[0].cert_pem - validity_period_hours = 43800 + validity_period_hours = 9552 allowed_uses = [ "digital_signature", diff --git a/blueprints/third-party-solutions/gitlab/terraform.tfvars.sample b/blueprints/third-party-solutions/gitlab/terraform.tfvars.sample index e76642af4..d0e9081e7 100644 --- a/blueprints/third-party-solutions/gitlab/terraform.tfvars.sample +++ b/blueprints/third-party-solutions/gitlab/terraform.tfvars.sample @@ -16,4 +16,5 @@ network_config = { subnet_self_link = "subnetwork_self_link" } prefix = "prefix" -project_id = "prod-gitlab-0" \ No newline at end of file +project_id = "prod-gitlab-0" +region = "us-east4" diff --git a/blueprints/third-party-solutions/gitlab/variables.tf b/blueprints/third-party-solutions/gitlab/variables.tf index 7d6d24455..d9187b0d4 100644 --- a/blueprints/third-party-solutions/gitlab/variables.tf +++ b/blueprints/third-party-solutions/gitlab/variables.tf @@ -62,8 +62,9 @@ variable "gitlab_config" { }), null) ha_required = optional(bool, false) }) - default = {} - nullable = false + default = {} + nullable = false + sensitive = true } variable "gitlab_instance_config" { @@ -84,6 +85,8 @@ variable "gitlab_instance_config" { replica_zone = optional(string) }), {}) }) + default = {} + nullable = false } variable "network_config" { diff --git a/fast/stages-aw/0-bootstrap/automation.tf b/fast/stages-aw/0-bootstrap/automation.tf index ad5eddebe..e085f27a8 100644 --- a/fast/stages-aw/0-bootstrap/automation.tf +++ b/fast/stages-aw/0-bootstrap/automation.tf @@ -38,10 +38,6 @@ module "automation-project" { ) # human (groups) IAM bindings iam_by_principals = { - (local.principals.gcp-devops) = [ - "roles/iam.serviceAccountAdmin", - "roles/iam.serviceAccountTokenCreator", - ] (local.principals.gcp-organization-admins) = [ "roles/iam.serviceAccountTokenCreator", "roles/iam.workloadIdentityPoolAdmin" @@ -62,9 +58,8 @@ module "automation-project" { "roles/cloudbuild.builds.viewer" = [ module.automation-tf-resman-r-sa.iam_email ] - "roles/iam.serviceAccountAdmin" = [ - module.automation-tf-resman-sa.iam_email - ] + # roles/iam.serviceAccountAdmin is granted conditionally in iam_bindings + # to prevent modifying the privileged bootstrap-0 service account "roles/iam.serviceAccountViewer" = [ module.automation-tf-resman-r-sa.iam_email ] @@ -100,11 +95,39 @@ module "automation-project" { title = "resman_delegated_grant" description = "Resource manager service account delegated grant." expression = format( - "api.getAttribute('iam.googleapis.com/modifiedGrantsByRole', []).hasOnly(['%s'])", + "api.getAttribute('iam.googleapis.com/modifiedGrantsByRole', []).size() > 0 && api.getAttribute('iam.googleapis.com/modifiedGrantsByRole', []).hasOnly(['%s'])", "roles/serviceusage.serviceUsageConsumer" ) } } + sa_admin_scoped = { + members = [ + local.principals.gcp-devops, + module.automation-tf-resman-sa.iam_email, + ] + role = "roles/iam.serviceAccountAdmin" + condition = { + title = "exclude_bootstrap_sa" + description = "Prevent modifying the privileged bootstrap service accounts." + expression = format( + "!resource.name.endsWith('/serviceAccounts/%s') && !resource.name.endsWith('/serviceAccounts/%s')", + module.automation-tf-bootstrap-sa.email, + module.automation-tf-bootstrap-r-sa.email + ) + } + } + sa_token_creator_devops = { + members = [local.principals.gcp-devops] + role = "roles/iam.serviceAccountTokenCreator" + condition = { + title = "exclude_bootstrap_sa_impersonation" + description = "Prevent gcp-devops from impersonating the bootstrap service account." + expression = format( + "!resource.name.endsWith('/serviceAccounts/%s')", + module.automation-tf-bootstrap-sa.email + ) + } + } } iam_bindings_additive = { serviceusage_resman = { diff --git a/fast/stages-aw/0-bootstrap/checklist.tf b/fast/stages-aw/0-bootstrap/checklist.tf index c82300995..420f1ea30 100644 --- a/fast/stages-aw/0-bootstrap/checklist.tf +++ b/fast/stages-aw/0-bootstrap/checklist.tf @@ -38,12 +38,12 @@ locals { ) # check that files are for the correct organization and ignore them if not _cl_data = ( - try(local._cl_data_raw.cloud_setup_config.organization.id, null) != tostring(var.organization.id) + tostring(try(local._cl_data_raw.cloud_setup_config.organization.id, "")) != tostring(var.organization.id) ? null : local._cl_data_raw.cloud_setup_config ) _cl_org = ( - try(local._cl_org_raw.cloud_setup_org_iam.organization.id, null) != tostring(var.organization.id) + tostring(try(local._cl_org_raw.cloud_setup_org_iam.organization.id, "")) != tostring(var.organization.id) ? null : local._cl_org_raw.cloud_setup_org_iam ) @@ -86,9 +86,9 @@ locals { location = try(local._cl_data.logging.sinks[0].destination.location, null) } uses_checklist = ( - var.factories_config.checklist_data != null + local._cl_data != null || - var.factories_config.checklist_org_iam != null + local._cl_org != null ) } check "checklist" { @@ -112,14 +112,14 @@ check "checklist" { assert { condition = ( var.factories_config.checklist_data == null || - try(local._cl_data_raw.cloud_setup_config.organization.id, null) == tostring(var.organization.id) + tostring(try(local._cl_data_raw.cloud_setup_config.organization.id, "")) == tostring(var.organization.id) ) error_message = "Checklist data organization id mismatch, file ignored." } assert { condition = ( var.factories_config.checklist_org_iam == null || - try(local._cl_org_raw.cloud_setup_org_iam.organization.id, null) == tostring(var.organization.id) + tostring(try(local._cl_org_raw.cloud_setup_org_iam.organization.id, "")) == tostring(var.organization.id) ) error_message = "Checklist org IAM organization id mismatch, file ignored." } @@ -140,14 +140,14 @@ module "automation-tf-checklist-gcs" { } resource "google_storage_bucket_object" "checklist_data" { - count = var.factories_config.checklist_data != null ? 1 : 0 + count = local._cl_data != null ? 1 : 0 bucket = module.automation-tf-checklist-gcs[0].name name = "checklist/data.tfvars.json" source = var.factories_config.checklist_data } resource "google_storage_bucket_object" "checklist_org_iam" { - count = var.factories_config.checklist_org_iam != null ? 1 : 0 + count = local._cl_org != null ? 1 : 0 bucket = module.automation-tf-checklist-gcs[0].name name = "checklist/org-iam.tfvars.json" source = var.factories_config.checklist_org_iam diff --git a/fast/stages-aw/0-bootstrap/log-export.tf b/fast/stages-aw/0-bootstrap/log-export.tf index 4a68a45fd..c8b244962 100644 --- a/fast/stages-aw/0-bootstrap/log-export.tf +++ b/fast/stages-aw/0-bootstrap/log-export.tf @@ -73,8 +73,9 @@ module "log-export-project" { # Read project-level Cloud Logging settings to provision the project's # CMEK service agent before granting KMS permissions and creating CMEK buckets. data "google_logging_project_settings" "log_export" { - count = contains(local.log_types, "logging") ? 1 : 0 - project = module.log-export-project.project_id + count = contains(local.log_types, "logging") ? 1 : 0 + project = module.log-export-project.project_id + depends_on = [module.log-export-project] } resource "google_compute_project_metadata" "metadata-log-export" { diff --git a/fast/stages-aw/0-bootstrap/log-metric-alerts.tf b/fast/stages-aw/0-bootstrap/log-metric-alerts.tf index 719f53ba9..56a5b24bd 100644 --- a/fast/stages-aw/0-bootstrap/log-metric-alerts.tf +++ b/fast/stages-aw/0-bootstrap/log-metric-alerts.tf @@ -49,7 +49,7 @@ module "bootstrap_log_alerts" { project = each.key combiner = "OR" - duration = "60s" + duration = "0s" comparison = "COMPARISON_GT" alignment_period = "60s" per_series_aligner = "ALIGN_RATE" diff --git a/fast/stages-aw/0-bootstrap/main.tf b/fast/stages-aw/0-bootstrap/main.tf index c8bf8c25f..45c51b87b 100644 --- a/fast/stages-aw/0-bootstrap/main.tf +++ b/fast/stages-aw/0-bootstrap/main.tf @@ -30,7 +30,7 @@ locals { locations = { bq = var.regions.primary gcs = var.regions.primary - logging = coalesce(try(local.checklist.location, null), var.regions.primary) + logging = coalesce(try(local._cl_data.logging.sinks[0].destination.location, null), var.regions.primary) pubsub = [var.regions.primary] kms = var.regions.primary } diff --git a/fast/stages-aw/0-bootstrap/organization-iam.tf b/fast/stages-aw/0-bootstrap/organization-iam.tf index 8d1a30486..0a0003604 100644 --- a/fast/stages-aw/0-bootstrap/organization-iam.tf +++ b/fast/stages-aw/0-bootstrap/organization-iam.tf @@ -53,8 +53,6 @@ locals { "roles/axt.admin", "roles/cloudasset.owner", "roles/cloudsupport.admin", - "roles/compute.osAdminLogin", - "roles/compute.osLoginExternalUser", "roles/resourcemanager.folderAdmin", "roles/resourcemanager.organizationAdmin", "roles/resourcemanager.projectCreator", diff --git a/fast/stages-aw/0-bootstrap/organization.tf b/fast/stages-aw/0-bootstrap/organization.tf index cfd8c3c81..e42a9ab20 100644 --- a/fast/stages-aw/0-bootstrap/organization.tf +++ b/fast/stages-aw/0-bootstrap/organization.tf @@ -137,16 +137,16 @@ locals { } : null enforce = try(r.enforce, null) parameters = ( - can(r.parameters) && r.parameters != null + try(r.parameters, null) != null ? try(tostring(r.parameters), jsonencode(r.parameters)) : null ) - condition = { + condition = can(r.condition) ? { description = try(r.condition.description, null) expression = try(r.condition.expression, null) location = try(r.condition.location, null) title = try(r.condition.title, null) - } + } : null } ] } @@ -278,7 +278,7 @@ module "organization" { role = module.organization.custom_role_id["organization_iam_admin"] condition = { expression = format( - "api.getAttribute('iam.googleapis.com/modifiedGrantsByRole', []).hasOnly([%s])", + "api.getAttribute('iam.googleapis.com/modifiedGrantsByRole', []).size() > 0 && api.getAttribute('iam.googleapis.com/modifiedGrantsByRole', []).hasOnly([%s])", join(",", formatlist("'%s'", [ "roles/accesscontextmanager.policyAdmin", "roles/cloudasset.viewer", @@ -301,7 +301,7 @@ module "organization" { role = module.organization.custom_role_id["organization_iam_admin"] condition = { expression = format( - "api.getAttribute('iam.googleapis.com/modifiedGrantsByRole', []).hasOnly([%s])", + "api.getAttribute('iam.googleapis.com/modifiedGrantsByRole', []).size() > 0 && api.getAttribute('iam.googleapis.com/modifiedGrantsByRole', []).hasOnly([%s])", join(",", formatlist("'%s'", [ "roles/billing.admin", "roles/billing.costsManager", diff --git a/fast/stages-aw/0-bootstrap/outputs-files.tf b/fast/stages-aw/0-bootstrap/outputs-files.tf index 541b4a9fa..757b9b635 100644 --- a/fast/stages-aw/0-bootstrap/outputs-files.tf +++ b/fast/stages-aw/0-bootstrap/outputs-files.tf @@ -18,28 +18,28 @@ resource "local_file" "providers" { for_each = var.outputs_location == null ? {} : local.providers - file_permission = "0644" + file_permission = "0600" filename = "${try(pathexpand(var.outputs_location), "")}/providers/${each.key}-providers.tf" content = try(each.value, null) } resource "local_file" "tfvars" { for_each = var.outputs_location == null ? {} : { 1 = 1 } - file_permission = "0644" + file_permission = "0600" filename = "${try(pathexpand(var.outputs_location), "")}/tfvars/0-bootstrap.auto.tfvars.json" content = jsonencode(local.tfvars) } resource "local_file" "tfvars_globals" { for_each = var.outputs_location == null ? {} : { 1 = 1 } - file_permission = "0644" + file_permission = "0600" filename = "${try(pathexpand(var.outputs_location), "")}/tfvars/0-globals.auto.tfvars.json" content = jsonencode(local.tfvars_globals) } resource "local_file" "workflows" { for_each = var.outputs_location == null ? {} : local.cicd_workflows - file_permission = "0644" + file_permission = "0600" filename = "${try(pathexpand(var.outputs_location), "")}/workflows/${each.key}-workflow.yaml" content = try(each.value, null) } \ No newline at end of file diff --git a/fast/stages-aw/0-bootstrap/outputs-gcs.tf b/fast/stages-aw/0-bootstrap/outputs-gcs.tf index c9f662161..d60af7a70 100644 --- a/fast/stages-aw/0-bootstrap/outputs-gcs.tf +++ b/fast/stages-aw/0-bootstrap/outputs-gcs.tf @@ -20,29 +20,29 @@ resource "google_storage_bucket_object" "providers" { for_each = local.providers bucket = module.automation-tf-output-gcs.name # provider suffix allows excluding via .gitignore when linked from stages - name = "providers/${each.key}-providers.tf" - content = each.value - # kms_key_name = module.gcs-kms.keys.gcs.id # may need to add back in + name = "providers/${each.key}-providers.tf" + content = sensitive(each.value) + kms_key_name = module.gcs-kms.keys.gcs.id } resource "google_storage_bucket_object" "tfvars" { - bucket = module.automation-tf-output-gcs.name - name = "tfvars/0-bootstrap.auto.tfvars.json" - content = jsonencode(local.tfvars) - # kms_key_name = module.gcs-kms.keys.gcs.id # may need to add back in + bucket = module.automation-tf-output-gcs.name + name = "tfvars/0-bootstrap.auto.tfvars.json" + content = sensitive(jsonencode(local.tfvars)) + kms_key_name = module.gcs-kms.keys.gcs.id } resource "google_storage_bucket_object" "tfvars_globals" { - bucket = module.automation-tf-output-gcs.name - name = "tfvars/0-globals.auto.tfvars.json" - content = jsonencode(local.tfvars_globals) - # kms_key_name = module.gcs-kms.keys.gcs.id # may need to add back in + bucket = module.automation-tf-output-gcs.name + name = "tfvars/0-globals.auto.tfvars.json" + content = sensitive(jsonencode(local.tfvars_globals)) + kms_key_name = module.gcs-kms.keys.gcs.id } resource "google_storage_bucket_object" "workflows" { - for_each = local.cicd_workflows - bucket = module.automation-tf-output-gcs.name - name = "workflows/${each.key}-workflow.yaml" - content = each.value - # kms_key_name = module.gcs-kms.keys.gcs.id # may need to add back in + for_each = local.cicd_workflows + bucket = module.automation-tf-output-gcs.name + name = "workflows/${each.key}-workflow.yaml" + content = sensitive(each.value) + kms_key_name = module.gcs-kms.keys.gcs.id } diff --git a/fast/stages-aw/1-resman/branch-gcve.tf b/fast/stages-aw/1-resman/branch-gcve.tf index 26cffea5d..b7ad6f148 100644 --- a/fast/stages-aw/1-resman/branch-gcve.tf +++ b/fast/stages-aw/1-resman/branch-gcve.tf @@ -96,12 +96,9 @@ module "branch-gcve-prod-sa" { display_name = "Terraform GCVE production service account." prefix = var.prefix iam = { - "roles/iam.serviceAccountTokenCreator" = concat( - [local.principals.gcp-devops], - compact([ - try(module.branch-gcve-prod-sa-cicd[0].iam_email, null) - ]) - ) + "roles/iam.serviceAccountTokenCreator" = compact([ + try(module.branch-gcve-prod-sa-cicd[0].iam_email, null) + ]) } iam_project_roles = { (var.automation.project_id) = ["roles/serviceusage.serviceUsageConsumer"] diff --git a/fast/stages-aw/1-resman/branch-sandbox.tf b/fast/stages-aw/1-resman/branch-sandbox.tf index 2866612cc..ab86b21a9 100644 --- a/fast/stages-aw/1-resman/branch-sandbox.tf +++ b/fast/stages-aw/1-resman/branch-sandbox.tf @@ -36,15 +36,12 @@ locals { module "branch-sandbox-folder" { - source = "../../../modules/folder" - count = var.fast_features.sandbox ? 1 : 0 - parent = var.assured_workloads.folder - name = "Sandbox" - iam = local._sandbox_folder_iam - org_policies = { - "sql.restrictPublicIp" = { rules = [{ enforce = false }] } - "compute.vmExternalIpAccess" = { rules = [{ allow = { all = true } }] } - } + source = "../../../modules/folder" + count = var.fast_features.sandbox ? 1 : 0 + parent = var.assured_workloads.folder + name = "Sandbox" + iam = local._sandbox_folder_iam + org_policies = {} tag_bindings = null } diff --git a/fast/stages-aw/1-resman/checklist.tf b/fast/stages-aw/1-resman/checklist.tf index 55b57bc8c..d7f9689f9 100644 --- a/fast/stages-aw/1-resman/checklist.tf +++ b/fast/stages-aw/1-resman/checklist.tf @@ -23,7 +23,7 @@ locals { ) # check that files are for the correct organization and ignore them if not _cl_data = ( - try(local._cl_data_raw.cloud_setup_config.organization.id, null) != tostring(var.organization.id) + tostring(try(local._cl_data_raw.cloud_setup_config.organization.id, "")) != tostring(var.organization.id) ? null : local._cl_data_raw.cloud_setup_config ) @@ -65,7 +65,7 @@ check "checklist" { assert { condition = ( var.factories_config.checklist_data == null || - try(local._cl_data_raw.cloud_setup_config.organization.id, null) == tostring(var.organization.id) + tostring(try(local._cl_data_raw.cloud_setup_config.organization.id, "")) == tostring(var.organization.id) ) error_message = "Checklist data organization id mismatch, file ignored." } diff --git a/fast/stages-aw/1-resman/cicd-data-platform.tf b/fast/stages-aw/1-resman/cicd-data-platform.tf index a544f5c36..a1ac24397 100644 --- a/fast/stages-aw/1-resman/cicd-data-platform.tf +++ b/fast/stages-aw/1-resman/cicd-data-platform.tf @@ -45,7 +45,7 @@ module "branch-dp-dev-cicd-repo" { substitutions = {} template = { project_id = null - branch_name = each.value.branch + branch_name = each.value.branch == null ? null : "^${trim(each.value.branch, "^$")}$" repo_name = each.value.name tag_name = null } @@ -64,8 +64,12 @@ module "branch-dp-prod-cicd-repo" { project_id = var.automation.project_id name = each.value.name iam = { - "roles/source.admin" = [module.branch-dp-prod-sa[0].iam_email] - "roles/source.reader" = [module.branch-dp-prod-sa-cicd[0].iam_email] + "roles/source.admin" = compact([ + try(module.branch-dp-prod-sa[0].iam_email, "") + ]) + "roles/source.reader" = compact([ + try(module.branch-dp-prod-sa-cicd[0].iam_email, "") + ]) } triggers = { fast-03-dp-prod = { @@ -77,7 +81,7 @@ module "branch-dp-prod-cicd-repo" { substitutions = {} template = { project_id = null - branch_name = each.value.branch + branch_name = each.value.branch == null ? null : "^${trim(each.value.branch, "^$")}$" repo_name = each.value.name tag_name = null } @@ -107,20 +111,18 @@ module "branch-dp-dev-sa-cicd" { } # impersonated via workload identity federation for external repos : { - "roles/iam.workloadIdentityUser" = [ + "roles/iam.workloadIdentityUser" = ( each.value.branch == null - ? format( - local.identity_providers[each.value.identity_provider].principal_repo, - var.automation.federated_identity_pool, - each.value.name - ) - : format( - local.identity_providers[each.value.identity_provider].principal_branch, - var.automation.federated_identity_pool, - each.value.name, - each.value.branch - ) - ] + ? [] + : [ + format( + local.identity_providers[each.value.identity_provider].principal_branch, + var.automation.federated_identity_pool, + each.value.name, + each.value.branch + ) + ] + ) } ) iam_project_roles = { @@ -150,20 +152,18 @@ module "branch-dp-prod-sa-cicd" { } # impersonated via workload identity federation for external repos : { - "roles/iam.workloadIdentityUser" = [ + "roles/iam.workloadIdentityUser" = ( each.value.branch == null - ? format( - local.identity_providers[each.value.identity_provider].principal_repo, - var.automation.federated_identity_pool, - each.value.name - ) - : format( - local.identity_providers[each.value.identity_provider].principal_branch, - var.automation.federated_identity_pool, - each.value.name, - each.value.branch - ) - ] + ? [] + : [ + format( + local.identity_providers[each.value.identity_provider].principal_branch, + var.automation.federated_identity_pool, + each.value.name, + each.value.branch + ) + ] + ) } ) iam_project_roles = { diff --git a/fast/stages-aw/1-resman/cicd-gcve.tf b/fast/stages-aw/1-resman/cicd-gcve.tf index 119ae8bba..618a5de06 100644 --- a/fast/stages-aw/1-resman/cicd-gcve.tf +++ b/fast/stages-aw/1-resman/cicd-gcve.tf @@ -45,7 +45,7 @@ module "branch-gcve-dev-cicd-repo" { substitutions = {} template = { project_id = null - branch_name = each.value.branch + branch_name = each.value.branch == null ? null : "^${trim(each.value.branch, "^$")}$" repo_name = each.value.name tag_name = null } @@ -64,8 +64,12 @@ module "branch-gcve-prod-cicd-repo" { project_id = var.automation.project_id name = each.value.name iam = { - "roles/source.admin" = [module.branch-gcve-prod-sa[0].iam_email] - "roles/source.reader" = [module.branch-gcve-prod-sa-cicd[0].iam_email] + "roles/source.admin" = compact([ + try(module.branch-gcve-prod-sa[0].iam_email, "") + ]) + "roles/source.reader" = compact([ + try(module.branch-gcve-prod-sa-cicd[0].iam_email, "") + ]) } triggers = { fast-03-gcve-prod = { @@ -77,7 +81,7 @@ module "branch-gcve-prod-cicd-repo" { substitutions = {} template = { project_id = null - branch_name = each.value.branch + branch_name = each.value.branch == null ? null : "^${trim(each.value.branch, "^$")}$" repo_name = each.value.name tag_name = null } @@ -107,20 +111,18 @@ module "branch-gcve-dev-sa-cicd" { } # impersonated via workload identity federation for external repos : { - "roles/iam.workloadIdentityUser" = [ + "roles/iam.workloadIdentityUser" = ( each.value.branch == null - ? format( - local.identity_providers[each.value.identity_provider].principal_repo, - var.automation.federated_identity_pool, - each.value.name - ) - : format( - local.identity_providers[each.value.identity_provider].principal_branch, - var.automation.federated_identity_pool, - each.value.name, - each.value.branch - ) - ] + ? [] + : [ + format( + local.identity_providers[each.value.identity_provider].principal_branch, + var.automation.federated_identity_pool, + each.value.name, + each.value.branch + ) + ] + ) } ) iam_project_roles = { @@ -150,20 +152,18 @@ module "branch-gcve-prod-sa-cicd" { } # impersonated via workload identity federation for external repos : { - "roles/iam.workloadIdentityUser" = [ + "roles/iam.workloadIdentityUser" = ( each.value.branch == null - ? format( - local.identity_providers[each.value.identity_provider].principal_repo, - var.automation.federated_identity_pool, - each.value.name - ) - : format( - local.identity_providers[each.value.identity_provider].principal_branch, - var.automation.federated_identity_pool, - each.value.name, - each.value.branch - ) - ] + ? [] + : [ + format( + local.identity_providers[each.value.identity_provider].principal_branch, + var.automation.federated_identity_pool, + each.value.name, + each.value.branch + ) + ] + ) } ) iam_project_roles = { diff --git a/fast/stages-aw/1-resman/cicd-gke.tf b/fast/stages-aw/1-resman/cicd-gke.tf index 4125104bb..6337975ba 100644 --- a/fast/stages-aw/1-resman/cicd-gke.tf +++ b/fast/stages-aw/1-resman/cicd-gke.tf @@ -45,7 +45,7 @@ module "branch-gke-dev-cicd-repo" { substitutions = {} template = { project_id = null - branch_name = each.value.branch + branch_name = each.value.branch == null ? null : "^${trim(each.value.branch, "^$")}$" repo_name = each.value.name tag_name = null } @@ -64,8 +64,12 @@ module "branch-gke-prod-cicd-repo" { project_id = var.automation.project_id name = each.value.name iam = { - "roles/source.admin" = [module.branch-gke-prod-sa[0].iam_email] - "roles/source.reader" = [module.branch-gke-prod-sa-cicd[0].iam_email] + "roles/source.admin" = compact([ + try(module.branch-gke-prod-sa[0].iam_email, "") + ]) + "roles/source.reader" = compact([ + try(module.branch-gke-prod-sa-cicd[0].iam_email, "") + ]) } triggers = { fast-03-gke-prod = { @@ -77,7 +81,7 @@ module "branch-gke-prod-cicd-repo" { substitutions = {} template = { project_id = null - branch_name = each.value.branch + branch_name = each.value.branch == null ? null : "^${trim(each.value.branch, "^$")}$" repo_name = each.value.name tag_name = null } @@ -107,20 +111,18 @@ module "branch-gke-dev-sa-cicd" { } # impersonated via workload identity federation for external repos : { - "roles/iam.workloadIdentityUser" = [ + "roles/iam.workloadIdentityUser" = ( each.value.branch == null - ? format( - local.identity_providers[each.value.identity_provider].principal_repo, - var.automation.federated_identity_pool, - each.value.name - ) - : format( - local.identity_providers[each.value.identity_provider].principal_branch, - var.automation.federated_identity_pool, - each.value.name, - each.value.branch - ) - ] + ? [] + : [ + format( + local.identity_providers[each.value.identity_provider].principal_branch, + var.automation.federated_identity_pool, + each.value.name, + each.value.branch + ) + ] + ) } ) iam_project_roles = { @@ -150,20 +152,18 @@ module "branch-gke-prod-sa-cicd" { } # impersonated via workload identity federation for external repos : { - "roles/iam.workloadIdentityUser" = [ + "roles/iam.workloadIdentityUser" = ( each.value.branch == null - ? format( - local.identity_providers[each.value.identity_provider].principal_repo, - var.automation.federated_identity_pool, - each.value.name - ) - : format( - local.identity_providers[each.value.identity_provider].principal_branch, - var.automation.federated_identity_pool, - each.value.name, - each.value.branch - ) - ] + ? [] + : [ + format( + local.identity_providers[each.value.identity_provider].principal_branch, + var.automation.federated_identity_pool, + each.value.name, + each.value.branch + ) + ] + ) } ) iam_project_roles = { diff --git a/fast/stages-aw/1-resman/cicd-networking.tf b/fast/stages-aw/1-resman/cicd-networking.tf index 2215e9baa..fc226f079 100644 --- a/fast/stages-aw/1-resman/cicd-networking.tf +++ b/fast/stages-aw/1-resman/cicd-networking.tf @@ -39,7 +39,7 @@ module "branch-network-cicd-repo" { substitutions = {} template = { project_id = null - branch_name = each.value.branch + branch_name = each.value.branch == null ? null : "^${trim(each.value.branch, "^$")}$" repo_name = each.value.name tag_name = null } @@ -69,20 +69,18 @@ module "branch-network-sa-cicd" { } # impersonated via workload identity federation for external repos : { - "roles/iam.workloadIdentityUser" = [ + "roles/iam.workloadIdentityUser" = ( each.value.branch == null - ? format( - local.identity_providers[each.value.identity_provider].principal_repo, - var.automation.federated_identity_pool, - each.value.name - ) - : format( - local.identity_providers[each.value.identity_provider].principal_branch, - var.automation.federated_identity_pool, - each.value.name, - each.value.branch - ) - ] + ? [] + : [ + format( + local.identity_providers[each.value.identity_provider].principal_branch, + var.automation.federated_identity_pool, + each.value.name, + each.value.branch + ) + ] + ) } ) iam_project_roles = { diff --git a/fast/stages-aw/1-resman/cicd-project-factory.tf b/fast/stages-aw/1-resman/cicd-project-factory.tf index 009199c2e..1d491f928 100644 --- a/fast/stages-aw/1-resman/cicd-project-factory.tf +++ b/fast/stages-aw/1-resman/cicd-project-factory.tf @@ -28,8 +28,12 @@ module "branch-pf-dev-cicd-repo" { project_id = var.automation.project_id name = each.value.name iam = { - "roles/source.admin" = [module.branch-pf-dev-sa[0].iam_email] - "roles/source.reader" = [module.branch-pf-dev-sa-cicd[0].iam_email] + "roles/source.admin" = compact([ + try(module.branch-pf-dev-sa[0].iam_email, "") + ]) + "roles/source.reader" = compact([ + try(module.branch-pf-dev-sa-cicd[0].iam_email, "") + ]) } triggers = { fast-03-pf-dev = { @@ -41,7 +45,7 @@ module "branch-pf-dev-cicd-repo" { substitutions = {} template = { project_id = null - branch_name = each.value.branch + branch_name = each.value.branch == null ? null : "^${trim(each.value.branch, "^$")}$" repo_name = each.value.name tag_name = null } @@ -60,8 +64,12 @@ module "branch-pf-prod-cicd-repo" { project_id = var.automation.project_id name = each.value.name iam = { - "roles/source.admin" = [module.branch-pf-prod-sa[0].iam_email] - "roles/source.reader" = [module.branch-pf-prod-sa-cicd[0].iam_email] + "roles/source.admin" = compact([ + try(module.branch-pf-prod-sa[0].iam_email, "") + ]) + "roles/source.reader" = compact([ + try(module.branch-pf-prod-sa-cicd[0].iam_email, "") + ]) } triggers = { fast-03-pf-prod = { @@ -73,7 +81,7 @@ module "branch-pf-prod-cicd-repo" { substitutions = {} template = { project_id = null - branch_name = each.value.branch + branch_name = each.value.branch == null ? null : "^${trim(each.value.branch, "^$")}$" repo_name = each.value.name tag_name = null } @@ -103,20 +111,18 @@ module "branch-pf-dev-sa-cicd" { } # impersonated via workload identity federation for external repos : { - "roles/iam.workloadIdentityUser" = [ + "roles/iam.workloadIdentityUser" = ( each.value.branch == null - ? format( - local.identity_providers[each.value.identity_provider].principal_repo, - var.automation.federated_identity_pool, - each.value.name - ) - : format( - local.identity_providers[each.value.identity_provider].principal_branch, - var.automation.federated_identity_pool, - each.value.name, - each.value.branch - ) - ] + ? [] + : [ + format( + local.identity_providers[each.value.identity_provider].principal_branch, + var.automation.federated_identity_pool, + each.value.name, + each.value.branch + ) + ] + ) } ) iam_project_roles = { @@ -146,20 +152,18 @@ module "branch-pf-prod-sa-cicd" { } # impersonated via workload identity federation for external repos : { - "roles/iam.workloadIdentityUser" = [ + "roles/iam.workloadIdentityUser" = ( each.value.branch == null - ? format( - local.identity_providers[each.value.identity_provider].principal_repo, - var.automation.federated_identity_pool, - each.value.name - ) - : format( - local.identity_providers[each.value.identity_provider].principal_branch, - var.automation.federated_identity_pool, - each.value.name, - each.value.branch - ) - ] + ? [] + : [ + format( + local.identity_providers[each.value.identity_provider].principal_branch, + var.automation.federated_identity_pool, + each.value.name, + each.value.branch + ) + ] + ) } ) iam_project_roles = { diff --git a/fast/stages-aw/1-resman/cicd-security.tf b/fast/stages-aw/1-resman/cicd-security.tf index d0dab9236..6efc06f32 100644 --- a/fast/stages-aw/1-resman/cicd-security.tf +++ b/fast/stages-aw/1-resman/cicd-security.tf @@ -39,7 +39,7 @@ module "branch-security-cicd-repo" { substitutions = {} template = { project_id = null - branch_name = each.value.branch + branch_name = each.value.branch == null ? null : "^${trim(each.value.branch, "^$")}$" repo_name = each.value.name tag_name = null } @@ -69,20 +69,18 @@ module "branch-security-sa-cicd" { } # impersonated via workload identity federation for external repos : { - "roles/iam.workloadIdentityUser" = [ + "roles/iam.workloadIdentityUser" = ( each.value.branch == null - ? format( - local.identity_providers[each.value.identity_provider].principal_repo, - var.automation.federated_identity_pool, - each.value.name - ) - : format( - local.identity_providers[each.value.identity_provider].principal_branch, - var.automation.federated_identity_pool, - each.value.name, - each.value.branch - ) - ] + ? [] + : [ + format( + local.identity_providers[each.value.identity_provider].principal_branch, + var.automation.federated_identity_pool, + each.value.name, + each.value.branch + ) + ] + ) } ) iam_project_roles = { diff --git a/fast/stages-aw/1-resman/cicd-teams.tf b/fast/stages-aw/1-resman/cicd-teams.tf index 67e04c109..b8f757b75 100644 --- a/fast/stages-aw/1-resman/cicd-teams.tf +++ b/fast/stages-aw/1-resman/cicd-teams.tf @@ -38,7 +38,7 @@ module "branch-teams-team-cicd-repo" { substitutions = {} template = { project_id = null - branch_name = each.value.cicd.branch + branch_name = each.value.cicd.branch == null ? null : "^${trim(each.value.cicd.branch, "^$")}$" repo_name = each.value.cicd.name tag_name = null } @@ -68,20 +68,18 @@ module "branch-teams-team-sa-cicd" { } # impersonated via workload identity federation for external repos : { - "roles/iam.workloadIdentityUser" = [ + "roles/iam.workloadIdentityUser" = ( each.value.cicd.branch == null - ? format( - local.identity_providers[each.value.cicd.identity_provider].principal_repo, - var.automation.federated_identity_pool, - each.value.cicd.name - ) - : format( - local.identity_providers[each.value.cicd.identity_provider].principal_branch, - var.automation.federated_identity_pool, - each.value.cicd.name, - each.value.cicd.branch - ) - ] + ? [] + : [ + format( + local.identity_providers[each.value.cicd.identity_provider].principal_branch, + var.automation.federated_identity_pool, + each.value.cicd.name, + each.value.cicd.branch + ) + ] + ) } ) iam_project_roles = { diff --git a/fast/stages-aw/1-resman/log-metric-alerts.tf b/fast/stages-aw/1-resman/log-metric-alerts.tf index 6e4ea0bf1..61790e8d4 100644 --- a/fast/stages-aw/1-resman/log-metric-alerts.tf +++ b/fast/stages-aw/1-resman/log-metric-alerts.tf @@ -48,7 +48,7 @@ module "core_log_alerts" { project = module.tenant-self-iac-projects[each.key].id combiner = "OR" - duration = "60s" + duration = "0s" comparison = "COMPARISON_GT" alignment_period = "60s" per_series_aligner = "ALIGN_RATE" @@ -64,7 +64,7 @@ module "main_log_alerts" { project = module.tenant-self-main-projects[each.key].id combiner = "OR" - duration = "60s" + duration = "0s" comparison = "COMPARISON_GT" alignment_period = "60s" per_series_aligner = "ALIGN_RATE" diff --git a/fast/stages-aw/1-resman/outputs-files.tf b/fast/stages-aw/1-resman/outputs-files.tf index 2f13adfc5..79c9d7e1d 100644 --- a/fast/stages-aw/1-resman/outputs-files.tf +++ b/fast/stages-aw/1-resman/outputs-files.tf @@ -22,21 +22,21 @@ locals { resource "local_file" "providers" { for_each = var.outputs_location == null ? {} : local.providers - file_permission = "0644" + file_permission = "0600" filename = "${local.outputs_location}/providers/${each.key}-providers.tf" content = try(each.value, null) } resource "local_file" "tfvars" { for_each = var.outputs_location == null ? {} : { 1 = 1 } - file_permission = "0644" + file_permission = "0600" filename = "${local.outputs_location}/tfvars/1-resman.auto.tfvars.json" content = jsonencode(local.tfvars) } resource "local_file" "workflows" { for_each = var.outputs_location == null ? {} : merge(local.cicd_workflows, local.team_cicd_workflows) - file_permission = "0644" + file_permission = "0600" filename = "${local.outputs_location}/workflows/${replace(each.key, "_", "-")}-workflow.yaml" content = try(each.value, null) } diff --git a/fast/stages-aw/1-resman/outputs-gcs.tf b/fast/stages-aw/1-resman/outputs-gcs.tf index 8e102a410..ed96dd708 100644 --- a/fast/stages-aw/1-resman/outputs-gcs.tf +++ b/fast/stages-aw/1-resman/outputs-gcs.tf @@ -20,18 +20,18 @@ resource "google_storage_bucket_object" "providers" { for_each = local.providers bucket = var.automation.outputs_bucket name = "providers/${each.key}-providers.tf" - content = each.value + content = sensitive(each.value) } resource "google_storage_bucket_object" "tfvars" { bucket = var.automation.outputs_bucket name = "tfvars/1-resman.auto.tfvars.json" - content = jsonencode(local.tfvars) + content = sensitive(jsonencode(local.tfvars)) } resource "google_storage_bucket_object" "workflows" { for_each = merge(local.cicd_workflows, local.team_cicd_workflows) bucket = var.automation.outputs_bucket name = "workflows/${replace(each.key, "_", "-")}-workflow.yaml" - content = each.value + content = sensitive(each.value) } diff --git a/fast/stages-aw/1-resman/outputs-tenants.tf b/fast/stages-aw/1-resman/outputs-tenants.tf index d1105d38e..f260cca5d 100644 --- a/fast/stages-aw/1-resman/outputs-tenants.tf +++ b/fast/stages-aw/1-resman/outputs-tenants.tf @@ -63,9 +63,9 @@ locals { resource "local_file" "tenant-core-tfvars" { for_each = var.outputs_location == null ? {} : local.tenant_tfvars - file_permission = "0644" + file_permission = "0600" filename = ( - "${pathexpand(var.outputs_location)}/tfvars/tenant/${each.key}.auto.tfvars.json" + "${pathexpand(var.outputs_location)}/tfvars/tenant/${basename(each.key)}.auto.tfvars.json" ) content = jsonencode(each.value) } @@ -74,9 +74,9 @@ resource "local_file" "tenant-core-providers" { for_each = ( var.outputs_location == null ? {} : local.tenant_core_providers ) - file_permission = "0644" + file_permission = "0600" filename = ( - "${pathexpand(var.outputs_location)}/providers/tenant/${each.key}-providers.tf" + "${pathexpand(var.outputs_location)}/providers/tenant/${basename(each.key)}-providers.tf" ) content = each.value } @@ -84,14 +84,14 @@ resource "local_file" "tenant-core-providers" { resource "google_storage_bucket_object" "tenant-core-tfvars" { for_each = local.tenant_tfvars bucket = var.automation.outputs_bucket - name = "tfvars/tenant/${each.key}.auto.tfvars.json" + name = "tfvars/tenant/${basename(each.key)}.auto.tfvars.json" content = jsonencode(each.value) } resource "google_storage_bucket_object" "tenant-core-providers" { for_each = local.tenant_core_providers bucket = var.automation.outputs_bucket - name = "providers/tenant/${each.key}-providers.tf" + name = "providers/tenant/${basename(each.key)}-providers.tf" content = each.value } @@ -101,9 +101,9 @@ resource "local_file" "tenant-self-providers" { for_each = ( var.outputs_location == null ? {} : local.tenant_self_providers ) - file_permission = "0644" + file_permission = "0600" filename = ( - "${pathexpand(var.outputs_location)}/providers/tenant/${each.key}-self-providers.tf" + "${pathexpand(var.outputs_location)}/providers/tenant/${basename(each.key)}-self-providers.tf" ) content = each.value } @@ -111,13 +111,13 @@ resource "local_file" "tenant-self-providers" { resource "google_storage_bucket_object" "tenant-self-tfvars" { for_each = local.tenant_tfvars bucket = module.tenant-self-iac-gcs-outputs[each.key].name - name = "tfvars/${each.key}.auto.tfvars.json" + name = "tfvars/${basename(each.key)}.auto.tfvars.json" content = jsonencode(each.value) } resource "google_storage_bucket_object" "tenant-self-providers" { for_each = local.tenant_self_providers bucket = module.tenant-self-iac-gcs-outputs[each.key].name - name = "providers/${each.key}-providers.tf" + name = "providers/${basename(each.key)}-providers.tf" content = each.value } \ No newline at end of file diff --git a/fast/stages-aw/2-networking-a-fedramp/branch-net-envs.tf b/fast/stages-aw/2-networking-a-fedramp/branch-net-envs.tf index 58c6ca923..f988d7e57 100644 --- a/fast/stages-aw/2-networking-a-fedramp/branch-net-envs.tf +++ b/fast/stages-aw/2-networking-a-fedramp/branch-net-envs.tf @@ -56,9 +56,8 @@ module "env-spoke-projects" { metric_scopes = [module.vdss-host-project.project_id] iam = { "roles/dns.admin" = compact([ - try(local.service_accounts.gke-dev, null), - try(local.service_accounts.project-factory-dev, null), - try(local.service_accounts.project-factory-prod, null), + try(local.service_accounts["gke-${lower(each.key)}"], null), + try(local.service_accounts["project-factory-${lower(each.key)}"], null), ]) } # # allow specific service accounts to assign a set of roles @@ -96,7 +95,7 @@ module "env-spoke-vpc" { logging = var.dns.enable_logging } delete_default_routes_on_create = true - psa_configs = var.psa_ranges.dev + psa_configs = try(var.psa_ranges[lower(each.key)], var.psa_ranges.dev) # Set explicit routes for googleapis; send everything else to NVAs create_googleapis_routes = { private = true @@ -170,10 +169,10 @@ module "peering-envs" { peer_network = module.vdss-vpc.self_link routes_config = { local = { - public_import = true + public_import = false } peer = { - public_export = true + public_export = false } } } diff --git a/fast/stages-aw/2-networking-a-fedramp/net-vdss.tf b/fast/stages-aw/2-networking-a-fedramp/net-vdss.tf index 53449f5b5..15db0f38c 100644 --- a/fast/stages-aw/2-networking-a-fedramp/net-vdss.tf +++ b/fast/stages-aw/2-networking-a-fedramp/net-vdss.tf @@ -69,8 +69,11 @@ module "dmz-vpc" { inbound = true logging = var.dns.enable_logging } - create_googleapis_routes = null - subnets = try(var.subnets.dmz, []) + create_googleapis_routes = { + private = true + restricted = true + } + subnets = try(var.subnets.dmz, []) } module "dmz-firewall" { @@ -153,6 +156,7 @@ module "landing-dns-policy-googleapis" { name = "googleapis" rules = var.dns_policy_rules networks = { + dmz = module.dmz-vpc.self_link landing = module.vdss-vpc.self_link } } diff --git a/fast/stages-aw/2-networking-a-fedramp/nva.tf b/fast/stages-aw/2-networking-a-fedramp/nva.tf index eb88e5c74..afd98d9cd 100644 --- a/fast/stages-aw/2-networking-a-fedramp/nva.tf +++ b/fast/stages-aw/2-networking-a-fedramp/nva.tf @@ -214,7 +214,6 @@ module "kms" { iam = { "roles/cloudkms.cryptoKeyEncrypterDecrypter" = [ - google_service_account.compute.member, module.vdss-host-project.service_agents.compute.iam_email ] } diff --git a/fast/stages-aw/2-networking-a-fedramp/outputs.tf b/fast/stages-aw/2-networking-a-fedramp/outputs.tf index 2c06e801e..6ff910467 100644 --- a/fast/stages-aw/2-networking-a-fedramp/outputs.tf +++ b/fast/stages-aw/2-networking-a-fedramp/outputs.tf @@ -46,7 +46,7 @@ locals { resource "local_file" "tfvars" { for_each = var.outputs_location == null ? {} : { 1 = 1 } - file_permission = "0644" + file_permission = "0600" filename = "${try(pathexpand(var.outputs_location), "")}/tfvars/2-networking.auto.tfvars.json" content = jsonencode(local.tfvars) } @@ -54,7 +54,7 @@ resource "local_file" "tfvars" { resource "google_storage_bucket_object" "tfvars" { bucket = var.automation.outputs_bucket name = "tfvars/2-networking.auto.tfvars.json" - content = jsonencode(local.tfvars) + content = sensitive(jsonencode(local.tfvars)) } # outputs diff --git a/fast/stages-aw/2-networking-b-il5-ngfw/branch-net-envs.tf b/fast/stages-aw/2-networking-b-il5-ngfw/branch-net-envs.tf index 1028ca645..2e2d1a945 100644 --- a/fast/stages-aw/2-networking-b-il5-ngfw/branch-net-envs.tf +++ b/fast/stages-aw/2-networking-b-il5-ngfw/branch-net-envs.tf @@ -56,9 +56,8 @@ module "env-spoke-projects" { metric_scopes = [module.vdss-host-project.project_id] iam = { "roles/dns.admin" = compact([ - try(local.service_accounts.gke-dev, null), - try(local.service_accounts.project-factory-dev, null), - try(local.service_accounts.project-factory-prod, null), + try(local.service_accounts["gke-${lower(each.key)}"], null), + try(local.service_accounts["project-factory-${lower(each.key)}"], null), ]) } # # allow specific service accounts to assign a set of roles @@ -96,7 +95,7 @@ module "env-spoke-vpc" { logging = var.dns.enable_logging } delete_default_routes_on_create = true - psa_configs = var.psa_ranges.dev + psa_configs = try(var.psa_ranges[lower(each.key)], var.psa_ranges.dev) # Set explicit routes for googleapis; send everything else to NVAs create_googleapis_routes = { private = true @@ -170,10 +169,10 @@ module "peering-envs" { peer_network = module.vdss-vpc.self_link routes_config = { local = { - public_import = true + public_import = false } peer = { - public_export = true + public_export = false } } } diff --git a/fast/stages-aw/2-networking-b-il5-ngfw/log-metric-alerts.tf b/fast/stages-aw/2-networking-b-il5-ngfw/log-metric-alerts.tf index 5cf9731e2..87d26cc97 100644 --- a/fast/stages-aw/2-networking-b-il5-ngfw/log-metric-alerts.tf +++ b/fast/stages-aw/2-networking-b-il5-ngfw/log-metric-alerts.tf @@ -44,7 +44,7 @@ module "vdss_log_alerts" { project = module.vdss-host-project.id combiner = "OR" - duration = "60s" + duration = "0s" comparison = "COMPARISON_GT" alignment_period = "60s" per_series_aligner = "ALIGN_RATE" @@ -60,7 +60,7 @@ module "spoke_log_alerts" { project = module.env-spoke-projects[each.key].id combiner = "OR" - duration = "60s" + duration = "0s" comparison = "COMPARISON_GT" alignment_period = "60s" per_series_aligner = "ALIGN_RATE" diff --git a/fast/stages-aw/2-networking-b-il5-ngfw/net-vdss.tf b/fast/stages-aw/2-networking-b-il5-ngfw/net-vdss.tf index b86eb27a4..aaa16d30e 100644 --- a/fast/stages-aw/2-networking-b-il5-ngfw/net-vdss.tf +++ b/fast/stages-aw/2-networking-b-il5-ngfw/net-vdss.tf @@ -65,8 +65,11 @@ module "dmz-vpc" { inbound = true logging = var.dns.enable_logging } - create_googleapis_routes = null - subnets = try(var.subnets.dmz, []) + create_googleapis_routes = { + private = true + restricted = true + } + subnets = try(var.subnets.dmz, []) } module "dmz-firewall" { @@ -199,6 +202,8 @@ module "landing-dns-policy-googleapis" { name = "googleapis" rules = var.dns_policy_rules networks = { + dmz = module.dmz-vpc.self_link landing = module.vdss-vpc.self_link + mgmt = module.mgmt-vpc.self_link } } diff --git a/fast/stages-aw/2-networking-b-il5-ngfw/ngfw.tf b/fast/stages-aw/2-networking-b-il5-ngfw/ngfw.tf index d9a8c87a1..590d8eaef 100644 --- a/fast/stages-aw/2-networking-b-il5-ngfw/ngfw.tf +++ b/fast/stages-aw/2-networking-b-il5-ngfw/ngfw.tf @@ -68,7 +68,7 @@ resource "google_project_iam_custom_role" "ngfw-custom-role" { description = "Many of the permissions required for the Palo Alto NGFW, not including compute.viewer" permissions = [ "storage.buckets.get", - "logging.buckets.write", + "logging.logEntries.create", "opsconfigmonitoring.resourceMetadata.write", "autoscaling.sites.writeMetrics", "monitoring.metricDescriptors.create", @@ -122,7 +122,7 @@ module "ngfw-bootstrap-bucket" { resource "google_storage_bucket_iam_binding" "binding" { bucket = module.ngfw-bootstrap-bucket[each.key].name for_each = var.regions - role = "roles/storage.objectUser" + role = "roles/storage.objectViewer" members = [ "serviceAccount:service-${module.vdss-host-project.number}@compute-system.iam.gserviceaccount.com", module.ngfw-service-account.service_account.member diff --git a/fast/stages-aw/2-networking-b-il5-ngfw/openssl-helper.sh b/fast/stages-aw/2-networking-b-il5-ngfw/openssl-helper.sh index 516274f1c..7f3e03b77 100644 --- a/fast/stages-aw/2-networking-b-il5-ngfw/openssl-helper.sh +++ b/fast/stages-aw/2-networking-b-il5-ngfw/openssl-helper.sh @@ -21,10 +21,10 @@ set -e # FOO and BAZ shell variables. # jq will ensure that the values are properly quoted # and escaped for consumption by the shell. -eval "$(jq -r '@sh "ALGO=\(.algo) PLAINTEXT=\(.plaintext) SALT=\(.salt)"')" +eval "$(jq -r '"ALGO=\(.algo | @sh) PLAINTEXT=\(.plaintext | @sh) SALT=\(.salt | @sh)"')" -# Placeholder for whatever data-fetching logic your script implements -hash=$(openssl passwd "-${ALGO}" -salt "${SALT}" "${PLAINTEXT}") +# Compute password hash reading plaintext from standard input +hash=$(printf '%s' "${PLAINTEXT}" | openssl passwd "-${ALGO}" -salt "${SALT}" -stdin) # Safely produce a JSON object containing the result value. # jq will ensure that the value is properly quoted diff --git a/fast/stages-aw/2-networking-b-il5-ngfw/outputs.tf b/fast/stages-aw/2-networking-b-il5-ngfw/outputs.tf index 77da4a04b..37600d241 100644 --- a/fast/stages-aw/2-networking-b-il5-ngfw/outputs.tf +++ b/fast/stages-aw/2-networking-b-il5-ngfw/outputs.tf @@ -46,7 +46,7 @@ locals { resource "local_file" "tfvars" { for_each = var.outputs_location == null ? {} : { 1 = 1 } - file_permission = "0644" + file_permission = "0600" filename = "${try(pathexpand(var.outputs_location), "")}/tfvars/2-networking.auto.tfvars.json" content = jsonencode(local.tfvars) } @@ -54,7 +54,7 @@ resource "local_file" "tfvars" { resource "google_storage_bucket_object" "tfvars" { bucket = var.automation.outputs_bucket name = "tfvars/2-networking.auto.tfvars.json" - content = jsonencode(local.tfvars) + content = sensitive(jsonencode(local.tfvars)) } # outputs @@ -70,13 +70,15 @@ output "host_project_numbers" { } resource "local_file" "rsa-out" { - content = nonsensitive(tls_private_key.ngfw-ssh.private_key_openssh) - filename = "${path.module}/id_rsa" + content = tls_private_key.ngfw-ssh.private_key_openssh + file_permission = "0600" + filename = "${path.module}/id_rsa" } resource "local_file" "rsa-pub-out" { - content = nonsensitive(tls_private_key.ngfw-ssh.public_key_openssh) - filename = "${path.module}/id_rsa.pub" + content = tls_private_key.ngfw-ssh.public_key_openssh + file_permission = "0600" + filename = "${path.module}/id_rsa.pub" } output "ngfw_password" { diff --git a/fast/stages-aw/2-networking-b-il5-ngfw/variables.tf b/fast/stages-aw/2-networking-b-il5-ngfw/variables.tf index 294dba371..5d8760ff9 100644 --- a/fast/stages-aw/2-networking-b-il5-ngfw/variables.tf +++ b/fast/stages-aw/2-networking-b-il5-ngfw/variables.tf @@ -68,6 +68,15 @@ variable "common_services_folder" { default = null } +variable "custom_roles" { + # tfdoc:variable:source 0-bootstrap + description = "Custom roles defined at the org level, in key => id format." + type = object({ + service_project_network_admin = string + }) + default = null +} + variable "dns" { description = "DNS configuration." type = object({ @@ -180,6 +189,14 @@ variable "groups" { default = null } +variable "kms_protection_level" { + # tfdoc:variable:source 0-bootstrap + description = "KMS protection level." + type = string + nullable = true + default = null +} + variable "logging" { description = "Logging configuration." type = any diff --git a/fast/stages-aw/3-security/log-metric-alerts.tf b/fast/stages-aw/3-security/log-metric-alerts.tf index 08544731d..cf88df146 100644 --- a/fast/stages-aw/3-security/log-metric-alerts.tf +++ b/fast/stages-aw/3-security/log-metric-alerts.tf @@ -48,7 +48,7 @@ module "security_log_alerts" { project = each.key combiner = "OR" - duration = "60s" + duration = "0s" comparison = "COMPARISON_GT" alignment_period = "60s" per_series_aligner = "ALIGN_RATE" diff --git a/fast/stages-aw/3-security/main.tf b/fast/stages-aw/3-security/main.tf index 88365d6b6..bcd3adcb2 100644 --- a/fast/stages-aw/3-security/main.tf +++ b/fast/stages-aw/3-security/main.tf @@ -23,6 +23,7 @@ locals { description = "Automation service account delegated grants." expression = format( <<-EOT + api.getAttribute('iam.googleapis.com/modifiedGrantsByRole', []).size() > 0 && api.getAttribute('iam.googleapis.com/modifiedGrantsByRole', []).hasOnly([%s]) && resource.type == 'cloudkms.googleapis.com/CryptoKey' EOT diff --git a/fast/stages-aw/3-security/vpc-sc.tf b/fast/stages-aw/3-security/vpc-sc.tf index f7d7b6f4f..4c26248ac 100644 --- a/fast/stages-aw/3-security/vpc-sc.tf +++ b/fast/stages-aw/3-security/vpc-sc.tf @@ -22,8 +22,13 @@ locals { identities = values(var.logging.writer_identities) } to = { - operations = [{ service_name = "*" }] - resources = ["projects/${var.logging.project_number}"] + operations = [ + { service_name = "bigquery.googleapis.com" }, + { service_name = "logging.googleapis.com" }, + { service_name = "pubsub.googleapis.com" }, + { service_name = "storage.googleapis.com" }, + ] + resources = ["projects/${var.logging.project_number}"] } } } diff --git a/tests/tools/test_blueprint_and_module_validations.py b/tests/tools/test_blueprint_and_module_validations.py index 9c25e2512..0ec25a773 100644 --- a/tests/tools/test_blueprint_and_module_validations.py +++ b/tests/tools/test_blueprint_and_module_validations.py @@ -54,6 +54,202 @@ def test_modules_enforce_30_char_id_preconditions(self): "exceeding the 30-character GCP service account ID limit", sa_tf ) + def _iter_terraform_dirs(self): + """Yield every directory under fast/, modules/, and blueprints/ containing .tf files.""" + seen = set() + for root_name in ("fast", "blueprints"): + base = REPO_ROOT / root_name + for tf_file in base.rglob("*.tf"): + if ".terraform" in tf_file.parts: + continue + parent = tf_file.parent + if parent not in seen: + seen.add(parent) + yield parent + + @staticmethod + def _strip_comments_and_moved(text: str) -> str: + import re + lines = [] + in_block = False + for line in text.splitlines(): + stripped = line.strip() + if in_block: + if "*/" in stripped: + in_block = False + continue + if stripped.startswith("/*"): + if "*/" not in stripped: + in_block = True + continue + if stripped.startswith("#") or stripped.startswith("//"): + continue + line = re.sub(r"\s+#.*$", "", line) + lines.append(line) + cleaned = "\n".join(lines) + # Remove moved { ... } blocks and description = "..." strings + cleaned = re.sub(r"moved\s*\{[^}]*\}", "", cleaned, flags=re.DOTALL) + cleaned = re.sub(r'description\s*=\s*"[^"]*"', "", cleaned) + return cleaned + + def test_all_terraform_dirs_have_valid_variable_and_resource_references(self): + """Ensure every Terraform directory declares all referenced vars, resources, data sources, and modules.""" + import re + errors = [] + for tf_dir in sorted(self._iter_terraform_dirs()): + rel_dir = tf_dir.relative_to(REPO_ROOT) + raw_combined = "\n".join( + p.read_text(encoding="utf-8") for p in sorted(tf_dir.glob("*.tf")) + ) + cleaned = self._strip_comments_and_moved(raw_combined) + + # Catch common HCL prefix typos like vvar. or lcoal. + for typo in re.findall(r"\b(?:vvar|lcoal| moudle)\.[a-zA-Z0-9_-]+", cleaned): + errors.append(f"{rel_dir}: suspicious typo reference '{typo}'") + + decl_vars = set(re.findall(r'variable\s+"([^"]+)"', raw_combined)) + ref_vars = set(re.findall(r"\bvar\.([a-zA-Z0-9_-]+)", cleaned)) + missing_vars = sorted(ref_vars - decl_vars) + if missing_vars: + errors.append(f"{rel_dir}: undeclared var reference(s): {missing_vars}") + + decl_res = { + f"{m.group(1)}.{m.group(2)}" + for m in re.finditer(r'resource\s+"([^"]+)"\s+"([^"]+)"', raw_combined) + } + ref_res = set() + for m in re.finditer( + r'(? 0: + if cleaned[i] == "{": + depth += 1 + elif cleaned[i] == "}": + depth -= 1 + i += 1 + block = cleaned[start : i - 1] + b_depth = 0 + for bline in block.splitlines(): + if b_depth == 0: + km = re.match(r"^\s*([a-zA-Z0-9_-]+)\s*=", bline) + if km: + decl_locals.add(km.group(1)) + b_depth += bline.count("{") + bline.count("[") + bline.count("(") + b_depth -= bline.count("}") + bline.count("]") + bline.count(")") + pos = i + ref_locals = set(re.findall(r"(? 0: + if raw_vars[i] == "{": + depth += 1 + elif raw_vars[i] == "}": + depth -= 1 + i += 1 + vbody = raw_vars[start : i - 1] + if not re.search(r"^\s*default\s*=", vbody, flags=re.M): + req_vars.add(vname) + pos = i + + stext = sample.read_text(encoding="utf-8") + slines = [ + re.sub(r"(#|//).*$", "", l) + for l in stext.splitlines() + if not l.strip().startswith(("#", "//")) + ] + s_depth = 0 + assigned = set() + for l in slines: + if re.search(r"=\s*base64encode\(", l): + errors.append(f"{rel}: function call not allowed in tfvars: {l.strip()}") + if s_depth == 0: + am = re.match(r"^\s*([a-zA-Z0-9_-]+)\s*=", l) + if am: + assigned.add(am.group(1)) + s_depth += l.count("{") + l.count("[") + l.count("(") + s_depth -= l.count("}") + l.count("]") + l.count(")") + undecl = sorted(assigned - decl_vars) + missing_req = sorted(req_vars - assigned) + if undecl: + errors.append(f"{rel}: undeclared variable(s) in sample tfvars: {undecl}") + if missing_req: + errors.append(f"{rel}: missing required variable(s) in sample tfvars: {missing_req}") + + self.assertEqual( + errors, + [], + "Invalid blueprint terraform.tfvars.sample files:\n" + "\n".join(errors), + ) + def test_custom_stellar_engine_modules_validations(self): diff --git a/tests/tools/test_bootstrap_logging_order.py b/tests/tools/test_bootstrap_logging_order.py index bf9a3291e..9de8a90e4 100644 --- a/tests/tools/test_bootstrap_logging_order.py +++ b/tests/tools/test_bootstrap_logging_order.py @@ -69,6 +69,10 @@ def test_org_policy_parameters_not_double_encoded(self): org_tf, 'org_policies should not unconditionally jsonencode string parameters', ) + self.assertIn( + 'try(r.parameters, null) != null', + org_tf, + ) self.assertIn( 'try(tostring(r.parameters), jsonencode(r.parameters))', org_tf, @@ -83,6 +87,10 @@ def test_log_export_project_settings_provisioned_before_cmek(self): 'data "google_logging_project_settings" "log_export"', log_export_tf, ) + self.assertIn( + 'depends_on = [module.log-export-project]', + log_export_tf, + ) self.assertIn( 'data.google_logging_project_settings.log_export', log_export_tf, @@ -92,6 +100,35 @@ def test_log_export_project_settings_provisioned_before_cmek(self): kms_tf, ) + def test_logging_location_avoids_checklist_cycle(self): + main_tf = (_BOOTSTRAP_DIR / 'main.tf').read_text(encoding='utf-8') + self.assertNotIn('local.checklist.location', main_tf) + self.assertIn( + 'try(local._cl_data.logging.sinks[0].destination.location, null)', + main_tf, + ) + + def test_delegated_iam_conditions_guard_empty_grant_list(self): + for rel_path in ( + 'fast/stages-aw/0-bootstrap/organization.tf', + 'fast/stages-aw/0-bootstrap/automation.tf', + 'fast/stages-aw/3-security/main.tf', + ): + content = (_REPO_ROOT / rel_path).read_text(encoding='utf-8') + self.assertIn( + "api.getAttribute('iam.googleapis.com/modifiedGrantsByRole', []).size() > 0", + content, + ) + + def test_automation_project_protects_bootstrap_service_accounts(self): + automation_tf = (_BOOTSTRAP_DIR / 'automation.tf').read_text(encoding='utf-8') + self.assertIn('sa_admin_scoped', automation_tf) + self.assertIn('sa_token_creator_devops', automation_tf) + self.assertIn( + "!resource.name.endsWith('/serviceAccounts/%s')", + automation_tf, + ) + if __name__ == '__main__': unittest.main() diff --git a/tests/tools/test_check_boilerplate.py b/tests/tools/test_check_boilerplate.py index a80f3156c..86ee4591e 100644 --- a/tests/tools/test_check_boilerplate.py +++ b/tests/tools/test_check_boilerplate.py @@ -44,6 +44,18 @@ def test_match_files_extensions(self): self.assertIn('.tf', check_boilerplate._MATCH_FILES) self.assertIn('.yaml', check_boilerplate._MATCH_FILES) + def test_skips_symlinks(self): + import os + import tempfile + with tempfile.TemporaryDirectory() as tmpdir: + target = os.path.join(tmpdir, 'unlicensed.txt') + with open(target, 'w', encoding='utf-8') as f: + f.write('no boilerplate here\n') + link_py = os.path.join(tmpdir, 'linked.py') + os.symlink(target, link_py) + # Should not raise SystemExit(1) because symlinks are skipped + check_boilerplate.main([tmpdir]) + if __name__ == '__main__': unittest.main() diff --git a/tests/tools/test_gemini_enterprise_fixes.py b/tests/tools/test_gemini_enterprise_fixes.py index 540706f7f..4b5bf3d22 100644 --- a/tests/tools/test_gemini_enterprise_fixes.py +++ b/tests/tools/test_gemini_enterprise_fixes.py @@ -129,6 +129,24 @@ def test_deploy_sh_uses_existing_compatible_terraform_before_tfenv(self): self.assertIn('printf "%s\\n1.7.4\\n" "$tf_ver" | sort -V | head -n 1', deploy_sh) self.assertIn('if [[ "$tf_compatible" != "true" ]]; then', deploy_sh) + def test_cloudarmor_and_cli_token_handling(self): + """Ensure Cloud Armor rules deny matches and CLI passes tokens via stdin.""" + cloudarmor_tf = ( + REPO_ROOT + / "blueprints/fedramp-high/gemini-enterprise/gemini-stage-0/cloudarmor.tf" + ).read_text(encoding="utf-8") + self.assertNotIn( + 'action = "allow"\n priority = rules.value.priority', + cloudarmor_tf, + ) + self.assertIn( + 'action = "deny(403)"\n priority = rules.value.priority', + cloudarmor_tf, + ) + + py_content = GEM4GOV_PATH.read_text(encoding="utf-8") + self.assertNotIn("'-H', f\"Authorization: Bearer {access_token}\"", py_content) + self.assertIn("'-H', '@-'", py_content) if __name__ == "__main__": diff --git a/tests/tools/test_stage2_networking.py b/tests/tools/test_stage2_networking.py index cac7c3e34..fe6ff9654 100644 --- a/tests/tools/test_stage2_networking.py +++ b/tests/tools/test_stage2_networking.py @@ -62,6 +62,16 @@ def test_nva_defaults_to_standard_on_demand_vms(self): self.assertIn('variable "nva_spot_vms"', vars_tf) self.assertIn('default = false', vars_tf) + def test_nva_and_ngfw_service_account_and_helper_hardening(self): + nva_tf = (_FEDRAMP_NET_DIR / 'nva.tf').read_text(encoding='utf-8') + self.assertNotIn('google_service_account_key', nva_tf) + + ngfw_tf = (_IL5_NET_DIR / 'ngfw.tf').read_text(encoding='utf-8') + self.assertNotIn('-compute@developer.gserviceaccount.com', ngfw_tf) + + openssl_sh = (_IL5_NET_DIR / 'openssl-helper.sh').read_text(encoding='utf-8') + self.assertIn('-stdin', openssl_sh) + if __name__ == '__main__': unittest.main() diff --git a/tests/tools/test_state_iam.py b/tests/tools/test_state_iam.py index a01919002..4fcea90bc 100644 --- a/tests/tools/test_state_iam.py +++ b/tests/tools/test_state_iam.py @@ -81,6 +81,26 @@ def test_get_bindings_with_prefix(self): self.assertFalse(b.authoritative) self.assertEqual(b.resource_id, "proj-1") + def test_get_bindings_multi_underscore_resource_type(self): + resources = [ + { + "type": "google_kms_crypto_key_iam_member", + "instances": [ + { + "attributes": { + "crypto_key_id": "projects/p1/locations/us/keyRings/kr1/cryptoKeys/k1", + "role": "roles/cloudkms.cryptoKeyEncrypterDecrypter", + "member": "serviceAccount:sa@p1.iam.gserviceaccount.com", + "condition": [] + } + } + ] + } + ] + bindings = list(state_iam.get_bindings(resources)) + self.assertEqual(len(bindings), 1) + self.assertEqual(bindings[0].resource_type, "kms_crypto_key") + if __name__ == '__main__': unittest.main() diff --git a/tests/tools/test_tfdoc.py b/tests/tools/test_tfdoc.py index a0aa85612..f3284e925 100644 --- a/tests/tools/test_tfdoc.py +++ b/tests/tools/test_tfdoc.py @@ -72,6 +72,21 @@ def test_output_regex_parsing(self): self.assertEqual(item["name"], "vpc_id") self.assertIn("The VPC network self link.", "".join(item["description"])) + def test_parse_files_skips_symlinks(self): + import os + import tempfile + with tempfile.TemporaryDirectory() as tmpdir: + real_tf = os.path.join(tmpdir, "main.tf") + with open(real_tf, "w", encoding="utf-8") as f: + f.write("# main\n") + ext_tf = os.path.join(tmpdir, "external.txt") + with open(ext_tf, "w", encoding="utf-8") as f: + f.write("# external\n") + os.symlink(ext_tf, os.path.join(tmpdir, "linked.tf")) + files = list(tfdoc.parse_files(tmpdir)) + self.assertEqual(len(files), 1) + self.assertEqual(files[0].name, "main.tf") + if __name__ == '__main__': unittest.main() diff --git a/tools/changelog.py b/tools/changelog.py index f3204eac2..243993b72 100755 --- a/tools/changelog.py +++ b/tools/changelog.py @@ -108,9 +108,15 @@ def format_pull(pull): prefix = '' if 'incompatible change' in pull.labels: prefix = '**incompatible change:** ' + safe_title = ( + pull.title.replace('[', '\\[') + .replace(']', '\\]') + .replace('<', '<') + .replace('>', '>') + ) return (f'- [[#{pull.id}]({pull_url}/{pull.id})] ' f'{prefix}' - f'{pull.title} ' + f'{safe_title} ' f'([{pull.author}]({url}/{pull.author})) ') @@ -152,14 +158,16 @@ def get_pulls(api): def get_release_pulls(api, releases): 'Get and add pull requests for releases.' + if not releases: + return releases i = 0 for p in get_pulls(api): if releases[i].published and p.merged_at >= releases[i].published: continue - if releases[i].since and p.merged_at <= releases[i].since: + while i < len(releases) and releases[i].since and p.merged_at <= releases[i].since: i += 1 - if i == len(releases): - break + if i == len(releases): + break releases[i].pulls.append(p) return releases diff --git a/tools/check_boilerplate.py b/tools/check_boilerplate.py index 46724e7ed..a43af928b 100755 --- a/tools/check_boilerplate.py +++ b/tools/check_boilerplate.py @@ -46,10 +46,13 @@ def main(base_dirs): for fname in files: if fname in _MATCH_FILES or os.path.splitext(fname)[1] in _MATCH_FILES: fpath = os.path.abspath(os.path.join(root, fname)) - content = open(fpath).read() - if _EXCLUDE_RE.search(content): + if os.path.islink(fpath): continue try: + with open(fpath, encoding='utf-8', errors='replace') as f: + content = f.read(8192) + if _EXCLUDE_RE.search(content): + continue if not _MATCH_RE.search(content): errors.append(fpath) except (IOError, OSError): diff --git a/tools/check_documentation.py b/tools/check_documentation.py index 7fbf5032a..b451647e4 100755 --- a/tools/check_documentation.py +++ b/tools/check_documentation.py @@ -103,21 +103,21 @@ def _check_dir(dir_name, exclude_files=None, files=False, show_extra=False): diff = ''.join([header] + [x for x in ndiff if x[0] != ' ']) elif empty := [v.name for v in newvars if not v.description]: - state = state.FAIL_VARIABLE_DESCRIPTION + state = State.FAIL_VARIABLE_DESCRIPTION diff = "\n".join([ f'----- {readme_rel} variables missing description -----', ', '.join(empty), ]) elif empty := [o.name for o in newouts if not o.description]: - state = state.FAIL_VARIABLE_DESCRIPTION + state = State.FAIL_OUTPUT_DESCRIPTION diff = "\n".join([ f'----- {readme_rel} outputs missing description -----', ', '.join(empty), ]) elif variables != sorted(variables): - state = state.FAIL_UNSORTED_VARS + state = State.FAIL_UNSORTED_VARS diff = "\n".join([ f'----- {readme_rel} variables -----', 'variables should be in this order: ', @@ -125,7 +125,7 @@ def _check_dir(dir_name, exclude_files=None, files=False, show_extra=False): ]) elif outputs != sorted(outputs): - state = state.FAIL_UNSORTED_OUTPUTS + state = State.FAIL_UNSORTED_OUTPUTS diff = "\n".join([ f'----- {readme_rel} outputs -----', 'outputs should be in this order: ', @@ -133,21 +133,21 @@ def _check_dir(dir_name, exclude_files=None, files=False, show_extra=False): ]) elif nc := [v.name for v in newvars if not v.description.endswith('.')]: - state = state.FAIL_VARIABLE_PERIOD + state = State.FAIL_VARIABLE_PERIOD diff = "\n".join([ f'----- {readme_rel} variable descriptions missing ending period -----', ', '.join(nc), ]) elif nc := [o.name for o in newouts if not o.description.endswith('.')]: - state = state.FAIL_OUTPUT_PERIOD + state = State.FAIL_OUTPUT_PERIOD diff = "\n".join([ f'----- {readme_rel} output descriptions missing ending period -----', ', '.join(nc), ]) elif no_types := [v.name for v in newvars if not v.type]: - state = state.FAIL_MISSING_TYPES + state = State.FAIL_MISSING_TYPES diff = "\n".join([ f'----- {readme_rel} variables without types -----', ', '.join(no_types), diff --git a/tools/check_links.py b/tools/check_links.py index 202bc6401..1ba14a4b3 100755 --- a/tools/check_links.py +++ b/tools/check_links.py @@ -20,7 +20,9 @@ ''' import collections +import ipaddress import pathlib +import socket import requests import urllib.parse @@ -39,16 +41,26 @@ def check_link(link, readme_path, external): # If the link is public, say the link is anyway valid # if --external is not set; check the link otherwise if url.scheme: + if url.scheme not in ('http', 'https'): + return LINK(link.dest, False) link_valid = True if external: try: - response = requests.get(link.dest) + hostname = url.hostname + if not hostname: + return LINK(link.dest, False) + for info in socket.getaddrinfo(hostname, None): + ip = ipaddress.ip_address(info[4][0]) + if ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_reserved: + return LINK(link.dest, False) + response = requests.get(link.dest, timeout=10) link_valid = response.ok - except requests.exceptions.RequestException: + except (requests.exceptions.RequestException, socket.gaierror, ValueError): link_valid = False # The link is private else: - link_valid = (readme_path.parent / url.path).exists() + target = (readme_path.parent / url.path).resolve() + link_valid = target.is_relative_to(BASEDIR.resolve()) and target.exists() return LINK(link.dest, link_valid) diff --git a/tools/plan_summary.py b/tools/plan_summary.py index 441c638c6..af185e079 100755 --- a/tools/plan_summary.py +++ b/tools/plan_summary.py @@ -21,11 +21,11 @@ from pathlib import Path +BASEDIR = Path(__file__).resolve().parents[1] try: import fixtures except ImportError: - BASEDIR = Path(__file__).parents[1] - sys.path.append(str(BASEDIR / 'tests')) + sys.path.insert(0, str(BASEDIR / 'tests')) import fixtures @@ -34,6 +34,7 @@ @click.argument('module', type=click.Path(), nargs=1) @click.argument('tfvars', type=click.Path(exists=True), nargs=-1) def main(example, module, tfvars): + tmp_dir = None try: if example: tmp_dir = tempfile.TemporaryDirectory() @@ -44,7 +45,10 @@ def main(example, module, tfvars): (tmp_path / 'fabric').symlink_to(BASEDIR) module = tmp_path else: - module = BASEDIR / module + resolved = (BASEDIR / module).resolve() + if not resolved.is_relative_to(BASEDIR.resolve()): + raise SystemExit(f'Module path must be within {BASEDIR}') + module = resolved summary = fixtures.plan_summary(module, Path(), tfvars) print(yaml.dump({'values': summary.values})) @@ -54,7 +58,7 @@ def main(example, module, tfvars): } print(yaml.dump({'outputs': outputs})) finally: - if example: + if tmp_dir is not None: tmp_dir.cleanup() diff --git a/tools/state_iam.py b/tools/state_iam.py index 41cfa3285..d519bc8b4 100755 --- a/tools/state_iam.py +++ b/tools/state_iam.py @@ -15,6 +15,7 @@ 'Parse and output IAM bindings from Terraform state file.' import collections +import csv import json import itertools import re @@ -26,7 +27,7 @@ 'member_type', 'member_id', 'conditions') ORG_IDS = {} RESOURCE_SORT = {'organization': 0, 'folder': 1, 'project': 2} -RESOURCE_TYPE_RE = re.compile(r'^google_([^_]+)_iam_([^_]+)$') +RESOURCE_TYPE_RE = re.compile(r'^google_(.+?)_iam_(binding|member|policy)$') Binding = collections.namedtuple('Binding', ' '.join(FIELDS)) Folder = collections.namedtuple('Folder', 'id name parent_id') @@ -52,7 +53,7 @@ def get_bindings(resources, prefix=None, folders=None): if resource_type == 'organization': resource_id = _org_id(attrs['org_id']) else: - resource_id = attrs[resource_type] + resource_id = attrs.get(resource_type) or attrs.get('name') or attrs.get('id', '') if prefix and resource_id.startswith(prefix): resource_id = resource_id[len(prefix) + 1:] role = attrs['role'] @@ -111,9 +112,10 @@ def get_folders(resources): def output_csv(bindings): 'Output bindings in CSV format.' - print(','.join(FIELDS)) + writer = csv.writer(sys.stdout, lineterminator='\n') + writer.writerow(FIELDS) for b in bindings: - print(','.join(str(getattr(b, f)) for f in FIELDS)) + writer.writerow([getattr(b, f) for f in FIELDS]) def output_principals(bindings): diff --git a/tools/tfdoc.py b/tools/tfdoc.py index 42018bd32..c67e8f77b 100755 --- a/tools/tfdoc.py +++ b/tools/tfdoc.py @@ -153,7 +153,7 @@ def _parse(body, enum=VAR_ENUM, re=VAR_RE, template=VAR_TEMPLATE): if not item: continue context = m.group(m.lastindex - 1) - item[context].append(data) + item.setdefault(context, []).append(data) elif token == enum.SKIP: context = token elif token == enum.COMMENT: @@ -162,7 +162,7 @@ def _parse(body, enum=VAR_ENUM, re=VAR_RE, template=VAR_TEMPLATE): item['tags'][k[6:]] = v elif token == enum.TXT: if context and context != enum.SKIP: - item[context].append(data) + item.setdefault(context, []).append(data) def create_toc(readme): @@ -171,7 +171,17 @@ def create_toc(readme): lines = [] headings = [x for x in doc.children if x.get_type() == 'Heading'] for h in headings[1:]: - title = h.children[0].children + if not h.children: + continue + first_child = h.children[0] + title = ( + first_child.children + if isinstance(getattr(first_child, 'children', None), str) + else ''.join( + c.children if isinstance(getattr(c, 'children', None), str) else '' + for c in h.children + ) + ) slug = title.lower().strip() slug = re.sub(r'[^\w\s-]', '', slug) slug = re.sub(r'[-\s]+', '-', slug) @@ -327,7 +337,7 @@ def get_readme(readme_path): def get_tfref_parts(readme): 'Check if README file is marked, and return current doc.' - m = re.search('(?sm)%s(.*)%s' % (MARK_BEGIN, MARK_END), readme) + m = re.search('(?sm)%s(.*?)%s' % (MARK_BEGIN, MARK_END), readme) if not m: return return {'doc': m.group(1).strip(), 'start': m.start(), 'end': m.end()} @@ -350,7 +360,7 @@ def get_tfref_opts(readme): def get_toc_parts(readme): 'Check if README file is marked, and return current toc.' - t = re.search('(?sm)%s(.*)%s' % (TOC_BEGIN, TOC_END), readme) + t = re.search('(?sm)%s(.*?)%s' % (TOC_BEGIN, TOC_END), readme) if not t: return return {'toc': t.group(1).strip(), 'start': t.start(), 'end': t.end()} @@ -414,7 +424,7 @@ def parse_outputs(basepath, exclude_files=None): except (IOError, OSError): raise SystemExit(f'Cannot open outputs file {shortname}.') for item in _parse(body, enum=OUT_ENUM, re=OUT_RE, template=OUT_TEMPLATE): - description = ''.join(item['description']) + description = (''.join(item['description'])).replace('|', '\\|') sensitive = item['sensitive'] != [] consumers = item['tags'].get('output:consumers', '') yield Output(name=item['name'], description=description, diff --git a/tools/validate_metadata.py b/tools/validate_metadata.py index 2e5f55cc3..c282e6efb 100755 --- a/tools/validate_metadata.py +++ b/tools/validate_metadata.py @@ -43,9 +43,15 @@ class ValidationResult: def _validate(path: Path, validator) -> ValidationResult: - with open(path) as f: + with open(path, encoding="utf-8") as f: metadata = yaml.safe_load(f) + if not isinstance(metadata, dict): + return ValidationResult( + state=State.INVALID, + errors={"$": "Metadata root must be a YAML mapping."}, + ) + errors = { error.json_path: error.message for error in validator.iter_errors(metadata) @@ -70,7 +76,9 @@ def main(dirs: list[str], verbose: bool, failed_only=False) -> int: validator = jsonschema.validators.Draft202012Validator(schema) failed_files = {} - for instance in instances: + for instance in sorted(instances): + if Path(instance).is_symlink(): + continue result = _validate(instance, validator) if result.state == State.OK: if not failed_only: