Affected components
The problem
When creating folder-level logging sinks targeting a log bucket in a different project using the CFF folder module, log routing fails with the error log_bucket_not_found.
This is caused by a latent mismatch in the automatic project-level IAM binding (google_project_iam_member.bucket_sinks_binding) generated by the module:
The module generates a CEL condition that matches the target resource name against the Project ID (string).
However, Google Cloud's IAM CEL condition evaluator evaluates resource.name using the numeric Project Number.
As a result, the condition evaluates to false, the permission is denied, and the sink is unable to write logs to the target bucket.
Release version
v56.1.0
Terraform version
Terraform v1.14.8
Steps to reproduce
Instaniate CFF folder module with a folder log sink pointing to a log bucket:
hcl
module "folder" {
source = "git::https://github.com/GoogleCloudPlatform/cloud-foundation-fabric//modules/folder"
id = "folders/123456789"
folder_create = false
logging_sinks = {
my-sink = {
destination = "logging.googleapis.com/projects/my-observe-project/locations/us-central1/buckets/my-log-bucket"
type = "logging"
}
}
}
Apply the HCL.
Check the folder's logging sinks in the GCP Console or run gcloud logging read "resource.type="logging_sink" AND severity=ERROR" --folder=123456789. You will find the routing error log_bucket_not_found.
Expected behavior
Folder's gcp-sa-logging agent should be able to write logs to log sink logging bucket and shouldnt get IAM issue.
Actual behavior
In modules/folder/logging.tf, the project-level IAM binding for logging bucket sinks is defined as:
resource "google_project_iam_member" "bucket_sinks_binding" {
for_each = local.sink_bindings["logging"]
project = split("/", each.value.destination)[1]
role = "roles/logging.bucketWriter"
member = google_logging_folder_sink.sink[each.key].writer_identity
condition {
title = "${each.key} bucket writer"
description = "Grants bucketWriter to serviceAccount:${google_logging_folder_sink.sink[each.key].writer_identity} used by log sink ${each.key} on folders/${local.folder_id}"
expression = "resource.name.endsWith('projects/${split("/", each.value.destination)[1]}/locations/${split("/", each.value.destination)[3]}/buckets/${split("/", each.value.destination)[5]}')"
}
}
The Bug:
The expression uses split("/", each.value.destination)[1], which extracts the Project ID string (e.g., hhs-observe-p-mxty) because the destination attribute is passed in from Terraform resource IDs.
This generates a CEL condition: resource.name.endsWith('projects/PROJECT_ID/locations/LOCATION/buckets/BUCKET_ID')
During live log routing, GCP IAM evaluates this condition. However, GCP's IAM policy engine populates resource.name using the numeric Project Number (e.g., 650484525631) instead of the Project ID.
Because projects/650484525631/... does not end with projects/PROJECT_ID/..., the condition evaluates to false, causing log routing to fail silently or log a log_bucket_not_found configuration error.
Additional context
Proposed Fix
The CEL expression should be updated to be project-number-independent or robust enough to support both formats.
Since the target bucket's location and bucket ID are the only parts needed to uniquely identify the destination bucket on the project, the expression can be simplified using endsWith to match only the trailing path:
hcl
expression = "resource.name.endsWith('locations/${split("/", each.value.destination)[3]}/buckets/${split("/", each.value.destination)[5]}')"
Alternatively, the module can resolve the project number from the project resource (if available in context) or allow it to be passed in, but matching on the trailing location and bucket ID path is the cleanest and most robust project-number-independent solution.
Affected components
The problem
When creating folder-level logging sinks targeting a log bucket in a different project using the CFF folder module, log routing fails with the error log_bucket_not_found.
This is caused by a latent mismatch in the automatic project-level IAM binding (google_project_iam_member.bucket_sinks_binding) generated by the module:
The module generates a CEL condition that matches the target resource name against the Project ID (string).
However, Google Cloud's IAM CEL condition evaluator evaluates resource.name using the numeric Project Number.
As a result, the condition evaluates to false, the permission is denied, and the sink is unable to write logs to the target bucket.
Release version
v56.1.0
Terraform version
Terraform v1.14.8
Steps to reproduce
Instaniate CFF folder module with a folder log sink pointing to a log bucket:
hcl
Apply the HCL.
Check the folder's logging sinks in the GCP Console or run gcloud logging read "resource.type="logging_sink" AND severity=ERROR" --folder=123456789. You will find the routing error log_bucket_not_found.
Expected behavior
Folder's gcp-sa-logging agent should be able to write logs to log sink logging bucket and shouldnt get IAM issue.
Actual behavior
In modules/folder/logging.tf, the project-level IAM binding for logging bucket sinks is defined as:
The Bug:
The expression uses split("/", each.value.destination)[1], which extracts the Project ID string (e.g., hhs-observe-p-mxty) because the destination attribute is passed in from Terraform resource IDs.
This generates a CEL condition: resource.name.endsWith('projects/PROJECT_ID/locations/LOCATION/buckets/BUCKET_ID')
During live log routing, GCP IAM evaluates this condition. However, GCP's IAM policy engine populates resource.name using the numeric Project Number (e.g., 650484525631) instead of the Project ID.
Because projects/650484525631/... does not end with projects/PROJECT_ID/..., the condition evaluates to false, causing log routing to fail silently or log a log_bucket_not_found configuration error.
Additional context
Proposed Fix
The CEL expression should be updated to be project-number-independent or robust enough to support both formats.
Since the target bucket's location and bucket ID are the only parts needed to uniquely identify the destination bucket on the project, the expression can be simplified using endsWith to match only the trailing path:
hcl
expression = "resource.name.endsWith('locations/${split("/", each.value.destination)[3]}/buckets/${split("/", each.value.destination)[5]}')"
Alternatively, the module can resolve the project number from the project resource (if available in context) or allow it to be passed in, but matching on the trailing location and bucket ID path is the cleanest and most robust project-number-independent solution.