Home / Networking

Private Google Access + Cloud NAT: Zero Public IPs Architecture | 2026

Private Google Access + Cloud NAT: Zero Public IPs Architecture | 2026

Attaching public IP addresses directly to Compute Engine VMs or GKE nodes exposes internal workloads to automated internet port scanners and brute-force attacks. A Zero-Public-IP architecture keeps all compute resources strictly on private RFC 1918 subnets. Traffic destined for Google APIs (Cloud Storage, BigQuery, Secret Manager) is routed internally via Private Google Access, while external outbound internet traffic (3rd party APIs, package updates) is managed securely via Cloud NAT with static external IPs.

By Mateusz Chmielewski · Aug 29, 2026 · 15 min read

What is a Zero-Public-IP Architecture?

A Zero-Public-IP Architecture is a network design pattern where compute workloads (Compute Engine VMs, GKE nodes, Cloud Run instances) are provisioned exclusively with private internal IP addresses (RFC 1918). Direct inbound internet connections are completely blocked at the network layer.

Think of your VPC like a secure corporate headquarters. In an insecure setup, every employee desk has a window facing the street where anyone outside can knock or look inside. In a Zero-Public-IP setup, no desk has a window to the outside. Employees use an internal inter-department tunnel (Private Google Access) to talk to corporate archives, and send outgoing mail via a central mailroom clerk (Cloud NAT) who stamps the corporate return address on outgoing packages.

ConceptExplanationWhen to use
Private Google Access (PGA)Subnet-level setting that enables VMs with private IPs to reach Google APIs (storage.googleapis.com) internally without using a public IP or NAT.Mandatory on every GCP subnet containing private compute resources.
Cloud NATRegional managed Network Address Translation service that grants private VMs outbound internet access without exposing public inbound IPs.Required when private workloads must fetch third-party API dependencies or OS patches.
Static Egress IPPre-allocated external IP addresses assigned to Cloud NAT, allowing external partners to whitelist your outbound traffic.Use manual IP allocation for production Cloud NAT gateways.
Org Policy ConstraintEnforces `constraints/compute.vmExternalIpAccess` to physically prevent developers from assigning public IPs.Enable at GCP Folder or Organization level.

Private Google Access vs Cloud NAT Egress Flow

Routing ALL outbound traffic (including Google Storage & BigQuery data uploads) through Cloud NAT incurs high NAT data processing fees ($0.045/GB) and puts unnecessary load on NAT gateway port pools.

Combining Private Google Access with Cloud NAT optimizes both security and cost. PGA handles high-volume Google service traffic for FREE over internal backplanes, reserving Cloud NAT strictly for external third-party internet egress. Pair with [Private Service Connect](/tutorial/private-service-connect-google-apis-terraform) for Google API consumption and static egress via [Serverless VPC Access](/tutorial/cloud-run-gke-static-egress-ip-terraform-serverless-vpc-access-nat).

FeaturethisServicealtAaltB
Traffic DestinationPGA: Google APIs & ServicesCloud NAT: External Internet / 3rd Party APIsPublic IP: Unrestricted Inbound & Outbound
Inbound Access AllowedNo (Outbound request only)No (Stateful translation only)Yes (Exposed to internet scanners)
Data Processing Cost$0.00 / GB (Free internal routing)$0.045 / GB data processed$0.00 / GB (Standard network egress applies)
Egress IP SourceInternal Google Virtual IPConfigured Static Cloud NAT IPInstance-specific Ephemeral Public IP
Security RiskZero internet exposureZero inbound vulnerabilityHigh exposure to external attacks

Prerequisites

  • GCP Project with Compute Engine API (`compute.googleapis.com`) enabled
  • Custom Mode VPC Network
  • Terraform CLI v1.6+
  • gcloud CLI v480.0+ configured

Step-by-Step Guide

Step 1: Enable Private Google Access on VPC Subnet

Enable `private_ip_google_access = true` on your Terraform VPC subnets. Without Private Google Access enabled, VMs lacking public IPs cannot reach `storage.googleapis.com` or `pkg.dev` to pull container images or packages.

# subnet.tf — Private Subnet with Private Google Access enabled
resource "google_compute_subnetwork" "private_subnet" {
  name                     = "prod-private-europe-west1"
  ip_cidr_range             = "10.100.0.0/24"
  region                   = "europe-west1"
  network                  = google_compute_network.custom_vpc.id
  private_ip_google_access = true # Enables PGA internally

  secondary_ip_range {
    range_name    = "gke-pods"
    ip_cidr_range = "10.101.0.0/16"
  }

  secondary_ip_range {
    range_name    = "gke-services"
    ip_cidr_range = "10.102.0.0/20"
  }

  project = var.project_id
}

Step 2: Allocate Static Egress External IPs for Cloud NAT

Reserve static external IP addresses in Terraform to dedicate to the Cloud NAT gateway. Using manual static IP allocation ensures your outbound egress IP address never changes, allowing external partners and vendors to whitelist your NAT IPs.

# nat_ips.tf — Static External IPs for Cloud NAT
resource "google_compute_address" "nat_external_ip_1" {
  name    = "nat-static-ip-1"
  region  = "europe-west1"
  project = var.project_id
}

resource "google_compute_address" "nat_external_ip_2" {
  name    = "nat-static-ip-2"
  region  = "europe-west1"
  project = var.project_id
}

output "nat_egress_ips" {
  value = [
    google_compute_address.nat_external_ip_1.address,
    google_compute_address.nat_external_ip_2.address
  ]
  description = "Static egress IP addresses for Cloud NAT"
}

Step 3: Deploy Cloud Router and Cloud NAT Gateway in Terraform

Provision a Cloud Router and Cloud NAT gateway configured for static IP allocation and dynamic port allocation. Cloud NAT handles stateful egress translation without accepting incoming connections. Dynamic port allocation prevents port exhaustion spikes during traffic bursts.

# cloud_nat.tf — Cloud Router & Cloud NAT Gateway
resource "google_compute_router" "nat_router" {
  name    = "nat-router-europe-west1"
  region  = "europe-west1"
  network = google_compute_network.custom_vpc.id
  project = var.project_id
}

resource "google_compute_router_nat" "cloud_nat" {
  name                               = "cloud-nat-europe-west1"
  router                             = google_compute_router.nat_router.name
  region                             = "europe-west1"
  nat_ip_allocate_option             = "MANUAL_ONLY"
  source_subnetwork_ip_ranges_to_nat = "ALL_SUBNETWORKS_ALL_IP_RANGES"

  nat_ips = [
    google_compute_address.nat_external_ip_1.self_link,
    google_compute_address.nat_external_ip_2.self_link
  ]

  # Dynamic Port Allocation Settings
  min_ports_per_vm                 = 64
  max_ports_per_vm                 = 2048
  enable_dynamic_port_allocation   = true
  enable_endpoint_independent_mapping = false

  log_config {
    enable = true
    filter = "ERRORS_ONLY" # Log NAT errors (e.g. port exhaustion)
  }

  project = var.project_id
}

Step 4: Create Private VM Instance (No Public IP)

Deploy a Compute Engine instance with zero external IP addresses inside the private subnet. Omitting the `access_config` block inside the `network_interface` block ensures the instance receives only an internal RFC 1918 IP address.

# compute_private_vm.tf — VM Instance with Zero Public IP
resource "google_compute_instance" "private_app_server" {
  name         = "private-app-server"
  machine_type = "e2-medium"
  zone         = "europe-west1-b"
  project      = var.project_id

  boot_disk {
    initialize_params {
      image = "debian-cloud/debian-12"
    }
  }

  network_interface {
    subnetwork = google_compute_subnetwork.private_subnet.id
    # NOTE: NO access_config block here! Zero Public IP.
  }

  metadata = {
    enable-oslogin = "TRUE"
  }

  service_account {
    scopes = ["cloud-platform"]
  }

  depends_on = [google_compute_router_nat.cloud_nat]
}

Step 5: Enforce Org Policy Constraint Disabling External IPs

Apply the `constraints/compute.vmExternalIpAccess` organization policy to block external IP creation project-wide. Infrastructure guardrails prevent human error or unauthorized Terraform edits from exposing workloads to the public internet.

# org_policy.tf — Restrict External IP Access
resource "google_project_organization_policy" "disable_external_ips" {
  project    = var.project_id
  constraint = "compute.vmExternalIpAccess"

  list_policy {
    deny {
      all = true # Deny external public IPs for ALL VMs in project
    }
  }
}

Step 6: Test Outbound Connectivity and PGA Routing

SSH into the private instance via IAP and test outbound internet access via Cloud NAT and internal Google API access via PGA. Verifying routing paths empirically proves that external traffic uses Cloud NAT static IPs while Google API traffic bypasses NAT.

# Step 1: Connect to private VM via Identity-Aware Proxy (IAP)
gcloud compute ssh private-app-server \
  --zone=europe-west1-b \
  --tunnel-through-iap \
  --project=PROJECT_ID

# Step 2: Verify outbound internet IP matches Cloud NAT Static IP
curl ifconfig.me

# Expected output: 34.140.x.x (Matches one of your Cloud NAT static IPs!)

# Step 3: Test Google API access (Private Google Access path)
curl -vvv https://storage.googleapis.com

# Expected output: Connected to storage.googleapis.com (142.250.x.x) over internal gateway.

Verification & Health Check

Best Practices

  • Omit access_config Block Completely for Zero Public IP VMs
  • Enable Dynamic Port Allocation on Cloud NAT

Common Mistakes

  • {"errorCode":"IAP_SSH_FIREWALL_MISSING","symptoms":"gcloud compute ssh --tunnel-through-iap returns Connection Refused or Timeout.","rootCause":"VPC missing firewall rule allowing ingress from Google's IAP proxy CIDR `35.190.247.0/20`.","fixCommand":"gcloud compute firewall-rules create allow-iap-ssh --allow=tcp:22 --source-ranges=35.190.247.0/20 --network=VPC_NAME","code":"resource \"google_compute_firewall\" \"allow_iap_ssh\" {\n name = \"allow-iap-ssh\"\n network = google_compute_network.custom_vpc.id\n allow {\n protocol = \"tcp\"\n ports = [\"22\"]\n }\n source_ranges = [\"35.190.247.0/20\"]\n}\n","language":"hcl","filename":"fix_iap_fw.tf","prevention":"Include IAP SSH ingress firewall rules in all private subnet modules."}

Cost Analysis

Featuremetriccost1kcost10kcost100kcost1m
Cloud NAT Gateway Hourly Fee ($0.045 / hour)$32.40/mo$32.40/mo$32.40/mo$388.80/yr
Cloud NAT Data Processing (500 GB × $0.045 / GB)$22.50/mo$22.50/mo$22.50/mo$270.00/yr
Private Google Access (Internal API traffic)$0.00 (Free)$0.00 (Free)$0.00 (Free)$0.00 (Free)
Total Zero-Public-IP Networking Cost~$54.90/mo~$54.90/mo~$54.90/mo~$658.80/yr

References

Browse all tutorials