This issue has been assessed as high severity. Review affected configurations immediately.
VPC Service Controls (VPC-SC) is one of GCP’s most powerful data protection features and one of its most widely misunderstood. The premise is compelling: create a security perimeter around your GCP services so that even if an attacker compromises credentials, they can’t exfiltrate data outside the perimeter. In practice, VPC-SC is effective when configured correctly and nearly useless when misconfigured — which is frequent.
This guide covers how VPC-SC actually works, the four most common misconfiguration patterns that leave organisations exposed, and how to test whether your perimeters are functioning as intended.
What VPC Service Controls Actually Do
VPC-SC creates service perimeters around GCP projects and resource groups. Once a service is inside a perimeter, access to that service is restricted based on context rules you define — which identities, from which networks or devices, can access the protected APIs.
The primary threat model VPC-SC addresses is data exfiltration via stolen credentials. Without VPC-SC, a stolen GCP service account key can be used from anywhere on the internet to read BigQuery tables, download Cloud Storage objects, or copy Cloud SQL data. With VPC-SC, the same stolen key is rejected unless the request comes from a trusted network, device, or identity context defined in your Access Context Manager policy.
VPC-SC also enforces resource perimeter integrity: it prevents data from crossing from a protected project to an unprotected one (for example, copying a Cloud Storage bucket from your data project to a personal GCP project outside the perimeter).
What VPC-SC does not do:
- It does not encrypt data at rest or in transit beyond standard GCP defaults
- It does not audit or control what authorised users do within the perimeter
- It does not protect against insider threats or compromised identities operating from trusted networks
- It does not replace IAM — overpermissive IAM within the perimeter is still overpermissive
The Four Most Common Misconfiguration Patterns
1. Dry Run Mode Left Enabled in Production
VPC-SC perimeters can be deployed in enforced mode (requests that violate the perimeter are denied) or dry run mode (violations are logged but not blocked). Dry run mode exists for testing and for understanding what a perimeter would block before enforcing it.
The problem: dry run mode is frequently enabled in production environments and left there. The security team believes they have a functioning perimeter; the perimeter is logging violations but permitting all requests regardless.
Check your current mode:
gcloud access-context-manager perimeters list \
--policy=POLICY_ID \
--format="table(name, spec.vpcAccessibleServices, useExplicitDryRunSpec)"
If useExplicitDryRunSpec is True for any perimeter protecting production resources, investigate whether this is intentional. The audit log event type google.identity.accesscontextmanager.v1.AccessContextManager.UpdateServicePerimeter shows when mode changes were made.
2. Missing Ingress and Egress Rules for Service Dependencies
GCP services communicate with each other constantly. A Cloud Dataflow job reads from Cloud Storage and writes to BigQuery. A Cloud Function calls the Pub/Sub API. When you put resources inside a perimeter, these cross-service communications require explicit ingress/egress rules.
When organisations deploy VPC-SC without modelling all their service dependencies, two bad outcomes follow:
- Pipelines and jobs start failing because service-to-service calls are denied. Teams pressure security to punch holes in the perimeter. Holes are punched. Perimeter integrity degrades.
- Alternatively, teams add overly broad ingress/egress rules to stop the failures, effectively making the perimeter permissive enough to allow the exfiltration paths they were trying to close.
The correct approach is to audit all cross-service API calls before deploying a perimeter in enforced mode. VPC-SC’s dry run mode is legitimately useful here — run it for several weeks, review the violation logs in Cloud Audit Logs, and map every cross-perimeter API call before switching to enforced mode.
# Query VPC-SC violation logs in BigQuery (if audit logs exported)
SELECT
protopayload_auditlog.authenticationInfo.principalEmail,
resource.labels.project_id,
protopayload_auditlog.requestMetadata.callerIp,
JSON_EXTRACT(protopayload_auditlog.metadataJson, '$.dryRun') AS dry_run_violation
FROM
`your-project.your-dataset.cloudaudit_googleapis_com_data_access`
WHERE
JSON_EXTRACT(protopayload_auditlog.metadataJson, '$.violationReason') IS NOT NULL
AND timestamp > TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 7 DAY)
3. Service Accounts Excluded from Perimeters via Access Levels
Access Context Manager access levels define which identities and contexts are permitted to access protected resources. One common pattern is creating access levels that permit specific service accounts unconditionally — intended to allow data pipeline service accounts to function across projects.
The problem is that these access levels often become very broad. A service account with an access level that bypasses perimeter checks can be used from any IP address, including from outside your corporate network or GCP environment. If that service account’s key is compromised, the VPC-SC perimeter provides no protection.
Better practice: restrict service accounts using both the access level (identity) and network-based conditions. Combine identity conditions with an IP range or VPC network condition so the access level only applies when the service account is operating from an expected network.
# Example: Check which access levels are unconstrained (identity only, no network condition)
gcloud access-context-manager levels list \
--policy=POLICY_ID \
--format=json | python3 -c "
import json, sys
levels = json.load(sys.stdin)
for level in levels:
conditions = level.get('basic', {}).get('conditions', [])
for cond in conditions:
if 'members' in cond and 'ipSubnetworks' not in cond and 'vpcNetworkSources' not in cond:
print(f'Identity-only condition in {level[\"name\"]}: {cond.get(\"members\")}')
"
4. Perimeters That Don’t Cover All Data-Holding Projects
VPC-SC perimeters protect the projects explicitly included in them. In large GCP organisations, it’s common for the security team to protect the “primary” data project but miss:
- Development and staging environments that mirror production data
- Data science projects where analysts make copies of production datasets for analysis
- Backup projects where Cloud Storage or BigQuery exports are stored
- Projects created by teams outside the core data platform team
An attacker with access to a production BigQuery table can often access the same data via a dev or staging copy that sits outside any perimeter. Data discovery in GCP organisations should identify all projects containing sensitive data, not just the ones assumed to be sensitive.
# List all projects in the organisation with BigQuery datasets containing tables
gcloud projects list --format="value(projectId)" | while read PROJECT; do
TABLES=$(gcloud --project=$PROJECT bq ls --format=json 2>/dev/null | python3 -c "
import json,sys
try:
data = json.load(sys.stdin)
print(len(data))
except:
print(0)
")
if [ "$TABLES" -gt 0 ]; then
echo "$PROJECT: $TABLES datasets"
fi
done
Compare the output against your list of projects included in VPC-SC perimeters.
Testing Your Perimeters
The only way to verify a VPC-SC perimeter is actually blocking requests is to test it from outside. Create a test GCP project outside your perimeter, create a service account with access to a protected resource, and attempt to access that resource from outside the perimeter network.
# From outside the perimeter (e.g., a personal GCP project or external VM):
gcloud config set project EXTERNAL_TEST_PROJECT
gcloud auth activate-service-account --key-file=test-sa-key.json
# Attempt to read from a protected BigQuery table
bq query --project_id=PROTECTED_PROJECT \
"SELECT COUNT(*) FROM \`PROTECTED_PROJECT.dataset.table\`"
A correctly configured enforced perimeter should return an error like:
Error: Request is prohibited by organization's policy.
vpcServiceControls
If the query succeeds, your perimeter is not blocking external access.
Access Context Manager Policy Audit
Regularly audit your Access Context Manager policy for conditions that are too permissive:
# Export full policy including all access levels and their conditions
gcloud access-context-manager policies get-iam-policy POLICY_ID --format=json > acm-policy.json
# List all perimeters and their included resources
gcloud access-context-manager perimeters list \
--policy=POLICY_ID \
--format="table(name, status.resources, status.restrictedServices, status.accessLevels)"
VPC-SC is a powerful control when deployed correctly. The investment in mapping service dependencies and auditing perimeter coverage pays off precisely because it eliminates the exfiltration path that compromised credentials provide — but only if the perimeter actually covers the data, actually runs in enforced mode, and actually restricts the identities and networks you intend to restrict.