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:

Vertical flow of how an external assertion becomes a Google-authorized identity, in seven steps. An external identity provider issues a signed assertion, which may be an OIDC ID token, a SAML assertion, or an AWS SigV4 request. The client presents it to sts.googleapis.com. STS verifies the signature against the provider's published keys, applies the attribute mapping for google.subject and any attribute fields, then applies the attribute condition and either accepts or rejects. STS returns a short-lived federated token, which IAM then evaluates as an ordinary principal.

Four configuration objects define the trust:

ObjectPurpose
Poola namespace of external identities; the trust boundary
Providerthe relationship to one IdP: issuer, protocol, keys
Attribute mappingCEL turning IdP claims into google.subject, google.groups, attribute.NAME
Attribute conditionCEL 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 attributeRule
google.subjectrequired, must be unique, maximum 127 characters
google.groupsoptional, enables principalSet://.../group/GROUP_ID bindings
attribute.NAMEup 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:

Diagram of a workload identity pool and provider feeding two access models. An external workload sends a signed assertion, OIDC, SAML, or AWS SigV4, to the pool and provider, which issues a federated token. The token then takes one of two paths. On the left, direct resource access, where a role is granted on the resource itself and the workload reaches the GCP resource directly. On the right, impersonation, where roles/iam.workloadIdentityUser lets the workload act as a service account, which in turn acts on the resource.

  • Direct resource access — grant the principal:// or principalSet:// identity a role directly on the resource. Fewer moving parts, no service account at all.
  • Service account impersonation — grant the federated identity roles/iam.workloadIdentityUser on 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 use in or 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 plus assertion.environment for production.
  • Condition on job_workflow_ref to 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 environment claim 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 sub claim 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:

  1. Confirm the system issues an OIDC token with a verifiable issuer and publishes a JWKS.
  2. Identify the claim that uniquely and immutably names the specific pipeline, not the account or organization.
  3. Create a dedicated pool for that system in the CI/CD project, per environment.
  4. Write an attribute condition pinning that claim, plus any environment or branch claim available.
  5. Set --allowed-audiences explicitly.
  6. Grant roles/iam.workloadIdentityUser on a per-environment deploy service account, bound to attribute.*/VALUE (§3.34).
  7. 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, because sts.windows.net serves all of them from per-tenant issuer URLs but token contents are attacker-influenceable if the issuer is templated loosely.
  • appid or oid — 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 hostNetwork in admission policy.
  • The metadata server enforces a 500 concurrent connection limit per node.
  • --workload-metadata=GKE_METADATA must 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.

HumanMachine
Authenticationinteractive, MFA, phishing-resistantnon-interactive token exchange
Lifecyclejoiner-mover-leaver in the directorycreated and destroyed with the workload
Right patternCloud Identity or Workforce Identity Federationattached SA, WIF, GKE Workload Identity
Standing privilegenone in production; PAM on demand (§3.27)narrow standing roles are correct
Session lengthshort, revocable, device-conditionedone hour, auto-refreshed
Correct policy bindinggroup: or principalSet://.../group/serviceAccount: or principalSet://.../attribute./
Audit questionwho was this person, on what devicewhich 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:

  1. 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.
  2. Access is granted per request, not per session, and re-evaluated as context changes.
  3. The decision inputs are identity, device, and context — who, on what machine, from where, at what time, with what signals.
  4. All access is authenticated, authorized, and encrypted, including access to internal applications.
  5. 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:

PrincipleComponent
Per-request decision at the applicationIdentity-Aware Proxy (§4.16, §7.17)
Context as a policy inputAccess Context Manager access levels (§4.15)
Device posture as a signalEndpoint Verification and third-party partners (§4.17)
Strong identityCloud Identity with phishing-resistant MFA, or Workforce Identity Federation (§4.2)
No network-position authority for administrationIAP TCP forwarding (§4.18, §4.19)
Data perimeter independent of identityVPC 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:

RoleGrants
roles/iap.httpsResourceAccessorreach an IAP-protected web resource
roles/iap.tunnelResourceAccessoropen 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:

TierDevice requirementWhat it may reach
Unmanagednonenothing in production
Managedscreen lock, disk encryption, current OSproduction read, non-production write
Corporate-owned + MDMall of the above plus requireCorpOwnedproduction 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:

  1. The engineer authenticates to Cloud Identity or a workforce pool with phishing-resistant MFA.
  2. Endpoint Verification supplies device signals; the request satisfies the trusted_corp_device access level (§4.15, §4.17).
  3. The engineer holds only read roles by standing grant (§3.35).
  4. For a change, they request a PAM grant with a justification; an approver in a different group consents (§3.27).
  5. The grant issues a time-bound role binding, itself conditioned on the access level.
  6. Host access, where needed, is gcloud compute ssh --tunnel-through-iap to a VM with no external IP, authorized by roles/iap.tunnelResourceAccessor and OS Login (§4.16).
  7. 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:

  1. 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.tunnelResourceAccessor to the operator group.
  2. Enforce OS Login so host authorization moves to IAM (constraints/compute.managed.requireOsLogin).
  3. Move web-based internal tools behind IAP, published through a load balancer with an external Application Load Balancer and roles/iap.httpsResourceAccessor bound to the appropriate groups (§7.17).
  4. Add access levels to both IAP policies and the IAM conditions on operator roles, initially permissive (§4.15).
  5. 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.
  6. Remove the office IP ranges from firewall rules and from any allowlists. This is the step that actually eliminates network-position authority.
  7. 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, global location, 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.subject is required, must be unique, and is capped at 127 characters; up to 50 custom attribute.* values can be mapped.
  • Never bind a role to principalSet://.../workloadIdentityPools/POOL_ID/* — bind to attribute.NAME/VALUE.
  • Condition on numeric identifiers: Google warns that name-based claims such as repository and repository_owner are exposed to cybersquatting and typosquatting.
  • GitHub's issuer is https://token.actions.githubusercontent.com/ with claims including repository_id, repository_owner_id, ref, environment, and job_workflow_ref; GitLab's is https://gitlab.com with namespace_id, project_id, and environment.
  • AWS federation pins the account with --account-id and 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) and appid/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_METADATA on 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 §

ControlWhy it mattersHow to verify (CLI + Console)
Every workload identity pool provider has an attribute conditionWithout one, the provider trusts every identity the issuer will ever sign forgcloud 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 namesName-based claims are exposed to cybersquatting and typosquattinginspect each condition for *_id claims
No role bound to a whole pool (.../POOL_ID/*)Grants access to every identity the provider acceptsgcloud asset search-all-iam-policies --scope="organizations/ORG_ID" --query="policy:workloadIdentityPools"
Separate pools per environmentLimits a compromised non-production IdP tenant to non-productiongcloud iam workload-identity-pools list --project=PROJECT_ID --location=global
--allowed-audiences set on OIDC providersRejects tokens minted for another relying party at the same issuergcloud iam workload-identity-pools providers describe ... --format="value(oidc.allowedAudiences)"
No user-managed service account keys remain in CI systemsFederation exists precisely to remove themgcloud iam service-accounts keys list --iam-account=SA_EMAIL --managed-by=user (§3.25)
Workforce pool session duration set explicitlyWorkspace session controls do not apply to federated usersgcloud iam workforce-pools describe POOL_ID --location=global --format="value(sessionDuration)"
GKE clusters set --workload-pool and --workload-metadata=GKE_METADATAOtherwise pods receive the node's service accountgcloud container clusters describe CLUSTER --region=REGION --format="value(workloadIdentityConfig.workloadPool)"
hostNetwork denied by admission policy in Workload Identity clustersHost-networked pods bypass Workload Identity and get the node identityPolicy Controller / admission policy review; Console → Kubernetes Engine → Security Posture
Node service accounts are minimalThey are the escalation target when Workload Identity is bypassedgcloud container clusters describe CLUSTER --format="value(nodeConfig.serviceAccount)"
No production VM has an external IPAn external IP is a path around IAPgcloud compute instances list --project=PROJECT_ID --filter="networkInterfaces[].accessConfigs[].natIP:*"
Firewall allows 35.235.240.0/20 to 22/3389 and no office CIDR doesThis is what replaces the VPN and the bastiongcloud compute firewall-rules list --filter="allowed.ports:22" --format="table(name,sourceRanges.list())"
OS Login enforced organization-wideMoves host authorization to IAM and kills metadata SSH keysgcloud org-policies describe compute.managed.requireOsLogin --organization=ORG_ID --effective
IAP-protected applications verify the signed JWT headerAn alternate path to the app would otherwise accept a forged identitycode review; confirm issuer and audience checks
Access levels required in IAM conditions for privileged operator rolesIdentity alone does not establish a trustworthy request contextgcloud access-context-manager levels list --policy=POLICY_ID; Console → Security → Access Context Manager
Rejection of an out-of-scope pipeline has been testedAn untested attribute condition is a hypothesisattempt the exchange from a repository that should fail

Sources §