Chapter 4
Federation and Zero-Trust Identity
Scope. This chapter covers how an identity that does not exist in Google Cloud becomes a principal that Google Cloud IAM can authorize: Workforce Identity Federation for people, Workload Identity Federation for machines, and the specific configurations for GitHub, GitLab, AWS, Azure, and Kubernetes. It then covers the zero-trust access layer built on those identities — BeyondCorp principles, Context-Aware Access, Identity-Aware Proxy, and device trust — and ends with the elimination of VPN-dependent administration. IAP as a load balancer front door is §7.17; the IAM model itself is Chapter 3. Prerequisites. Chapter 3, particularly §3.6 (workload identities), §3.7 (workforce identities), §3.24 (impersonation), and §3.25 (why keys are an anti-pattern). Verified against. Google Cloud console and API surface as of 2026-09, Cloud SDK 583.0.0; see sources at end.
Federation is the mechanism that makes Chapter 3's ambition achievable. Every argument for eliminating service account keys ends at the same question — then how does the GitHub Actions job authenticate? — and federation is the answer. An external system presents a token its own identity provider signed, Google's Security Token Service verifies that signature against the provider's published keys, applies the attribute mapping and attribute condition you configured, and returns a short-lived Google credential. No secret was stored anywhere.
The security of the whole arrangement rests on one thing that is easy to get wrong: the attribute condition. A workload identity pool provider trusts an issuer, and an issuer such as token.actions.githubusercontent.com signs tokens for every repository on GitHub. A provider with no attribute condition, or a permissive one, is an open door — anyone who can create a public repository can obtain a token from that issuer. Most federation incidents are this mistake, and it is why every configuration in this chapter pins the identity down to a specific numeric repository, group, or role.
The second half of the chapter is the human side. Zero trust means the network position of a request carries no authority: being inside the VPC, on the VPN, or on the corporate range is not a credential. What replaces it is a per-request decision combining identity, device posture, and context, enforced by Identity-Aware Proxy in front of both applications and administrative access. The practical outcome is specific and measurable: no bastion host with a public IP, no VPN required to administer a VM, and no SSH key in project metadata.
4.1 Identity Federation Concepts §
Federation is the exchange of a credential issued by an identity provider you trust for a credential the resource provider accepts. Google Cloud implements it with the Security Token Service following the OAuth 2.0 Token Exchange specification, RFC 8693.
The flow, identically for people and machines:

Four configuration objects define the trust:
| Object | Purpose |
|---|---|
| Pool | a namespace of external identities; the trust boundary |
| Provider | the relationship to one IdP: issuer, protocol, keys |
| Attribute mapping | CEL turning IdP claims into google.subject, google.groups, attribute.NAME |
| Attribute condition | CEL that must be true for the assertion to be accepted at all |
The design rule that prevents most incidents: the mapping decides who this is; the condition decides whether we talk to them at all. Put the security decision in the condition. A mapping that produces a distinctive subject is not a substitute for a condition that rejects everything else, because IAM bindings can be added by anyone with policy-write access, while the condition is a property of the provider.
Pitfall. Federation removes the key, not the privilege. A federated principal with a broad role is exactly as dangerous as a key with a broad role — with the advantage that you cannot lose it in a Git commit, and the disadvantage that its blast radius is easier to overlook because there is no secret to inventory.
4.2 Workforce Identity Federation §
Workforce Identity Federation admits people from an external IdP — Okta, Entra ID, AD FS, Ping — into Google Cloud without provisioning Cloud Identity accounts for them. Google describes it as "sync-less": there is no Google Cloud Directory Sync, no user objects to reconcile, and no shadow directory.
Structure. Workforce pools live at the organization level in the global location, and the pool ID must be globally unique across all of Google Cloud. Providers under the pool speak OIDC or SAML 2.0.
gcloud iam workforce-pools create partners \
--organization=123456789012 \
--location=global \
--display-name="Partner engineers" \
--description="Federated from the partner IdP; no Cloud Identity accounts." \
--session-duration=3600s
gcloud iam workforce-pools providers create-oidc partner-idp \
--workforce-pool=partners \
--location=global \
--issuer-uri="https://idp.partner.example/oidc" \
--client-id="CLIENT_ID" \
--attribute-mapping="google.subject=assertion.sub,\
google.groups=assertion.groups,attribute.department=assertion.department" \
--web-sso-response-type=code \
--web-sso-assertion-claims-behavior=merge-user-info-over-id-token-claims
Principal identifiers (bind to sets, not to subjects):
principal://iam.googleapis.com/locations/global/workforcePools/POOL_ID/subject/SUBJECT
principalSet://iam.googleapis.com/locations/global/workforcePools/POOL_ID/group/GROUP_ID
principalSet://iam.googleapis.com/locations/global/workforcePools/POOL_ID/attribute.NAME/VALUE
principalSet://iam.googleapis.com/locations/global/workforcePools/POOL_ID/*
The last form — every identity in the pool — should appear in no production binding. Bind to group/ or attribute./, mapped from a claim your IdP controls, so the external directory continues to drive access.
Console access. Federated users sign in at the workforce console, a distinct entry point from console.cloud.google.com, using the pool's sign-in URL. Not every console surface supports federated users; verify the products your users need before committing.
When to choose it over Cloud Identity accounts. Contractors, partners, and enterprises unwilling to duplicate their directory. Permanent staff are better served by Cloud Identity accounts, which can own resources and use the full console and Workspace surface (§2.2).
Pitfall. Set --session-duration deliberately. Workforce pool principals are not Cloud Identity users, so Workspace-side session controls do not apply to them; the pool's own session duration is the only lever.
4.3 Workload Identity Federation §
Workload Identity Federation is the machine counterpart, and the single most important control in this book's elimination of service account keys (§3.26).
Pools and providers. Pools live in a project and are addressed by project number. Google recommends a separate pool per external environment — a development pool and a production pool, not one pool with conditions distinguishing them. Providers exist for AWS, OIDC (GitHub, GitLab, Okta, AD FS, Kubernetes, Terraform Cloud), and SAML.
gcloud iam workload-identity-pools create github-prod \
--project=rc-saas-shared-cicd-01 \
--location=global \
--display-name="GitHub Actions — production deploys"
Attribute mapping uses CEL over the assertion:
| Mapped attribute | Rule |
|---|---|
google.subject | required, must be unique, maximum 127 characters |
google.groups | optional, enables principalSet://.../group/GROUP_ID bindings |
attribute.NAME | up to 50 custom attributes, usable in conditions and principalSet:// |
Mappings can transform: attribute.username=assertion.email.split("@")[0], or attribute.env=assertion.arn.contains(":prod") ? "prod" : "test".
Two access models, and the choice is visible in the last hop:

- Direct resource access — grant the
principal://orprincipalSet://identity a role directly on the resource. Fewer moving parts, no service account at all. - Service account impersonation — grant the federated identity
roles/iam.workloadIdentityUseron a service account, and let it act as that account. Preferred where the workload needs a stable identity that other IAM policies already reference, and where you want the extra revocation point (§3.24).
The rule about pool-wide bindings. Granting a role to principalSet://.../workloadIdentityPools/POOL_ID/* grants it to every identity the provider will ever accept. Google's documentation warns about this directly. Bind to attribute.*/VALUE, always.
Pitfall. Pool and provider IDs cannot be reused after deletion within the same project for a retention period, and the principal identifiers embed the project number, not the project ID. Automation that templates the project ID into a principalSet:// string produces a binding that silently matches nothing.
4.4 Federation with External Identity Providers §
Whatever the IdP, the same five decisions determine whether the configuration is safe.
1. Which issuer do you trust? The --issuer-uri must be the IdP's exact issuer, and the provider fetches its signing keys from that issuer's OIDC discovery document. For a private IdP with no internet-reachable discovery endpoint, supply the key set directly with --jwk-json-path.
2. What makes a subject unique? google.subject must be stable and unique across the issuer's whole population. An email address is a poor choice — it can be reassigned. A GUID or a numeric ID is better. Where an IdP's sub is unique only within a tenant, namespace it: google.subject='tenant-a::' + assertion.sub.
3. What is the attribute condition? The condition should pin the assertion to identities you control, using numeric identifiers wherever the IdP offers them. Google's own guidance is explicit that name-based fields such as repository and repository_owner are vulnerable to cybersquatting and typosquatting, and that the *_id numeric equivalents should be preferred.
4. Direct access or impersonation? §4.3.
5. What is the blast radius if the IdP is compromised? A federated trust is a delegation of authentication. If the IdP can be made to issue an assertion with an arbitrary sub, your attribute condition is the only remaining control. Separate pools per environment mean a compromised non-production IdP tenant cannot reach production.
gcloud iam workload-identity-pools providers describe PROVIDER_ID \
--workload-identity-pool=github-prod \
--project=rc-saas-shared-cicd-01 \
--location=global \
--format="value(attributeCondition,attributeMapping,oidc.issuerUri)"
Pitfall. An IdP that allows users to set their own claim values — a self-service SaaS with an editable profile field — makes any condition on that field worthless. Condition only on claims the IdP itself asserts.
4.5 OIDC Federation §
OpenID Connect is the protocol behind most workload federation. The provider verifies the ID token's signature against the issuer's JWKS, checks iss, aud, and expiry, applies the mapping, then applies the condition.
gcloud iam workload-identity-pools providers create-oidc okta-workloads \
--project=rc-saas-shared-cicd-01 \
--location=global \
--workload-identity-pool=github-prod \
--issuer-uri="https://rickcollette.okta.example/oauth2/default" \
--allowed-audiences="//iam.googleapis.com/projects/123456789012/locations/\
global/workloadIdentityPools/github-prod/providers/okta-workloads" \
--attribute-mapping="google.subject=assertion.sub,attribute.client_id=assertion.cid" \
--attribute-condition="assertion.cid == 'APPROVED_CLIENT_ID'"
--allowed-audiences matters. Without it, the provider accepts the default audience, which is the provider's own full resource name. Setting an explicit allowed audience means a token minted for a different relying party at the same issuer is rejected — a real defense against token replay across services sharing an IdP.
Claims worth conditioning on, in rough order of trustworthiness: numeric tenant and subject IDs, the aud value, service-principal identifiers, and — last — anything derived from a display name or email.
Pitfall. OIDC discovery is fetched over the internet. A provider whose issuer is behind a firewall will fail to configure; use --jwk-json-path to supply the key material inline, and then own the rotation, because Google can no longer fetch new keys automatically.
4.6 SAML Federation §
SAML 2.0 federation is used mainly for workforce identities and for enterprise IdPs that do not offer OIDC for the flow in question. The provider is configured with the IdP's metadata document rather than an issuer URI.
gcloud iam workload-identity-pools providers create-saml adfs-workloads \
--project=rc-saas-shared-cicd-01 \
--location=global \
--workload-identity-pool=onprem-prod \
--idp-metadata-path=adfs-metadata.xml \
--attribute-mapping="google.subject=assertion.subject,\
attribute.group=assertion.attributes['group'][0]" \
--attribute-condition="'platform-automation' in assertion.attributes['group']"
Operational differences from OIDC that matter:
- Certificate rotation is yours. The metadata document embeds the IdP's signing certificate. When the IdP rotates it, you must re-upload metadata; there is no automatic discovery refresh as with a JWKS endpoint. Put a calendar reminder ahead of certificate expiry, because the failure mode is a total outage of that federation path.
- Assertion attributes are multi-valued.
assertion.attributes['group']is a list, so conditions useinor index explicitly. Getting this wrong produces a condition that silently never matches. - Assertion replay is bounded by the assertion's own validity window, which is typically longer than an OIDC token's. Prefer OIDC where the IdP offers both.
Pitfall. SAML metadata files are frequently pasted from an email. Verify the certificate fingerprint out of band before uploading; a metadata file is a complete authentication trust decision in one XML document.
4.7 GitHub Actions Federation §
This is the canonical replacement for a service account key in CI, and the configuration most often done insecurely.
gcloud iam workload-identity-pools providers create-oidc github-prod-provider \
--project=rc-saas-shared-cicd-01 \
--location=global \
--workload-identity-pool=github-prod \
--issuer-uri="https://token.actions.githubusercontent.com/" \
--attribute-mapping="google.subject=assertion.sub,\
attribute.repository_id=assertion.repository_id,\
attribute.repository_owner_id=assertion.repository_owner_id,\
attribute.ref=assertion.ref" \
--attribute-condition="assertion.repository_owner_id == 'NUMERIC_ORG_ID' \
&& assertion.repository_id == 'NUMERIC_REPO_ID' \
&& assertion.ref == 'refs/heads/main'"
Then bind the pinned attribute — never the pool — to the deploy account:
gcloud iam service-accounts add-iam-policy-binding \
sa-deploy-prod@rc-saas-shared-cicd-01.iam.gserviceaccount.com \
--project=rc-saas-shared-cicd-01 \
--member="principalSet://iam.googleapis.com/projects/123456789012/locations/\
global/workloadIdentityPools/github-prod/attribute.repository_id/NUMERIC_REPO_ID" \
--role="roles/iam.workloadIdentityUser"
Available claims include sub, repository, repository_id, repository_owner, repository_owner_id, ref, workflow, environment, and job_workflow_ref.
Use the numeric *_id claims. Google's guidance is explicit: name-based claims such as repository and repository_owner invite cybersquatting and typosquatting — a deleted organization name can be re-registered by someone else, and your condition would then match their repository. Numeric IDs cannot be reclaimed.
Additional hardening worth applying:
- Condition on
assertion.ref == 'refs/heads/main'so a pull-request branch cannot deploy, and use GitHub environments with required reviewers plusassertion.environmentfor production. - Condition on
job_workflow_refto pin the exact reusable workflow, so a new workflow file in the same repository cannot assume the identity. - One pool and provider per environment; the production provider's condition names only the production ref and environment.
Anti-pattern. An attribute condition of assertion.repository_owner == 'rickcollette' alone. It trusts a mutable name, and it grants every repository in the organization — including one a compromised developer account creates — the ability to deploy to production.
4.8 GitLab Federation §
GitLab SaaS federation follows the same shape with GitLab's issuer and claims.
gcloud iam workload-identity-pools providers create-oidc gitlab-prod-provider \
--project=rc-saas-shared-cicd-01 \
--location=global \
--workload-identity-pool=gitlab-prod \
--issuer-uri="https://gitlab.com" \
--attribute-mapping="google.subject=assertion.sub,\
attribute.project_id=assertion.project_id,\
attribute.namespace_id=assertion.namespace_id,\
attribute.environment=assertion.environment" \
--attribute-condition="assertion.namespace_id == 'NUMERIC_GROUP_ID' \
&& assertion.project_id == 'NUMERIC_PROJECT_ID' \
&& assertion.environment == 'production'"
Available claims include sub, namespace_id, project_id, user_id, environment, and ref_path. As with GitHub, condition on the numeric IDs.
GitLab-specific notes:
- The
environmentclaim is present only when the job declares a GitLab environment. Conditioning on it therefore also enforces that production deploys run through a declared, protected environment — a useful coupling. - Self-managed GitLab uses your own instance URL as the issuer, which must be publicly reachable for OIDC discovery or supplied via
--jwk-json-path. - GitLab's
subclaim encodes project path, ref type, and ref. It is human-readable and therefore mutable; map it for identity but condition on the numeric fields.
Pitfall. GitLab group renames change path-based claims and leave numeric ones alone — another reason the numeric fields belong in the condition.
4.9 External CI/CD Systems §
The same pattern extends to any OIDC-issuing CI system: Terraform Cloud, CircleCI, Jenkins with an OIDC plugin, Buildkite, Azure DevOps.
The checklist for onboarding a new system:
- Confirm the system issues an OIDC token with a verifiable issuer and publishes a JWKS.
- Identify the claim that uniquely and immutably names the specific pipeline, not the account or organization.
- Create a dedicated pool for that system in the CI/CD project, per environment.
- Write an attribute condition pinning that claim, plus any environment or branch claim available.
- Set
--allowed-audiencesexplicitly. - Grant
roles/iam.workloadIdentityUseron a per-environment deploy service account, bound toattribute.*/VALUE(§3.34). - Verify by attempting the exchange from a pipeline that should be rejected, and confirming it is.
Step 7 is the one that gets skipped and the one that catches the mistake. A condition is a security control; an untested security control is a hypothesis.
Where a system cannot federate. Some CI products still offer no OIDC token. The fallback is not a key in that system's secret store — it is to move the deployment step to a system that can federate, and let the non-federating system trigger it. Where that is genuinely impossible, apply the §3.25 exception process: narrow account, key in Secret Manager, enforced rotation, source-IP alerting, documented expiry.
Pitfall. Self-hosted runners inherit the runner's network position and any credentials on the host, and they can be scheduled by any job in the repository. A federated identity on a shared self-hosted runner is only as isolated as the runner.
4.10 AWS-to-GCP Federation §
AWS federation uses a dedicated provider type. Instead of an OIDC token, the workload signs a GetCallerIdentity request with its AWS credentials; Google verifies it against AWS STS and derives the caller's ARN.
gcloud iam workload-identity-pools providers create-aws aws-prod-provider \
--project=rc-saas-shared-cicd-01 \
--location=global \
--workload-identity-pool=aws-prod \
--account-id="AWS_ACCOUNT_ID" \
--attribute-mapping="google.subject=assertion.arn,\
attribute.aws_role=assertion.arn.extract('assumed-role/{role}/')" \
--attribute-condition="attribute.aws_role == 'northbound-replication-role'"
What the condition must do. --account-id pins the AWS account, which is necessary but not sufficient — every role and user in that account can otherwise obtain a Google credential. Condition on the extracted role name so only the intended IAM role is accepted. Google's documentation frames this explicitly as preventing the confused deputy problem.
Where this pattern is right: an AWS workload that must read a GCS bucket or publish to Pub/Sub during a migration, cross-cloud replication, and multi-cloud data pipelines. It is materially better than the alternative, which is an exported Google service account key sitting in AWS Secrets Manager.
Reciprocity. The reverse direction — a GCP workload authenticating to AWS — uses AWS's own OIDC federation against Google's identity token issuer, and is configured on the AWS side. The two directions are independent; configuring one does not imply the other.
Pitfall. assertion.arn for an assumed role includes the session name, which varies per session. Mapping google.subject=assertion.arn therefore produces a subject that changes between sessions, which breaks principal:// bindings. Bind on the extracted attribute.aws_role instead, as above.
4.11 Azure-to-GCP Federation §
Azure workloads federate through the OIDC provider type, using Microsoft Entra ID as issuer and an Azure managed identity or app registration as the subject.
gcloud iam workload-identity-pools providers create-oidc azure-prod-provider \
--project=rc-saas-shared-cicd-01 \
--location=global \
--workload-identity-pool=azure-prod \
--issuer-uri="https://sts.windows.net/AZURE_TENANT_ID/" \
--allowed-audiences="api://AzureADTokenExchange" \
--attribute-mapping="google.subject=assertion.sub,\
attribute.tenant=assertion.tid,attribute.appid=assertion.appid" \
--attribute-condition="assertion.tid == 'AZURE_TENANT_ID' && assertion.appid == 'AZURE_APP_ID'"
The two claims that must both be pinned:
tid— the Entra tenant. Without it, any Entra tenant on earth is trusted, becausests.windows.netserves all of them from per-tenant issuer URLs but token contents are attacker-influenceable if the issuer is templated loosely.appidoroid— the specific application or managed identity. Without it, every workload in your tenant can obtain the credential.
Azure DevOps pipelines use a workload identity service connection, which issues tokens with the same Entra issuer; the condition should additionally pin the service connection's application ID.
Pitfall. Entra issues tokens from both https://sts.windows.net/TENANT/ (v1) and https://login.microsoftonline.com/TENANT/v2.0 (v2), with different claim shapes — notably appid in v1 versus azp in v2. Configure the provider for the version your workload actually requests, and verify with a real token rather than assuming.
4.12 Kubernetes Workload Identity §
Workload Identity Federation for GKE lets a pod obtain Google credentials from its Kubernetes ServiceAccount, with no node-level key and no secret in the cluster.
The fixed pool. Enabling it on a cluster creates a pool named PROJECT_ID.svc.id.goog. The name cannot be changed, and the pool persists even if every cluster in the project is deleted.
gcloud container clusters create rc-saas-prod-gke-01 \
--project=rc-saas-prod-app-01 \
--region=us-central1 \
--workload-pool=rc-saas-prod-app-01.svc.id.goog \
--workload-metadata=GKE_METADATA \
--enable-shielded-nodes \
--enable-private-nodes
Two binding models, and Google now recommends the first:
Direct principal binding — grant IAM roles straight to the Kubernetes ServiceAccount, with no Google service account in the middle:
gcloud projects add-iam-policy-binding rc-saas-prod-app-01 \
--member="principal://iam.googleapis.com/projects/123456789012/locations/\
global/workloadIdentityPools/rc-saas-prod-app-01.svc.id.goog/subject/\
ns/api/sa/api-runtime" \
--role="roles/pubsub.publisher"
Service account impersonation — the older pattern: annotate the Kubernetes ServiceAccount with a Google service account and grant it roles/iam.workloadIdentityUser. Still valid, and still preferable where other IAM policies already reference the Google service account by email.
How it works underneath. A GKE metadata server runs on every node — a DaemonSet on Linux — intercepting requests to metadata.google.internal. It obtains a Kubernetes ServiceAccount token from the API server and exchanges it through STS for a federated access token, defaulting to a one-hour lifetime that client libraries refresh automatically.
Three restrictions to design around:
- Pods using host networking bypass Workload Identity entirely and reach the Compute Engine metadata server instead, receiving the node's service account. This is a privilege escalation path: keep the node service account minimal, and deny
hostNetworkin admission policy. - The metadata server enforces a 500 concurrent connection limit per node.
--workload-metadata=GKE_METADATAmust be set on node pools; a pool left on the legacy metadata mode exposes the node identity to pods.
Pitfall. The Kubernetes ServiceAccount subject encodes namespace and name (ns/NAMESPACE/sa/NAME). Anyone who can create a ServiceAccount with that name in that namespace obtains the binding, so namespace creation is a privileged operation in a Workload Identity cluster.
4.13 Human vs. Machine Identity §
The two identity classes need different controls, and applying one class's controls to the other is a recurring design error.
| Human | Machine | |
|---|---|---|
| Authentication | interactive, MFA, phishing-resistant | non-interactive token exchange |
| Lifecycle | joiner-mover-leaver in the directory | created and destroyed with the workload |
| Right pattern | Cloud Identity or Workforce Identity Federation | attached SA, WIF, GKE Workload Identity |
| Standing privilege | none in production; PAM on demand (§3.27) | narrow standing roles are correct |
| Session length | short, revocable, device-conditioned | one hour, auto-refreshed |
| Correct policy binding | group: or principalSet://.../group/ | serviceAccount: or principalSet://.../attribute./ |
| Audit question | who was this person, on what device | which pipeline, from which repo and ref |
What follows from the table:
- MFA is meaningless for machines, so the equivalent control is the attribute condition — it is what proves the workload is the one you meant.
- JIT elevation is meaningless for machines in the interactive sense; a pipeline's privilege is bounded by scoping and by separating plan from apply (§3.34).
- Device posture is meaningless for machines; the equivalent is workload attestation and admission control (Chapter 37).
- A human should never authenticate as a service account with a key, and a machine should never authenticate as a human. Both happen, and both erase the audit distinction that makes the logs usable.
Pitfall. Shared "team" accounts. A human identity used by several people, or a service account used interactively by a human, produces log entries nobody can attribute. Where a human genuinely needs a service account's privileges, impersonation is the answer, because it logs both identities (§3.24).
4.14 BeyondCorp Principles §
BeyondCorp is Google's internal implementation of zero trust, published as a model and productized for customers. The current commercial packaging is Chrome Enterprise Premium, which is the branding that replaced BeyondCorp Enterprise; Chrome Enterprise Core is included with Google Cloud at no cost, and the premium tier carries the advanced access controls and data protections.
The principles, stated as design constraints:
- Network location grants no authority. Being on the corporate LAN, on the VPN, or inside the VPC is not a credential. The internal network is treated as hostile.
- Access is granted per request, not per session, and re-evaluated as context changes.
- The decision inputs are identity, device, and context — who, on what machine, from where, at what time, with what signals.
- All access is authenticated, authorized, and encrypted, including access to internal applications.
- Applications are published, not tunneled to. A user reaches an application through a proxy that enforces policy, rather than being placed on a network that contains it.
The Google Cloud components that implement them:
| Principle | Component |
|---|---|
| Per-request decision at the application | Identity-Aware Proxy (§4.16, §7.17) |
| Context as a policy input | Access Context Manager access levels (§4.15) |
| Device posture as a signal | Endpoint Verification and third-party partners (§4.17) |
| Strong identity | Cloud Identity with phishing-resistant MFA, or Workforce Identity Federation (§4.2) |
| No network-position authority for administration | IAP TCP forwarding (§4.18, §4.19) |
| Data perimeter independent of identity | VPC Service Controls (Chapter 20) |
What BeyondCorp does not give you. It is an access model, not a data-exfiltration control. An authorized user on a compliant device can still copy data to an unauthorized destination; that is what VPC Service Controls addresses, and the two are complementary rather than alternatives.
4.15 Context-Aware Access §
Context-Aware Access evaluates request attributes beyond identity and feeds the result into the authorization decision. The policy objects are access levels, managed by Access Context Manager at the organization.
Signals available to an access level: IP ranges (including corporate egress ranges), geographic region, device policy (screen lock, disk encryption, OS version, whether the device is corporate-owned or company-managed), and third-party device signals from partners such as CrowdStrike and Tanium.
gcloud access-context-manager policies list --organization=123456789012
gcloud access-context-manager levels create trusted_corp_device \
--policy=POLICY_ID \
--title="Corporate managed device from an approved region" \
--basic-level-spec=trusted-device.yaml \
--combine-function=AND
# trusted-device.yaml
- devicePolicy:
requireScreenlock: true
requireCorpOwned: true
osConstraints:
- osType: DESKTOP_MAC
minimumVersion: "14.0.0"
- osType: DESKTOP_WINDOWS
minimumVersion: "10.0.0"
regions:
- US
- GB
Access levels are then consumed in two places:
- In IAM conditions, via
request.auth.access_levels, so a role applies only from a compliant context (§3.18). - In IAP access policy, so an application is reachable only from a compliant context (§4.16).
# condition-managed-device.yaml
title: managed-device-only
description: Operator access requires a managed device.
expression: |-
"accessPolicies/POLICY_ID/accessLevels/trusted_corp_device"
in request.auth.access_levels
gcloud projects add-iam-policy-binding rc-saas-prod-app-01 \
--member="group:gcp-app-operators@rickcollette.domain" \
--role="roles/compute.instanceAdmin.v1" \
--condition-from-file=condition-managed-device.yaml
Design rule. Start with a permissive access level in a dry-run posture, measure how many legitimate sessions would fail, then tighten. An access level that excludes half the workforce during an incident is an outage you created.
Pitfall. Access levels are evaluated on signals the client supplies through Endpoint Verification. A device with the extension uninstalled has no signals rather than failing signals, so the level must be written to require the positive attestation rather than to exclude known-bad values.
4.16 Identity-Aware Proxy §
Identity-Aware Proxy establishes a central authorization layer in front of resources, so the application itself never sees an unauthenticated request. This section owns IAP as a zero-trust access control; its role as a load balancer front door is §7.17.
What IAP protects: App Engine applications, Cloud Run services (directly or through a load balancer backend), Compute Engine and GKE workloads behind external or internal Application Load Balancers, on-premises applications through the IAP connector, and — the case this section cares about — TCP forwarding for administrative SSH and RDP to VMs in Google Cloud and other clouds.
The two IAM roles:
| Role | Grants |
|---|---|
roles/iap.httpsResourceAccessor | reach an IAP-protected web resource |
roles/iap.tunnelResourceAccessor | open an IAP TCP tunnel to a VM |
Both are grantable at the project or on the individual resource, and both accept IAM conditions — which is where access levels enter (§4.15).
Administrative access with no bastion and no public IP:
gcloud compute ssh api-01 \
--project=rc-saas-prod-app-01 \
--zone=us-central1-a \
--tunnel-through-iap \
--internal-ip
gcloud compute start-iap-tunnel db-01 3306 \
--project=rc-saas-prod-app-01 \
--zone=us-central1-a \
--local-host-port=localhost:3306
For the tunnel to work, a firewall rule must allow ingress from IAP's forwarding range 35.235.240.0/20 to the target ports; that is the only ingress the VM needs, and it replaces every rule that previously allowed SSH from an office range.
Signed headers. For HTTPS resources, IAP adds a signed JWT header identifying the authenticated user. Applications must verify that JWT — checking signature, issuer, and audience — rather than trusting a plain header, because an application reachable by any path other than IAP would otherwise accept a forged identity. Verifying the signed header is what makes IAP an authentication mechanism rather than a suggestion.
gcloud iap settings get --project=rc-saas-prod-app-01 --resource-type=compute
Pitfall. IAP protects the front door only. A VM that also has a public IP, or a Cloud Run service whose ingress setting still permits direct internet traffic, can be reached without passing IAP at all. Pair IAP with constraints/compute.managed.vmExternalIpAccess and ingress settings that admit only the load balancer.
4.17 Device Trust §
Device trust makes the endpoint a factor in the access decision. On Google Cloud the signal source is Endpoint Verification, delivered as a Chrome extension and a native helper, reporting attributes such as OS and version, screen lock, disk encryption, whether the device is company-owned, and whether it is managed by your MDM. Third-party partners including CrowdStrike and Tanium can supply additional signals.
How the signal becomes a control:
Endpoint Verification → device attributes in the directory
→ Access Context Manager access level (§4.15)
→ IAM condition on request.auth.access_levels (API and console access)
→ IAP access policy (application and SSH access)
A realistic tiering:
| Tier | Device requirement | What it may reach |
|---|---|---|
| Unmanaged | none | nothing in production |
| Managed | screen lock, disk encryption, current OS | production read, non-production write |
| Corporate-owned + MDM | all of the above plus requireCorpOwned | production write via PAM, break-glass |
Security posture. Device trust is a compensating control for credential theft. Phishing-resistant MFA stops most credential attacks; device trust stops the residue, because a stolen credential replayed from an attacker's machine produces no valid device attestation. The two together are the practical zero-trust baseline for human access.
Pitfall. Device signals are collected by software running on a device the user controls. They raise the cost of an attack; they are not attestation in the TPM sense. Do not treat a device signal as sufficient on its own for the most privileged operations — those get a second person's approval (§3.27) as well.
4.18 Zero-Trust Administrative Access §
Administrative access is where zero trust either happens or does not. The target design, assembled from the components in this chapter and Chapter 3:
The path a production administrative action takes:
- The engineer authenticates to Cloud Identity or a workforce pool with phishing-resistant MFA.
- Endpoint Verification supplies device signals; the request satisfies the
trusted_corp_deviceaccess level (§4.15, §4.17). - The engineer holds only read roles by standing grant (§3.35).
- For a change, they request a PAM grant with a justification; an approver in a different group consents (§3.27).
- The grant issues a time-bound role binding, itself conditioned on the access level.
- Host access, where needed, is
gcloud compute ssh --tunnel-through-iapto a VM with no external IP, authorized byroles/iap.tunnelResourceAccessorand OS Login (§4.16). - Every step — sign-in, access level evaluation, grant request, approval, tunnel establishment, and the API calls themselves — is logged and correlated by principal and time.
What is absent from that path, deliberately: a VPN, a bastion host with a public IP, an SSH key in project metadata, a standing admin role, and a shared administrative account.
OS Login is the other half of host access. With constraints/compute.managed.requireOsLogin enforced, SSH keys in metadata are ignored and Linux account access is governed by IAM roles (roles/compute.osLogin for unprivileged access, roles/compute.osAdminLogin for sudo). That makes host login an IAM decision with an audit trail, rather than a key-distribution problem.
gcloud compute os-login ssh-keys list --format="value(key.fingerprint,key.expirationTimeUsec)"
Pitfall. A "temporary" bastion created during a migration and never removed. Detect it: any VM with an external IP in a production project is a finding, and constraints/compute.managed.vmExternalIpAccess should make creating one impossible.
4.19 Eliminating VPN-Dependent Administration §
A VPN grants network reachability to everyone who connects and then relies on separate controls to decide what they may do. That is the model zero trust replaces. Removing the dependency is a concrete migration with an ordered sequence.
The sequence, each step reversible:
- Enable IAP TCP forwarding. Add a firewall rule allowing ingress from 35.235.240.0/20 to ports 22 and 3389 on administrative targets; grant
roles/iap.tunnelResourceAccessorto the operator group. - Enforce OS Login so host authorization moves to IAM (
constraints/compute.managed.requireOsLogin). - Move web-based internal tools behind IAP, published through a load balancer with an external Application Load Balancer and
roles/iap.httpsResourceAccessorbound to the appropriate groups (§7.17). - Add access levels to both IAP policies and the IAM conditions on operator roles, initially permissive (§4.15).
- Measure. Log VPN sessions and IAP sessions side by side for a full change cycle. The VPN's remaining traffic tells you exactly what has not been migrated.
- Remove the office IP ranges from firewall rules and from any allowlists. This is the step that actually eliminates network-position authority.
- Tighten access levels to require managed devices, and remove the VPN.
What legitimately keeps a VPN or Interconnect afterward: traffic between on-premises systems and Google Cloud workloads — that is data-plane connectivity and belongs in Chapter 6. What goes away is the administrative dependency: nobody should need to be on a network to change a cloud resource.
Measuring the outcome. Three queries answer whether the migration is real: no VM in a production project has an external IP; no firewall rule references an office CIDR for port 22; and every SSH session in the last 30 days appears in IAP tunnel logs.
gcloud compute instances list \
--project=rc-saas-prod-app-01 \
--filter="networkInterfaces[].accessConfigs[].natIP:*" \
--format="table(name,zone,networkInterfaces[].accessConfigs[].natIP)"
Pitfall. Removing the VPN before the break-glass path works without it. Test the break-glass identity's access through IAP explicitly, from a machine outside the corporate network, before the VPN goes away.
Chapter Summary §
- Federation exchanges an IdP-signed assertion for a short-lived Google credential through the Security Token Service, following RFC 8693; the attribute mapping decides who the identity is and the attribute condition decides whether it is accepted at all.
- Put the security decision in the attribute condition, not in the IAM binding: a provider trusts an issuer, and issuers such as GitHub's sign tokens for everyone.
- Workforce Identity Federation admits people without Cloud Identity accounts; pools are organization-scoped, globally unique,
globallocation, OIDC or SAML, with their own session duration because Workspace session controls do not apply. - Workload Identity Federation pools live in a project and are addressed by project number; use a separate pool per external environment.
google.subjectis required, must be unique, and is capped at 127 characters; up to 50 customattribute.*values can be mapped.- Never bind a role to
principalSet://.../workloadIdentityPools/POOL_ID/*— bind toattribute.NAME/VALUE. - Condition on numeric identifiers: Google warns that name-based claims such as
repositoryandrepository_ownerare exposed to cybersquatting and typosquatting. - GitHub's issuer is
https://token.actions.githubusercontent.com/with claims includingrepository_id,repository_owner_id,ref,environment, andjob_workflow_ref; GitLab's ishttps://gitlab.comwithnamespace_id,project_id, andenvironment. - AWS federation pins the account with
--account-idand must additionally condition on the extracted role name, because otherwise every principal in that AWS account is trusted. - Azure federation must pin both
tid(tenant) andappid/oid(workload), and v1 versus v2 Entra issuers produce different claim shapes. - GKE Workload Identity uses the fixed pool
PROJECT_ID.svc.id.goog; direct principal binding to the Kubernetes ServiceAccount is the recommended model, and pods with host networking bypass it entirely and get the node's identity. - Set
--workload-metadata=GKE_METADATAon node pools; the GKE metadata server allows 500 concurrent connections per node. - SAML federation requires you to own certificate rotation, because there is no automatic key discovery as with a JWKS endpoint.
- Human and machine identities need different controls: MFA and JIT elevation for people, attribute conditions and scoping for workloads.
- BeyondCorp Enterprise is now branded Chrome Enterprise Premium; the principles are that network location grants no authority and every request is decided on identity, device, and context.
- IAP protects web resources (
roles/iap.httpsResourceAccessor) and administrative TCP tunnels (roles/iap.tunnelResourceAccessor); the tunnel needs ingress from 35.235.240.0/20. - Applications behind IAP must verify the signed JWT header, or an alternate path to the application accepts a forged identity.
- Access levels combine IP, region, and device posture and are consumed both by IAM conditions (
request.auth.access_levels) and by IAP policy; roll them out permissively first. - The end state for administration is no VPN, no public bastion, no metadata SSH keys, no standing admin role — IAP TCP forwarding plus OS Login plus a PAM grant.
Security Checklist §
| Control | Why it matters | How to verify (CLI + Console) |
|---|---|---|
| Every workload identity pool provider has an attribute condition | Without one, the provider trusts every identity the issuer will ever sign for | gcloud iam workload-identity-pools providers describe PROVIDER_ID --workload-identity-pool=POOL_ID --location=global --format="value(attributeCondition)"; Console → IAM & Admin → Workload Identity Federation |
| Attribute conditions use numeric IDs, not names | Name-based claims are exposed to cybersquatting and typosquatting | inspect each condition for *_id claims |
No role bound to a whole pool (.../POOL_ID/*) | Grants access to every identity the provider accepts | gcloud asset search-all-iam-policies --scope="organizations/ORG_ID" --query="policy:workloadIdentityPools" |
| Separate pools per environment | Limits a compromised non-production IdP tenant to non-production | gcloud iam workload-identity-pools list --project=PROJECT_ID --location=global |
--allowed-audiences set on OIDC providers | Rejects tokens minted for another relying party at the same issuer | gcloud iam workload-identity-pools providers describe ... --format="value(oidc.allowedAudiences)" |
| No user-managed service account keys remain in CI systems | Federation exists precisely to remove them | gcloud iam service-accounts keys list --iam-account=SA_EMAIL --managed-by=user (§3.25) |
| Workforce pool session duration set explicitly | Workspace session controls do not apply to federated users | gcloud iam workforce-pools describe POOL_ID --location=global --format="value(sessionDuration)" |
GKE clusters set --workload-pool and --workload-metadata=GKE_METADATA | Otherwise pods receive the node's service account | gcloud container clusters describe CLUSTER --region=REGION --format="value(workloadIdentityConfig.workloadPool)" |
hostNetwork denied by admission policy in Workload Identity clusters | Host-networked pods bypass Workload Identity and get the node identity | Policy Controller / admission policy review; Console → Kubernetes Engine → Security Posture |
| Node service accounts are minimal | They are the escalation target when Workload Identity is bypassed | gcloud container clusters describe CLUSTER --format="value(nodeConfig.serviceAccount)" |
| No production VM has an external IP | An external IP is a path around IAP | gcloud compute instances list --project=PROJECT_ID --filter="networkInterfaces[].accessConfigs[].natIP:*" |
| Firewall allows 35.235.240.0/20 to 22/3389 and no office CIDR does | This is what replaces the VPN and the bastion | gcloud compute firewall-rules list --filter="allowed.ports:22" --format="table(name,sourceRanges.list())" |
| OS Login enforced organization-wide | Moves host authorization to IAM and kills metadata SSH keys | gcloud org-policies describe compute.managed.requireOsLogin --organization=ORG_ID --effective |
| IAP-protected applications verify the signed JWT header | An alternate path to the app would otherwise accept a forged identity | code review; confirm issuer and audience checks |
| Access levels required in IAM conditions for privileged operator roles | Identity alone does not establish a trustworthy request context | gcloud access-context-manager levels list --policy=POLICY_ID; Console → Security → Access Context Manager |
| Rejection of an out-of-scope pipeline has been tested | An untested attribute condition is a hypothesis | attempt the exchange from a repository that should fail |
Sources §
- Workload Identity Federation — https://cloud.google.com/iam/docs/workload-identity-federation (last validated 2026-09-03)
- Workforce Identity Federation — https://cloud.google.com/iam/docs/workforce-identity-federation (last validated 2026-09-03)
- Workload Identity Federation with deployment pipelines (GitHub, GitLab) — https://cloud.google.com/iam/docs/workload-identity-federation-with-deployment-pipelines (last validated 2026-09-03)
- Principal identifiers — https://cloud.google.com/iam/docs/principal-identifiers (last validated 2026-09-03)
- Workload Identity Federation for GKE — https://cloud.google.com/kubernetes-engine/docs/concepts/workload-identity (last validated 2026-09-03)
- Identity-Aware Proxy overview — https://cloud.google.com/iap/docs/concepts-overview (last validated 2026-09-03)
- Chrome Enterprise Premium (formerly BeyondCorp Enterprise) overview — https://cloud.google.com/beyondcorp-enterprise/docs/overview (last validated 2026-09-03)
- Best practices for using service accounts — https://cloud.google.com/iam/docs/best-practices-service-accounts (last validated 2026-09-03)
- Cloud SDK command surface resolved offline against Google Cloud SDK 583.0.0 (core 2026.08.31), 2026-09-03