Repository navigation
feat(auth): support GKE agent identity credential bundle for bound tokens and mTLS #14595
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
2247ddd
fb43614
6b1f7c7
0c92971
d4200d9
45df129
d764396
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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; | ||
|
|
@@ -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; | ||
|
|
||
| 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
|
||
| 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
|
||
| 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
|
||
| EnvironmentProvider envProvider, PropertyProvider propProvider) { | ||
| String useClientCertificate = envProvider.getEnv("GOOGLE_API_USE_CLIENT_CERTIFICATE"); | ||
| if ("false".equalsIgnoreCase(useClientCertificate)) { | ||
|
|
@@ -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( | ||
|
|
@@ -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( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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; | ||
|
|
@@ -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 | ||
|
|
@@ -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() { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Keeping
|
||
| return new X509Provider( | ||
| SystemEnvironmentProvider.getInstance(), SystemPropertyProvider.getInstance(), null, true); | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Updated in On the opt-in point: in GAX ( |
||
| * </ul> | ||
| * | ||
| * @return a KeyStore containing the X.509 certificate specified by the certificate configuration. | ||
|
|
@@ -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) { | ||
|
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); | ||
|
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); | ||
|
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())); | ||
|
|
||
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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@TempDirfile viasetGkeCredentialBundlePathForTestinginstead of/var/run/secrets/...(similar towellKnownDirinAgentIdentityUtils). Added a comment above the field in0c929719c69to make this clear.