The Scenario
You’re pushing a commit to a repo on a Friday afternoon, triggering a Cloud Build trigger. The build starts, fetches the source, then immediately fails when trying to pull a private Docker image from Artifact Registry or push a build artifact to a Cloud Storage bucket. The step logs show a terse “Permission denied” with no further context.
Symptoms
- Cloud Build step fails with:
ERROR: (gcloud.builds.submit) HTTPError 403: <resource> is not accessible. - Logs show:
denied: Permission "artifactregistry.repositories.downloadArtifacts" denied on resource - The build succeeds locally with
gcloud builds submit --no-sourcebut fails in the trigger.
Root Cause
Cloud Build triggers run under a Cloud Build service account (<project-number>@cloudbuild.gserviceaccount.com), not your user credentials. This service account lacks the necessary IAM roles (e.g., roles/artifactregistry.reader, roles/storage.objectAdmin) on the target resources. The error is not a network issue or misconfiguration—it’s purely an IAM permission gap.
Resolution (Step-by-Step)
-
Identify the Cloud Build service account email:
PROJECT_ID=$(gcloud config get-value project) PROJECT_NUMBER=$(gcloud projects describe $PROJECT_ID --format="value(projectNumber)") echo "${PROJECT_NUMBER}@cloudbuild.gserviceaccount.com" -
Grant the necessary role on the specific resource (e.g., Artifact Registry repository):
# For pulling images from Artifact Registry gcloud artifacts repositories add-iam-policy-binding <repo-name> \ --location=<region> \ --member="serviceAccount:${PROJECT_NUMBER}@cloudbuild.gserviceaccount.com" \ --role="roles/artifactregistry.reader" # For pushing to Cloud Storage gsutil iam ch serviceAccount:${PROJECT_NUMBER}@cloudbuild.gserviceaccount.com:objectAdmin \ gs://<bucket-name> -
Alternatively, grant a project-level role (less granular, use with caution):
gcloud projects add-iam-policy-binding $PROJECT_ID \ --member="serviceAccount:${PROJECT_NUMBER}@cloudbuild.gserviceaccount.com" \ --role="roles/artifactregistry.reader" -
Wait for IAM propagation (up to 60 seconds) and retrigger the build.
Why This Sometimes Doesn’t Work
If you’re using a custom service account specified in the Cloud Build trigger (under “Service account” in the trigger settings), the default @cloudbuild.gserviceaccount.com is not used. You must grant permissions to the custom service account instead. Check the trigger’s YAML or UI for serviceAccount field. Also, if the error involves a cross-project resource (e.g., pulling from an Artifact Registry in another project), you need to grant permissions in that project’s resource, not the build’s project.
Verification
Rerun the build and check the logs for the specific step that failed. If the step now succeeds, the fix worked. For a quick test, impersonate the service account locally:
gcloud builds submit --impersonate-service-account=${PROJECT_NUMBER}@cloudbuild.gserviceaccount.com
Common Follow-up Questions
- How do I find which specific permission is missing?
Run
gcloud projects get-iam-policy $PROJECT_ID --flatten="bindings[].members" --format="table(bindings.role)"for the service account, then cross-reference with the resource’s required roles. - Can I use a custom service account with Cloud Build triggers?
Yes, create a service account with the needed roles, then select it in the trigger settings under “Service account” or set it in the
cloudbuild.yamlviaserviceAccountfield. - Why does it work locally but not in Cloud Build?
Local
gcloud builds submituses your user credentials (which likely have broader permissions), while Cloud Build triggers use the service account.
Prevent This in the Future
Proactively monitor Cloud Build failures for IAM-related errors using structured logging and alerting. Tools like Better Stack can ingest Cloud Build logs and trigger alerts when “Permission denied” appears, letting you catch permission gaps before they block deployments. Set up uptime monitoring and alerts with Better Stack