Replacing Bastion Hosts with IAP TCP Forwarding for Secure VM Access
Replace bastion hosts with Google Cloud IAP TCP forwarding to give administrators SSH/RDP access to VMs without public IPs. IAM controls access, Cloud Audit Logs provide a trail, and no extra instances to patch.
04 Oct 2026, 18:28 UTC

The problem with bastion hosts
Teams running Compute Engine VMs without public IP addresses often reach for a bastion host or a VPN to give administrators SSH or RDP access. That approach works, but it adds a moving part you must patch, monitor, and log. A bastion host also becomes a high-value target: compromise it and you gain a foothold into the private network.
Google Cloud Identity-Aware Proxy (IAP) TCP forwarding removes the bastion host from the picture. Authorized users connect directly to a VM's internal IP on ports like 22 or 3389, with access controlled by Cloud IAM and recorded in Cloud Audit Logs. The VM never needs a public IP, and there is no extra instance to maintain.
How IAP TCP forwarding works
When a user runs gcloud compute start-iap-tunnel (or an HTTP CONNECT client with an OAuth bearer token), the IAP proxy terminates the TLS connection on Google's edge, checks the caller's IAM permissions, and forwards the raw TCP stream to the target VM's internal IP. The VM sees the connection coming from Google's documented IAP proxy IP ranges, not the user's original address.
Access is granted through the roles/iap.tunnelResourceAccessor role, which can be assigned at the project, folder, or individual VM level. That role only authorizes the tunnel; the VM's guest OS still authenticates the user via SSH keys, RDP credentials, or other mechanisms. IAP provides network reachability, not application authorization.
Prerequisites and firewall rules
Before users can tunnel, two network-level conditions must be met:
- The IAP API is enabled in the project and billing is active.
- A VPC firewall rule (or host firewall) allows ingress from the IAP proxy IP ranges to the target port on the VM.
Google publishes the current IAP proxy ranges in the IAP documentation. A typical firewall rule looks like:
gcloud compute firewall-rules create allow-iap-ssh \
--network=my-vpc \
--allow=tcp:22 \
--source-ranges=35.235.240.0/20 \
--target-tags=iap-accessible \
--description="Allow IAP TCP forwarding for SSH"
Attach the iap-accessible tag to VMs that should accept IAP tunnels. Without this rule, the tunnel will be established at the IAP layer but the TCP SYN will never reach the VM.
Worked example: SSH to a VM with no public IP
Assume a project my-project, zone us-central1-a, and a VM named app-server that has only an internal IP. The administrator [contact removed] needs SSH access.
- Grant the IAP tunnel role on the VM (run from a workstation with
gcloudauthenticated as a project owner):gcloud compute instances add-iam-policy-binding app-server \ --zone=us-central1-a \ --member=user:[contact removed] \ --role=roles/iap.tunnelResourceAccessor - Ensure the VM's service account or OS login is configured for SSH (OS Login is recommended).
- As Alice, start the tunnel and SSH in one command:
This command wrapsgcloud compute ssh app-server \ --zone=us-central1-a \ --tunnel-through-iapstart-iap-tunneland the SSH client. If you prefer separate steps:# Terminal 1: establish tunnel gcloud compute start-iap-tunnel app-server 22 \ --zone=us-central1-a \ --local-host-port=localhost:2222 # Terminal 2: connect through the tunnel ssh -p 2222 alice@localhost - Verify the connection succeeded. In Cloud Logging, filter for:
You should see an entry with Alice's identity and the target VM.resource.type="iap_tunnel" protoPayload.methodName="google.cloud.iap.v1.TunnelService.StartTunnel"
Expected checks: The SSH session opens without a public IP on the VM. The VM's /var/log/auth.log shows a connection from an IP in the 35.235.240.0/20 range. Cloud Audit Logs contain a tunnel-establishment record.
Risks: If the firewall rule is missing, the tunnel starts but hangs at TCP SYN. If OS Login is not configured, Alice must have a pre-provisioned SSH key on the VM. Revoking the IAM role stops new tunnels but does not drop existing ones; they time out after the TCP connection closes.
Trade-offs and limitations
- Cost: IAP TCP forwarding is billed per GB of data egress from the proxy to the VM, plus standard network egress rates. For occasional admin access the cost is negligible; for heavy automated workloads (e.g., CI/CD agents) it can add up. Check the current pricing page before committing.
- Automation: Long-lived scripts need to handle OAuth token refresh. Service accounts with the
iap.tunnelResourceAccessorrole and short-lived access tokens are preferable to interactive user accounts for automation. - OS hardening still required: IAP does not replace patch management, key rotation, or least-privilege OS accounts. A compromised credential still grants VM access.
- No UDP or non-TCP protocols: IAP TCP forwarding only supports TCP. For UDP-based services (e.g., some database replication), a different solution is needed.
Next steps
If you are running a bastion host today, try migrating one low-risk VM to IAP TCP forwarding. Enable the IAP API, add the firewall rule for the IAP proxy ranges, grant roles/iap.tunnelResourceAccessor to a test user, and run the gcloud compute ssh --tunnel-through-iap command. Verify the audit log entry appears. Once comfortable, expand the IAM bindings and retire the bastion host. The reduction in attack surface and operational overhead is usually worth the small per-GB cost.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.