Chapter 23
Cloud Build
Scope. This chapter owns the build: Cloud Build's architecture, triggers, steps, workers and private pools, the build service account and least-privilege design, private network access, secrets in a build, artifact creation, in-build testing, and provenance generation. Source and pre-build scanning are Chapter 22; artifact storage is Chapter 24; deployment and provenance verification are Chapter 25; attestation authoring and the SLSA framework in depth are Chapter 37. §8.33 cites this chapter for golden image pipelines. Prerequisites. Chapter 4 (§4.7 impersonation, §4.9 service account keys), Chapter 13 (§13.7 consuming secrets), Chapter 22, Chapter 24 for where artifacts land. Verified against. Google Cloud console and API surface as of 2026-09, Cloud SDK 583.0.0,
hashicorp/googleprovider 8.x; see sources at end.
A build system is a machine that takes source you trust and produces an artifact you will run in production, using an identity that can usually do far more than build. That last clause is where the security work is. A Cloud Build service account is, in most estates, the single most powerful non-human principal in the project — it can push images, write to buckets, deploy services, and frequently modify IAM — and it executes arbitrary code from a repository on every commit.
Which means the build identity is the thing to design first, and Cloud Build's own default makes that harder than it should be. Google's current wording is that "Depending on your organization's settings, Cloud Build may use the Compute Engine default service account or the legacy Cloud Build service account to execute builds on your behalf." Both defaults are shared across the project, both accumulate grants, and neither belongs in a production pipeline. Google's recommendation is the same as this book's: "we recommend that you specify your own service account to run your builds."
The second structural decision is where builds run. The default worker is a Google-managed machine on a Google-managed network, which is fine for a build that clones a public repository and pushes an image, and unworkable for one that must reach a private GKE control plane, a Cloud SQL private IP, or anything inside a service perimeter (Chapter 20). Private pools are the answer, and they are worth adopting before you need them, because retrofitting network access into a pipeline is a much larger change than starting with a pool.
23.1 Cloud Build Architecture §
Cloud Build executes a build — an ordered list of steps, each a container image run against a shared /workspace volume — on a worker, as a service account, producing logs and optionally artifacts.
Five objects make up everything you configure:
| Object | What it is |
|---|---|
| Build config | cloudbuild.yaml: steps, options, substitutions, artifacts, timeout |
| Trigger | What starts a build, and with which config and identity |
| Worker pool | Where builds execute; default pool or a private pool |
| Build service account | The identity every step runs as |
| Connection and repository | The link to the source host, for 2nd-gen triggers (§22.1) |
Steps share a filesystem and nothing else. Each step is a fresh container; /workspace persists between them, environment variables do not unless declared. That isolation is useful — a step that needs a credential can be the only step that receives it (§23.9).
Builds are regional. --region on submit, on triggers, and on worker pools selects where the build runs and where its resources live, which matters for both latency and data residency. Pick one region per estate and set it in every trigger rather than relying on a default.
The console path is Cloud Build → History, Triggers, Repositories, and Worker pools, with build detail pages showing each step's log inline.
Judgment. Keep cloudbuild.yaml in the repository it builds, and keep it short. A build config that has grown to forty steps is a shell script in YAML, and it is not reviewable. Push logic into scripts the config invokes, so it can be tested.
Pitfall. Anything a step writes to /workspace is visible to every later step, including a credential written to a file "temporarily". Steps are isolated processes, not isolated trust domains.
23.2 Build Triggers §
A trigger connects a source event to a build config and an identity. It is where the pipeline's trust boundary is configured, and most misconfiguration lives here.
Triggers exist for GitHub, GitLab, Bitbucket in two flavors, Pub/Sub, webhook, manual, and the legacy Cloud Source Repositories. The 2nd-generation path creates a connection first, then a repository link, then a trigger that references it by --repository (§22.1).
gcloud builds triggers create github \
--name=trg-billing-main \
--region=us-central1 \
--repository=projects/rc-saas-shared-cicd-01/locations/us-central1/\
connections/gh-rc-saas/repositories/billing \
--branch-pattern="^main$" \
--build-config=cloudbuild.yaml \
--service-account=projects/rc-saas-shared-cicd-01/serviceAccounts/\
sa-build-billing@rc-saas-shared-cicd-01.iam.gserviceaccount.com \
--included-files="src/**,cloudbuild.yaml" \
--description="Build and publish billing on merge to main."
--service-account on the trigger is the most important flag in this chapter. It overrides the project default and binds this pipeline to an identity scoped to what this pipeline does. The caller creating the trigger needs iam.serviceAccounts.actAs on that account, which is the intended control: creating a trigger that runs as a powerful identity requires permission to use that identity.
--require-approval is the control for untrusted input. A pull-request trigger without it builds a stranger's code with your credentials the moment they open a pull request. With it, a human with roles/cloudbuild.builds.approver must approve before the build runs.
Narrow the trigger with --included-files and --ignored-files. A build that fires on every commit to every path is both slow and a larger attack surface — a documentation change should not run a pipeline holding deploy permissions.
Pitfall. --pull-request-pattern with --comment-control=COMMENTS_DISABLED and no approval requirement is the exact configuration that lets a fork author run arbitrary code as your build identity. The default COMMENTS_ENABLED requires a comment from a collaborator; do not change it without adding approval.
23.3 Build Steps §
A step is a container image plus arguments, executed in order. The image is an input to your build with the same trust requirements as a dependency.
The fields that matter for security:
| Field | Use |
|---|---|
name | The builder image — pin it by digest |
entrypoint, args, script | What runs; script is the readable form for shell |
env, secretEnv | Plain and secret environment variables (§23.9) |
dir | Working directory under /workspace |
waitFor | Parallelism and ordering; ['-'] starts immediately |
allowFailure, allowExitCodes | Non-fatal steps — use sparingly |
steps:
- name: 'golang@sha256:DIGEST'
id: test
script: |
go test ./...
- name: 'docker@sha256:DIGEST'
id: build
waitFor: ['test']
args:
- build
- '-t'
- 'us-central1-docker.pkg.dev/$PROJECT_ID/prod-docker/billing:$SHORT_SHA'
- '.'
Pin builder images by digest, not by tag. Google publishes a set of prebuilt cloud-builders images and the ecosystem publishes thousands more; every one of them is code that runs with your build identity. A tag is a mutable pointer, so a build "pinned" to :latest or even :v2 executes whatever the publisher pushed this morning.
Substitutions are string interpolation and they are injectable. Built-in substitutions such as $SHORT_SHA, $BRANCH_NAME, and $_USER_DEFINED are substituted into args and script before execution. A branch name is attacker-controlled in a fork workflow, so never interpolate one into a shell command without quoting.
Pitfall. allowFailure: true on a security-scanning step converts a gate into a decoration. If a scan step is allowed to fail, delete it and be honest, or fix why it is flaky.
23.4 Build Workers §
A worker is the machine a build runs on. Cloud Build offers a default pool of Google-managed workers and private pools you control (§23.5).
The default pool is a shared, Google-managed environment. Builds get a fresh worker, an internet-routable egress path, and no connectivity to your VPCs. --machine-type and --disk-size on gcloud builds submit size it.
Three properties of the default pool decide whether you can use it:
| Property | Consequence |
|---|---|
| No VPC connectivity | Cannot reach private GKE control planes, private Cloud SQL, or internal services |
| Google-managed egress IPs | Cannot be allow-listed by a partner or a firewall rule |
| Outside your perimeter | Restricted-service calls from the build are denied (§20.10) |
Build isolation is per build, not per project. Each build gets a fresh worker and the workspace is discarded afterward, so a build does not inherit another build's filesystem. It does inherit the same service account, which is why §23.7 exists.
Judgment. Use the default pool for building and testing code that needs nothing private, and a private pool for anything that touches your network. Splitting the pipeline — a public-pool build stage and a private-pool deploy stage — is usually cheaper than putting everything in a private pool.
Pitfall. The default pool has open egress. A compromised dependency in a build step can exfiltrate the source, the build identity's tokens, and anything in /workspace, and nothing in the default pool constrains it. --no-public-egress on a private pool is the only mechanism that does.
23.5 Private Pools §
A private pool is a set of Cloud Build workers running in a Google-managed network that is VPC-peered to yours, so builds can reach private resources.
gcloud builds worker-pools create pool-us-central1-prod \
--region=us-central1 \
--project=rc-saas-shared-cicd-01 \
--peered-network=projects/rc-saas-shared-net-01/global/networks/vpc-prod-global \
--peered-network-ip-range=/24 \
--worker-machine-type=e2-standard-4 \
--worker-disk-size=100 \
--no-public-egress
--no-public-egress is the security flag. Without it the pool has internet access; with it, workers have no external route, and a compromised build step cannot post your source to an external host. Everything the build needs must then be reachable privately: Artifact Registry through Private Google Access (§5.17), dependencies through remote repositories (§24.1), and any external fetch through a proxy you control.
--peered-network-ip-range consumes address space from your plan. A /24 per pool, allocated from the CIDR plan rather than invented, and it cannot overlap anything else peered to that VPC.
Private pools are what make a build work inside a service perimeter. A pool peered to a network inside the perimeter needs no ingress rule, which is materially better than granting an external CI runner standing access (§20.10).
Constrain which pools a build may use. constraints/cloudbuild.allowedWorkerPools restricts builds to a named list, which prevents a trigger from quietly falling back to the default pool and its open egress.
Pitfall. A no-public-egress pool breaks every build that fetches from a public package index, and the failure looks like a network flake. Plan the dependency path — remote repositories in Artifact Registry, or a vendored lockfile — in the same change that creates the pool.
23.6 Build Service Accounts §
The build service account is the identity every step runs as, and choosing it explicitly is the single highest-value change in this chapter.
Three candidates, and only one is correct:
| Identity | Form | Verdict |
|---|---|---|
| Legacy Cloud Build service account | PROJECT_NUMBER@cloudbuild.gserviceaccount.com | Legacy; cannot generate ID tokens |
| Compute Engine default service account | PROJECT_NUMBER-compute@developer.gserviceaccount.com | Shared with every VM in the project; never |
| A dedicated account per pipeline | sa-build-<app>@rc-saas-shared-cicd-01.iam.gserviceaccount.com | The only acceptable choice |
Google's own guidance is explicit — "As a best practice, we recommend that you specify your own service account to run your builds" — and there is a hard functional reason too: "If you need to authenticate between services using an ID token, you must run your builds with a user-specified service account. You can't use the legacy Cloud Build service account to generate ID tokens."
Resolve what your project currently uses before changing anything:
gcloud builds get-default-service-account \
--project=rc-saas-shared-cicd-01 --region=us-central1
Five organization policy constraints govern this, and they are how the choice is enforced rather than requested:
constraints/cloudbuild.disableCreateDefaultServiceAccount— stop the default account being created for new projectsconstraints/cloudbuild.useBuildServiceAccountandconstraints/cloudbuild.useComputeServiceAccount— control which default applies where one is usedconstraints/cloudbuild.allowedWorkerPools— restrict execution location (§23.5)constraints/cloudbuild.allowedIntegrations— restrict which source hosts may be connected
The config field is serviceAccount, and Google notes a limitation worth knowing: "You can't specify the legacy Cloud Build service account in this field."
Pitfall. Switching a pipeline to a dedicated account breaks it, because the default account had grants nobody documented. Run the new account in a non-production trigger first and read the permission denials; that list is the least-privilege specification you were missing.
23.7 Least-Privilege Builds §
A least-privilege build holds exactly the permissions its steps need, for exactly the resources they touch, and holds them nowhere else.
Start from what a build actually does, and grant per resource, not per project:
| Build action | Role | Scope |
|---|---|---|
| Push an image | roles/artifactregistry.writer | The one repository (§24.4) |
| Read a secret | roles/secretmanager.secretAccessor | The one secret version (§13.7) |
| Write logs | roles/logging.logWriter | The build's project |
| Read source | Connection-level, via the repository link | The one repository |
What a build service account must never hold: any basic role, roles/iam.serviceAccountKeyAdmin, project-level roles/iam.serviceAccountUser, or the deploy permissions of Chapter 25. §25.13 makes the last one a rule: the identity that builds is not the identity that deploys.
Separate build and deploy accounts even when the same pipeline does both. A build step producing an artifact runs as sa-build-billing; a deploy step runs as sa-deploy-prod and is reached through Cloud Deploy (§25.1) rather than by attaching both roles to one identity. A single identity that can both write an image and deploy it can ship anything it likes with no gate between.
Give each pipeline its own account. Shared build identities accumulate the union of every pipeline's permissions, and the least important pipeline then holds the most important pipeline's access.
Scope the impersonation chain. Where a build must act as another identity, grant iam.serviceAccounts.getAccessToken on the specific target (§4.7) and log it; never grant project-level service account user.
Pitfall. Permissions granted to a build for a one-off migration are never removed. Time-bound them with an IAM condition, or put them behind Privileged Access Manager, so the grant expires without anyone having to remember.
23.8 Private Network Access §
Private network access means a build reaching resources that have no public endpoint — a private GKE control plane, a Cloud SQL private IP, an internal load balancer, an on-premises service over Interconnect.
It requires a private pool (§23.5). There is no way to give a default-pool build a route into your VPC, and workarounds that expose the resource publicly for the duration of a build are worse than the problem.
Once peered, the build is a client on your network and every ordinary control applies:
- Firewall rules must permit the pool's peered range to the target (§5.13). The pool range is the source, and it should be allowed to reach exactly the ports it needs.
- Private Google Access (§5.17) governs whether the build can reach Google APIs without an external route, which matters immediately once
--no-public-egressis set. - Cloud DNS private zones must resolve for the peered network, or the build sees a name it cannot resolve.
A build reaching a database is a design smell worth resisting. Migration steps that connect to a production database from a build give the build identity data access, which is a much larger grant than artifact publication. Prefer a migration job run by the deployment (§25.5) with its own identity.
Pitfall. VPC peering is not transitive. A private pool peered to vpc-prod-global cannot reach resources in a network that is itself only peered to that VPC. Builds that need a second network need a second pool, or the target must move.
23.9 Secret Manager Integration §
Cloud Build reads secrets from Secret Manager at build time and exposes them to named steps as environment variables. §13.7 owns the consumption model; this is the Cloud Build surface.
The declaration is availableSecrets, and the consumption is secretEnv per step:
availableSecrets:
secretManager:
- versionName: projects/rc-saas-shared-sec-01/secrets/billing-prod-npm-token/versions/latest
env: 'NPM_TOKEN'
steps:
- name: 'node@sha256:DIGEST'
id: install
entrypoint: 'bash'
args: ['-c', 'echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > .npmrc && npm ci']
secretEnv: ['NPM_TOKEN']
Only steps that list the secret in secretEnv receive it. That is the whole security value: a ten-step build where one step needs a registry token gives the token to one container, and the other nine cannot read it from the environment.
Reference a pinned version, not latest, for anything that matters. latest resolves at build time, so a rotation mid-pipeline produces two builds with two different secret values and no record of which was which. §13.5 owns the rotation model.
Grant roles/secretmanager.secretAccessor on the secret, never on the project. The build service account should be able to read the secrets its pipeline declares and no others, and the secrets live in rc-saas-shared-sec-01 (§2.21) while the build runs elsewhere — so this is a cross-project resource-level grant.
Pitfall. A secret in an environment variable is visible to every process in that step's container and to anything that prints the environment. Build logs are the usual leak: a step running with set -x, or a tool that echoes its configuration, writes the secret into Cloud Logging where it inherits the log's retention and readership (§17.6).
23.10 Artifact Creation §
A build's output is an artifact, and Cloud Build's artifacts block declares what to publish and where.
Four artifact kinds are supported — container images, Cloud Storage objects, and Maven, npm, and Python packages — and declaring them rather than pushing by hand is what makes provenance work (§23.13):
images:
- 'us-central1-docker.pkg.dev/$PROJECT_ID/prod-docker/billing:$SHORT_SHA'
artifacts:
objects:
location: 'gs://rc-saas-shared-log-archive-01/build-reports/$BUILD_ID/'
paths: ['reports/*.sarif']
Tag with the commit, always, and never only with a moving tag. $SHORT_SHA produces an immutable, traceable name; :latest produces an artifact nobody can trace back to a source revision. §24.5 makes immutable tags a registry-level enforcement of the same rule.
Everything downstream should reference the digest. The tag is for humans; the deployment (§25.5) and the Binary Authorization policy (§25.10) reference IMAGE@sha256:..., because a digest is the only identifier that cannot be repointed.
Publish to a repository the build can write and cannot delete from. roles/artifactregistry.writer on the target repository lets the build push; it should not hold roles/artifactregistry.repoAdmin, which would let a compromised build erase the evidence of what it published.
Pitfall. An image pushed by a docker push inside a step, rather than declared in images, is not recorded as a build artifact — so it gets no provenance and does not appear in the build's output. The two paths look identical in the logs and diverge completely in what they produce.
23.11 Automated Testing §
Tests in a build are a security control when they gate the artifact, and only then.
Order the pipeline so that cheap failures happen first: format and lint, unit tests, the security scans of §23.12, then the image build, then integration tests against the built artifact. A build that produces an image before running unit tests has wasted the isolation the pipeline provides.
Tests that need credentials need the §23.9 discipline. An integration test against a real dependency requires a secret, and that secret should be a test-environment credential in a separate project, never a production one. The most common accidental production write in an estate comes from an integration test pointed at the wrong environment.
waitFor gives you parallelism without losing the gate. Independent test suites can run concurrently with waitFor: ['-'] while the image build still waits on all of them, which keeps the pipeline fast without letting an artifact escape an unfinished check.
Publish test results as artifacts (§23.10) so a failure is diagnosable from the build record rather than from a log nobody kept. Reports in a Cloud Storage path keyed by $BUILD_ID are the cheapest version of this.
Pitfall. Tests that pass because they were skipped are the most common silent gate failure. A test runner that exits zero when it matched no tests will report success for a build where the test directory was mis-mounted. Assert on the test count, not only on the exit code.
23.12 Security Testing §
Security testing in a build is the enforcement point for the scanning classes Chapter 22 introduced, plus the artifact-level scan that only exists once an image is built.
On-demand scanning runs in a step and is generally available:
gcloud artifacts docker images scan \
us-central1-docker.pkg.dev/rc-saas-shared-art-01/dev-docker/billing:$SHORT_SHA \
--location=us
Scan everything by default and subtract deliberately. Google's help states that "scanning for all package types is now the default" and that --additional-package-types is deprecated for that reason; --skip-package-types is the current flag and it only ever narrows coverage. Reach for it to shorten a scan, never to make a finding go away.
Four checks belong in every production build, each failing the build rather than warning:
| Check | Catches |
|---|---|
| Dependency scan of the manifest | Known-vulnerable direct dependencies (§22.11) |
| Image scan of the built artifact | Vulnerabilities from the base image |
| Static analysis | Source defects (§22.10) |
| Infrastructure plan validation | Policy violations before apply (§22.12) |
Fail on severity, not on count. A threshold of "no new CRITICAL or HIGH in a direct dependency" is enforceable; "zero vulnerabilities" is not, and a build gate nobody can satisfy gets bypassed within a month.
Pitfall. A scan step that runs after the image is pushed protects nothing — the artifact is already available to anything that can pull it. Scan the local image before the push, or push to a quarantine repository and promote only on a clean result (§24.7).
23.13 Build Provenance §
Build provenance is the signed, machine-readable record of how an artifact was produced. Google's definition: "Build provenance is a collection of verifiable data about a build. Provenance metadata includes details such as the digests of the built images, the input source locations, the build arguments, and the build duration."
It is generated by Cloud Build, not by you, and it is emitted as in-toto Statement documents carrying SLSA predicates.
One condition governs whether you get it at all: "Cloud Build only generates build provenance for artifacts stored in Artifact Registry." An image pushed anywhere else has no provenance, which is one more reason the registry choice in Chapter 24 is not incidental.
Read it back with the registry command:
gcloud artifacts docker images describe \
us-central1-docker.pkg.dev/rc-saas-shared-art-01/prod-docker/billing@sha256:DIGEST \
--show-provenance --format=json
Provenance survives cleanup policies but not deletion. Google states that "Repository attachments, including build provenance, aren't subject to cleanup policies. Instead, attachments are deleted when the image they are attached to is deleted." So an aggressive cleanup policy (§24.12) that removes an image also removes the record of how it was built — which matters if you need to answer questions about something that shipped last quarter.
This section generates; §25.12 verifies. The provenance is only a control when something checks it before deployment, and that check is a deployment-time gate.
Pitfall. Provenance describes the build, not the source's trustworthiness. It faithfully records that a compromised commit was built from a named repository at a named revision. Provenance plus branch protection (§22.6) is a control; provenance alone is a receipt.
23.14 SLSA §
SLSA is a framework of levels describing how resistant a build pipeline is to tampering. Chapter 37 owns it in depth; this section states what Cloud Build gives you and what it does not.
Cloud Build's published position: its provenance supports "SLSA level 3 assurance based on the specifications for SLSA version 0.1 and 1.0."
Level 3 is about the build platform, not about your pipeline. It means the build service generated the provenance, the build ran in an environment you did not control and could not tamper with, and the provenance is non-forgeable. It says nothing about whether your source was reviewed, your dependencies were pinned, or your deployment verified anything.
What you still owe, to make the level meaningful:
| Requirement | Where in this book |
|---|---|
| Source is reviewed and protected | §22.6, §22.7 |
| Build is defined in the repository, not in a console | §23.1 |
| Dependencies are pinned and lockfiles committed | §22.11 |
| Build has no network egress it does not need | §23.5 |
| Provenance is verified before deployment | §25.12 |
Do not claim a level you have not tested. SLSA is used in customer questionnaires and contracts, and the honest answer is "our build platform provides level 3 provenance for artifacts in Artifact Registry, and here is our source and verification posture" — not a bare number.
Pitfall. A build that pushes an image built elsewhere — pulled from a public registry and retagged — carries provenance saying Cloud Build produced it, because Cloud Build did produce that push. The assurance is about the pipeline's integrity, not about the artifact's origin.
23.15 Supply Chain Security §
Supply chain security for a build means treating every input as untrusted and making every output attributable. Chapter 37 covers the full model; this section is Cloud Build's share of it.
Five inputs enter every build, and each needs a control:
| Input | Risk | Control |
|---|---|---|
| Source | A malicious commit | Branch protection and review (§22.6) |
| Builder images | Arbitrary code as the build identity | Pin by digest (§23.3) |
| Dependencies | A compromised package | Pinned lockfiles, remote repositories (§24.1) |
| Secrets | Credential exposure in logs | Per-step secretEnv (§23.9) |
| Build config | An edited pipeline | The config is in the protected repository |
The output side is two things: provenance (§23.13) and where it goes (§23.10). An artifact with provenance, in a repository with immutable tags, referenced by digest downstream, is attributable end to end. Break any of those three and the chain has a gap.
The build config must live in the protected repository, not in the trigger. --inline-config puts the build definition in the trigger object, where it is edited by anyone with roles/cloudbuild.builds.editor and reviewed by nobody. Use --build-config pointing at a path in the repository, so changing the pipeline requires a reviewed commit.
Alert on trigger and pool changes. A modified trigger, a new connection, or a build running on an unexpected pool are all Admin Activity events (§17.2) and belong in the §18.15 alert set — they are the control-plane equivalent of a modified log sink.
Pitfall. The build project is frequently the least-governed project in an estate, because it started as somebody's automation experiment. It holds an identity that can deploy to production. Give it the same organization policy, perimeter membership, and IAM review as the production projects it feeds.
Chapter Summary §
- The build identity is usually the most powerful non-human principal in a project and it executes repository code on every commit.
- Cloud Build's default identity depends on organization settings and may be the legacy build account or the Compute Engine default; neither belongs in production.
- Google recommends specifying your own service account, and the legacy account cannot generate ID tokens.
gcloud builds get-default-service-accountresolves what a project currently uses; five Cloud Build organization policy constraints enforce the choice.--service-accounton a trigger requiresiam.serviceAccounts.actAs, which is the intended control on who may create a powerful pipeline.--require-approvalis what stops a fork's pull request from running arbitrary code as your build identity.- Steps share
/workspaceand nothing else, so a secret written to a file is available to every later step. - Pin builder images by digest; a tag is a mutable pointer to code that runs with your build identity.
allowFailure: trueon a scanning step converts a gate into a decoration.- The default worker pool has no VPC connectivity and open egress;
--no-public-egresson a private pool is the only constraint on exfiltration from a build. - A private pool peered inside a service perimeter needs no ingress rule, which beats granting an external runner standing access.
- Only steps listing a secret in
secretEnvreceive it; reference a pinned secret version, notlatest. - Declare artifacts in
imagesandartifacts— a baredocker pushproduces no provenance and no build output record. - Cloud Build generates in-toto provenance supporting SLSA level 3 assurance, and only for artifacts stored in Artifact Registry.
- Provenance is not subject to cleanup policies but is deleted with the image it describes.
- SLSA level 3 describes the build platform; source review, pinned dependencies, and deployment-time verification are still yours.
- Keep the build config in the protected repository, not inline in the trigger, or the pipeline is editable without review.
Security Checklist §
| Control | Why it matters | How to verify (CLI + Console) |
|---|---|---|
Every trigger sets --service-account | The default identity is shared and over-granted | gcloud builds triggers list --region=REGION --format="table(name,serviceAccount)" |
| Build service accounts are per pipeline | A shared account holds the union of every pipeline's grants | IAM policy review in the CI/CD project |
| No build account holds deploy permissions | Build and deploy identities must differ (§25.13) | gcloud projects get-iam-policy filtered to sa-build-* |
| Pull-request triggers require approval | Otherwise a fork runs code as your build identity | Trigger --require-approval setting |
constraints/cloudbuild.disableCreateDefaultServiceAccount enforced | Removes the shared default before it accumulates grants | gcloud org-policies describe constraints/cloudbuild.disableCreateDefaultServiceAccount --organization=ORG_ID |
constraints/cloudbuild.allowedWorkerPools enforced | Prevents a silent fallback to the open-egress default pool | gcloud org-policies describe constraints/cloudbuild.allowedWorkerPools --organization=ORG_ID |
Production pools set --no-public-egress | The only constraint on exfiltration from a build step | gcloud builds worker-pools describe POOL --region=REGION |
| Builder images pinned by digest | A tag executes whatever the publisher pushed today | Grep cloudbuild.yaml for name: values without @sha256: |
Secrets scoped per step with secretEnv | Otherwise every step can read every secret | Build config review |
Build config comes from the repository, not --inline-config | Inline configs are edited without review | gcloud builds triggers describe TRIGGER --region=REGION |
| Images referenced downstream by digest | A tag can be repointed after verification | Deployment manifests and Binary Authorization policy |
| Alerts on trigger, connection, and pool changes | These are the control-plane events of the pipeline | Alert policy list (§18.15) |
Sources §
- https://cloud.google.com/build/docs — Cloud Build documentation hub (last validated 2026-09-03)
- https://cloud.google.com/build/docs/cloud-build-service-account — default and user-specified build service accounts (last validated 2026-09-03)
- https://cloud.google.com/build/docs/build-config-file-schema — build config fields,
options.logging, andserviceAccount(last validated 2026-09-03) - https://cloud.google.com/build/docs/private-pools/private-pools-overview — private pool networking model (last validated 2026-09-03)
- https://cloud.google.com/build/docs/securing-builds/use-secrets — Secret Manager integration in builds (last validated 2026-09-03)
- https://cloud.google.com/build/docs/securing-builds/view-build-provenance — provenance format, SLSA assurance level, and retrieval (last validated 2026-09-03)
- https://cloud.google.com/build/docs/repositories — 2nd-generation repository connections (last validated 2026-09-03)