GCP Lesson 37 of 98

Google Cloud Identity-Aware Proxy (IAP), In Depth: Zero-Trust Access to Apps, VMs & APIs

For thirty years the standard way to protect an internal application was a perimeter: put it behind a firewall, hand the right people a VPN, and assume that anyone who reached the application over the trusted network was allowed to be there. That assumption is now the single largest cause of lateral movement in breaches. A stolen VPN credential, a compromised laptop already inside the network, or a flat internal subnet means one foothold becomes the whole estate. Google hit this wall internally a decade ago, abandoned the VPN-and-perimeter model for its own employees, and published the result as BeyondCorp: trust nothing because of where it sits on the network; verify who the caller is and what context they are in, on every single request, at a proxy that sits in front of the resource. Identity-Aware Proxy (IAP) is the managed Google Cloud product that implements BeyondCorp for your own applications and virtual machines.

This is the exhaustive lesson. IAP wears two quite different hats and we cover both to the last option. IAP for HTTPS sits in front of a Google Cloud HTTP(S) Load Balancer and gates web applications and HTTP APIs running on App Engine, Cloud Run, GKE, Compute Engine, or any internet-facing backend — it runs the OAuth dance, checks IAM, and forwards a cryptographically signed identity header your application can trust. IAP for TCP forwarding wraps arbitrary TCP — most importantly SSH and RDP — so you can administer VMs that have no external IP at all, replacing the bastion host entirely. We go concept by concept and knob by knob: the proxy model and how it differs from a VPN or a bastion, every IAM role and binding, the OAuth consent screen, the exact signed JWT your code must validate (and how), the firewall source range 35.235.240.0/20 that trips everyone up, Context-Aware Access through Access Context Manager device and IP and region levels, programmatic access for service accounts, headers and logging — until you can whiteboard a zero-trust access path from memory and answer every follow-up the Professional Cloud Security Engineer or Associate Cloud Engineer exam will throw at you.

Learning objectives

Prerequisites & where this fits

You need a Google Cloud project with billing enabled, the gcloud CLI from the earlier Fundamentals lessons, and a working mental model of IAM (members, roles, bindings, inheritance) and VPC (subnets, firewall rules) — IAP is built on top of both. This is the Security deep-dive on application and instance access in the GCP Zero-to-Hero course. It sits next to two lessons it complements rather than repeats: VPC firewall, routes and Cloud NAT (IAP’s TCP tunnel needs exactly one firewall rule, which we cover here, and removes the need for the 0.0.0.0/0 SSH rule that lesson warns against), and VPC Service Controls (which guards the Google API control plane, where IAP guards your applications and VMs — different layers that compose). If “OAuth”, “JWT”, and “load balancer backend service” are brand new, skim those primers first; this lesson defines the IAP-specific parts but assumes you know what an HTTP request and an IP range are.

Core concepts: the zero-trust proxy model

Strip IAP to its essence and it is a policy enforcement point that authenticates and authorizes every request before the resource ever sees it. Three ideas carry the whole design.

Identity, not network location, is the control. In the perimeter model, being on the corporate LAN or VPN is the authorization. In IAP, the network grants nothing. Every request must carry — or be made to carry — a Google identity (a Google Account, a Cloud Identity / Workspace user, a service account, or a federated external identity), and IAP checks that identity against an IAM policy attached to the specific resource. There is no “internal, therefore trusted.” This is what “zero trust” means concretely.

The proxy sits in the data path and is unavoidable — if you wire it correctly. For HTTPS, IAP is a capability of the Google HTTP(S) Load Balancer: enable it on a backend service and every request to that backend is intercepted at Google’s edge, authenticated, and only then forwarded. For TCP, the IAP tunnel is the only path in because the VM has no external IP, so there is simply no other route from the internet. The recurring failure mode — covered in detail later — is leaving a second path open (a public IP on the VM, or an application that trusts its raw URL), which lets a caller go around the proxy. Zero trust only holds if the proxy is the sole door.

Context can be required on top of identity. A correct identity is necessary but you can demand more: that the request comes from a Google-managed, encrypted, screen-locked device, from a corporate IP range, from an allowed country, or presenting a client certificate. These conditions are access levels defined in Access Context Manager and bound to the resource through Context-Aware Access. Identity says who; context says under what conditions — and IAP AND-s them.

A few key terms before we build:

IAP for HTTPS vs IAP for TCP at a glance

The two modes share the identity-at-the-proxy philosophy but differ in almost every mechanic. Keep them straight:

Dimension IAP for HTTPS IAP for TCP forwarding
Protects Web apps & HTTP(S) APIs Arbitrary TCP — SSH (22), RDP (3389), DB ports, etc.
Sits on An external or internal Application Load Balancer backend service (and App Engine directly) A tunnel from the client to a specific VM:port; no load balancer
Backends App Engine, Cloud Run, GKE, Compute Engine MIGs/NEGs, hybrid (internet/serverless NEG) Compute Engine VM instances
How users reach it Browser → load-balancer URL; OAuth login page gcloud compute ssh --tunnel-through-iap / start-iap-tunnel / IAP Desktop
IAM role roles/iap.httpsResourceAccessor roles/iap.tunnelResourceAccessor
Identity in request Signed JWT header + X-Goog-Authenticated-User-* The tunnelled protocol’s own auth (SSH keys / OS Login / RDP creds)
Firewall need None special (traffic comes via the LB) Allow ingress TCP from 35.235.240.0/20 to the target port
Replaces Public app exposure / app-level login / a web VPN The bastion host and public SSH/RDP
External IP on resource LB has the public IP; backends can be private VM needs no external IP

We now take each mode to exhaustion.

IAP for HTTPS: protecting web apps and APIs

The request path

A browser hits the load balancer’s URL. IAP intercepts at Google’s edge before the request reaches your backend. If there is no valid session, IAP redirects the browser through Google’s OAuth sign-in; the user authenticates as a Google/Cloud Identity account. IAP then checks whether that identity holds roles/iap.httpsResourceAccessor on the resource and whether the request satisfies any bound access levels. Only if both pass does IAP forward the request to the backend — now carrying the signed identity headers. Every subsequent request re-checks the (cached, short-lived) authorization. Your backend never sees an unauthenticated request if it trusts only IAP-verified traffic.

What you can put behind IAP-for-HTTPS

Backend How it attaches Notes
App Engine (standard & flexible) IAP toggled directly on the App Engine app; no separate LB to build The simplest path; App Engine has IAP built in.
Cloud Run Via a serverless NEG behind an external/internal Application LB, IAP on that backend service Set Cloud Run ingress to “Internal and Cloud Load Balancing” so it can’t be hit directly; the Cloud Run deep-dive covers ingress.
GKE An Ingress (or Gateway) that provisions a Google LB; enable IAP via a BackendConfig referencing the OAuth client secret The BackendConfig CRD wires IAP to the Service.
Compute Engine A backend service with an instance-group or NEG backend behind an Application LB Classic VM web app behind the global/regional external ALB.
Hybrid / on-prem An internet NEG (or hybrid connectivity NEG) behind the LB IAP can front workloads outside Google Cloud reached via the LB.

IAP works with the Global external Application LB, the Regional external Application LB, the Classic ALB, and the internal Application LB (for internal-only apps reached over private connectivity or from on-prem). It does not apply to the network load balancers (L4 passthrough/proxy) — IAP-for-HTTPS is an L7/HTTP capability. See the Load Balancing deep-dive for which LB to pick.

Prerequisite 1 — the OAuth consent screen (brand)

IAP signs users in with OAuth, so the project needs an OAuth consent screen (a “brand”). For corporate apps make it Internal (only your Workspace/Cloud Identity org can use it) rather than External. You set the application name and a support email; that name appears on the Google sign-in page.

PROJECT_ID=$(gcloud config get-value project)

# Create the OAuth brand (Internal == only users in your Cloud Identity org).
# The support email must be your own user or a group you own.
gcloud iap oauth-brands create \
  --application_title="Internal Admin Portal" \
  --support_email="you@example.com"

# List it back to get the brand resource name.
gcloud iap oauth-brands list

For App Engine, Cloud Run, and GKE backends IAP can manage the OAuth client for you automatically once a brand exists; for some setups (notably GKE BackendConfig) you create an OAuth client explicitly and hand its client-ID/secret to the backend.

# Explicit OAuth client (needed e.g. for GKE BackendConfig).
gcloud iap oauth-clients create BRAND_NAME \
  --display_name="iap-client"
# Returns a client_id and client_secret you reference from the backend config.

Prerequisite 2 — the backend (an LB-fronted app)

You need a backend service fronted by an Application Load Balancer (or an App Engine app, which has the LB built in). The Compute Engine and Load Balancing lessons cover building the LB itself; the lab below stands up the minimal version.

Enabling IAP and granting access — every option

Enable IAP on the resource, then grant the accessor role. Enabling IAP and granting access are two separate steps — flipping IAP on without a binding locks everyone out (a deliberate fail-closed default).

# Enable IAP on a Compute/GKE backend service.
gcloud compute backend-services update my-backend-service \
  --global \
  --iap=enabled

# Or on App Engine:
gcloud iap web enable --resource-type=app-engine

# Grant a user the HTTPS accessor role at the resource level.
gcloud iap web add-iam-policy-binding \
  --resource-type=backend-services \
  --service=my-backend-service \
  --member="user:alice@example.com" \
  --role="roles/iap.httpsResourceAccessor"

# Grant a whole Google group (the right pattern at scale):
gcloud iap web add-iam-policy-binding \
  --resource-type=backend-services \
  --service=my-backend-service \
  --member="group:app-users@example.com" \
  --role="roles/iap.httpsResourceAccessor"

The full set of HTTPS-side roles and member types:

Role / member option What it grants / means When to use
roles/iap.httpsResourceAccessor Permission iap.webServiceVersions.accessViaIAP — the right to reach an IAP-protected HTTPS resource The everyday “let this person/group use the app” grant. Prefer groups.
roles/iap.admin Manage IAP settings and IAM policies for IAP resources Platform/security admins who configure IAP.
roles/iap.settingsAdmin Edit IAP settings (consent screen, access settings) without managing IAM Narrower admin for settings only.
Member user: / group: A specific user or Google group Default; groups scale.
Member serviceAccount: A service account (machine caller) Programmatic access (see that section).
Member domain: An entire Workspace/Cloud Identity domain Broad “anyone in the company” access.
Member allAuthenticatedUsers Any signed-in Google account, anywhere Rarely — only for apps meant to be open to all Google users; not “your company”.
Member allUsers Everyone, no sign-in Effectively disables IAP; almost never.
IAM Condition on the binding CEL expression — e.g. time-bound or path-scoped access Temporary access; restrict by request.path/host.
Resource scope Whole app, a backend service, or a specific path/version Grant at the narrowest scope that works.

You can scope a binding to a specific path of the app via an IAM condition on request.path, so /admin requires a tighter group than /. Bindings can also be time-bound with an request.time condition for just-in-time access.

The signed JWT header — and why your app must verify it

This is the most-missed, most-tested point. When IAP forwards a request it adds headers:

The convenience headers are easy to read but must not be trusted alone: if a request reaches your backend without going through IAP (because someone left a public IP on the VM, or hits a Cloud Run service whose ingress allows direct traffic), an attacker can simply spoof those headers. The defence is to validate the signed JWT on every request inside your application:

  1. Read x-goog-iap-jwt-assertion.
  2. Verify the signature against Google’s IAP public keys (https://www.gstatic.com/iap/verify/public_key), confirm alg=ES256.
  3. Check the aud (audience) claim equals your resource’s exact expected value:
    • Compute/GKE backend service: /projects/PROJECT_NUMBER/global/backendServices/BACKEND_SERVICE_ID
    • App Engine: /projects/PROJECT_NUMBER/apps/PROJECT_ID
  4. Check iss is https://cloud.google.com/iap, and that exp/iat are valid.
  5. Use the email/sub claims as the authenticated identity.

Validating aud is what makes spoofing impossible — only Google can mint a JWT with the correct audience and signature for your backend. Google’s auth libraries (Python google.auth, Go, Java, Node) provide a one-call verifier. Treat “validate the IAP JWT, pin the audience” as the rule, not optional hardening.

Programmatic access from outside a browser

Service accounts, CI jobs, and curl can call an IAP-protected HTTPS app without a browser by presenting an OIDC ID token whose audience is the app’s OAuth client ID:

# A service account calling an IAP-protected app:
# mint an ID token with the app's OAuth client ID as the audience.
TOKEN=$(gcloud auth print-identity-token \
  --audiences="OAUTH_CLIENT_ID.apps.googleusercontent.com")

curl -H "Authorization: Bearer ${TOKEN}" https://app.example.com/api

The calling identity must hold roles/iap.httpsResourceAccessor. From inside Google Cloud, a workload uses its attached service account; for keyless external callers (GitHub Actions, etc.) combine this with Workload Identity Federation so no service-account key ever exists.

IAP for TCP forwarding: SSH/RDP to VMs with no public IP

The model — and why it kills the bastion

Traditionally, to administer private VMs you stand up a bastion host: a hardened VM with a public IP and SSH open to the world (or to your office IP), which you hop through to reach everything else. The bastion is a permanent, internet-exposed attack surface that you must patch, monitor, and rotate keys on. IAP for TCP forwarding deletes it. Your VMs have no external IP. The client opens an encrypted tunnel to iap.googleapis.com, presents a Google identity, IAP checks roles/iap.tunnelResourceAccessor, and — if allowed — forwards the TCP stream to VM:port from inside Google’s network. The only ingress your VM ever accepts is from Google’s IAP range, not the internet. No public IP, no bastion, full audit trail, identity-gated.

The one firewall rule everyone forgets — 35.235.240.0/20

Because the tunnelled connection arrives at the VM from inside Google’s network, sourced from the IAP forwarding range, your VPC firewall must allow ingress from 35.235.240.0/20 to the destination port. This single CIDR is where most “I set up IAP SSH and it just times out” tickets end. Allow only this range — never 0.0.0.0/0.

# Allow IAP TCP forwarding to reach SSH and RDP on tagged VMs.
gcloud compute firewall-rules create allow-iap-ingress \
  --network=default \
  --direction=INGRESS \
  --action=ALLOW \
  --rules=tcp:22,tcp:3389 \
  --source-ranges=35.235.240.0/20 \
  --target-tags=iap-ssh

35.235.240.0/20 is a Google-owned, documented constant for IAP TCP forwarding. Scope the rule with target tags or a target service account so it applies only to the VMs you mean to administer this way — and the VPC deep-dive explains why that source range, not the bastion’s 0.0.0.0/0 SSH rule, is the secure pattern.

Connecting — every method

# 1) SSH through IAP — the everyday command. The flag does the tunnelling.
gcloud compute ssh my-private-vm \
  --zone=us-central1-a \
  --tunnel-through-iap

# 2) A raw local tunnel to any TCP port (RDP, a database, an admin UI).
#    Forwards localhost:LOCAL_PORT -> VM:REMOTE_PORT through IAP.
gcloud compute start-iap-tunnel my-private-vm 3389 \
  --local-host-port=localhost:3389 \
  --zone=us-central1-a
#    Then point your RDP client / psql at localhost:3389.

# 3) SCP a file over the same tunnel.
gcloud compute scp ./file my-private-vm:~/ \
  --zone=us-central1-a \
  --tunnel-through-iap

Other entry points to the same tunnel:

The IAM role and grant for TCP

# Project-wide: this user may open IAP tunnels to any VM in the project.
gcloud projects add-iam-policy-binding "$PROJECT_ID" \
  --member="user:alice@example.com" \
  --role="roles/iap.tunnelResourceAccessor"

# Scoped to ONE instance (least privilege) via a resource condition:
gcloud projects add-iam-policy-binding "$PROJECT_ID" \
  --member="user:alice@example.com" \
  --role="roles/iap.tunnelResourceAccessor" \
  --condition='expression=resource.name == "projects/PROJECT/zones/us-central1-a/instances/my-private-vm",title=only-one-vm'
TCP-side role Permission Grant scope options
roles/iap.tunnelResourceAccessor iap.tunnelInstances.accessViaIAP — open a tunnel to a VM Project, folder, org, or a single instance/zone via IAM Condition
roles/iap.admin Manage IAP TCP settings & IAM Admins

Two layers still apply after IAP lets the tunnel through: the firewall rule above, and the VM’s own login — SSH key pairs or, far better, OS Login (which maps Linux logins to IAM identities and supports 2FA). IAP authorizes the tunnel; OS Login/SSH authorizes the session. Pair them.

TCP forwarding capabilities and limits

Context-Aware Access: requiring device and context, not just identity

Identity alone may be too weak for sensitive apps — a correct password from an unmanaged personal laptop in an unexpected country is exactly the breach you want to stop. Context-Aware Access layers access levels (from Access Context Manager, ACM) on top of IAM so a request must also satisfy contextual conditions.

An access level is a named, reusable condition you attach to IAP. Two flavours:

Attribute Examples Needs
IP subnet Request must come from 203.0.113.0/24 (corporate egress)
Geographic region Allow only IN, US; block others
Required access levels Compose other levels
Device policy Screen lock on, disk encrypted, OS min version, company-owned, approved Endpoint Verification; richer signals need BeyondCorp Enterprise
Client certificate Present a valid certificate (mTLS) Certificate-based access
Identity / principal Restrict to certain users

Create a level and bind it to the resource:

# Create an access level: only company-owned, encrypted devices from India.
gcloud access-context-manager levels create trusted_device \
  --policy=POLICY_ID \
  --title="trusted-device-india" \
  --basic-level-spec=trusted-device.yaml

# Bind the access level to an IAP-protected HTTPS resource.
gcloud iap web set-iam-policy ... # via the access-settings / Console binding

When bound, a user who has roles/iap.httpsResourceAccessor (or tunnelResourceAccessor) is still denied unless the request satisfies the access level — identity AND context. This is how you implement “admins can reach the console, but only from a managed device on the corporate network.” Endpoint Verification (a free Chrome extension / agent) feeds the device signals; the deeper device-trust, threat, and data-protection signals require a BeyondCorp Enterprise licence.

Google Cloud Identity-Aware Proxy: zero-trust access for HTTPS apps and TCP/SSH

The diagram shows both planes: a browser and a service account reaching an Application Load Balancer where IAP authenticates, checks iap.httpsResourceAccessor plus an access level, and forwards a signed JWT to App Engine / Cloud Run / GKE / Compute backends; and an admin opening a --tunnel-through-iap SSH/RDP tunnel through iap.googleapis.com to a no-public-IP VM, gated by iap.tunnelResourceAccessor and the 35.235.240.0/20 firewall rule.

Hands-on lab

We build the TCP-forwarding path end to end (no LB or domain required, so it runs entirely on the Free Tier / $300 credit), then sketch the HTTPS path. Goal: SSH into a VM that has no external IP, purely through IAP, and prove the firewall range is what makes it work.

1. Set variables and enable the API.

PROJECT_ID=$(gcloud config get-value project)
ZONE=us-central1-a
gcloud services enable iap.googleapis.com compute.googleapis.com

2. Create a VM with NO external IP. The --no-address flag is the whole point.

gcloud compute instances create iap-lab-vm \
  --zone="$ZONE" \
  --machine-type=e2-micro \
  --image-family=debian-12 --image-project=debian-cloud \
  --no-address \
  --tags=iap-ssh

3. Add the IAP firewall rule. Allow SSH only from the IAP range, only to the tagged VM.

gcloud compute firewall-rules create allow-iap-ssh \
  --network=default --direction=INGRESS --action=ALLOW \
  --rules=tcp:22 \
  --source-ranges=35.235.240.0/20 \
  --target-tags=iap-ssh

4. Grant yourself the tunnel role.

ME=$(gcloud config get-value account)
gcloud projects add-iam-policy-binding "$PROJECT_ID" \
  --member="user:${ME}" \
  --role="roles/iap.tunnelResourceAccessor"

5. SSH through IAP.

gcloud compute ssh iap-lab-vm --zone="$ZONE" --tunnel-through-iap

Expected output: after a “Testing if tunnel connection works…” line, you land in a shell on iap-lab-vm — on a VM with no public IP, reached only via IAP.

6. Validation — prove the firewall is load-bearing. Exit, delete the rule, and retry:

exit
gcloud compute firewall-rules delete allow-iap-ssh -q
gcloud compute ssh iap-lab-vm --zone="$ZONE" --tunnel-through-iap
# Expect a timeout / "connection refused" — IAP authorized you, but the
# VPC firewall now drops the 35.235.240.0/20 traffic. Re-create the rule to fix.
gcloud compute firewall-rules create allow-iap-ssh \
  --network=default --direction=INGRESS --action=ALLOW \
  --rules=tcp:22 --source-ranges=35.235.240.0/20 --target-tags=iap-ssh

That failure is the lesson: IAM said yes, the firewall said no. Both gates must open.

7. (HTTPS sketch — optional, costs a few rupees while up.) Create an OAuth brand (gcloud iap oauth-brands create …), deploy a tiny App Engine app (gcloud app deploy), enable IAP (gcloud iap web enable --resource-type=app-engine), then gcloud iap web add-iam-policy-binding … --role=roles/iap.httpsResourceAccessor. Visit the app URL: you are redirected to Google sign-in, and only listed users get in.

Cleanup.

gcloud compute instances delete iap-lab-vm --zone="$ZONE" -q
gcloud compute firewall-rules delete allow-iap-ssh -q
# If you did step 7:
# gcloud app services delete default -q   # (App Engine can't be fully deleted; disable instead)

Cost note. An e2-micro is within the Free Tier allowance in eligible US regions; even outside it, a few hours costs a handful of rupees, and the VM has no external IP, so there is no public-IP charge. IAP itself is free — you pay only for the underlying resources (VM, and for the HTTPS path the load balancer / App Engine). Delete the VM and firewall rule to drop the bill to zero. BeyondCorp Enterprise (advanced device/threat signals) is a separately licensed add-on.

Common mistakes & troubleshooting

Symptom Likely cause Fix
--tunnel-through-iap SSH times out No firewall rule allowing 35.235.240.0/20 to port 22 Add ingress allow from 35.235.240.0/20 to the target port, scoped by tag/SA
Tunnel returns 403 / permission denied Caller lacks roles/iap.tunnelResourceAccessor Grant the tunnel role (ideally scoped to the instance)
Enabled IAP and now everyone is locked out of the app IAP is fail-closed; no httpsResourceAccessor bindings exist Add roles/iap.httpsResourceAccessor for the right users/groups
App still reachable bypassing IAP A backend has a public IP, or Cloud Run ingress allows direct traffic Remove public IPs; set Cloud Run ingress to “Internal and Cloud Load Balancing”; validate the JWT
User authenticates but is denied anyway A bound access level isn’t satisfied (device/IP/region) Check Context-Aware Access; verify Endpoint Verification / source IP
“OAuth consent screen not configured” when enabling IAP No brand exists for the project Create the brand (gcloud iap oauth-brands create) first
Backend trusts X-Goog-Authenticated-User-Email and gets spoofed Convenience headers are not cryptographically verified Validate x-goog-iap-jwt-assertion and pin the aud claim
GKE Ingress IAP not enforced BackendConfig not referenced by the Service, or OAuth secret missing Wire BackendConfig (with iap.enabled + secret) onto the Service
Service account curl gets 401 Wrong token audience Mint an ID token with the app’s OAuth client ID as --audiences

Best practices

Security notes

IAP is a control-plane gate, and its security rests on it being unavoidable. The two ways callers bypass IAP are (1) a second network path to the resource — a leftover public IP, a permissive firewall, or a Cloud Run/GKE service that accepts direct (non-LB) traffic — and (2) an application that trusts unverified input, i.e. believes the X-Goog-Authenticated-User-* headers without verifying the JWT. Close both: backends private and reachable only via the IAP-protected LB (or only via the IAP tunnel), and JWT validation with a pinned audience on every request.

IAP is identity-and-context, not a network firewall and not data-exfiltration control. It composes with — does not replace — VPC firewall rules (which still gate the 35.235.240.0/20 ingress) and VPC Service Controls (which draw a perimeter around Google managed APIs). A complete posture uses all three: IAP for who/what-context reaches my app/VM, firewall for L3/L4 reachability, VPC-SC for can data leave over the Google API surface. IAP enables true zero trust only when you also remove the implicit network trust it is meant to replace — so audit for stray public IPs and broad firewall rules continuously. Every IAP authorization and tunnel is recorded in Cloud Audit Logs / Data Access logs, giving you a per-request, per-identity trail that a VPN/bastion never provided — enable it and feed it to your SIEM.

Interview & exam questions

  1. What problem does IAP solve, and how is it different from a VPN? A VPN authenticates you onto the network once and then trusts the network; anyone (or anything compromised) on that network can move laterally. IAP trusts nothing by network location — it verifies identity and context at the proxy on every request to the specific resource. No tunnel onto a flat network, per-resource IAM, full audit, and it works for browser apps and SSH/RDP alike.

  2. A teammate enabled IAP on the app and now nobody can log in. Why? IAP is fail-closed: enabling it without any roles/iap.httpsResourceAccessor binding denies everyone. You must separately grant the accessor role to the intended users/groups. (Enable and authorize are two steps.)

  3. You set up gcloud compute ssh --tunnel-through-iap and it times out. First thing to check? The VPC firewall: there must be an ingress-allow rule for source range 35.235.240.0/20 to TCP 22 on the target VM. The tunnelled traffic arrives from that Google range, not from your IP. (Then check the user has roles/iap.tunnelResourceAccessor.)

  4. Why must the application validate the IAP JWT instead of reading X-Goog-Authenticated-User-Email? Those plaintext headers can be spoofed if a request reaches the backend without passing through IAP (stray public IP, direct Cloud Run hit). The signed x-goog-iap-jwt-assertion can only be minted by Google for your resource; validating its signature and pinning the aud claim makes bypass impossible.

  5. Which IAM roles gate IAP, and how do they differ? roles/iap.httpsResourceAccessor allows reaching an IAP-protected HTTPS resource; roles/iap.tunnelResourceAccessor allows opening an IAP TCP tunnel (SSH/RDP) to a VM. roles/iap.admin manages IAP configuration. They are separate — HTTPS access does not grant tunnel access.

  6. How does IAP let you remove bastion hosts? VMs get no external IP; admins open an encrypted IAP tunnel to iap.googleapis.com, identity-checked via tunnelResourceAccessor, which forwards SSH/RDP from inside Google’s network. The only allowed ingress is 35.235.240.0/20. No internet-exposed bastion to patch, monitor, or get breached.

  7. What is Context-Aware Access and when do you use it? Binding Access Context Manager access levels (device posture, IP/CIDR, geographic region, client certificate) to IAP so a request must satisfy identity AND context. Use it to require, say, a company-owned, encrypted device on the corporate network for an admin console — a correct password alone won’t get in.

  8. How does a service account or CI job call an IAP-protected HTTPS app? Present an OIDC ID token whose audience is the app’s OAuth client ID (gcloud auth print-identity-token --audiences=CLIENT_ID...), in the Authorization: Bearer header; the caller’s identity must hold httpsResourceAccessor. For external automation, combine with Workload Identity Federation so there’s no key.

  9. Can IAP protect Cloud Run, GKE, and on-prem workloads? Yes — Cloud Run via a serverless NEG behind an Application LB (set ingress to internal+LB), GKE via a BackendConfig on the Ingress/Gateway, and on-prem/hybrid via an internet/hybrid NEG behind the LB. App Engine has IAP built in directly.

  10. IAP vs VPC Service Controls — what’s the difference? IAP gates access to your applications and VMs (the app data path). VPC-SC draws a perimeter around Google managed APIs (Storage, BigQuery, etc.) to stop data exfiltration over the control plane. Different layers; you typically use both.

  11. What is the OAuth “brand” and why does IAP need it? The brand is the OAuth consent screen (“Sign in to App”) shown to browser users; IAP authenticates via OAuth, so a brand (Internal for corporate apps) must exist before you can enable HTTPS IAP. Some backends also need an explicit OAuth client (client-id/secret), notably GKE BackendConfig.

  12. Does IAP cost anything, and what’s BeyondCorp Enterprise? IAP itself is free; you pay only for the underlying resources (VM, LB, App Engine). BeyondCorp Enterprise is the licensed tier adding advanced device-trust, threat, and data-protection signals (richer Context-Aware Access) on top of the free IAP/ACM building blocks.

Quick check

  1. Which firewall source range must you allow for IAP TCP forwarding to SSH/RDP a VM?
  2. You enabled IAP on a web app and everyone is locked out. What did you forget?
  3. Which header should your backend cryptographically validate, and which claim must you pin?
  4. Which IAM role lets a user open an IAP SSH tunnel to a VM?
  5. Name two contextual conditions an Access Context Manager access level can enforce.

Answers

  1. 35.235.240.0/20 — ingress allow to the target TCP port, scoped by tag/service account (never 0.0.0.0/0).
  2. To grant roles/iap.httpsResourceAccessor to the intended users/groups — IAP is fail-closed, so enabling it without bindings denies everyone.
  3. Validate x-goog-iap-jwt-assertion (the signed JWT) and pin the aud (audience) claim to your exact backend; don’t trust the plaintext X-Goog-Authenticated-User-* headers alone.
  4. roles/iap.tunnelResourceAccessor (ideally scoped to a single instance via an IAM Condition).
  5. Any two of: device policy (managed/encrypted/screen-locked), IP subnet/CIDR, geographic region, client certificate (and required-access-level composition).

Exercise

Design and document (no need to fully deploy) zero-trust access for a fictional company’s two assets: an internal admin web console (Cloud Run) and a fleet of Linux app servers (Compute Engine, no public IP). For the console: choose external vs internal ALB, set the Cloud Run ingress, list the exact IAP role and the group you’d bind it to, and write the JWT-validation steps (which header, which aud value, which issuer) your app must perform. For the app servers: write the firewall rule (source range, ports, target scoping), the IAP tunnel role bound to one instance via an IAM Condition, and the gcloud command an SRE runs to SSH in. Finally, add one Context-Aware Access level (“company-owned, encrypted device, from India only”) and explain, in two sentences, how identity and context are AND-ed. Bonus: explain how a GitHub Actions job (no key) would call the admin console’s API.

Certification mapping

Glossary

Next steps

GCPIAPZero TrustBeyondCorpSecurityIAM
Need this built for real?

Vinod is a Senior Cloud Architect (22+ yrs) — available for Azure / AWS / GCP architecture, landing zones, and migrations.

Work with me

Comments