Cloud Security Wire
AWS Azure GCP RSS
GCP Hardening Guide high

GCP Cloud Build Security: Secrets, Artifact Poisoning, and CI/CD Hardening

Google Cloud Build pipelines routinely leak secrets, run with overprivileged service accounts, and produce unsigned artifacts. This guide covers the five most common Cloud Build security failures — plaintext secrets, over-scoped service accounts, build log exfiltration, unsigned artifacts, and missing audit configuration — with gcloud remediation for each.

By Cloud Security Wire · ·
#GCP#Cloud Build#CI/CD#secrets-management#Secret Manager#Binary Authorization#artifact-signing#supply-chain-security#service-accounts#IAM#audit-logs#SLSA#pipeline-security#google-cloud
High Severity

This issue has been assessed as high severity. Review affected configurations immediately.

Google Cloud Build is one of the most widely used CI/CD platforms in GCP environments, and it has a reliable set of security problems that appear in audit after audit. Over-permissioned service accounts give build pipelines access to production resources they have no business touching. Secrets end up in cloudbuild.yaml environment variables where they appear in plaintext in build logs. Artifacts move from build to deployment without signing or verification. And the audit configuration required to detect a supply chain attack is frequently absent.

None of these are exotic attack paths. They’re predictable outcomes of defaults that optimise for ease of use over security. This guide covers each failure mode with concrete gcloud remediation.

Failure 1: Over-Scoped Cloud Build Service Accounts

By default, Cloud Build uses the built-in Cloud Build service account ([email protected]). This account receives the Editor role on your project automatically — an almost certainly excessive permission set that gives it read/write access to most GCP services in the project.

An attacker who can inject code into a build (through a pull request, a compromised upstream dependency, or a webhook misconfiguration) inherits those permissions. From a Cloud Build job running as the default service account, an attacker can read Secret Manager secrets, write to GCS buckets including production artifact stores, modify Cloud Run services, and query Cloud SQL.

Check what role the default service account holds:

PROJECT_NUMBER=$(gcloud projects describe YOUR_PROJECT --format="value(projectNumber)")

gcloud projects get-iam-policy YOUR_PROJECT \
  --flatten="bindings[].members" \
  --filter="bindings.members:${PROJECT_NUMBER}@cloudbuild.gserviceaccount.com" \
  --format="table(bindings.role)"

Remediation: Create a minimal service account per pipeline

# Create a dedicated build service account
gcloud iam service-accounts create cloud-build-deployer \
  --display-name="Cloud Build Deployer" \
  --project=YOUR_PROJECT

# Grant only what this pipeline actually needs
# Example: deploy to Cloud Run only
gcloud projects add-iam-policy-binding YOUR_PROJECT \
  --member="serviceAccount:cloud-build-deployer@YOUR_PROJECT.iam.gserviceaccount.com" \
  --role="roles/run.developer"

gcloud projects add-iam-policy-binding YOUR_PROJECT \
  --member="serviceAccount:cloud-build-deployer@YOUR_PROJECT.iam.gserviceaccount.com" \
  --role="roles/iam.serviceAccountUser"

# Remove the default Editor binding from the Cloud Build service account
gcloud projects remove-iam-policy-binding YOUR_PROJECT \
  --member="serviceAccount:${PROJECT_NUMBER}@cloudbuild.gserviceaccount.com" \
  --role="roles/editor"

Specify the service account in your build configuration:

# cloudbuild.yaml
serviceAccount: "projects/YOUR_PROJECT/serviceAccounts/cloud-build-deployer@YOUR_PROJECT.iam.gserviceaccount.com"
steps:
  - name: "gcr.io/cloud-builders/gcloud"
    args: ["run", "deploy", "my-service", "--image", "$_IMAGE", "--region", "us-central1"]

Failure 2: Secrets in cloudbuild.yaml Environment Variables

The most common pattern for passing credentials to Cloud Build steps is environment variable substitution directly in cloudbuild.yaml:

# DO NOT DO THIS
steps:
  - name: "gcr.io/cloud-builders/docker"
    env:
      - "DATABASE_URL=postgresql://user:ACTUAL_PASSWORD@host/db"
      - "API_KEY=sk-live-1234567890abcdef"

This approach exposes secrets in:

  • The cloudbuild.yaml file itself (likely in version control)
  • Cloud Build logs, visible to anyone with cloudbuild.builds.get permission
  • The GCS bucket where Cloud Build stores build logs by default

Identify secrets potentially in build logs:

# List recent builds and check for credential patterns in logs
gcloud builds list --limit=20 --project=YOUR_PROJECT --format="value(id,status,createTime)"

# Fetch a specific build's log to audit it
gcloud builds log BUILD_ID --project=YOUR_PROJECT | grep -iE "(password|secret|key|token|credential)"

Remediation: Use Secret Manager with secretEnv

# Store secrets in Secret Manager
echo -n "your-api-key-value" | gcloud secrets create my-api-key \
  --data-file=- \
  --project=YOUR_PROJECT

# Grant Cloud Build service account access to the secret
gcloud secrets add-iam-policy-binding my-api-key \
  --member="serviceAccount:cloud-build-deployer@YOUR_PROJECT.iam.gserviceaccount.com" \
  --role="roles/secretmanager.secretAccessor" \
  --project=YOUR_PROJECT
# cloudbuild.yaml — correct approach
availableSecrets:
  secretManager:
    - versionName: projects/YOUR_PROJECT/secrets/my-api-key/versions/latest
      env: "API_KEY"

steps:
  - name: "gcr.io/cloud-builders/curl"
    secretEnv: ["API_KEY"]
    script: |
      curl -H "Authorization: Bearer $$API_KEY" https://api.example.com/deploy

With secretEnv, the secret value is injected into the step’s environment without appearing in the build log. Cloud Build redacts values sourced from secretEnv from log output.

Failure 3: Build Log Exposure

Cloud Build stores build logs in a GCS bucket under gs://YOUR_PROJECT_cloudbuild/log-BUILD_ID.txt by default. The project-default bucket often has overly permissive IAM. Logs may also be forwarded to Cloud Logging with retention and access policies that don’t match what you’d apply to a bucket holding credential material.

Check the default log bucket’s IAM:

# Find your Cloud Build log bucket
gcloud builds describe BUILD_ID --project=YOUR_PROJECT --format="value(logsBucket)"

# Check who can read it
gsutil iam get gs://YOUR_PROJECT_REGION.cloudbuild-logs.googleusercontent.com

Remediation: Use a dedicated, restricted log bucket

# Create a logging bucket with restricted access
gsutil mb -p YOUR_PROJECT -l us-central1 gs://YOUR_PROJECT-cloudbuild-logs-restricted

# Restrict to specific IAM principals only
gsutil iam set - gs://YOUR_PROJECT-cloudbuild-logs-restricted <<EOF
{
  "bindings": [
    {
      "role": "roles/storage.objectViewer",
      "members": ["group:[email protected]"]
    },
    {
      "role": "roles/storage.objectCreator",
      "members": ["serviceAccount:${PROJECT_NUMBER}@cloudbuild.gserviceaccount.com"]
    }
  ]
}
EOF

# Set a retention policy (90 days)
gsutil retention set 90d gs://YOUR_PROJECT-cloudbuild-logs-restricted

# Configure Cloud Build to use this bucket in triggers
gcloud builds triggers update TRIGGER_ID \
  --log-streaming-option=STREAM_ON \
  --project=YOUR_PROJECT

For most teams, the most effective control is simply preventing allUsers and allAuthenticatedUsers from having any access to the log bucket, which is occasionally set during initial project setup and forgotten.

Failure 4: Unsigned Artifacts and Missing Binary Authorization

Cloud Build typically pushes container images to Artifact Registry or Container Registry. Without artifact signing, there’s no mechanism to ensure that the image deployed to production was actually produced by your CI pipeline rather than a modified version pushed by an attacker who gained write access to the registry.

Binary Authorization enforces that only attested, signed images can be deployed to GKE or Cloud Run. Without it, anyone with write access to the registry can push an image that will deploy without challenge.

Implement image signing with Cloud KMS and Binary Authorization:

# Create a KMS key for signing
gcloud kms keyrings create build-signing \
  --location=global \
  --project=YOUR_PROJECT

gcloud kms keys create artifact-signer \
  --keyring=build-signing \
  --location=global \
  --purpose=asymmetric-signing \
  --default-algorithm=RSA_SIGN_PKCS1_4096_SHA512 \
  --project=YOUR_PROJECT

# Create a Binary Authorization attestor
gcloud container binauthz attestors create build-verified \
  --attestation-authority-note=projects/YOUR_PROJECT/notes/build-verified \
  --attestation-authority-note-project=YOUR_PROJECT \
  --project=YOUR_PROJECT

# Link KMS key to attestor
gcloud container binauthz attestors public-keys add \
  --attestor=build-verified \
  --keyversion-project=YOUR_PROJECT \
  --keyversion-location=global \
  --keyversion-keyring=build-signing \
  --keyversion-key=artifact-signer \
  --keyversion-version=1 \
  --project=YOUR_PROJECT

Add signing step to cloudbuild.yaml:

steps:
  - name: "gcr.io/cloud-builders/docker"
    args: ["build", "-t", "us-central1-docker.pkg.dev/YOUR_PROJECT/my-repo/app:$SHORT_SHA", "."]

  - name: "gcr.io/cloud-builders/docker"
    args: ["push", "us-central1-docker.pkg.dev/YOUR_PROJECT/my-repo/app:$SHORT_SHA"]

  # Get image digest after push
  - name: "gcr.io/cloud-builders/gcloud"
    entrypoint: "bash"
    args:
      - "-c"
      - |
        IMAGE_DIGEST=$(gcloud artifacts docker images describe \
          us-central1-docker.pkg.dev/YOUR_PROJECT/my-repo/app:$SHORT_SHA \
          --format="value(image_summary.digest)")
        
        gcloud container binauthz attestations sign-and-create \
          --artifact-url="us-central1-docker.pkg.dev/YOUR_PROJECT/my-repo/app@${IMAGE_DIGEST}" \
          --attestor=build-verified \
          --attestor-project=YOUR_PROJECT \
          --keyversion-project=YOUR_PROJECT \
          --keyversion-location=global \
          --keyversion-keyring=build-signing \
          --keyversion-key=artifact-signer \
          --keyversion-version=1

Enforce Binary Authorization policy on Cloud Run:

gcloud run services update my-service \
  --binary-authorization=default \
  --region=us-central1 \
  --project=YOUR_PROJECT

Configure the Binary Authorization policy to require the build-verified attestation for all deployments.

Failure 5: Missing Audit Configuration

Detecting a supply chain attack against your CI/CD pipeline requires that Cloud Build operations appear in Cloud Audit Logs, that log-based alerting exists for anomalous build activity, and that the audit log retention is sufficient for post-incident investigation.

Cloud Build generates audit log entries for build creation, completion, and configuration changes — but data access auditing (who viewed logs, who triggered builds) is typically disabled by default.

Enable data access audit logs for Cloud Build:

# Get current policy
gcloud projects get-iam-policy YOUR_PROJECT --format=json > /tmp/policy.json

# Add data access audit logging for Cloud Build
cat > /tmp/audit-update.json <<EOF
{
  "auditConfigs": [
    {
      "service": "cloudbuild.googleapis.com",
      "auditLogConfigs": [
        {"logType": "ADMIN_READ"},
        {"logType": "DATA_READ"},
        {"logType": "DATA_WRITE"}
      ]
    },
    {
      "service": "artifactregistry.googleapis.com",
      "auditLogConfigs": [
        {"logType": "ADMIN_READ"},
        {"logType": "DATA_READ"},
        {"logType": "DATA_WRITE"}
      ]
    }
  ]
}
EOF

gcloud projects set-iam-policy YOUR_PROJECT /tmp/policy-with-audit.json

Create log-based alerts for high-risk build activity:

# Alert on builds triggered from unexpected sources
gcloud logging metrics create unusual-build-source \
  --description="Builds triggered from non-approved sources" \
  --log-filter='resource.type="build" AND protoPayload.methodName="google.devtools.cloudbuild.v1.CloudBuild.CreateBuild" AND NOT protoPayload.requestMetadata.callerIp=~"10\."'

# Alert on service account key creation (often done post-compromise to maintain access)
gcloud logging metrics create build-sa-key-creation \
  --description="Service account key created during build context" \
  --log-filter='protoPayload.methodName="google.iam.admin.v1.CreateServiceAccountKey" AND protoPayload.authenticationInfo.principalEmail=~"cloudbuild"'

Build Trigger Security: Webhook and Pull Request Configuration

A frequently overlooked attack surface is Cloud Build trigger configuration. Builds triggered on pull requests from forked repositories execute build steps — including those that might access secrets or push to registries — in the context of unreviewed, potentially attacker-controlled code.

# List all triggers and check their configurations
gcloud builds triggers list --project=YOUR_PROJECT \
  --format="table(name,github.pullRequest.branch,github.push.branch,disabled)"

# For triggers on pull requests, ensure comment control is required
gcloud builds triggers describe TRIGGER_NAME --project=YOUR_PROJECT \
  --format="json" | jq ".github.pullRequest.commentControl"

For GitHub-connected triggers, set commentControl to COMMENTS_ENABLED_FOR_EXTERNAL_CONTRIBUTORS_ONLY. This prevents builds from running automatically on PRs from fork repositories unless a maintainer explicitly approves with a comment — a critical control for open source or mixed public/private repositories.

Quick Audit Checklist

Run these checks against any GCP project using Cloud Build:

PROJECT=YOUR_PROJECT
PROJECT_NUMBER=$(gcloud projects describe $PROJECT --format="value(projectNumber)")

echo "=== Default Cloud Build SA permissions ==="
gcloud projects get-iam-policy $PROJECT \
  --flatten="bindings[].members" \
  --filter="bindings.members:${PROJECT_NUMBER}@cloudbuild.gserviceaccount.com" \
  --format="table(bindings.role)"

echo "=== Cloud Build triggers (check for PRs from forks) ==="
gcloud builds triggers list --project=$PROJECT \
  --format="table(name,github.pullRequest,github.push)"

echo "=== Binary Authorization policy ==="
gcloud container binauthz policy export --project=$PROJECT 2>/dev/null || echo "No policy configured"

echo "=== Artifact Registry write permissions (check for allUsers) ==="
gcloud artifacts repositories list --project=$PROJECT \
  --format="value(name)" | while read REPO; do
  gcloud artifacts repositories get-iam-policy $REPO \
    --location=us-central1 --project=$PROJECT 2>/dev/null | grep -i "allUsers" && echo "PROBLEM: $REPO has allUsers"
done

The five failures covered here — over-scoped service accounts, plaintext secrets, exposed build logs, unsigned artifacts, and missing audit logs — collectively represent the standard attack surface for supply chain compromise via CI/CD. Each is independently exploitable; remediating all five closes the most common paths used in documented build pipeline compromises.

← All Analysis Subscribe via RSS