Repository navigation
fix(auth,gax): align Java mTLS and agent identity certificate handling with Python parity - #14658
Conversation
…g with Python parity - Fail closed in GAX (EndpointContext, InstantiatingGrpcChannelProvider, InstantiatingHttpJsonChannelProvider) when DefaultMtlsProviderFactory.create() throws IOException while useMtlsClientCertificate() is true. - Only poll on startup for agent identity certificates when GOOGLE_API_CERTIFICATE_CONFIG explicitly points inside the well-known workload SPIFFE directory, resolving parent directory symlinks and requiring non-empty regular files (> 0 bytes). - Return certsPresent=false immediately without polling or throwing when GOOGLE_API_CERTIFICATE_CONFIG is unset and no well-known certificates exist on disk. - Treat any non-'true' value of GOOGLE_API_USE_CLIENT_CERTIFICATE as disabled in MtlsUtils.
There was a problem hiding this comment.
Code Review
This pull request refactors mTLS configuration and certificate resolution. Key changes include optimizing the handling of the GOOGLE_API_USE_CLIENT_CERTIFICATE environment variable, ensuring that file checks verify that paths are non-empty regular files, resolving symlinks while preserving trailing missing elements, and removing startup polling when the certificate configuration is unset. Additionally, the default MtlsProvider creation is refactored to propagate IOExceptions rather than silently falling back to non-mTLS channels. Review feedback suggests optimizing useMtlsClientCertificate by avoiding redundant environment variable fetches and adding logging for unexpected exceptions caught during certificate checks in AgentIdentityUtils.
|
|
||
| AgentIdentityUtils.CertInfo info = AgentIdentityUtils.getAgentIdentityCertInfo(); | ||
| assertNotNull(info); | ||
| assertEquals(2, fakeTime.getSleepCount()); |
There was a problem hiding this comment.
This test still passes if the attrs.size() > 0 check in checkExistsOrAccessDenied is reverted. Without the check, the 0-byte files count as ready and polling returns right away, but loadAndVerifyCredentials then makes 3 read attempts with a timeService.sleep between them on the same fake clock. The callback fills in the cert and key on those two retry sleeps, so the test still ends with 2 sleeps and a valid CertInfo.
Does it make sense to fill the files in after the load retries would have run out (e.g. the cert on sleep 4 and the key on sleep 5, and assert 5 sleeps)?
There was a problem hiding this comment.
Good catch. Updated the test in a14152d5ca4 to populate the certificate on sleep 4 and the private key on sleep 5, and assert 5 total sleeps so loadAndVerifyCredentials's 3 attempts (2 sleeps) cannot mask a missing 0-byte check.
| } | ||
|
|
||
| @Test | ||
| void endpointContextBuild_whenDefaultMtlsProviderThrowsIOException_propagatesIOException() |
There was a problem hiding this comment.
nit: Could we also add a test where createDefaultMtlsProvider() throws CertificateSourceUnavailableException (here and in the two channel provider tests)? That covers the GOOGLE_API_USE_CLIENT_CERTIFICATE=true fallback when no certs exist on disk, where build() should still succeed without mTLS on the regular endpoint.
There was a problem hiding this comment.
Done in a14152d5ca4. Added unit tests covering CertificateSourceUnavailableException from createDefaultMtlsProvider() in EndpointContextTest, InstantiatingGrpcChannelProviderTest, and InstantiatingHttpJsonChannelProviderTest.
|
|
||
| /** | ||
| * Resolves symlinks along all existing prefix components of {@code path}, preserving any trailing | ||
| * non-existent path elements (matching Python's {@code os.path.realpath} behavior before startup |
There was a problem hiding this comment.
nit: this doesn't follow a symlink whose target doesn't exist yet (e.g. a config path outside the well-known dir that links to a file Cloud Run hasn't written), so it isn't quite os.path.realpath. That's fine for the /var/run -> /run case this targets, but could we reword the comment so it doesn't claim full parity?
There was a problem hiding this comment.
Done in a14152d5ca4. Reworded the Javadoc on resolveRealPathAllowingMissing to note that it resolves symlinks on existing ancestor directories (such as /var/run to /run before startup files are created) without claiming full os.path.realpath parity.
c40c533
into
googleapis:agentic-identities-bound-token
Why
An audit comparing the Java (
agentic-identities-bound-token) and Python (googleapis/google-cloud-python#18559) agent identity and mTLS implementations surfaced several edge-case behavioral differences:CertificateBasedAccess.useMtlsClientCertificate()istrueandDefaultMtlsProviderFactory.create()throws anIOException(for example,CertificateSourceUnavailableExceptionwhen a certificate file disappears mid-read or a configured certificate path is invalid/corrupt),EndpointContext,InstantiatingGrpcChannelProvider, andInstantiatingHttpJsonChannelProvidercaught theIOExceptionand silently degraded tomtlsProvider = null(sending requests over an unauthenticated channel or regular endpoint without client certificates).GOOGLE_API_CERTIFICATE_CONFIGis unset: InAgentIdentityUtils.getWellKnownCertificatePathWithRetry(), whenGOOGLE_API_CERTIFICATE_CONFIGwas unset andGOOGLE_API_USE_CLIENT_CERTIFICATE=truewas set on a non-agent GCE VM where no certificates exist, Java polled the well-known Cloud Run paths for ~3 seconds and threw anIOExceptionon timeout instead of falling back to unbound tokens in 0ms.AgentIdentityUtils.isPathInWellKnownDir(String)usedFiles.exists(path)before callingtoRealPath(), which skipped symlink resolution when the leaf certificate file did not yet exist on disk (e.g., on systems where/var/runis a symlink to/runandGOOGLE_API_CERTIFICATE_CONFIGuses/run/secrets/workload-spiffe-credentials/...).AgentIdentityCacheUtils.checkExistsOrAccessDenied(Path)only checked file existence viaBasicFileAttributeswithout verifyingattrs.isRegularFile() && attrs.size() > 0, which could cause startup polling to return immediately on a newly created 0-byte file before certificate contents were written.GOOGLE_API_USE_CLIENT_CERTIFICATEnon-boolean values:MtlsUtils.useMtlsClientCertificate()andMtlsUtils.getWorkloadCertPath()only checked for"true"and"false", allowing arbitrary non-boolean values (e.g.,"invalid"or"1") to fall through to auto-discovery instead of treating any non-"true"value as disabled.What changes
CertificateBasedAccess,EndpointContext,InstantiatingGrpcChannelProvider,InstantiatingHttpJsonChannelProvider):IOExceptionfromDefaultMtlsProviderFactory.create()(routed via package-privateCertificateBasedAccess.createDefaultMtlsProvider()) whencertificateBasedAccess.useMtlsClientCertificate()istrue, failing closed instead of degrading tomtlsProvider = null.useMtlsClientCertificate()isfalse,IOExceptionduring defaultMtlsProvidercreation continues to be logged atFINEand ignored.AgentIdentityUtils&AgentIdentityCacheUtils:IOExceptionthrow fromgetWellKnownCertificatePathWithRetry()whenGOOGLE_API_CERTIFICATE_CONFIGis unset; check well-known paths once (GKE_CREDENTIAL_BUNDLE_PATH,WELL_KNOWN_CREDENTIAL_BUNDLE_PATH,WELL_KNOWN_CERT_PATH+WELL_KNOWN_KEY_PATH) and immediately returncertsPresent=falseif none are present.GOOGLE_API_CERTIFICATE_CONFIGpointing inside/var/run/secrets/workload-spiffe-credentials/.resolveRealPathAllowingMissing(Path)to resolve symlinks on the nearest existing ancestor directory and re-append remaining relative path components inisPathInWellKnownDir(String).attrs.isRegularFile() && attrs.size() > 0inAgentIdentityCacheUtils.checkExistsOrAccessDenied(Path)so polling waits until certificate files are non-empty regular files (> 0bytes).MtlsUtils:GOOGLE_API_USE_CLIENT_CERTIFICATEother than"true"(case-insensitive) as disabled (false/null) in bothuseMtlsClientCertificate()andgetWorkloadCertPath().Tests
EndpointContextTest,InstantiatingGrpcChannelProviderTest,InstantiatingHttpJsonChannelProviderTest:IOExceptionfromcreateDefaultMtlsProvider()is propagated whenuseMtlsClientCertificate()istrueand ignored whenuseMtlsClientCertificate()isfalse.AgentIdentityUtilsTest:GOOGLE_API_CERTIFICATE_CONFIGtests to verify 0-sleep non-polling behavior and immediatenullreturn when no well-known certificates exist, even whenGOOGLE_API_USE_CLIENT_CERTIFICATE=true.getConfigFilePathWithRetryandgetWorkloadCertPathFromConfigWithRetry.isPathInWellKnownDirwith symlinked parent directory and non-existent leaf file.MtlsUtilsTest:GOOGLE_API_USE_CLIENT_CERTIFICATE("invalid") returnfalseinuseMtlsClientCertificate()andnullingetWorkloadCertPath()even when workload certificates exist on disk.