Back to blog
Best PracticesCloud SecurityCompute & ContainersGCPNetworking

VM Has Public IP Address: Why It Matters and How to Fix It on GCP

Compute Engine VMs with public IPs expose your attack surface to the entire internet. Learn the risks and how to remove external IPs using Cloud NAT, org policy, and IaC.

TL;DR

This check flags Compute Engine VMs that have a public IP address, which exposes them directly to the internet and widens your attack surface. Strip the external IP and route outbound traffic through Cloud NAT instead.

A public IP on a Compute Engine instance is one of those settings that feels harmless when you spin up a VM in the console. GCP attaches an ephemeral external IP by default, the VM gets internet access, your app works, and you move on. The problem is that a public IP makes the VM reachable from anywhere on the internet, and reachability is the first link in most cloud attack chains.

The compute_publicip check identifies any Compute Engine VM instance with an external IP address assigned to a network interface, whether ephemeral or static.


What this check detects

Every Compute Engine network interface can have two kinds of address:

  • Internal IP — a private RFC 1918 address used for communication inside your VPC. Every VM has one.
  • External IP — a public, internet-routable address. Optional, and what this check looks for.

The check inspects each instance's networkInterfaces[].accessConfigs[]. If an access config of type ONE_TO_ONE_NAT exists with a natIP assigned, the VM has a public IP and the check fails.

Note: "Ephemeral" external IPs are not safer than static ones. They change when the VM stops and starts, but while the VM is running the address is fully reachable from the public internet. The exposure is identical.


Why it matters

A public IP turns a VM into a target that anyone on the internet can find. Automated scanners sweep the entire IPv4 space continuously, so a newly exposed instance starts receiving probe traffic within minutes. Here is where that leads.

Exposed management ports

The most common failure is an external IP combined with a permissive firewall rule. SSH on 22, RDP on 3389, or a database port left open to 0.0.0.0/0 gives attackers a direct login surface to brute-force. Public IP plus open port is the recipe behind a large share of compromised cloud instances.

Unpatched service exploitation

If the VM runs a web server, message broker, or admin panel that falls behind on patches, a public IP means a known CVE can be exploited the same day it drops. Internal-only VMs buy you time because an attacker has to get inside the VPC first.

Data exfiltration and lateral movement

Once a public-facing VM is breached, it becomes a foothold. Attackers use it to query the metadata server for service account tokens, then pivot to Cloud Storage, BigQuery, or other VMs over the internal network. The public IP was only the front door.

Danger: A VM with a public IP and a broadly scoped service account is a critical risk. If compromised, the attacker inherits every permission that service account holds across your project. Audit service account scopes on any internet-facing instance.


How to fix it

The fix is to remove the external IP. Most VMs that talk to the internet only need outbound access, which Cloud NAT provides without exposing the instance to inbound traffic. For instances that must accept inbound traffic, put a load balancer in front instead of exposing the VM directly.

Step 1: Confirm what has a public IP

gcloud compute instances list \
  --format="table(name, zone, networkInterfaces[0].accessConfigs[0].natIP)" \
  --filter="networkInterfaces[0].accessConfigs[0].natIP:*"

Any row with a value in the last column has an external IP.

Step 2: Remove the external IP from an existing VM

Warning: Deleting the access config drops the VM's direct internet access immediately. If the instance reaches external services for updates, package mirrors, or APIs, set up Cloud NAT first (Step 3) or those calls will start failing.

Delete the access config on the relevant network interface. The default interface name is nic0 and the default access config name is external-nat:

gcloud compute instances delete-access-config INSTANCE_NAME \
  --zone=ZONE \
  --network-interface=nic0 \
  --access-config-name="external-nat"

If you are not sure of the access config name, describe the instance first:

gcloud compute instances describe INSTANCE_NAME \
  --zone=ZONE \
  --format="yaml(networkInterfaces)"

Step 3: Provide outbound access with Cloud NAT

Create a Cloud Router and a NAT gateway so private VMs can still reach the internet outbound. NAT is regional, so do this per region.

# Create a Cloud Router
gcloud compute routers create nat-router \
  --network=YOUR_VPC \
  --region=REGION

# Create the NAT config for all subnets and primary IP ranges
gcloud compute routers nats create nat-config \
  --router=nat-router \
  --region=REGION \
  --nat-all-subnet-ip-ranges \
  --auto-allocate-nat-external-ips

Note: Cloud NAT gives outbound-only connectivity. Traffic initiated from outside cannot reach the VM through it, which is exactly the property you want. Inbound is allowed only for established return traffic.

Step 4: Use a load balancer for inbound traffic

If the VM genuinely needs to serve traffic from the internet, do not put a public IP on the VM itself. Place it behind a Google Cloud load balancer, keep the VM on internal IPs only, and let the load balancer absorb the public exposure and provide DDoS protection through Cloud Armor.

Fixing it in Terraform

If you manage VMs with Terraform, the public IP comes from an access_config block. Removing the block removes the external IP:

resource "google_compute_instance" "app" {
  name         = "app-vm"
  machine_type = "e2-medium"
  zone         = "us-central1-a"

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

  network_interface {
    subnetwork = google_compute_subnetwork.private.id

    # No access_config block = no external IP.
    # Remove or comment out any access_config to drop the public IP.
  }
}

Tip: An empty access_config {} block actually assigns an ephemeral external IP. To have no public IP, omit the block entirely. This trips up a lot of teams.


How to prevent it from happening again

Manual cleanup does not scale. Block public IPs at the policy layer so a misconfigured deploy never reaches production.

Org policy constraint

GCP ships a built-in constraint that denies external IPs on Compute Engine VMs across a project, folder, or organization:

gcloud resource-manager org-policies enable-enforce \
  compute.vmExternalIpAccess \
  --project=YOUR_PROJECT

This denies all external IPs by default. If specific VMs need one, switch to an allowlist policy listing only those instances by full resource name. Anything not on the list is rejected at creation time.

Warning: Enabling this org policy will not retroactively remove external IPs from running VMs, but it will block new ones and prevent reassignment after a stop/start. Audit existing instances before relying on it as your only control.

CI/CD gate for Terraform

Catch access_config blocks in pull requests before apply. A simple Conftest/OPA policy run against your Terraform plan works well:

package main

deny[msg] {
  resource := input.resource_changes[_]
  resource.type == "google_compute_instance"
  iface := resource.change.after.network_interface[_]
  iface.access_config != null
  count(iface.access_config) > 0
  msg := sprintf("VM %s assigns an external IP; remove the access_config block", [resource.address])
}

Wire this into your pipeline so a plan that adds a public IP fails the build.

Tip: Run the check continuously, not just at deploy time. Lensix flags any VM that drifts into a public-IP state, including ones created manually in the console or reassigned an IP after a restart, so the org policy and your scanning back each other up.


Best practices

  • Private by default. New VMs should have no external IP unless there is a documented reason. Make the public IP the exception that requires justification.
  • Cloud NAT for egress. Centralize outbound access through NAT so you get a stable set of egress IPs to allowlist with downstream services, and zero inbound exposure.
  • Load balancers for ingress. Front internet-facing apps with a load balancer and Cloud Armor rather than exposing VMs directly.
  • IAP for admin access. Use Identity-Aware Proxy for SSH and RDP instead of opening management ports on a public IP. IAP authenticates users at the edge and needs no public address on the VM.
  • Tighten firewall rules. Even on private VMs, scope ingress rules to specific source ranges. Never leave 0.0.0.0/0 on management ports.
  • Scope service accounts narrowly. Assume any internet-facing VM could be breached and limit what its service account can do.

Removing public IPs is one of the highest-leverage changes you can make to shrink your attack surface. Most VMs never needed one, and the ones that do are better served by NAT, a load balancer, or IAP. Pair an org policy with continuous scanning and the door stays shut.