Summary
Discovered while verifying #50 (Azure Logic App plaintext secret). The Azure side has been fixed via per-workflow user-assigned managed identities + a get-secret first-action (see terraform/modules/compute/azure/container-apps/scheduled-tasks.tf:4-15 for the security model and lines 198/303/398 for the runtime-resolved Authorization = "Bearer @{body('get-secret')['value']}" headers).
The GCP equivalent has the same plaintext-interpolation anti-pattern that #50 flagged on Azure:
terraform/modules/compute/gcp/cloud-run/main.tf:273 — Cloud Run service-to-Cloud-Run scheduler call.
terraform/modules/compute/gcp/cloud-run/main.tf:326 — second call site, same shape.
terraform/modules/compute/gcp/gke/main.tf:676 — GKE-flavoured equivalent.
Each of those reads:
"Authorization" = "Bearer ${var.scheduled_task_secret}"
The plaintext therefore lives in:
- The Cloud Scheduler / Cloud Run job definition stored in GCP.
- Terraform state (any
var.* value flowing into a resource attribute is captured in state).
terraform plan / terraform show output captured in CI logs and operator screens.
Expected behaviour
Match the Azure model: the Cloud Scheduler / Cloud Run resource pulls the secret from GCP Secret Manager at runtime so the plaintext never lands in the resource definition or Terraform state.
Proposed fix
Two GCP-native paths, pick whichever is simpler:
- Cloud Scheduler with OIDC + Secret Manager: have Cloud Scheduler authenticate to Cloud Run via OIDC (already supported by the SDK) and drop the bespoke
Bearer header entirely. The receiving service validates the OIDC token instead of the shared secret. Eliminates the secret outright.
- Secret Manager API call from the scheduler body: keep the
Bearer auth scheme but have the scheduler/job fetch the secret at runtime (via Secret Manager REST + the runtime SA's identity), analogous to Azure's get-secret action.
Path 1 is the cleaner endpoint — modern GCP best practice for service-to-service auth. Path 2 mirrors the Azure migration and may be cheaper to implement if other callers still rely on the shared-secret scheme.
Steps to reproduce
cd terraform/environments/gcp
terraform plan -out tfplan
terraform show -json tfplan | jq '.. | select(.values.headers? != null) | .values.headers'
# Observe the Bearer token's plaintext value in the rendered headers map.
Or post-apply: gcloud scheduler jobs describe <job> --format=yaml returns the job definition with the plaintext header value.
References
Severity
Medium — same shared-secret as the Azure-side and Container App caller, exposed via a different surface. Both other surfaces are now closed; GCP is the remaining gap.
Effort
Medium — Cloud Scheduler + Secret Manager wiring. Larger if path 1 (OIDC migration) is chosen because the receiving service needs to verify OIDC tokens instead of the shared secret; smaller if path 2 (mirror Azure) is chosen because the Cloud Run side stays unchanged.
Summary
Discovered while verifying #50 (Azure Logic App plaintext secret). The Azure side has been fixed via per-workflow user-assigned managed identities + a
get-secretfirst-action (seeterraform/modules/compute/azure/container-apps/scheduled-tasks.tf:4-15for the security model and lines 198/303/398 for the runtime-resolvedAuthorization = "Bearer @{body('get-secret')['value']}"headers).The GCP equivalent has the same plaintext-interpolation anti-pattern that #50 flagged on Azure:
terraform/modules/compute/gcp/cloud-run/main.tf:273— Cloud Run service-to-Cloud-Run scheduler call.terraform/modules/compute/gcp/cloud-run/main.tf:326— second call site, same shape.terraform/modules/compute/gcp/gke/main.tf:676— GKE-flavoured equivalent.Each of those reads:
The plaintext therefore lives in:
var.*value flowing into a resource attribute is captured in state).terraform plan/terraform showoutput captured in CI logs and operator screens.Expected behaviour
Match the Azure model: the Cloud Scheduler / Cloud Run resource pulls the secret from GCP Secret Manager at runtime so the plaintext never lands in the resource definition or Terraform state.
Proposed fix
Two GCP-native paths, pick whichever is simpler:
Bearerheader entirely. The receiving service validates the OIDC token instead of the shared secret. Eliminates the secret outright.Bearerauth scheme but have the scheduler/job fetch the secret at runtime (via Secret Manager REST + the runtime SA's identity), analogous to Azure'sget-secretaction.Path 1 is the cleaner endpoint — modern GCP best practice for service-to-service auth. Path 2 mirrors the Azure migration and may be cheaper to implement if other callers still rely on the shared-secret scheme.
Steps to reproduce
Or post-apply:
gcloud scheduler jobs describe <job> --format=yamlreturns the job definition with the plaintext header value.References
terraform/modules/compute/azure/container-apps/scheduled-tasks.tf:4-15terraform/modules/compute/gcp/cloud-run/main.tf:273,326terraform/modules/compute/gcp/gke/main.tf:676Severity
Medium — same shared-secret as the Azure-side and Container App caller, exposed via a different surface. Both other surfaces are now closed; GCP is the remaining gap.
Effort
Medium — Cloud Scheduler + Secret Manager wiring. Larger if path 1 (OIDC migration) is chosen because the receiving service needs to verify OIDC tokens instead of the shared secret; smaller if path 2 (mirror Azure) is chosen because the Cloud Run side stays unchanged.