VPC Service Controls blocking Cloud Storage / API access
gsutil cp fails with 403 'denied by VPC Service Controls' or PERMISSION_DENIED
Focused incident mode — strips away everything but this runbook.
Symptoms
- gsutil cp fails with 403 'denied by VPC Service Controls' or PERMISSION_DENIED
- APIs inside the perimeter stop working for a client outside it
- Access works from an in-perimeter VM but not from a developer laptop
- Cloud Functions/Cloud Run in a restricted project cannot reach Storage
VPC SC blocks fail with 403 but are a policy decision, not an IAM bug. Confirm the caller's access level and the perimeter's ingress/egress rules before touching IAM bindings.
Evidence to grab
Collect these artifacts before you change anything — they prove (or rule out) each cause.
Offending command/error
gsutil ls gs://<BUCKET>/In-scope resources and perimeter
gcloud access-context-manager perimeters list
Diagnostic flow
Run the steps in order, marking each claim confirmed or refuted. The evidence you gather feeds the ranked causes below.
Guided decision tree is ready.
Steps with commands, expected output, and pass/fail branches.
Ranked root causes
Evidence checklist
- Works from an in-perimeter VM, fails from a laptop
- Error body names VPC SC
- Caller's IP/identity not in the perimeter access level
Confirm before fixing
gcloud access-context-manager levels describe <LEVEL> --organization <ORG> --format='value(name,basic)'Review the access level requirements
Expected: An IP subnetwork or identity requirement the caller misses
Fix
gcloud access-context-manager perimeters update <PERIMETER> --organization <ORG> --add-access-levels <LEVEL>Add the caller's access level to the perimeter (or use a bridge)
Expected: Updated [<PERIMETER>].
Resolution runbook
Apply from top to bottom after the cause is confirmed. Review every command before running it — risk tags are shown on each.
gsutil ls gs://<BUCKET>/Reproduce and read the VPC SC error
gcloud access-context-manager perimeters list --organization <ORG> --format='table(name,status.accessLevels[])'Find the perimeter covering the resource
gcloud access-context-manager perimeters update <PERIMETER> --organization <ORG> --add-access-levels <LEVEL>Add the caller's access level (correct of the analysis is an access level issue)
gsutil ls gs://<BUCKET>/Verify access is restored
Prevention
- Rehearse perimeter access levels with a test client inside and outside the perimeter.
- Keep perimeter changes reviewed and version-controlled; they are privacy/security controls.
- Document which projects/services are in and out of scope to avoid surprise blocks.
- Log VPC SC denials to Cloud Logging and alert on them as access-control events.
Related runbooks
Same stack, same failure family — worth a skim.
gsutil cp/ls fails with AccessDeniedException: 403
Outbound requests from VMs time out while inbound to the VPC works
curl to the Cloud Run URL returns 401 Unauthorized
Commands marked READ CHANGE DESTRUCTIVE — never run DESTRUCTIVE commands against production data you cannot restore. Copy → verify → execute.