Repository navigation
feat(auth): add DISABLE_BOUND_ID_TOKEN option to opt out of certificate-bound ID tokens - #14559
Merged
macastelaz merged 5 commits intoOct 10, 2026
Conversation
Contributor
There was a problem hiding this comment.
Code Review
This pull request introduces AgentIdentityUtils to support Agent Identity runtime certificate discovery and token binding, enabling ComputeEngineCredentials to request certificate-bound access tokens from the GCE metadata server. Feedback on the changes highlights several locations in ComputeEngineCredentials where HttpResponse connections are not disconnected or closed upon receiving a 404 status code, potentially leading to HTTP connection leaks.
macastelaz
force-pushed
the
defer-bound-id-tokens
branch
from
October 6, 2026 15:04
8b9072e to
dbf54df
Compare
This was referenced Oct 6, 2026
macastelaz
added a commit
that referenced
this pull request
Oct 6, 2026
…igquery pom (#14587) ## Why Every PR into `agentic-identities-bound-token` is currently failing CI (for example #14559 and #14557, about 35 jobs each) with: ``` [ERROR] 'dependencies.dependency.(groupId:artifactId:type:classifier)' must be unique: com.google.cloud:google-cloud-storage:jar -> duplicate declaration of version (?) [ERROR] 'dependencies.dependency.(groupId:artifactId:type:classifier)' must be unique: com.google.cloud:google-cloud-datacatalog:jar -> duplicate declaration of version 1.102.0-SNAPSHOT [ERROR] The build could not read 1 project ``` `java-bigquery/google-cloud-bigquery/pom.xml` declares `google-cloud-storage` and `google-cloud-datacatalog` twice in `<dependencies>`. This has been the case for a while, but Maven 3.9 only logged a `[WARNING]`. The GitHub `ubuntu-24.04` runner image `20261004` upgraded Maven from 3.9.16 to 3.10.0, which treats it as an error, so any job that loads the reactor now fails immediately. ## Change Remove the second declaration of each dependency (11 lines, one file): - `google-cloud-storage`: drops the later test-scoped entry. This is the same change as #14586 on `main`. - `google-cloud-datacatalog`: drops the second, identical test-scoped entry. `main` no longer has this dependency in this pom, so #14586 doesn't need it. ## Verification - `mvn help:effective-pom` on the module before and after: the effective `<dependencies>` section is identical. `google-cloud-storage` stays test-scoped because the parent's `dependencyManagement` sets `<scope>test</scope>`, and the two datacatalog entries were identical. - Each artifact now appears once in `<dependencies>`; the pom parses as valid XML.
…bound ID tokens ComputeEngineCredentials requests certificate-bound ID tokens by default when an agent identity certificate is available. Bound ID tokens are only accepted by targets that authenticate the caller over mTLS with the same certificate, so targets reached over standard HTTPS (for example a Cloud Run *.run.app URL or a custom domain) reject them with 401. Add IdTokenProvider.Option.BIND_ID_TOKEN_FALSE so callers can request an unbound ID token per target, through idTokenWithAudience() or IdTokenCredentials.Builder.setOptions(). When the option is present, the certificate lookup is skipped and the identity endpoint is called with a plain GET. Without it, behavior is unchanged: ID tokens are bound whenever a certificate is available and GOOGLE_API_ENABLE_RUNTIME_BOUND_TOKEN is not false. Other credential types ignore the option.
macastelaz
force-pushed
the
defer-bound-id-tokens
branch
from
October 6, 2026 17:08
dbf54df to
f58f3a3
Compare
macastelaz
marked this pull request as ready for review
October 6, 2026 17:21
lqiu96
reviewed
Oct 7, 2026
lqiu96
reviewed
Oct 7, 2026
lqiu96
approved these changes
Oct 9, 2026
lqiu96
left a comment
Member
There was a problem hiding this comment.
LGTM. FYi, I think you can update the PR and the CI should pass now
macastelaz
merged commit Oct 10, 2026
1de87fd
into
googleapis:agentic-identities-bound-token
282 of 284 checks passed
macastelaz
added a commit
that referenced
this pull request
Oct 10, 2026
…14557) > [!IMPORTANT] > **Merge after #14559**, which adds `IdTokenProvider.Option.DISABLE_BOUND_ID_TOKEN` referenced here. Both must merge before the `agentic-identities-bound-token` feature branch is merged to `main`. ## What this adds Docs only; no code changes. - **`ComputeEngineCredentials` class Javadoc**: adds a brief summary noting that when a workload certificate for an agent identity is available, `ComputeEngineCredentials` requests certificate-bound access tokens and ID tokens by default, with pointers to `IdTokenProvider.Option.DISABLE_BOUND_ID_TOKEN` and the [Google Auth Library guide](https://cloud.google.com/java/getting-started/getting-started-with-google-auth-library). - The full "Certificate-bound tokens for agent identities" user guide section (covering environment variables, per-target ID token opt-out, and the ADK for Java limitation) is being added to the DevSite `getting-started-with-google-auth-library` page (`cl/996749889`) alongside the Cloud Run documentation (`cl/990396526`). ## Release notes `google-auth-library-java/CHANGELOG.md` is generated, so release notes come from commit messages. Suggested text for the `BEGIN_COMMIT_OVERRIDE` block of the feature-branch → `main` merge PR: ``` feat(auth): request certificate-bound tokens for agent identities by default. Set GOOGLE_API_ENABLE_RUNTIME_BOUND_TOKEN=false to opt out. feat(auth): add IdTokenProvider.Option.DISABLE_BOUND_ID_TOKEN to request unbound ID tokens for specific targets. docs(auth): known limitation: ADK for Java doesn't support mTLS yet; agents that use it must set GOOGLE_API_ENABLE_RUNTIME_BOUND_TOKEN=false. ``` ## Testing - `google-java-format` and `javadoc:javadoc` pass.
macastelaz
added a commit
that referenced
this pull request
Oct 14, 2026
…kens and mTLS (#14595) ## Why GKE delivers agent identity credentials (Pod Certificates) as a single combined PEM file, holding the certificate chain and a PKCS#8 private key, at: ``` /var/run/secrets/workload-spiffe-credentials/x509.credential-bundle.private-key.pem ``` The feature branch already handles Cloud Run, where a certificate config points into the well-known directory. On GKE there is no certificate config, so neither the transport nor the token side found the certificate. This is the Java counterpart of googleapis/google-cloud-python#18559. Background: go/gke-security-identity/features/agent_identity_diy.md and go/sdk-mds-bound-token. ## What changes When **no certificate configuration exists** (`GOOGLE_API_CERTIFICATE_CONFIG` is unset and there is no `~/.config/gcloud/certificate_config.json`), a readable, non-empty bundle now: - **Transport:** turns on mTLS automatically. - `MtlsUtils.getWorkloadCertPath` / `useMtlsClientCertificate` return the bundle path. - `DefaultMtlsProviderFactory.create()` builds its keystore from the bundle via a new package-private `X509Provider.createForTransport()`. - Every GAX transport (`EndpointContext`, gRPC, HTTP/JSON) goes through that factory. - **Tokens:** `AgentIdentityUtils` resolves the same bundle, so `ComputeEngineCredentials` requests tokens bound to the certificate the transport presents. - With `GOOGLE_API_USE_CLIENT_CERTIFICATE=true`, the bundle takes precedence over the Cloud Run well-known files for the same reason. **Kept unchanged:** - `GOOGLE_API_USE_CLIENT_CERTIFICATE=false` and `GOOGLE_API_ENABLE_RUNTIME_BOUND_TOKEN=false` still opt out. - An explicit or default certificate config (including ECP) always beats the bundle, on both sides. - **The bundle is never polled for.** GKE writes it before the container starts, and polling for a missing file would delay every workload that isn't an agent. - **Workload identity federation X.509 is unchanged.** The public `X509Provider` constructors don't fall back to the bundle. Only the transport factory does. - If the bundle disappears during the read (rotation), the transport reports `CertificateSourceUnavailableException` rather than a generic `IOException`. If it's briefly missing after a credential was cached, the token side falls back to the cached credential, but only while the bundle is still allowed (no certificate configuration has appeared). **New API:** a public static `MtlsUtils.hasCertificateConfiguration(...)` on an `@InternalApi` class. This is additive only. ## Differences from Python (intentional) - Python also checks `CLOUDSDK_CONTEXT_AWARE_CERTIFICATE_CONFIG_FILE_PATH`. Java has no equivalent env var. - Python temporarily keeps default **ID tokens** unbound on Cloud Run (when `GOOGLE_API_CERTIFICATE_CONFIG` is set). That gate is temporary and Python-only, so it isn't copied here. On GKE, both libraries bind ID tokens by default. #14559 adds the per-target `DISABLE_BOUND_ID_TOKEN` opt-out. ## Related - #14556 (HTTP/JSON mTLS fix, merged) enables mTLS over HTTP/JSON transports. - #14559 (`DISABLE_BOUND_ID_TOKEN`) is the per-target ID-token opt-out. ## Tests ### Unit tests - `MtlsUtilsTest`: - bundle present, absent, a directory, or empty; - `USE_CLIENT_CERTIFICATE=false`; - a default config without a workload section (ECP) blocks the bundle; - an explicit config takes precedence; - `hasCertificateConfiguration` / `getGkeCredentialBundlePath`. - `X509ProviderTest`: - transport provider with cert-first and key-first bundles; - an explicit config, or an override path, takes precedence; - unavailable when there is no bundle; - the public constructor ignores the bundle (WIF). - `AgentIdentityUtilsTest`: - implicit discovery with zero sleeps; - the cached credential stays bound; - opt-outs, a default gcloud config, and a non-agent cert all leave the token unbound; - an empty or absent bundle returns null without polling; - explicit true finds the bundle, never polls for it, and prefers it over the Cloud Run files; - fallback to the cache after the bundle is deleted, and no fallback once a certificate config has appeared (implicit and explicit paths). Full `oauth2_http` suite: 1089 tests, 0 failures. `fmt` is clean. ### Live test on GKE - **Cluster:** a GKE cluster with agent identity enabled. - **Pods:** the test pods get the agent identity credential bundle at `/var/run/secrets/workload-spiffe-credentials/x509.credential-bundle.private-key.pem`. - **Image:** built from this PR's jars merged with #14556, which is already on the base branch and is required for HTTP/JSON mTLS. It uses ADC with default settings and no client options. - **Details:** cluster setup, manifests and full logs are in the internal links below. **Checks:** - **GCS over raw HTTP:** read through `storage.mtls.googleapis.com` with the transport certificate. The same token on plain `storage.googleapis.com` gets 401, which proves the token is bound. - **GAX Resource Manager client:** `getProject` over gRPC and HTTP/JSON. - **ID tokens:** sent to an agent-to-agent receiver pod. It accepts only bound tokens over mTLS whose `cnf` matches the client certificate, and only unbound tokens over plain HTTP. | Scenario | Token | GAX resolved endpoint | GAX gRPC / HTTP-JSON | GCS mTLS / plain | ID token mTLS / plain | |---|---|---|---|---|---| | Defaults | Bound; `cnf` = bundle leaf = transport cert | `cloudresourcemanager.mtls.googleapis.com` | ✅ / ✅ | 200 / **401** | **200** / 401 | | `GOOGLE_API_ENABLE_RUNTIME_BOUND_TOKEN=false` | Unbound | mTLS endpoint | ✅ / ✅ | 200 / 200 | 401 / **200** | | `GOOGLE_API_USE_CLIENT_CERTIFICATE=false` | Unbound | plain endpoint | ✅ / ✅ | 200 / 200 | 401 / **200** | | `GOOGLE_API_USE_CLIENT_CERTIFICATE=true` | Bound (bundle) | mTLS endpoint | ✅ / ✅ | 200 / **401** | **200** / 401 | | Pod without agent identity | Unbound, no mTLS | plain endpoint | authenticated / authenticated | — / authenticated | n/a | **Timing:** the first token fetch took about 0.5–1s in every scenario. A pod without the bundle has no polling delay. **Internal links (Googlers only):** [setup](https://paste.googleplex.com/5635555912712192) · [manifests](https://paste.googleplex.com/6258309762514944) · [harness](https://paste.googleplex.com/4882197073494016) · [build/run](https://paste.googleplex.com/5942061774274560) · [results](https://paste.googleplex.com/5072605959290880) **Out of scope for this PR (tracked separately):** - **Leaf-only keystore:** `X509Provider`'s keystore holds only the leaf certificate, through google-http-client `SecurityUtils.createMtlsKeyStore`. Google APIs over `*.mtls.googleapis.com` are unaffected. Agent-to-agent mTLS against a peer that trusts only the SPIFFE root fails unless the caller presents the full chain.
macastelaz
added a commit
that referenced
this pull request
Oct 14, 2026
…14626) ## Why After the `main` sync (`03af2000542`), the `dependencies` check fails on every PR targeting this branch, for example #14559: ``` [ERROR] Used undeclared dependencies found: [ERROR] com.google.code.findbugs:jsr305:jar:3.0.2:compile ... gax-grpc / gax-httpjson: Dependency problems found ``` `main` moved GAX off `jsr305`: it uses JSpecify for nullness, and `gax-grpc`/`gax-httpjson` no longer declare `jsr305`. Two `javax.annotation` uses that came in with #13995 (certificate rotation retries) are still on this branch. They compile, because `jsr305` still arrives transitively, but `dependency:analyze` correctly flags them. ## What changes These are annotation-only edits with no behavior change: - **`gax-grpc/.../ChannelPool.java`:** the `@GuardedBy("entryWriteLock")` annotation (from `javax.annotation.concurrent`) becomes a `// Guarded by entryWriteLock.` comment. I didn't switch to Error Prone's `@GuardedBy`, because `gax-grpc` doesn't depend on Error Prone annotations, so that would just trade one undeclared dependency for another. On `main`, `gax-grpc` uses no `@GuardedBy`. - **`gax-httpjson/.../RefreshingHttpJsonChannelTest.java`:** `javax.annotation.Nullable` becomes `org.jspecify.annotations.Nullable`, as in the rest of GAX. These are the only `javax.annotation` imports in `gax-java`, `google-auth-library-java` and `java-core` that are on this branch but not on `main`. ## Tests `dependency:analyze` was run the same way as `.kokoro/dependencies.sh` (`mvn dependency:analyze -Pquick-build -DfailOnWarning=true -Dmdep.analyze.skip=false`) on `gax-grpc` and `gax-httpjson`: - without this change: `Used undeclared dependencies found: com.google.code.findbugs:jsr305`, BUILD FAILURE (the same as CI); - with this change: BUILD SUCCESS. Unit tests: `gax-grpc` 252 and `gax-httpjson` 234 tests, 0 failures. `fmt` is clean.
macastelaz
added a commit
that referenced
this pull request
Oct 19, 2026
…r agent identities (#14668) ## Summary Merges the `agentic-identities-bound-token` feature branch into `main`, adding end-to-end support for certificate-bound tokens and automatic mTLS with transparent certificate rotation for agent identities on Cloud Run and GKE. ### `google-auth-library-java` (`oauth2_http`) - **Default certificate-bound tokens (`#13873`, `#14595`, `#14658`):** When an agent identity SPIFFE workload certificate is present (either via `GOOGLE_API_CERTIFICATE_CONFIG` on Cloud Run or the GKE credential bundle at `/var/run/secrets/workload-spiffe-credentials/x509.credential-bundle.private-key.pem`), `ComputeEngineCredentials` requests certificate-bound access tokens and ID tokens via `POST` to the Metadata Server with the leaf certificate chain. - **Opt-out controls (`#13873`, `#14559`, `#14557`):** - Process-wide: `GOOGLE_API_ENABLE_RUNTIME_BOUND_TOKEN=false` (or legacy `GOOGLE_API_PREVENT_AGENT_TOKEN_SHARING_FOR_GCP_SERVICES=false`) disables token binding. - Per-target ID tokens: `IdTokenProvider.Option.DISABLE_BOUND_ID_TOKEN` requests an unbound ID token for specific targets (such as plain HTTPS `*.run.app` URLs) while keeping other ID tokens and access tokens bound. - **Automatic mTLS & strict configuration (`#13995`, `#14595`, `#14658`):** - `MtlsUtils.useMtlsClientCertificate()` enables mTLS automatically when a workload certificate config or GKE credential bundle is present, unless `GOOGLE_API_USE_CLIENT_CERTIFICATE` is set to a non-`"true"` value. - Missing, unreadable, or malformed explicit `GOOGLE_API_CERTIFICATE_CONFIG` files fail closed with `IllegalStateException`. - **Full mTLS certificate chain (`#14621`):** `X509Provider` and `SecureConnectProvider` now populate the mTLS key store with the full certificate chain (leaf + intermediates) via `MtlsKeyStoreUtils.createMtlsKeyStore` rather than only the leaf certificate. ### `sdk-platform-java/gax-java` (`gax`, `gax-grpc`, `gax-httpjson`) - **Transparent retries on mTLS certificate rotation (`#13995`):** - `CertificateRotationTracker` checks the SHA-256 fingerprint of the workload certificate on disk after an `UNAUTHENTICATED` failure. - `ChannelPool` (gRPC) and `RefreshingHttpJsonChannel` (HTTP/JSON) rebuild transport channels with the rotated certificate without interrupting in-flight RPCs. - `AttemptCallable` (unary) and `ServerStreamingAttemptCallable` (server streaming) issue one immediate free retry on the refreshed channel when a certificate rotation is detected. - **Fail-closed default mTLS provider loading (`#14658`):** When `useMtlsClientCertificate()` is `true`, `EndpointContext`, `InstantiatingGrpcChannelProvider`, and `InstantiatingHttpJsonChannelProvider` propagate `IOException`s (while still falling back cleanly on `CertificateSourceUnavailableException` when no certificate source exists on disk). - **Conscrypt TLS 1.3 mTLS fix (`#14556`):** `InstantiatingHttpJsonChannelProvider` uses Conscrypt's own `TrustManagerFactory` and `KeyManagerFactory` for the mTLS `SSLContext`, fixing `Unknown authType: GENERIC` on TLS 1.3 and key-manager algorithm constraint mismatches on JDK 26+. ## Sub-PRs Included on `agentic-identities-bound-token` | PR | Description | | :--- | :--- | | `#13873` | `feat: Enable Bound Token for Agentic Identities` | | `#13995` | `feat(gax): support transparent retries during mTLS certificate rotations` | | `#14556` | `fix(gax-httpjson): use Conscrypt TrustManagerFactory for mTLS SSLContext` | | `#14559` | `feat(auth): add DISABLE_BOUND_ID_TOKEN option to opt out of certificate-bound ID tokens` | | `#14557` | `docs(auth): document certificate-bound tokens for agent identities` | | `#14595` | `feat(auth): support GKE agent identity credential bundle for bound tokens and mTLS` | | `#14626` | `fix(gax): drop jsr305 annotations left over after syncing with main` | | `#14621` | `fix(auth): present the full certificate chain for mTLS` | | `#14658` | `fix(auth,gax): align Java mTLS and agent identity certificate handling with Python parity` | ## Testing - **Automated tests:** Full unit test suites in `google-auth-library-java` (`credentials`, `oauth2_http`) and `sdk-platform-java/gax-java` (`gax`, `gax-grpc`, `gax-httpjson`) pass across JDK 8, 11, 17, 21, 25, and 26, including new unit tests for certificate discovery, bound token requests, startup polling, rotation retries, Conscrypt TLS 1.3 mTLS handshakes, and full certificate chains. - **Integrated manual testing (`45/45` scenarios passed):** Built all 9 changed artifacts (`google-auth-library-credentials`, `google-auth-library-oauth2-http`, `api-common`, `gax`, `gax-grpc`, `gax-httpjson`, `google-cloud-core`, `google-cloud-core-http`, `google-cloud-core-grpc`) together from the combined branch and ran 22 live GAPIC mTLS rotation scenarios, 14 local bound-token and GAX parity checks, 3 live Cloud Run Agent Identity job executions, and 6 live GKE Agent Identity pod scenarios. <details> <summary>Full manual test matrix (45 scenarios)</summary> ### 1. Live GAPIC mTLS Rotation (`22/22` Scenarios, `125/125` Checks) Executed real `KeyManagementServiceClient` (gRPC and HTTP/JSON) and `BigQueryReadClient` (server-streaming `ReadRows`) against live `.mtls.googleapis.com` endpoints (`cloudkms.mtls.googleapis.com`, `bigquerystorage.mtls.googleapis.com`) while rotating client certificates on disk. | Group | Scenarios | What Was Verified | Result | | :--- | :--- | :--- | :--- | | **Unary gRPC (`G1`–`G7`)** | `atomic-mv`, `inplace`, `symlink-swap`, `corrupt-recover`, `missing-recover`, `idle-gap`, `burst-25-rpcs` | `KeyManagementServiceClient` gRPC channel seamlessly picks up rotated certs across atomic rename, in-place write, Kubernetes-style symlink swap, corrupt/missing file recovery, idle gaps, and concurrent bursts. | **7/7 Passed** | | **Server-Streaming gRPC (`S1`–`S4`)** | `mid-stream`, `between-streams`, `reconnect-after-rotate`, `corrupt-mid-stream` | `BigQueryReadClient.readRowsCallable()` active streams continue uninterrupted during rotation; subsequent streams and reconnects use the new certificate. | **4/4 Passed** | | **Unary HTTP/JSON (`H1`–`H5`)** | `atomic-mv`, `inplace`, `corrupt-recover`, `missing-recover`, `burst-20-rpcs` | `KeyManagementServiceClient` HTTP/JSON transport reloads rotated certs and recovers from transient corrupt/missing files without dropping requests. | **5/5 Passed** | | **Startup & Providers (`N1a`, `N1b`, `C1`)** | `N1a`, `N1b`, `C1-conscrypt`, `C1-jdk` | Missing cert config handling at startup; full rotation works under both Conscrypt and JDK (`SunJSSE`) TLS providers. | **4/4 Passed** | | **Combined Assertions** | `G1`–`S4` assertions | Verified `125/125` individual assertions across 22 isolated JVMs. | **22/22 Passed** | ### 2. Local Bound-Token & GAX Parity (`14/14` Checks) | Check | PR / Area | What Was Verified | Result | | :--- | :--- | :--- | :--- | | **Scenarios 1–7** | `#13873` / `#14595` | Default bound token (`cnf.x5t#S256` matches leaf), `GOOGLE_API_ENABLE_RUNTIME_BOUND_TOKEN=false`, `GOOGLE_API_USE_CLIENT_CERTIFICATE=false`, cert rotation updates `x5t#S256`, and `GOOGLE_API_PREVENT_AGENT_TOKEN_SHARING_FOR_GCP_SERVICES` behavior. | **Passed** | | **Scenario 8d** | `#14658` | `GOOGLE_API_USE_CLIENT_CERTIFICATE=true` with no certs falls back immediately (`43ms` GET) with no 30s polling delay. | **Passed** | | **Scenario 8e** | `#14658` | Invalid explicit `GOOGLE_API_CERTIFICATE_CONFIG` fails closed with `IOException` instead of silently downgrading. | **Passed** | | **Scenario 10** | `#14658` | 0-byte startup cert file is polled until populated rather than failing immediately. | **Passed** | | **Scenario 11** | `#14658` | Kubernetes `..data` symlinked parent directory resolves to the real directory for file watching. | **Passed** | | **Scenario 12a** | `#14559` | `IdTokenProvider.Option.DISABLE_BOUND_ID_TOKEN` returns an unbound ID token without leaking state to subsequent default calls. | **Passed** | | **Scenario 12b** | `#14621` | `X509Provider` preserves the full 2-certificate chain (`leaf + intermediate`) in the `KeyStore`. | **Passed** | | **GAX `fail-closed`** | `#14658` | Missing explicit `GOOGLE_API_CERTIFICATE_CONFIG` throws `IllegalStateException` in both `InstantiatingGrpcChannelProvider` and `InstantiatingHttpJsonChannelProvider`. | **Passed** | | **GAX `zero-ms-fallback`** | `#14658` | `GOOGLE_API_USE_CLIENT_CERTIFICATE=true` with no certs builds plain gRPC and HTTP/JSON channels immediately (`0ms` polling delay). | **Passed** | ### 3. Live Cloud Run Agent Identity (`3/3` Scenarios) Tested on a Cloud Run Agent Identity job (`--functional-type=agent --identity-type=agent-identity`). | Scenario | Env Vars | Key Results | | :--- | :--- | :--- | | **Default (Bound)** | *(unset)* | `DefaultMtlsProviderFactory` chain length = `2` (`#14621`). Access token acquired in `414ms`. ID token `cnf.x5t#S256` matched leaf cert. `DISABLE_BOUND_ID_TOKEN` (`#14559`) returned `cnf: false` and next default call restored `cnf: true`. GAX `ProjectsClient` (gRPC & HTTP/JSON) resolved `cloudresourcemanager.mtls.googleapis.com:443` and passed mTLS + bound-token auth (`PERMISSION_DENIED` IAM check). Non-mTLS transport rejected bound token (`401`), true mTLS passed auth (`403`), and mismatched client cert was rejected (`401`). | | **Runtime Bound Opt-Out** | `GOOGLE_API_ENABLE_RUNTIME_BOUND_TOKEN=false` | Unbound access token and ID token (`cnf` absent). Non-mTLS endpoints passed auth (`403` IAM) and live Java ADK `LlmAgent` + `Gemini` call on Vertex AI succeeded (`200 OK`). | | **Client Cert Opt-Out** | `GOOGLE_API_USE_CLIENT_CERTIFICATE=false` | Unbound access token (`450ms`) and ID token (`cnf` absent). GAX `ProjectsClient` (gRPC & HTTP/JSON) resolved plain `cloudresourcemanager.googleapis.com:443` and passed auth (`PERMISSION_DENIED`). Live Java ADK `LlmAgent` + `Gemini` call on Vertex AI succeeded (`200 OK`). | ### 4. Live GKE Agent Identity (`6/6` Scenarios) Tested on a GKE Agent Identity cluster against live GCP APIs (`cloudresourcemanager`, `storage`) and an in-cluster SPIFFE mTLS receiver configured to trust **only** the SPIFFE root CA (requiring the full leaf + intermediate chain from `#14621`). | Scenario | Pod Annotations | Env Vars | Key Results | | :--- | :--- | :--- | :--- | | **`s1-default`** | `inject-podcertificates=true` | *(unset)* | `bundle exists=true`, first token fetch `1430ms`, SDK KeyStore chain length = `2`, mTLS receiver (SDK KeyStore) = `200 OK`, mTLS GCS passed auth (`404`), plain GCS rejected bound token (`401`), gRPC & HTTP/JSON resolved `.mtls.googleapis.com:443` and passed auth, `DISABLE_BOUND_ID_TOKEN` -> plain `200` / mTLS `401` / default after opt-out -> mTLS `200` / access token still bound -> plain GCS `401`. | | **`s2-runtime-bound-false`** | `inject-podcertificates=true` | `GOOGLE_API_ENABLE_RUNTIME_BOUND_TOKEN=false` | Unbound tokens (`cnf=null`), mTLS receiver rejected unbound token (`401`), plain receiver accepted (`200`), raw GCS and Storage SDK (HTTP & gRPC) passed auth (`404`), gRPC & HTTP/JSON resolved `.mtls.googleapis.com:443` and passed auth. | | **`s3-use-cert-false`** | `inject-podcertificates=true` | `GOOGLE_API_USE_CLIENT_CERTIFICATE=false` | Unbound tokens (`cnf=null`), gRPC & HTTP/JSON resolved plain `cloudresourcemanager.googleapis.com:443` and passed auth, plain receiver = `200`, plain GCS passed auth (`404`). | | **`s4-use-cert-true`** | `inject-podcertificates=true` | `GOOGLE_API_USE_CLIENT_CERTIFICATE=true` | Identical bound behavior to `s1-default`: SDK KeyStore mTLS receiver = `200`, `.mtls.googleapis.com:443` resolved and passed auth, all `DISABLE_BOUND_ID_TOKEN` checks passed. | | **`s5-no-annotation`** | *(none)* | *(unset)* | `bundle exists=false`, first token fetch `646ms` (no 30s hang), resolved plain `cloudresourcemanager.googleapis.com:443`, passed auth (`PERMISSION_DENIED` / `403`). | | **`s6-use-cert-true-no-annotation`** | *(none)* | `GOOGLE_API_USE_CLIENT_CERTIFICATE=true` | Verified `#14658` on real GKE: `bundle exists=false`, first token fetch `609ms` (no 30s polling delay), resolved plain `cloudresourcemanager.googleapis.com:443`, passed auth (`PERMISSION_DENIED` / `403`). | </details> BEGIN_COMMIT_OVERRIDE feat(auth): request certificate-bound tokens for agent identities by default. Set GOOGLE_API_ENABLE_RUNTIME_BOUND_TOKEN=false to opt out. feat(auth): add IdTokenProvider.Option.DISABLE_BOUND_ID_TOKEN to request unbound ID tokens for specific targets. feat(auth): support GKE agent identity credential bundle for bound tokens and mTLS. fix(auth): present the full certificate chain for mTLS. docs(auth): known limitation: ADK for Java doesn't support mTLS yet; agents that use it must set GOOGLE_API_ENABLE_RUNTIME_BOUND_TOKEN=false. feat(gax): enable mTLS automatically when a workload certificate is present and support transparent retries during mTLS certificate rotations. fix(gax-httpjson): use Conscrypt TrustManagerFactory and KeyManagerFactory for mTLS SSLContext. END_COMMIT_OVERRIDE --------- Co-authored-by: Kirill Logachev <kirl@google.com> Co-authored-by: Min Zhu <zhumin@google.com> Co-authored-by: Lawrence Qiu <lawrenceqiu@google.com> Co-authored-by: yangyzs <171981480+yangyzs@users.noreply.github.com> Co-authored-by: cloud-java-bot <122572305+cloud-java-bot@users.noreply.github.com> Co-authored-by: Keshav Dandeva <keshavdandeva@google.com> Co-authored-by: Mattie Fu <mattiefu@google.com> Co-authored-by: Will Howes <whowes@google.com> Co-authored-by: Jin Seop Kim <jinseop@google.com> Co-authored-by: Jiang Zhu <jiangzzhu@google.com> Co-authored-by: Knut Olav Løite <koloite@gmail.com> Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> Co-authored-by: rahul2393 <irahul@google.com> Co-authored-by: Sakthivel Subramanian <179120858+sakthivelmanii@users.noreply.github.com> Co-authored-by: Gaole Meng <gaole@google.com> Co-authored-by: Pranav Iyer <pjiyer@google.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Important
This must merge before the
agentic-identities-bound-tokenfeature branch is merged tomain. Docs PR #14557 documents this option.Why
When an agent identity certificate is available,
ComputeEngineCredentialsrequests certificate-bound ID tokens by default. A bound ID token is only accepted by targets that authenticate the caller over mTLS with the same certificate. Targets reached over standard HTTPS, such as a Cloud Run service's*.run.appURL or a custom domain, reject it with401 Unauthorized.Per the ID token binding 1-pager (Sep 30 decision), ID tokens stay bound by default, and callers get a per-target
bind_id_tokensetting to turn binding off where needed. The process-wide opt-out (GOOGLE_API_ENABLE_RUNTIME_BOUND_TOKEN=false) is unchanged.What changes
New
IdTokenProvider.Option.DISABLE_BOUND_ID_TOKEN. Callers pass it per target, the same way asFORMAT_FULLandLICENSES_TRUE:ComputeEngineCredentials.idTokenWithAudience(): with the option, the certificate lookup is skipped and the identity endpoint is called with a plain GET. Without it, behavior is unchanged (bound whenever a certificate is available and binding isn't turned off by the env var).Other credential types (
ServiceAccountCredentials,ImpersonatedCredentials,UserCredentials) already ignore options they don't use, so the same code works outside agent identity environments.Access tokens are unaffected.
API shape
Optionis a set of on/off flags rather than a value, so this adds only the "off" flag (DISABLE_BOUND_ID_TOKEN). Leaving it unset means "auto", which currently means bound. This matches the 1-pager's tri-state (unset = auto) without adding an "enable" flag that would do nothing today; one can be added later without breaking callers if "auto" starts deciding per target. Python exposes the same control asbind_id_token: Optional[bool](googleapis/google-cloud-python#18559).Tests
Unit tests
New
ComputeEngineCredentialsTestcases, all with an agent identity certificate present and binding enabled:idTokenWithAudience_disableBoundIdToken_requestsUnboundToken: GET, no request body, no forcedformat=full.idTokenWithAudience_disableBoundIdTokenWithFormatFull_requestsUnboundFullToken: still GET;format=fullis kept when the caller asks for it.idTokenWithAudience_disableBoundIdToken_skipsCertificateLookup: with a certificate config pointing to a missing file, the default request fails, but the opted-out request succeeds, which shows the certificate is never read.idTokenCredentials_withDisableBoundIdTokenOption_requestsUnboundToken: the option set onIdTokenCredentialsreachesComputeEngineCredentials.The existing bound-ID-token tests are unchanged and still pass. Full
oauth2_httpsuite: 1064 tests, 0 failures.fmtis clean.Live test on GKE
Run on a GKE cluster with agent identity enabled, using a test-only local merge of this PR with #14595 (GKE support) and #14556 (already on the base branch). A single process fetches ID tokens through
IdTokenCredentialsfromComputeEngineCredentials(ADC) and sends them to an agent-to-agent receiver pod. The receiver accepts only bound tokens over mTLS (whosecnfmust match the client certificate), and only unbound tokens over plain HTTP.GOOGLE_API_ENABLE_RUNTIME_BOUND_TOKEN=falseDISABLE_BOUND_ID_TOKENcnf); receiver: plain HTTP 200, mTLS 401cnf= certificate leaf); receiver: mTLS 200The option unbinds only the ID token it's set on. Other targets and access tokens are unaffected, and it's harmless when binding is already off.
Internal links (Googlers only): results · harness · cluster setup and manifests (shared with #14595)