-
Notifications
You must be signed in to change notification settings - Fork 1.1k
feat(auth): mTLS endpoint for Regional Access Boundaries #13318
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
base: regional-access-boundaries
Are you sure you want to change the base?
Changes from all commits
30662d6
401df41
62ddec4
1eca146
93dc126
2646ebb
48c3b59
af9e51f
c436e7f
d115ef9
b1a0607
d5aa91a
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 |
|---|---|---|
|
|
@@ -31,14 +31,19 @@ | |
| package com.google.auth.mtls; | ||
|
|
||
| import com.google.api.core.InternalApi; | ||
| import com.google.auth.http.HttpTransportFactory; | ||
| import com.google.auth.oauth2.EnvironmentProvider; | ||
| import com.google.auth.oauth2.OAuth2Utils; | ||
| import com.google.auth.oauth2.PropertyProvider; | ||
| import com.google.common.base.Strings; | ||
| import java.io.File; | ||
| import java.io.FileInputStream; | ||
| import java.io.IOException; | ||
| import java.io.InputStream; | ||
| import java.security.KeyStore; | ||
| import java.util.Locale; | ||
| import java.util.logging.Logger; | ||
| import javax.annotation.Nullable; | ||
|
|
||
| /** | ||
| * Utility class for mTLS related operations. | ||
|
|
@@ -47,10 +52,27 @@ | |
| */ | ||
| @InternalApi | ||
| public class MtlsUtils { | ||
| private static final Logger LOGGER = Logger.getLogger(MtlsUtils.class.getName()); | ||
|
|
||
| static final String CERTIFICATE_CONFIGURATION_ENV_VARIABLE = "GOOGLE_API_CERTIFICATE_CONFIG"; | ||
| static final String WELL_KNOWN_CERTIFICATE_CONFIG_FILE = "certificate_config.json"; | ||
| static final String CLOUDSDK_CONFIG_DIRECTORY = "gcloud"; | ||
|
|
||
| /** | ||
| * The policy determining when to use mutual TLS (mTLS) endpoints. | ||
| * | ||
| * <p>See <a href="https://google.aip.dev/auth/4114">AIP-4114</a> for the specification on mTLS | ||
| * endpoint usage. | ||
| */ | ||
| public enum MtlsEndpointUsagePolicy { | ||
| /** Always use the mTLS endpoint, and fail if client certificates are not configured. */ | ||
| ALWAYS, | ||
| /** Never use the mTLS endpoint. */ | ||
| NEVER, | ||
| /** Use the mTLS endpoint if client certificates are configured (auto-discovery). */ | ||
| AUTO | ||
| } | ||
|
vverman marked this conversation as resolved.
|
||
|
|
||
| private MtlsUtils() { | ||
| // Prevent instantiation for Utility class | ||
| } | ||
|
|
@@ -76,38 +98,27 @@ public static String getCertificatePath( | |
| } | ||
|
|
||
| /** | ||
| * Resolves and loads the workload certificate configuration. | ||
| * Resolves and parses the workload certificate configuration. | ||
| * | ||
| * <p>The configuration file is resolved in the following order of precedence: 1. The provided | ||
| * certConfigPathOverride (if not null). 2. The path specified by the | ||
| * GOOGLE_API_CERTIFICATE_CONFIG environment variable. 3. The well-known certificate configuration | ||
| * file in the gcloud config directory. | ||
| * <p>This locates the certificate configuration file via {@link #resolveCertificateConfigFile} | ||
| * and parses its contents into a {@link WorkloadCertificateConfiguration}. | ||
| * | ||
| * @param envProvider the environment provider to use for resolving environment variables | ||
| * @param propProvider the property provider to use for resolving system properties | ||
| * @param certConfigPathOverride optional override path for the configuration file | ||
| * @return the loaded WorkloadCertificateConfiguration | ||
| * @throws IOException if the configuration file cannot be found, read, or parsed | ||
| * @throws IOException if the configuration file cannot be resolved, read, or parsed | ||
| */ | ||
| static WorkloadCertificateConfiguration getWorkloadCertificateConfiguration( | ||
| EnvironmentProvider envProvider, PropertyProvider propProvider, String certConfigPathOverride) | ||
| throws IOException { | ||
| File certConfig; | ||
| if (certConfigPathOverride != null) { | ||
| certConfig = new File(certConfigPathOverride); | ||
| } else { | ||
| String envCredentialsPath = envProvider.getEnv(CERTIFICATE_CONFIGURATION_ENV_VARIABLE); | ||
| if (!Strings.isNullOrEmpty(envCredentialsPath)) { | ||
| certConfig = new File(envCredentialsPath); | ||
| } else { | ||
| certConfig = getWellKnownCertificateConfigFile(envProvider, propProvider); | ||
| } | ||
| } | ||
|
|
||
| if (!certConfig.isFile()) { | ||
| File certConfig = | ||
| resolveCertificateConfigFile(envProvider, propProvider, certConfigPathOverride); | ||
| if (certConfig == null) { | ||
| File wellKnownConfig = getWellKnownCertificateConfigFile(envProvider, propProvider); | ||
| throw new CertificateSourceUnavailableException( | ||
| "Certificate configuration file does not exist or is not a file: " | ||
| + certConfig.getAbsolutePath()); | ||
| + wellKnownConfig.getAbsolutePath()); | ||
| } | ||
| try (InputStream certConfigStream = new FileInputStream(certConfig)) { | ||
| return WorkloadCertificateConfiguration.fromCertificateConfigurationStream(certConfigStream); | ||
|
|
@@ -137,4 +148,177 @@ private static File getWellKnownCertificateConfigFile( | |
| } | ||
| return new File(cloudConfigPath, WELL_KNOWN_CERTIFICATE_CONFIG_FILE); | ||
| } | ||
|
|
||
| /** | ||
| * Centralized helper method to determine if mutual TLS (mTLS) can be enabled. | ||
| * | ||
| * @param envProvider the environment provider to use for resolving environment variables | ||
| * @param propProvider the property provider to use for resolving system properties | ||
| * @param certConfigPathOverride optional override path for the configuration file | ||
| * @return true if mTLS should be enabled, false otherwise | ||
| * @throws IOException if the configuration file is present but contains missing or malformed | ||
| * files | ||
| */ | ||
| public static boolean canBeEnabled( | ||
| EnvironmentProvider envProvider, PropertyProvider propProvider, String certConfigPathOverride) | ||
| throws IOException { | ||
|
|
||
| // Check if client certificate usage is allowed | ||
| String useClientCertificate = envProvider.getEnv("GOOGLE_API_USE_CLIENT_CERTIFICATE"); | ||
| MtlsEndpointUsagePolicy policy = getMtlsEndpointUsagePolicy(envProvider); | ||
| if ("false".equalsIgnoreCase(useClientCertificate)) { | ||
| if (policy == MtlsEndpointUsagePolicy.ALWAYS) { | ||
| throw new CertificateSourceUnavailableException( | ||
| "mTLS is configured to ALWAYS, but client certificate usage was explicitly disabled via GOOGLE_API_USE_CLIENT_CERTIFICATE=false."); | ||
| } | ||
| return false; | ||
| } | ||
|
|
||
| if (policy == MtlsEndpointUsagePolicy.NEVER) { | ||
| return false; | ||
| } | ||
|
vverman marked this conversation as resolved.
|
||
|
|
||
| if (policy == MtlsEndpointUsagePolicy.ALWAYS) { | ||
| return true; | ||
| } | ||
|
vverman marked this conversation as resolved.
|
||
|
|
||
| File certConfigFile = | ||
| resolveCertificateConfigFile(envProvider, propProvider, certConfigPathOverride); | ||
| return certConfigFile != null; | ||
| } | ||
|
|
||
| /** | ||
| * Resolves the mutual TLS (mTLS) certificate configuration file. | ||
| * | ||
| * <p>The configuration file is resolved in the following order of precedence: | ||
| * | ||
| * <ol> | ||
| * <li>The developer-provided {@code certConfigPathOverride} (if not null). | ||
| * <li>The path specified by the {@code GOOGLE_API_CERTIFICATE_CONFIG} environment variable. | ||
| * <li>The well-known automatic gcloud workload identity provisioning location (via {@link | ||
| * #getWellKnownCertificateConfigFile}). | ||
| * </ol> | ||
| * | ||
| * <p>If an explicit configuration file is specified (via override or environment variable) and it | ||
| * is missing or invalid, an exception is thrown. If no explicit file is specified and the default | ||
| * well-known file is missing, {@code null} is returned. | ||
| * | ||
| * @param envProvider the environment provider to use for resolving environment variables | ||
| * @param propProvider the property provider to use for resolving system properties | ||
| * @param certConfigPathOverride optional override path for the configuration file | ||
| * @return the resolved File object, or null if no configuration was found | ||
| * @throws IOException if an explicit configuration file is missing or malformed | ||
| */ | ||
| @Nullable | ||
| static File resolveCertificateConfigFile( | ||
| EnvironmentProvider envProvider, PropertyProvider propProvider, String certConfigPathOverride) | ||
| throws IOException { | ||
| // 1. Check explicit developer override | ||
| if (certConfigPathOverride != null) { | ||
| File certConfigFile = new File(certConfigPathOverride); | ||
| if (!certConfigFile.isFile()) { | ||
| throw new CertificateSourceUnavailableException( | ||
| "Certificate configuration file does not exist or is not a file: " | ||
| + certConfigFile.getAbsolutePath()); | ||
| } | ||
| return certConfigFile; | ||
| } | ||
|
|
||
| // 2. Check explicit environment variable | ||
| String envPath = envProvider.getEnv(CERTIFICATE_CONFIGURATION_ENV_VARIABLE); | ||
| if (!Strings.isNullOrEmpty(envPath)) { | ||
| File certConfigFile = new File(envPath); | ||
| if (!certConfigFile.isFile()) { | ||
| throw new CertificateSourceUnavailableException( | ||
| "Certificate configuration file does not exist or is not a file: " | ||
| + certConfigFile.getAbsolutePath()); | ||
| } | ||
| return certConfigFile; | ||
| } | ||
|
|
||
| // 3. Check optional well-known automatic provisioning location | ||
| try { | ||
| File wellKnownConfig = getWellKnownCertificateConfigFile(envProvider, propProvider); | ||
| if (wellKnownConfig.isFile()) { | ||
| return wellKnownConfig; | ||
| } | ||
| } catch (IOException e) { | ||
| LOGGER.info( | ||
| "Could not get the mutual TLS (mTLS) client certificate configuration. The library will fall back to making standard non-mTLS requests."); | ||
| } | ||
|
|
||
| return null; | ||
| } | ||
|
|
||
| /** | ||
| * Returns the current mutual TLS endpoint usage policy. | ||
| * | ||
| * @param envProvider the environment provider to use for resolving environment variables | ||
| * @return the MtlsEndpointUsagePolicy enum value | ||
| */ | ||
| public static MtlsEndpointUsagePolicy getMtlsEndpointUsagePolicy( | ||
| EnvironmentProvider envProvider) { | ||
| String mtlsEndpointUsagePolicy = envProvider.getEnv("GOOGLE_API_USE_MTLS_ENDPOINT"); | ||
| if ("never".equals(mtlsEndpointUsagePolicy)) { | ||
| return MtlsEndpointUsagePolicy.NEVER; | ||
| } else if ("always".equals(mtlsEndpointUsagePolicy)) { | ||
| return MtlsEndpointUsagePolicy.ALWAYS; | ||
| } | ||
| return MtlsEndpointUsagePolicy.AUTO; | ||
| } | ||
|
|
||
| /** | ||
| * Prepares and upgrades the HTTP transport factory for mutual TLS (mTLS) if applicable. | ||
| * | ||
| * @param baseTransportFactory the base HTTP transport factory to upgrade | ||
| * @param envProvider the environment provider to use for resolving environment variables | ||
| * @param propProvider the property provider to use for resolving system properties | ||
| * @param certConfigPathOverride optional override path for the configuration file | ||
| * @return the mTLS-configured HTTP transport factory, or the base factory if mTLS is not enabled | ||
| * @throws IOException if mTLS is required/enabled but certificate initialization fails or an | ||
| * incompatible transport factory was provided | ||
| */ | ||
| public static HttpTransportFactory prepareTransportFactoryIfMtlsEnabled( | ||
| HttpTransportFactory baseTransportFactory, | ||
| EnvironmentProvider envProvider, | ||
| PropertyProvider propProvider, | ||
| String certConfigPathOverride) | ||
| throws IOException { | ||
|
|
||
| MtlsEndpointUsagePolicy mtlsPolicy = getMtlsEndpointUsagePolicy(envProvider); | ||
| try { | ||
| if (baseTransportFactory instanceof MtlsHttpTransportFactory) { | ||
| // A custom MtlsHttpTransportFactory was already pre-configured by the user. | ||
| // Keep using it as-is without re-initializing. | ||
| return baseTransportFactory; | ||
| } | ||
|
|
||
| if (!canBeEnabled(envProvider, propProvider, certConfigPathOverride)) { | ||
| return baseTransportFactory; | ||
| } | ||
|
|
||
| if (baseTransportFactory == OAuth2Utils.HTTP_TRANSPORT_FACTORY) { | ||
| // This is the default HttpTransportFactory assigned by credentials. | ||
| // Automatically discover and load client certificates to construct an mTLS factory. | ||
| X509Provider x509Provider = | ||
| new X509Provider(envProvider, propProvider, certConfigPathOverride); | ||
| KeyStore mtlsKeyStore = x509Provider.getKeyStore(); | ||
| return new MtlsHttpTransportFactory(mtlsKeyStore); | ||
| } | ||
|
|
||
| // A user configured non-mTLS HttpTransportFactory was explicitly injected. | ||
| // Reject it to avoid bypassing mTLS enforcement or overriding the user's factory. | ||
| throw new IOException( | ||
| "mTLS is enabled on the system, but a user configured non-mTLS HttpTransportFactory was provided: " | ||
| + baseTransportFactory.getClass().getName()); | ||
|
|
||
| } catch (Exception e) { | ||
| if (mtlsPolicy == MtlsEndpointUsagePolicy.ALWAYS) { | ||
| throw new IOException( | ||
| "mTLS is configured to ALWAYS, but initialization failed: " + e.getMessage(), e); | ||
| } | ||
| // Graceful fallback to standard transport if mTLS initialization fails under AUTO policy | ||
| return baseTransportFactory; | ||
| } | ||
| } | ||
|
Comment on lines
+281
to
+323
Contributor
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. If To prevent this and handle the default case robustly, we should default public static HttpTransportFactory prepareTransportFactoryIfMtlsEnabled(
HttpTransportFactory baseTransportFactory,
EnvironmentProvider envProvider,
PropertyProvider propProvider,
String certConfigPathOverride)
throws IOException {
MtlsEndpointUsagePolicy mtlsPolicy = getMtlsEndpointUsagePolicy(envProvider);
try {
HttpTransportFactory factory =
baseTransportFactory == null ? OAuth2Utils.HTTP_TRANSPORT_FACTORY : baseTransportFactory;
if (!canBeEnabled(envProvider, propProvider, certConfigPathOverride)) {
return factory;
}
if (factory instanceof MtlsHttpTransportFactory) {
// A custom MtlsHttpTransportFactory was already pre-configured by the user.
// Keep using it as-is without re-initializing.
return factory;
}
if (factory == OAuth2Utils.HTTP_TRANSPORT_FACTORY) {
// This is the default HttpTransportFactory assigned by credentials.
// Automatically discover and load client certificates to construct an mTLS factory.
X509Provider x509Provider =
new X509Provider(envProvider, propProvider, certConfigPathOverride);
KeyStore mtlsKeyStore = x509Provider.getKeyStore();
return new MtlsHttpTransportFactory(mtlsKeyStore);
}
// A user configured non-mTLS HttpTransportFactory was explicitly injected.
// Reject it to avoid bypassing mTLS enforcement or overriding the user's factory.
throw new IOException(
"mTLS is enabled on the system, but a user configured non-mTLS HttpTransportFactory was provided: "
+ factory.getClass().getName());
} catch (Exception e) {
if (mtlsPolicy == MtlsEndpointUsagePolicy.ALWAYS) {
throw new IOException(
"mTLS is configured to ALWAYS, but initialization failed: " + e.getMessage(), e);
}
// Graceful fallback to standard transport if mTLS initialization fails under AUTO policy
return baseTransportFactory;
}
} |
||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -39,6 +39,7 @@ | |||||||||||||||||||||||||||||||||
| import com.google.auth.RequestMetadataCallback; | ||||||||||||||||||||||||||||||||||
| import com.google.auth.http.AuthHttpConstants; | ||||||||||||||||||||||||||||||||||
| import com.google.auth.http.HttpTransportFactory; | ||||||||||||||||||||||||||||||||||
| import com.google.auth.mtls.MtlsUtils; | ||||||||||||||||||||||||||||||||||
| import com.google.common.annotations.VisibleForTesting; | ||||||||||||||||||||||||||||||||||
| import com.google.common.base.MoreObjects; | ||||||||||||||||||||||||||||||||||
| import com.google.common.base.MoreObjects.ToStringHelper; | ||||||||||||||||||||||||||||||||||
|
|
@@ -397,6 +398,12 @@ void refreshRegionalAccessBoundaryIfExpired(@Nullable URI uri, @Nullable AccessT | |||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Automatically discover certificates or enforce mTLS policy if applicable | ||||||||||||||||||||||||||||||||||
| // TODO: https://github.com/googleapis/google-cloud-java/issues/13461 | ||||||||||||||||||||||||||||||||||
| transportFactory = | ||||||||||||||||||||||||||||||||||
| MtlsUtils.prepareTransportFactoryIfMtlsEnabled( | ||||||||||||||||||||||||||||||||||
| transportFactory, getEnvironmentProvider(), getPropertyProvider(), null); | ||||||||||||||||||||||||||||||||||
|
lqiu96 marked this conversation as resolved.
Comment on lines
+401
to
+405
Contributor
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. This setup triggers synchronous filesystem and key-parsing overhead on every outgoing API request. Since refreshRegionalAccessBoundaryIfExpired is called synchronously inside getRequestMetadata (the entry point for all API requests), Because the upgraded transport factory is never cached back on the credentials instance, every API request repeats this flow: calling canBeEnabled() (checking File.isFile()) and constructing a new X509Provider to synchronously read and parse the certificate and private key files via FileInputStream. |
||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| regionalAccessBoundaryManager.triggerAsyncRefresh( | ||||||||||||||||||||||||||||||||||
| transportFactory, (RegionalAccessBoundaryProvider) this, token); | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+401
to
408
Contributor
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. Mutating the shared Instead of reassigning the instance field, we can use a local variable for the upgraded transport factory and pass it to
Suggested change
|
||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
@@ -843,6 +850,14 @@ HttpTransportFactory getTransportFactory() { | |||||||||||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| EnvironmentProvider getEnvironmentProvider() { | ||||||||||||||||||||||||||||||||||
| return SystemEnvironmentProvider.getInstance(); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| PropertyProvider getPropertyProvider() { | ||||||||||||||||||||||||||||||||||
| return SystemPropertyProvider.getInstance(); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| public static class Builder extends OAuth2Credentials.Builder { | ||||||||||||||||||||||||||||||||||
| @Nullable protected String quotaProjectId; | ||||||||||||||||||||||||||||||||||
| @Nullable protected String universeDomain; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
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.
qq, is this change necessary for the PR? I know this is marked with @internalapi and this isn't exactly customers are expected to interact with directly.
There are some small source and binary compatibility changes (I think very small chance) but I would prefer to keep it as-is unless we absolutely need to
Uh oh!
There was an error while loading. Please reload this page.
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.
The reason I chose to do this is because ->
MtlsHttpTransportFactory It is an internal class and direct calls to .create() by library consumers are unlikely. Besides, NetHttpTransport is a child class of HttpTransport and the create() method is only used internally.
I use it for testing a scenario where mTLS RAB is called when mTLS is enabled.
regionalAccessBoundary_withMtlsEnabled_shouldCallAllowedLocationsUsingMtlsTransportFactoryThere 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.
Can we avoid modifying the signature if it was only needed to accommodate the testing?
I think if this was introduced to return a stubbed MockHttpTransport in RegionalAccessBoundaryTest, we can instead stub a generic HttpTransportFactory instance in the tests rather than mocking the concrete MtlsHttpTransportFactory.
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.
NetHttpTransport is simply a default (concrete) implementation of HttpTransport.
Aside from making it easier to test, this also helps align with the parent class of MtlsHttpTransportFactory i.e. HttpTransportFactory which defines
HttpTransport create();as the contract.So the origin of i.e. MtlsHttpTransportFactory
HttpTransportFactoryand the destination i.e. IdentityPoolCredentials both define the contract for HttpTransport only.I think this avoids adding additional lines of testing logic without affecting the logic.