VPC Service Controls: A Dry-Run Perimeter Before You Migrate Data

Illustration from docs.cloud.google.com
Illustration from docs.cloud.google.com

If you are about to move sensitive data into Google Cloud Storage or BigQuery, create the VPC Service Controls perimeter in dry-run mode first, point your migration jobs at the target projects, and read the violation logs for a few days before you enforce anything. Dry-run mode logs what a perimeter would block without actually blocking it, so you find the callers you forgot about while nothing is broken. Enforce only after the logs are quiet.

That order matters. VPC Service Controls is one of the few Google Cloud security controls that fails closed at the API layer, so a perimeter enforced blind will silently deny your own ETL jobs, your CI service accounts, and half your analysts on day one. This is a guide to doing it in the right sequence, with the config a practitioner actually needs.

What a service perimeter actually does

A service perimeter is a boundary around a set of projects and the Google-managed APIs used inside them. The service perimeter allows free communication within the perimeter but, by default, blocks communication to Google Cloud services across the perimeter. The point is data exfiltration: it denies access from unauthorized networks even when data is exposed by a misconfigured IAM policy, and it prevents reading data from or copying data to a resource outside the perimeter.

That last clause is the one people miss. This is not IAM. IAM answers "is this identity allowed to call this API." A perimeter answers "is this call crossing my boundary, regardless of who is making it." It is designed to protect against stolen OAuth or service account credentials used from an unauthorized network, and against malicious insiders or compromised code copying data out. A leaked service account key is much less useful to an attacker if it only works from inside your perimeter.

What it does not do: it does not replace firewall rules, it does not protect services it does not support, and it does not understand your business logic. It is a coarse, powerful gate. Treat it that way.

What to check before you create anything

Do this reconnaissance first. It is cheaper than an incident.

Inventory every caller of your target services. List the service accounts, CI/CD pipelines, third-party SaaS integrations, and interactive users that touch Cloud Storage, BigQuery, or whatever you are protecting. Anything you forget becomes a denied request the moment you enforce.

Confirm the services you rely on are supported. The gaps are specific and they will bite. VPC Service Controls treats Cloud Shell as outside the perimeter and denies access to protected data unless the request comes from a device that meets the perimeter's access level requirements. Cloud Billing is not supported, and Deployment Manager is not supported, though you can add its service account to an access level as a workaround. Check the supported products list for anything in your stack.

Decide the perimeter boundary. You can specify service perimeters at the project or VPC network level. Group projects that legitimately share data into one regular perimeter. If two separate perimeters must exchange data, that is a bridge perimeter or an ingress/egress rule, not a reason to merge everything into one giant boundary.

Plan the private path for your VMs. Hosts that call protected APIs need private connectivity. Hosts in a VPC network must have a private IP address only, with no public IP, and be in a subnet with Private Google Access enabled. If your migration VMs currently reach the internet over an external IP, that path stops working once the perimeter is enforced.

Build the perimeter in dry-run mode first

Dry-run mode is the whole reason this is safe to do on a live environment. Dry-run perimeters log violations as though the perimeter is enforced but do not prevent access to restricted services. You get the exact list of what would break, in production, with production traffic, breaking nothing.

Create the perimeter with restricted services

An access policy holds your perimeters and access levels for the organization. Create the perimeter and name the services you want protected. storage.googleapis.com and bigquery.googleapis.com are the usual first two for a data migration.

gcloud access-context-manager perimeters create prod-data \
  --title="Prod data perimeter" \
  --resources="projects/1234567890,projects/2345678901" \
  --restricted-services="storage.googleapis.com,bigquery.googleapis.com" \
  --perimeter-type="regular" \
  --policy=POLICY_ID

To run it in dry-run mode, update the dry-run configuration rather than the enforced one. A perimeter without an explicit dry-run configuration inherits the enforcement config in dry-run mode, so the dry-run update command clones the enforcement config, applies your change, and uses that as the explicit dry-run spec. In the perimeter object, set the useExplicitDryRunSpec field to True to evaluate the dry-run configuration.

gcloud access-context-manager perimeters dry-run update prod-data \
  --add-resources="projects/1234567890,projects/2345678901" \
  --add-restricted-services="storage.googleapis.com,bigquery.googleapis.com" \
  --policy=POLICY_ID

Read the violations in Cloud Audit Logs

Now let it run. Dry-run mode only logs a request if it is not already denied by the perimeter and it violates the dry-run configuration, and the audit log entry has metadata.dryRun set to True. Query Cloud Audit Logs for those entries. Each one is a caller you need to decide about: allow it with an ingress rule, move it inside the perimeter, or accept that it should be blocked.

One subtlety that saves confusion later: only requests that are allowed by the enforced config but denied by the dry-run config are logged as dry-run violations, so requests already denied by enforcement are not logged again. And access levels do not have a dry-run configuration, so to test an access level change you create a new access level and apply it to the dry-run perimeter.

Stay in dry-run until the violation stream is only things you intend to block. There is no fixed number of days; it is however long it takes to see your monthly and weekly batch jobs run at least once.

Ingress and egress rules break most migrations

The perimeter blocks cross-boundary traffic by default, so any legitimate flow that crosses it needs an explicit rule. Ingress and egress rules allow access to and from the resources and clients protected by service perimeters, and they can replace and simplify use cases that previously required perimeter bridges.

For a migration, the common cases are: an external service account (your on-prem migration tool, or a partner project) writing into the perimeter needs an ingress rule; a job inside the perimeter reading from a source project outside it needs an egress rule.

A rule has a from block describing the origin and identity, and a to block describing the target resources and operations. The sources attribute lists network origins, which can be projects, access levels, or Private Service Connect endpoints, and you enable source enforcement with sourceRestriction set to SOURCE_RESTRICTION_ENABLED. Keep the identity and resource scopes as narrow as the flow requires.

- ingressFrom:
    identities:
      - serviceAccount:migrator@source-project.iam.gserviceaccount.com
    sources:
      - accessLevel: accessPolicies/POLICY_ID/accessLevels/onprem_range
  ingressTo:
    operations:
      - serviceName: storage.googleapis.com
        methodSelectors:
          - method: "google.storage.objects.create"
    resources:
      - projects/1234567890

Two behaviours to keep in mind. If you configure multiple ingress rules, a request is allowed if it satisfies any one of them. And for ingress, sources and identityType are evaluated as an AND condition, while accessLevel, resource, and pscEndpoint within sources are evaluated as an OR condition. Write the rules deliberately; a too-broad ingress rule quietly reopens the hole the perimeter was meant to close.

Extending the perimeter to on-premises

This is the part that matters for a live migration, because your source data and tooling are still in the datacentre. For on-premises hosts to reach restricted Google API services, requests must go through a VPC network over a Cloud VPN tunnel or Cloud Interconnect connection, and the recommendation is to send all requests to the VIP address ranges for restricted.googleapis.com. Those IP ranges are not announced to the internet, and traffic to the VIP stays within Google's network.

On the routing side, use Cloud Router custom advertisement mode to announce the restricted VIP ranges to your on-premises network:

gcloud compute routers update-bgp-peer my-router \
  --peer-name=my-bgp-session \
  --add-advertisement-ranges=RANGES

You also point DNS at the restricted VIP: create a private zone for googleapis.com with an A record mapping restricted.googleapis.com and CNAME records for the *.googleapis.com names, and for on-premises resolution use a Cloud DNS inbound forwarding policy. Getting hybrid DNS and routing right before cutover is exactly the kind of unglamorous work that decides whether a migration is boring or a bad weekend, and it is where our cloud migration engineering team spends real time.

The trade-off and what it costs later

The perimeter buys you a hard exfiltration boundary that survives IAM mistakes. The cost is that it is now a standing dependency for every new integration.

ApproachWhat you getWhat it costs later
No perimeterNothing to maintainIAM misconfig or leaked key can exfiltrate data with no second gate
Enforced with no dry-runProtection immediatelySilent denials of your own jobs; a scramble to write rules under pressure
Dry-run first, then enforceProtection plus a tested rule setEvery future caller needs an ingress rule; a change process to own

The recurring tax is real: every new dataset share, every new analytics tool, every partner integration now needs an ingress or egress rule, and someone has to own that change process. That is a feature, not a bug, but only if you resource it. Perimeters that get worked around with over-broad rules protect nothing. If you are folding this into a broader control set for an audit, it pairs naturally with the least-privilege and encryption work our security and compliance practice does.

Frequently asked questions

How is VPC Service Controls different from IAM?

IAM decides whether an identity is authorized to perform an operation. A service perimeter decides whether a request may cross your data boundary at all, independent of identity. It adds a layer that denies access from unauthorized networks even when data is exposed by a misconfigured IAM policy, so the two are complementary rather than alternatives.

Will dry-run mode block any of my traffic?

No. Dry-run perimeters log violations as though enforced but do not prevent access to restricted services. That is the entire value: you see the complete list of what would break, in production, before you turn on enforcement.

How do I let a service outside the perimeter write data in?

Use an ingress rule scoped to that identity and the specific operation. Ingress and egress rules allow access to and from resources protected by a perimeter and can replace perimeter bridges. Keep the identity, source, and target as narrow as the flow needs, because a broad rule reopens the boundary you built.

Why do my Cloud Shell or Deployment Manager calls fail after enforcing?

Both have known limitations. Cloud Shell is treated as outside the perimeter and denied access unless it comes from a device meeting the perimeter's access level, and Deployment Manager is not supported, though adding its service account to an access level is a documented workaround. Check the supported products list before you enforce so these are decisions, not surprises.

Can on-premises hosts reach protected APIs during a migration?

Yes, over private connectivity. Requests must go through a VPC network via Cloud VPN or Cloud Interconnect, sent to the restricted.googleapis.com VIP ranges, which are not announced to the internet. You advertise those ranges with Cloud Router custom advertisements and resolve them with a private DNS zone.

Working on something like this?

Tell us what you are building and we will give you an honest read on it.