Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,10 @@ public class DefaultMtlsProviderFactory {

/**
* Creates an instance of {@link MtlsProvider}. It first attempts to create an {@link
* com.google.auth.mtls.X509Provider}. If the certificate source is unavailable, it falls back to
* creating a {@link SecureConnectProvider}. If the secure connect provider also fails, it throws
* a {@link com.google.auth.mtls.CertificateSourceUnavailableException}.
* com.google.auth.mtls.X509Provider}, which also falls back to the GKE credential bundle when no
* certificate configuration is present. If the certificate source is unavailable, it falls back
* to creating a {@link SecureConnectProvider}. If the secure connect provider also fails, it
* throws a {@link com.google.auth.mtls.CertificateSourceUnavailableException}.
*
* @return an instance of {@link MtlsProvider}.
* @throws com.google.auth.mtls.CertificateSourceUnavailableException if neither provider can be
Expand All @@ -53,7 +54,7 @@ public static MtlsProvider create() throws IOException {
// Note: The caller should handle CertificateSourceUnavailableException gracefully, since
// it is an expected error case. All other IOExceptions are unexpected and should be surfaced
// up the call stack.
MtlsProvider mtlsProvider = new X509Provider();
MtlsProvider mtlsProvider = X509Provider.createWithGkeCredentialBundleFallback();
if (mtlsProvider.isAvailable()) {
return mtlsProvider;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import com.google.api.core.InternalApi;
import com.google.auth.oauth2.EnvironmentProvider;
import com.google.auth.oauth2.PropertyProvider;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
import com.google.common.io.BaseEncoding;
import java.io.File;
Expand All @@ -58,17 +59,85 @@
static final String WELL_KNOWN_CERTIFICATE_CONFIG_FILE = "certificate_config.json";
static final String CLOUDSDK_CONFIG_DIRECTORY = "gcloud";

/**
* Well-known path of the combined certificate chain and private key bundle that GKE delivers to
* pods with Agent Identity (Pod Certificates). Used as a fallback client certificate source when
* no certificate configuration file is configured or present.
*/
static final String GKE_CREDENTIAL_BUNDLE_PATH =
"/var/run/secrets/workload-spiffe-credentials/x509.credential-bundle.private-key.pem";

// Overridable via setGkeCredentialBundlePathForTesting so unit tests can point to a temp file.
private static volatile String gkeCredentialBundlePath = GKE_CREDENTIAL_BUNDLE_PATH;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I follow what this is for

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This holds the GKE credential bundle path (initialized to GKE_CREDENTIAL_BUNDLE_PATH) so unit tests can point it at a @TempDir file via setGkeCredentialBundlePathForTesting instead of /var/run/secrets/... (similar to wellKnownDir in AgentIdentityUtils). Added a comment above the field in 0c929719c69 to make this clear.


private MtlsUtils() {
// Prevent instantiation for Utility class
}

/**
* Returns {@code true} if a certificate configuration is configured or present: either {@code
* GOOGLE_API_CERTIFICATE_CONFIG} is set, or the well-known gcloud {@code certificate_config.json}
* file exists. When this returns {@code true}, the GKE credential bundle fallback is not used, so
* 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 / 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

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
EnvironmentProvider envProvider, PropertyProvider propProvider) {
if (!Strings.isNullOrEmpty(envProvider.getEnv(CERTIFICATE_CONFIGURATION_ENV_VARIABLE))) {
return true;
}
try {
return getWellKnownCertificateConfigFile(envProvider, propProvider).exists();
} catch (IOException e) {
// APPDATA missing on Windows, etc. No well-known configuration file.
return false;
}
}

/**
* Returns the GKE credential bundle path if no certificate configuration is configured or present
* (see {@link #hasCertificateConfiguration}) and the bundle exists and is readable; otherwise
* returns {@code null}.
*/
static @Nullable String getGkeCredentialBundlePath(
EnvironmentProvider envProvider, PropertyProvider propProvider) {
if (hasCertificateConfiguration(envProvider, propProvider)) {
return null;
}
return findGkeCredentialBundle();
}

/**
* 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 / 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

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
File bundle = new File(path);
return bundle.isFile() && bundle.canRead() && bundle.length() > 0;
}

/**
* Returns the GKE credential bundle path if it is a readable, non-empty regular file, otherwise
* null.
*/
private static @Nullable String findGkeCredentialBundle() {
String path = gkeCredentialBundlePath;
return isGkeCredentialBundleReady(path) ? path : null;
}

/** Overrides the GKE credential bundle path for testing; {@code null} restores the default. */
@VisibleForTesting
static void setGkeCredentialBundlePathForTesting(@Nullable String path) {
gkeCredentialBundlePath = path != null ? path : GKE_CREDENTIAL_BUNDLE_PATH;
}

/**
* 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.
*/
public static boolean useMtlsClientCertificate(

Check warning on line 140 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

Check warning on line 140 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
EnvironmentProvider envProvider, PropertyProvider propProvider) {
String useClientCertificate = envProvider.getEnv("GOOGLE_API_USE_CLIENT_CERTIFICATE");
if ("false".equalsIgnoreCase(useClientCertificate)) {
Expand All @@ -89,16 +158,20 @@
* <ol>
* <li><b>Non-null {@link String} (Valid happy path):</b> A valid workload certificate
* configuration was found and both the certificate and private key files exist and are
* readable.
* readable; or no certificate configuration is set or present and the GKE credential bundle
* ({@code /var/run/secrets/workload-spiffe-credentials/
* x509.credential-bundle.private-key.pem}) exists and is readable, in which case the bundle
* path is returned.
* <li><b>{@link IllegalStateException} (Invalid state - fail closed):</b> An explicit {@code
* GOOGLE_API_CERTIFICATE_CONFIG} path or an existing default well-known certificate
* 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 the default well-known configuration file does not exist 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.
* 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(
Expand Down Expand Up @@ -175,9 +248,14 @@
checkCertAndKeyFilesReadable(config, defaultConfigFile.getAbsolutePath(), true);
return config.getCertPath();
}
// A default configuration file exists without a workload section (e.g. ECP); it takes
// precedence over the GKE credential bundle.
return null;
}

return null;
// 3. GKE credential bundle (Pod Certificates), used only when no certificate configuration
// file is configured or present. The combined bundle serves as both certificate and key.
return findGkeCredentialBundle();
}

private static void checkCertAndKeyFilesReadable(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import com.google.auth.oauth2.SystemPropertyProvider;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.SequenceInputStream;
Expand All @@ -54,7 +55,8 @@
public class X509Provider implements MtlsProvider {
private final EnvironmentProvider envProvider;
private final PropertyProvider propProvider;
private final String certConfigPathOverride;
private final @Nullable String certConfigPathOverride;
private final boolean allowGkeCredentialBundle;

/**
* Creates an X509 provider with an override path for the certificate configuration, bypassing the
Expand All @@ -70,9 +72,39 @@ public X509Provider(
EnvironmentProvider envProvider,
PropertyProvider propProvider,
String certConfigPathOverride) {
this(envProvider, propProvider, certConfigPathOverride, false);
}

/**
* Creates an X509 provider that optionally falls back to the GKE credential bundle when no
* certificate configuration is present.
*
* @param envProvider environment provider used for environment variables
* @param propProvider property provider used for system properties
* @param certConfigPathOverride the path to read the certificate configuration from.
* @param allowGkeCredentialBundle whether to fall back to the GKE credential bundle when no
* certificate configuration is present and {@code certConfigPathOverride} is null.
*/
X509Provider(
EnvironmentProvider envProvider,
PropertyProvider propProvider,
@Nullable String certConfigPathOverride,
boolean allowGkeCredentialBundle) {
this.envProvider = envProvider;
this.propProvider = propProvider;
this.certConfigPathOverride = certConfigPathOverride;
this.allowGkeCredentialBundle = allowGkeCredentialBundle;
}

/**
* Creates an X.509 provider that, in addition to the locations checked by {@link
* #X509Provider()}, falls back to the GKE credential bundle when no certificate configuration is
* present, so that the transport uses the same certificate that {@link
* MtlsUtils#getWorkloadCertPath} reports for certificate-bound tokens.
*/
static X509Provider createWithGkeCredentialBundleFallback() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: If this is just the default path, let's just call this .create(). any other override can just use the X509 package private constructor

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keeping createWithGkeCredentialBundleFallback() (from your earlier suggestion in #discussion_r4254873874) because it differs from X509Provider's default constructor behavior:

  • The public constructors (new X509Provider(), new X509Provider(certConfigPathOverride), and new X509Provider(envProvider, propProvider, explicitCertConfigPath) used by IdentityPoolCredentials.getX509Provider() for X.509 WIF) all set allowGkeCredentialBundle = false, since X.509 WIF requires a certificate config file and also calls MtlsUtils.getCertificatePath(...) to read the separate certificate path from that config.
  • Only DefaultMtlsProviderFactory.create() opts into allowGkeCredentialBundle = true. Naming this factory X509Provider.create() could make it look equivalent to new X509Provider() when new X509Provider() intentionally does not fall back to the GKE bundle.

return new X509Provider(
SystemEnvironmentProvider.getInstance(), SystemPropertyProvider.getInstance(), null, true);
}

/**
Expand Down Expand Up @@ -105,6 +137,10 @@ public X509Provider() {
* <li>The certificate config override path, if set.
* <li>The path pointed to by the "GOOGLE_API_CERTIFICATE_CONFIG" environment variable
* <li>The well known gcloud location for the certificate configuration file.
* <li>If none of the above is configured or present, and this provider was created with {@link
* #createWithGkeCredentialBundleFallback()}, the GKE credential bundle at {@code
* /var/run/secrets/workload-spiffe-credentials/x509.credential-bundle.private-key.pem},
* which contains both the certificate chain and the private key.
Comment on lines +140 to +143

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIURc, this doesn't seem to follow the order described here. getKeyStore immediate checks for the gkeCredentialBundle path when using DefaultMtlsProviderFactory (which looks be the default way it's instantiated in GAX)

while default on for agentic identities if an opt-in choice, the configs here look like it will always check for the gke bundle for all client libraries regardless if they have opted in (I don't think this should be the case unless it's opt-in for agentic identities)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated in 0c929719c69: moved the GKE credential bundle fallback in X509Provider.getKeyStore() after MtlsUtils.getWorkloadCertificateConfiguration(...) (inside the CertificateSourceUnavailableException catch block) so that the checks execute in the exact 1 -> 2 -> 3 -> 4 order described in the Javadoc, and added a unit test verifying that a default gcloud certificate_config.json takes precedence over the GKE bundle.

On the opt-in point: in GAX (EndpointContext.determineEndpoint()), DefaultMtlsProviderFactory.create() is gated behind MtlsUtils.useMtlsClientCertificate(). On GKE, opting a Pod into Agent Identity (via the iam.gke.io/identity and iam.gke.io/inject-podcertificates: "true" pod spec fields) only mounts /var/run/secrets/workload-spiffe-credentials/x509.credential-bundle.private-key.pem into the container and does not set GOOGLE_API_CERTIFICATE_CONFIG or GOOGLE_API_USE_CLIENT_CERTIFICATE. For workloads that have not opted into GKE Agent Identity, that file does not exist on disk, so useMtlsClientCertificate() returns false and DefaultMtlsProviderFactory / X509Provider is never invoked.

* </ul>
*
* @return a KeyStore containing the X.509 certificate specified by the certificate configuration.
Expand All @@ -114,9 +150,48 @@ public X509Provider() {
*/
@Override
public KeyStore getKeyStore() throws CertificateSourceUnavailableException, IOException {
WorkloadCertificateConfiguration workloadCertConfig =
MtlsUtils.getWorkloadCertificateConfiguration(
envProvider, propProvider, certConfigPathOverride);
WorkloadCertificateConfiguration workloadCertConfig;
try {
workloadCertConfig =
MtlsUtils.getWorkloadCertificateConfiguration(
envProvider, propProvider, certConfigPathOverride);
} catch (CertificateSourceUnavailableException configUnavailable) {
// Only fall back to the GKE credential bundle when created for the mTLS transport
// (allowGkeCredentialBundle == true, rather than public X.509 WIF constructors) and when no
// explicit certificate configuration path override was passed to the constructor.
if (allowGkeCredentialBundle && certConfigPathOverride == null) {
Comment thread
lqiu96 marked this conversation as resolved.
// getWorkloadCertificateConfiguration also throws CertificateSourceUnavailableException
// when GOOGLE_API_CERTIFICATE_CONFIG is explicitly set or when a well-known
// certificate_config.json exists with only a non-workload (e.g. ECP) configuration;
// getGkeCredentialBundlePath checks hasCertificateConfiguration so we do not fall back to
// the GKE bundle in those cases.
String gkeCredentialBundlePath =
MtlsUtils.getGkeCredentialBundlePath(envProvider, propProvider);
Comment thread
lqiu96 marked this conversation as resolved.
if (gkeCredentialBundlePath != null) {
try (InputStream bundleStream = new FileInputStream(new File(gkeCredentialBundlePath))) {
return SecurityUtils.createMtlsKeyStore(bundleStream);
} catch (FileNotFoundException e) {
// The bundle was removed (e.g. mid-rotation) after the availability check.
throw new CertificateSourceUnavailableException(
"GKE credential bundle at "
+ gkeCredentialBundlePath
+ " was removed before it could be read (for example, during certificate"
+ " rotation). Retry the request or verify that the GKE Pod Certificate volume"
+ " is mounted.",
e);
} catch (Exception e) {
throw new IOException(
"X509Provider: Unable to load GKE credential bundle at "
+ gkeCredentialBundlePath
+ ". Verify that the file contains a valid PEM-encoded X.509 certificate chain"
+ " and PKCS#8 private key, or set GOOGLE_API_USE_CLIENT_CERTIFICATE=false to"
+ " disable mTLS.",
e);
Comment thread
lqiu96 marked this conversation as resolved.
}
}
}
throw configUnavailable;
}

// Read the certificate and private key file paths into streams.
try (InputStream certStream = new FileInputStream(new File(workloadCertConfig.getCertPath()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,12 @@ static final class CachedAgentIdentityInfo {
*/
final @Nullable CertInfo certInfo;

/**
* Whether {@link #certMetadata} points to the well-known GKE credential bundle path ({@link
* AgentIdentityUtils#isGkeCredentialBundle}).
*/
final boolean isGkeCredentialBundle;

/**
* Constructs a cached snapshot of Agent Identity evaluation state and file metadata.
*
Expand All @@ -190,6 +196,16 @@ static final class CachedAgentIdentityInfo {
this.keyMetadata = keyMetadata;
this.shouldRequestBoundToken = shouldRequestBoundToken;
this.certInfo = certInfo;
this.isGkeCredentialBundle =
certMetadata != null && AgentIdentityUtils.isGkeCredentialBundle(certMetadata.getPath());
}

/**
* Returns {@code true} if this cached entry was discovered from the well-known directory (not a
* certificate config file) and has both certificate and key metadata available for fallback.
*/
boolean canUseWellKnownFallback() {
return configMetadata == null && certMetadata != null && keyMetadata != null;
}
}

Expand Down
Loading
Loading