Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@
* the certificate used for mTLS and the certificate bound to tokens always come from the same
* source.
*/
public static boolean hasCertificateConfiguration(

Check warning on line 84 in google-auth-library-java/oauth2_http/java/com/google/auth/mtls/MtlsUtils.java

View workflow job for this annotation

GitHub Actions / bom-content-test

Check warning on line 84 in google-auth-library-java/oauth2_http/java/com/google/auth/mtls/MtlsUtils.java

View workflow job for this annotation

GitHub Actions / bom-content-test

no @PARAM for propProvider

Check warning on line 84 in google-auth-library-java/oauth2_http/java/com/google/auth/mtls/MtlsUtils.java

View workflow job for this annotation

GitHub Actions / bom-content-test

no @PARAM for envProvider

Check warning on line 84 in google-auth-library-java/oauth2_http/java/com/google/auth/mtls/MtlsUtils.java

View workflow job for this annotation

GitHub Actions / BomContentAssertionsTest (Test for assertion logic in BomContentTest)

Check warning on line 84 in google-auth-library-java/oauth2_http/java/com/google/auth/mtls/MtlsUtils.java

View workflow job for this annotation

GitHub Actions / BomContentAssertionsTest (Test for assertion logic in BomContentTest)

no @PARAM for propProvider

Check warning on line 84 in google-auth-library-java/oauth2_http/java/com/google/auth/mtls/MtlsUtils.java

View workflow job for this annotation

GitHub Actions / BomContentAssertionsTest (Test for assertion logic in BomContentTest)

no @PARAM for envProvider
EnvironmentProvider envProvider, PropertyProvider propProvider) {
if (!Strings.isNullOrEmpty(envProvider.getEnv(CERTIFICATE_CONFIGURATION_ENV_VARIABLE))) {
return true;
Expand Down Expand Up @@ -111,7 +111,7 @@
* Returns {@code true} if the GKE credential bundle at {@code path} is a readable, non-empty
* regular file.
*/
public static boolean isGkeCredentialBundleReady(String path) {

Check warning on line 114 in google-auth-library-java/oauth2_http/java/com/google/auth/mtls/MtlsUtils.java

View workflow job for this annotation

GitHub Actions / bom-content-test

Check warning on line 114 in google-auth-library-java/oauth2_http/java/com/google/auth/mtls/MtlsUtils.java

View workflow job for this annotation

GitHub Actions / bom-content-test

no @PARAM for path

Check warning on line 114 in google-auth-library-java/oauth2_http/java/com/google/auth/mtls/MtlsUtils.java

View workflow job for this annotation

GitHub Actions / BomContentAssertionsTest (Test for assertion logic in BomContentTest)

Check warning on line 114 in google-auth-library-java/oauth2_http/java/com/google/auth/mtls/MtlsUtils.java

View workflow job for this annotation

GitHub Actions / BomContentAssertionsTest (Test for assertion logic in BomContentTest)

no @PARAM for path
File bundle = new File(path);
return bundle.isFile() && bundle.canRead() && bundle.length() > 0;
}
Expand All @@ -131,16 +131,23 @@
gkeCredentialBundlePath = path != null ? path : GKE_CREDENTIAL_BUNDLE_PATH;
}

private static boolean isUseClientCertificateExplicitlyDisabled(
@Nullable String useClientCertificate) {
return !Strings.isNullOrEmpty(useClientCertificate)
&& !"true".equalsIgnoreCase(useClientCertificate);
}

/**
* Returns if mutual TLS client certificate should be used. Returns true if valid workload
* certificates are configured or if GOOGLE_API_USE_CLIENT_CERTIFICATE is explicitly set to true
* (e.g. for Enterprise Certificate Proxy or custom MtlsProviders), unless explicitly disabled via
* GOOGLE_API_USE_CLIENT_CERTIFICATE=false.
* GOOGLE_API_USE_CLIENT_CERTIFICATE (set to a non-empty value other than "true").
*/
public static boolean useMtlsClientCertificate(

Check warning on line 146 in google-auth-library-java/oauth2_http/java/com/google/auth/mtls/MtlsUtils.java

View workflow job for this annotation

GitHub Actions / bom-content-test

no @PARAM for envProvider

Check warning on line 146 in google-auth-library-java/oauth2_http/java/com/google/auth/mtls/MtlsUtils.java

View workflow job for this annotation

GitHub Actions / BomContentAssertionsTest (Test for assertion logic in BomContentTest)

no @PARAM for envProvider
EnvironmentProvider envProvider, PropertyProvider propProvider) {
String useClientCertificate = envProvider.getEnv("GOOGLE_API_USE_CLIENT_CERTIFICATE");
if ("false".equalsIgnoreCase(useClientCertificate)) {
String rawVal = envProvider.getEnv("GOOGLE_API_USE_CLIENT_CERTIFICATE");
String useClientCertificate = rawVal != null ? rawVal.trim() : null;
if (isUseClientCertificateExplicitlyDisabled(useClientCertificate)) {
return false;
}
if (getWorkloadCertPath(envProvider, propProvider) != null) {
Expand All @@ -167,17 +174,18 @@
* configuration file is missing, unreadable, malformed, or references missing/unreadable
* certificate or private key files. This is treated as an unrecoverable misconfiguration.
* <li><b>{@code null} (Safe fallback / fail open):</b> Client certificates are explicitly
* disabled via {@code GOOGLE_API_USE_CLIENT_CERTIFICATE=false}, no explicit configuration
* is set and neither the default well-known configuration file nor a readable GKE
* credential bundle exists on disk, or the configuration specifies an non-workload source
* (e.g., ECP/PKCS11 without a {@code workload} section). Callers can proceed without
* workload certificate file polling.
* disabled via {@code GOOGLE_API_USE_CLIENT_CERTIFICATE} (any non-empty value other than
* {@code "true"}), no explicit configuration is set and neither the default well-known
* configuration file nor a readable GKE credential bundle exists on disk, or the
* configuration specifies an non-workload source (e.g., ECP/PKCS11 without a {@code
* workload} section). Callers can proceed without workload certificate file polling.
* </ol>
*/
public static @Nullable String getWorkloadCertPath(
EnvironmentProvider envProvider, PropertyProvider propProvider) {
String useClientCertificate = envProvider.getEnv("GOOGLE_API_USE_CLIENT_CERTIFICATE");
if ("false".equalsIgnoreCase(useClientCertificate)) {
String rawVal = envProvider.getEnv("GOOGLE_API_USE_CLIENT_CERTIFICATE");
String useClientCertificate = rawVal != null ? rawVal.trim() : null;
if (isUseClientCertificateExplicitlyDisabled(useClientCertificate)) {
return null;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -352,19 +352,22 @@ static boolean tryUpdateCache(
}

/**
* Checks if a file exists and is readable, throwing {@link AccessDeniedException} if permission
* is denied.
* Checks if a file exists, is a non-empty regular file, and is readable, throwing {@link
* AccessDeniedException} if permission is denied.
*/
static boolean checkExistsOrAccessDenied(final Path path) throws AccessDeniedException {
try {
Files.readAttributes(path, BasicFileAttributes.class);
BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class);
if (!attrs.isRegularFile()) {
return false;
}
if (!Files.isReadable(path)) {
if (!Files.exists(path)) {
return false; // Deleted mid-rotation; let the caller retry.
}
throw new AccessDeniedException(path.toString());
}
return true;
return attrs.size() > 0;
} catch (AccessDeniedException e) {
throw e;
} catch (IOException e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -286,14 +286,36 @@ private static boolean isMtlsExplicitlyDisabled() {
return !Strings.isNullOrEmpty(useClientCert) && !"true".equalsIgnoreCase(useClientCert);
}

/**
* Resolves symlinks on existing ancestor directories of {@code path} (for example, {@code
* /var/run} to {@code /run} before startup files are created), preserving any trailing
* non-existent path elements.
*/
private static Path resolveRealPathAllowingMissing(final Path path) {
Path normalized = path.toAbsolutePath().normalize();
Path current = normalized;
while (current != null) {
try {
Path realCurrent = current.toRealPath();
Path relative = current.relativize(normalized);
return relative.toString().isEmpty()
? realCurrent
: realCurrent.resolve(relative).normalize();
} catch (IOException e) {
current = current.getParent();
}
}
return normalized;
}

/** Checks whether the given path resides within the well-known certificate directory. */
private static boolean isPathInWellKnownDir(final @Nullable String pathStr) {
if (Strings.isNullOrEmpty(pathStr)) {
return false;
}
try {
Path path = Paths.get(pathStr).toAbsolutePath().normalize();
Path wellKnown = Paths.get(wellKnownDir).toAbsolutePath().normalize();
Path path = resolveRealPathAllowingMissing(Paths.get(pathStr));
Path wellKnown = resolveRealPathAllowingMissing(Paths.get(wellKnownDir));
return path.startsWith(wellKnown);
} catch (Exception e) {
return false;
Expand Down Expand Up @@ -686,19 +708,19 @@ private static ResolvedCertAndKeyPaths getPathsFromConfigWithRetry(
lastParseException);
}

/** Searches for certificates at well-known locations with retry logic. */
/** Searches for certificates at well-known locations. */
private static ResolvedCertAndKeyPaths getWellKnownCertificatePathWithRetry(
final @Nullable CachedAgentIdentityInfo initialCached) throws IOException {
if (isMtlsExplicitlyDisabled()) {
initialStartupCompleted = true;
return new ResolvedCertAndKeyPaths(null, null, false);
}

// Unlike the Cloud Run well-known files below (which may be delivered asynchronously and are
// polled for up to 30s on initial startup), GKE mounts the Pod Certificate volume before the
// container starts, so we perform a single immediate check per call without startup polling.
// When found, we mark initialStartupCompleted = true so subsequent refreshes during rotation do
// not enter the 30s startup polling loop.
// Startup polling is only performed when GOOGLE_API_CERTIFICATE_CONFIG is set and points to the
// well-known directory (see getPathsFromConfigWithRetry). When GOOGLE_API_CERTIFICATE_CONFIG is
// unset, GKE mounts the Pod Certificate volume before the container starts (and on GCE an empty
// well-known directory may exist without any certificates ever being delivered), so we perform
// a single immediate check without startup polling.
// The GKE credential bundle is only used when no well-known gcloud certificate configuration
// file exists (it would otherwise be the transport's mTLS certificate source).
boolean gkeBundleAllowed =
Expand Down Expand Up @@ -732,52 +754,31 @@ private static ResolvedCertAndKeyPaths getWellKnownCertificatePathWithRetry(
String certOnlyPath = Paths.get(wellKnownDir, "certificates.pem").toString();
String keyOnlyPath = Paths.get(wellKnownDir, "private_key.pem").toString();

boolean shouldPoll = !initialStartupCompleted;
int maxCycles = shouldPoll ? TOTAL_POLL_CYCLES : 1;

boolean warned = false;
for (int cycle = 0; cycle < maxCycles; cycle++) {
try {
if (AgentIdentityCacheUtils.checkExistsOrAccessDenied(Paths.get(bundlePath))) {
initialStartupCompleted = true;
// Combined bundle file contains both certificate chain and private key
return new ResolvedCertAndKeyPaths(bundlePath, bundlePath, false);
}
if (AgentIdentityCacheUtils.checkExistsOrAccessDenied(Paths.get(certOnlyPath))
&& AgentIdentityCacheUtils.checkExistsOrAccessDenied(Paths.get(keyOnlyPath))) {
initialStartupCompleted = true;
return new ResolvedCertAndKeyPaths(certOnlyPath, keyOnlyPath, false);
}
} catch (AccessDeniedException e) {
try {
if (AgentIdentityCacheUtils.checkExistsOrAccessDenied(Paths.get(bundlePath))) {
initialStartupCompleted = true;
LoggingUtils.log(
LOGGER_PROVIDER,
Level.FINE,
Collections.emptyMap(),
"Permission denied reading well-known certificates. Falling back to unbound token.");
return new ResolvedCertAndKeyPaths(null, null, false);
} catch (Exception e) {
// Fall through to retry
// Combined bundle file contains both certificate chain and private key
return new ResolvedCertAndKeyPaths(bundlePath, bundlePath, false);
}

if (cycle + 1 < maxCycles) {
if (shouldPoll && !warned) {
LoggingUtils.log(
LOGGER_PROVIDER,
Level.FINE,
Collections.emptyMap(),
String.format(
"Well-known certificate file not found at %s. Retrying for up to %d seconds.",
wellKnownDir, TOTAL_TIMEOUT_MS / 1000));
warned = true;
}
try {
timeService.sleep(getSleepIntervalMs(cycle));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IOException("Interrupted while waiting for well-known certificate files.", e);
}
if (AgentIdentityCacheUtils.checkExistsOrAccessDenied(Paths.get(certOnlyPath))
&& AgentIdentityCacheUtils.checkExistsOrAccessDenied(Paths.get(keyOnlyPath))) {
initialStartupCompleted = true;
return new ResolvedCertAndKeyPaths(certOnlyPath, keyOnlyPath, false);
}
} catch (AccessDeniedException e) {
initialStartupCompleted = true;
LoggingUtils.log(
LOGGER_PROVIDER,
Level.FINE,
Collections.emptyMap(),
"Permission denied reading well-known certificates. Falling back to unbound token.");
return new ResolvedCertAndKeyPaths(null, null, false);
} catch (Exception e) {
LoggingUtils.log(
LOGGER_PROVIDER,
Level.FINE,
Collections.emptyMap(),
"Unexpected error checking well-known certificates: " + e.getMessage());
}
Comment thread
macastelaz marked this conversation as resolved.
initialStartupCompleted = true;

Expand All @@ -792,12 +793,7 @@ private static ResolvedCertAndKeyPaths getWellKnownCertificatePathWithRetry(
fallbackCached.certMetadata.getPath(), fallbackCached.keyMetadata.getPath(), false);
}

throw new IOException(
String.format(
"Unable to find well-known Agent Identity certificate file at %s for bound token"
+ " request. Token binding protection is failing. You can turn off this"
+ " protection by setting %s to false to fall back to unbound tokens.",
wellKnownDir, GOOGLE_API_ENABLE_RUNTIME_BOUND_TOKEN));
return new ResolvedCertAndKeyPaths(null, null, false);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -823,6 +823,17 @@ void getWorkloadCertPath_useClientCertFalse_ignoresGkeBundle() throws IOExceptio
assertFalse(MtlsUtils.useMtlsClientCertificate(envProvider, homeInTempDir()));
}

@Test
void getWorkloadCertPath_useClientCertNonTrueValues_disablesMtls() throws IOException {
writeGkeBundle();
for (String disabledVal : new String[] {"0", "no", "FALSE", "off", "random"}) {
EnvironmentProvider envProvider =
name -> "GOOGLE_API_USE_CLIENT_CERTIFICATE".equals(name) ? disabledVal : null;
assertNull(MtlsUtils.getWorkloadCertPath(envProvider, homeInTempDir()));
assertFalse(MtlsUtils.useMtlsClientCertificate(envProvider, homeInTempDir()));
}
}

@Test
void getWorkloadCertPath_defaultConfigWithoutWorkload_ignoresGkeBundle() throws IOException {
writeGkeBundle();
Expand Down
Loading
Loading