Guide

Reach an internal load balancer from a Mac through IAP

An internal Application Load Balancer, an internal passthrough load balancer or a Cloud Run service with internal ingress has an address only inside your VPC. From a laptop you reach it the way you reach a private database: through a VM in the VPC, over Identity-Aware Proxy. This guide sets up that forward and deals with what is different about HTTP services behind a load balancer: host names and TLS.

Updated

Why IAP cannot reach the load balancer directly

gcloud compute start-iap-tunnel connects to a port on a VM instance. A load balancer's forwarding rule is not an instance, so there is nothing to name in that command. What you can reach is any VM on the same network, and that VM can reach the load balancer. So the path is: your Mac, IAP, a bastion VM, the load balancer's internal address.

Forward a local port through a bastion VM

The bastion needs no external IP: a firewall rule from 35.235.240.0/20 on tcp:22, roles/iap.tunnelResourceAccessor and SSH access are enough. Pick a VM in the load balancer's region. If the load balancer's address is 10.0.4.12 and it serves HTTPS on 443:

gcloud compute ssh example-bastion --tunnel-through-iap \
    --zone=europe-west1-b --project=acme-prod \
    -- -N -L 8443:10.0.4.12:443

The load balancer now answers on 127.0.0.1:8443 on your Mac. For a plain TCP service behind an internal passthrough load balancer, a database or a message broker, that is all: point the client at 127.0.0.1 and the local port. For a passthrough load balancer the backends see the bastion's address as the client, so their firewall must admit it.

Host names and TLS for HTTP services

Opening https://127.0.0.1:8443 in a browser usually fails for two reasons:

  • The load balancer presents a certificate for the service's name, such as api.internal.example, not for 127.0.0.1, so the browser warns.
  • An internal Application Load Balancer picks the backend by host name through its URL map. A request for 127.0.0.1 matches no host rule and lands on the default backend, or on none.

Both are fixed by using the real name and pointing it at your Mac. Add a line to /etc/hosts:

127.0.0.1   api.internal.example

Then open https://api.internal.example:8443. The certificate matches, the host name reaches the URL map, and the traffic still goes through your forward. Remove the line when you are done: while it is there, this Mac sends that name to 127.0.0.1 whether the forward runs or not.

Cloud Run services with internal ingress

A Cloud Run service with ingress set to internal refuses requests from the internet, including your laptop. Requests from your VPC are accepted, and so is a request that arrives through a VM in it. The same forward and host-name mapping works for the service's run.app URL, as long as the VM can reach Cloud Run from inside the VPC, for example through Private Google Access. Services that also require IAM authentication need an identity token in each request on top of this.

How Ostgate does it

Ostgate is a native macOS app that sets up the same path without gcloud.

  • Forward profiles (Connections › Forwards › New Forward Profile…) carry several ports through a bastion VM you choose, over one SSH session carried by IAP, to any address it can reach: an internal load balancer, a private database, another VM.
  • Host aliases. A forward can have a host alias, and a profile can manage /etc/hosts while it runs: it adds the name for 127.0.0.1 when the profile starts and removes it when the profile stops, after macOS asks for an administrator's password.
  • Cloud Run › Open in Browser opens an internal-only service through a local proxy that speaks TLS to the service under its own name, so no hosts entry is needed. Services that require IAM authentication cannot be opened this way yet.
Ostgate's New Forward Profile sheet over the Connections view: profile orders-db, bastion VM example-bastion in project acme-prod, zone europe-west1-b, and a PostgreSQL forward to remote host 10.0.8.3 port 5432 on local port 5432, with an optional host alias.
New Forward Profile through a bastion VM

When it does not connect

SymptomCheck
The SSH hop to the bastion fails Firewall from 35.235.240.0/20 on tcp:22, the IAP tunnel role, and OS Login or metadata key access to the VM.
The forward opens, requests time out The bastion is in another region and the load balancer has no global access, or it is on another network.
Certificate warning You are browsing by 127.0.0.1; map the service's name in /etc/hosts and use it.
404 or the wrong service answers The URL map routes by host name; use the service's name, not the address.
Cloud Run answers 403 The service requires IAM authentication, or the request did not arrive from inside the VPC.

The bastion pattern for databases is in Cloud SQL private IP from a Mac, and single ports on a VM in port forwarding over IAP.

Questions

Can IAP TCP forwarding connect to the load balancer directly?

Not with a plain start-iap-tunnel: it connects to a port on a VM instance. To reach a load balancer's address, go through a VM that can route to it.

Does the bastion VM need a public IP?

No. IAP reaches the VM's internal address, so it needs only a firewall rule from 35.235.240.0/20 on tcp:22 and SSH access for you.

Why does https://127.0.0.1:8443 show a certificate warning?

The load balancer presents a certificate for the service's host name, and an internal Application Load Balancer routes by that name. Map the name to 127.0.0.1 in /etc/hosts and browse by the name instead.

Which region should the bastion VM be in?

The load balancer's region, unless global access is turned on for it: regional internal load balancers accept clients from their own region only by default.

Internal services in your browser. Ostgate runs on Apple Silicon Macs with macOS 26.3 or later, with a 7-day trial and no sign-up. Setup is in the tunnels and forwards docs.