-
Notifications
You must be signed in to change notification settings - Fork 1.7k
fix(auth): align mTLS discovery and enforce fail-fast transport configuration. #17470
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: main
Are you sure you want to change the base?
Changes from all commits
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 | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -201,6 +201,11 @@ def get_and_parse_agent_identity_certificate(): | |||||||||||||
| if is_opted_out: | ||||||||||||||
| return None | ||||||||||||||
|
|
||||||||||||||
| # Respect explicit opt-out of mTLS / client certs | ||||||||||||||
| use_client_cert = os.environ.get(environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE) | ||||||||||||||
| if use_client_cert and use_client_cert.lower() == "false": | ||||||||||||||
| return None | ||||||||||||||
|
|
||||||||||||||
| cert_path = get_agent_identity_certificate_path() | ||||||||||||||
| if not cert_path: | ||||||||||||||
| return None | ||||||||||||||
|
|
@@ -312,7 +317,24 @@ def should_request_bound_token(cert): | |||||||||||||
| ).lower() | ||||||||||||||
| == "true" | ||||||||||||||
| ) | ||||||||||||||
| return is_agent_cert and is_opted_in | ||||||||||||||
| if not (is_agent_cert and is_opted_in): | ||||||||||||||
| return False | ||||||||||||||
|
|
||||||||||||||
| # Respect explicit opt-out of mTLS / client certs | ||||||||||||||
| use_client_cert = os.environ.get(environment_vars.GOOGLE_API_USE_CLIENT_CERTIFICATE) | ||||||||||||||
| if use_client_cert and use_client_cert.lower() == "false": | ||||||||||||||
| return False | ||||||||||||||
|
Comment on lines
+324
to
+326
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. Similar to the check in
Suggested change
|
||||||||||||||
|
|
||||||||||||||
| # Graceful degradation for auto-enablement: | ||||||||||||||
| # If GOOGLE_API_USE_CLIENT_CERTIFICATE is not set, we only request a bound token | ||||||||||||||
| # if the transport is capable of handling it. | ||||||||||||||
| if use_client_cert is None or use_client_cert == "": | ||||||||||||||
| from google.auth.transport import _mtls_helper | ||||||||||||||
|
|
||||||||||||||
| if not _mtls_helper.is_transport_mtls_capable(): | ||||||||||||||
| return False | ||||||||||||||
|
|
||||||||||||||
| return True | ||||||||||||||
|
|
||||||||||||||
|
|
||||||||||||||
| def get_cached_cert_fingerprint(cached_cert): | ||||||||||||||
|
|
||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -150,11 +150,11 @@ def __init__( | |||||||||||||
| async def configure_mtls_channel(self, client_cert_callback=None): | ||||||||||||||
| """Configure the client certificate and key for SSL connection. | ||||||||||||||
|
|
||||||||||||||
| The function does nothing unless `GOOGLE_API_USE_CLIENT_CERTIFICATE` is | ||||||||||||||
| explicitly set to `true`. In this case if client certificate and key are | ||||||||||||||
| successfully obtained (from the given client_cert_callback or from application | ||||||||||||||
| default SSL credentials), the underlying transport will be reconfigured | ||||||||||||||
| to use mTLS. | ||||||||||||||
| This method configures mTLS if client certificates are explicitly enabled | ||||||||||||||
| (via GOOGLE_API_USE_CLIENT_CERTIFICATE=true) or auto-enabled (when the env | ||||||||||||||
| variable is unset and workload certificates are discovered). In these cases, | ||||||||||||||
| the underlying transport will be reconfigured to use mTLS. | ||||||||||||||
|
|
||||||||||||||
| Note: This function does nothing if the `aiohttp` library is not | ||||||||||||||
| installed. | ||||||||||||||
| Important: Calling this method will close any ongoing API requests associated | ||||||||||||||
|
|
@@ -170,7 +170,8 @@ async def configure_mtls_channel(self, client_cert_callback=None): | |||||||||||||
|
|
||||||||||||||
| Raises: | ||||||||||||||
| google.auth.exceptions.MutualTLSChannelError: If mutual TLS channel | ||||||||||||||
| creation failed for any reason. | ||||||||||||||
| creation failed for any reason (e.g., missing dependencies, custom request | ||||||||||||||
| handler limitations, or missing certificates when mTLS was requested). | ||||||||||||||
| """ | ||||||||||||||
| if self._mtls_init_task is None: | ||||||||||||||
|
|
||||||||||||||
|
|
@@ -207,6 +208,20 @@ async def _do_configure(): | |||||||||||||
| self._auth_request = AiohttpRequest(session=new_session) | ||||||||||||||
|
|
||||||||||||||
| await old_auth_request.close() | ||||||||||||||
| else: | ||||||||||||||
| self._is_mtls = False | ||||||||||||||
| # If a custom request handler was provided, the library cannot configure | ||||||||||||||
| # the connection to use mTLS. Raise an error instead of silently falling back. | ||||||||||||||
| raise exceptions.MutualTLSChannelError( | ||||||||||||||
| "mTLS was configured/enabled but client certificate or private key could not be loaded." | ||||||||||||||
| ) | ||||||||||||||
|
Comment on lines
+215
to
+217
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. The exception message here is misleading. If this block is reached, it means a custom request handler (not an
Suggested change
|
||||||||||||||
| else: | ||||||||||||||
| # If mTLS is configured or intended, but we fail to find client certificates, | ||||||||||||||
| # we must fail fast by raising an error instead of silently falling back to | ||||||||||||||
| # standard TLS. | ||||||||||||||
| raise exceptions.MutualTLSChannelError( | ||||||||||||||
| "mTLS was configured/enabled but client certificate or private key could not be loaded." | ||||||||||||||
| ) | ||||||||||||||
|
|
||||||||||||||
| except ( | ||||||||||||||
| exceptions.ClientCertError, | ||||||||||||||
|
|
||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -28,6 +28,9 @@ | |||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| # Default gcloud config path, to be used with path.expanduser for cross-platform compatibility. | ||||||||||||||||||||||||||||
| CERTIFICATE_CONFIGURATION_DEFAULT_PATH = "~/.config/gcloud/certificate_config.json" | ||||||||||||||||||||||||||||
| _WELL_KNOWN_SPIFFE_CERT_PATH = ( | ||||||||||||||||||||||||||||
| "/var/run/secrets/workload-spiffe-credentials/certificates.pem" | ||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
| _CERT_PROVIDER_COMMAND = "cert_provider_command" | ||||||||||||||||||||||||||||
| _CERT_REGEX = re.compile( | ||||||||||||||||||||||||||||
| b"-----BEGIN CERTIFICATE-----.+-----END CERTIFICATE-----\r?\n?", re.DOTALL | ||||||||||||||||||||||||||||
|
|
@@ -200,12 +203,13 @@ def _get_workload_cert_and_key_paths(config_path, include_context_aware=True): | |||||||||||||||||||||||||||
| return None, None | ||||||||||||||||||||||||||||
| workload = cert_configs["workload"] | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| if "cert_path" not in workload: | ||||||||||||||||||||||||||||
| return None, None | ||||||||||||||||||||||||||||
| if "cert_path" not in workload or "key_path" not in workload: | ||||||||||||||||||||||||||||
| raise exceptions.ClientCertError( | ||||||||||||||||||||||||||||
| 'Workload certificate configuration is missing "cert_path" or "key_path" in {}'.format( | ||||||||||||||||||||||||||||
| absolute_path | ||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
| cert_path = workload["cert_path"] | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| if "key_path" not in workload: | ||||||||||||||||||||||||||||
| return None, None | ||||||||||||||||||||||||||||
| key_path = workload["key_path"] | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| # == BEGIN Temporary Cloud Run PATCH == | ||||||||||||||||||||||||||||
|
|
@@ -448,16 +452,31 @@ def client_cert_callback(): | |||||||||||||||||||||||||||
| return crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey) | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| def is_transport_mtls_capable(): | ||||||||||||||||||||||||||||
| """Returns True if the required dependencies for establishing an mTLS connection are present.""" | ||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||
| import OpenSSL | ||||||||||||||||||||||||||||
| from cryptography import x509 | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| return True | ||||||||||||||||||||||||||||
| except ImportError: | ||||||||||||||||||||||||||||
| return False | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| def check_use_client_cert(): | ||||||||||||||||||||||||||||
| """Returns boolean for whether the client certificate should be used for mTLS. | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| If GOOGLE_API_USE_CLIENT_CERTIFICATE is set to true or false, a corresponding | ||||||||||||||||||||||||||||
| bool value will be returned. If the value is set to an unexpected string, it | ||||||||||||||||||||||||||||
| will default to False. | ||||||||||||||||||||||||||||
| If GOOGLE_API_USE_CLIENT_CERTIFICATE is unset, the value will be inferred | ||||||||||||||||||||||||||||
| by reading a file pointed at by GOOGLE_API_CERTIFICATE_CONFIG, and verifying | ||||||||||||||||||||||||||||
| it contains a "workload" section. If so, the function will return True, | ||||||||||||||||||||||||||||
| otherwise False. | ||||||||||||||||||||||||||||
| as True (auto-enabled) if the required transport libraries (pyOpenSSL and cryptography) | ||||||||||||||||||||||||||||
| are installed AND either: | ||||||||||||||||||||||||||||
| 1. A workload config file exists (pointed at by GOOGLE_API_CERTIFICATE_CONFIG) | ||||||||||||||||||||||||||||
| containing a "workload" section. | ||||||||||||||||||||||||||||
| 2. A certificate file exists at the well-known SPIFFE path | ||||||||||||||||||||||||||||
| (/var/run/secrets/workload-spiffe-credentials/certificates.pem). | ||||||||||||||||||||||||||||
| Otherwise, it returns False. | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| Returns: | ||||||||||||||||||||||||||||
| bool: Whether the client certificate should be used for mTLS connection. | ||||||||||||||||||||||||||||
|
|
@@ -471,31 +490,44 @@ def check_use_client_cert(): | |||||||||||||||||||||||||||
| # Check if the value of GOOGLE_API_USE_CLIENT_CERTIFICATE is set. | ||||||||||||||||||||||||||||
| if use_client_cert: | ||||||||||||||||||||||||||||
| return use_client_cert.lower() == "true" | ||||||||||||||||||||||||||||
| else: | ||||||||||||||||||||||||||||
| # Check if the value of GOOGLE_API_CERTIFICATE_CONFIG is set. | ||||||||||||||||||||||||||||
| cert_path = getenv(environment_vars.GOOGLE_API_CERTIFICATE_CONFIG) | ||||||||||||||||||||||||||||
| if cert_path is None: | ||||||||||||||||||||||||||||
| cert_path = getenv( | ||||||||||||||||||||||||||||
| environment_vars.CLOUDSDK_CONTEXT_AWARE_CERTIFICATE_CONFIG_FILE_PATH | ||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| if cert_path: | ||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||
| with open(cert_path, "r") as f: | ||||||||||||||||||||||||||||
| content = json.load(f) | ||||||||||||||||||||||||||||
| # verify json has workload key | ||||||||||||||||||||||||||||
| content["cert_configs"]["workload"] | ||||||||||||||||||||||||||||
| return True | ||||||||||||||||||||||||||||
| except ( | ||||||||||||||||||||||||||||
| FileNotFoundError, | ||||||||||||||||||||||||||||
| OSError, | ||||||||||||||||||||||||||||
| KeyError, | ||||||||||||||||||||||||||||
| TypeError, | ||||||||||||||||||||||||||||
| json.JSONDecodeError, | ||||||||||||||||||||||||||||
| ) as e: | ||||||||||||||||||||||||||||
| _LOGGER.debug("error decoding certificate: %s", e) | ||||||||||||||||||||||||||||
| # Auto-enablement checks (when GOOGLE_API_USE_CLIENT_CERTIFICATE is not set) | ||||||||||||||||||||||||||||
| # Gracefully degrade to standard TLS if transport capabilities are missing | ||||||||||||||||||||||||||||
| if not is_transport_mtls_capable(): | ||||||||||||||||||||||||||||
| return False | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| # Check if the value of GOOGLE_API_CERTIFICATE_CONFIG is set. | ||||||||||||||||||||||||||||
| cert_path = getenv(environment_vars.GOOGLE_API_CERTIFICATE_CONFIG) | ||||||||||||||||||||||||||||
| if cert_path is None: | ||||||||||||||||||||||||||||
| cert_path = getenv( | ||||||||||||||||||||||||||||
| environment_vars.CLOUDSDK_CONTEXT_AWARE_CERTIFICATE_CONFIG_FILE_PATH | ||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| if cert_path: | ||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||
| with open(cert_path, "r") as f: | ||||||||||||||||||||||||||||
| content = json.load(f) | ||||||||||||||||||||||||||||
| # verify json has workload key | ||||||||||||||||||||||||||||
| content["cert_configs"]["workload"] | ||||||||||||||||||||||||||||
| return True | ||||||||||||||||||||||||||||
| except ( | ||||||||||||||||||||||||||||
| FileNotFoundError, | ||||||||||||||||||||||||||||
| OSError, | ||||||||||||||||||||||||||||
| KeyError, | ||||||||||||||||||||||||||||
| TypeError, | ||||||||||||||||||||||||||||
| json.JSONDecodeError, | ||||||||||||||||||||||||||||
| ) as e: | ||||||||||||||||||||||||||||
| _LOGGER.debug("error decoding certificate: %s", e) | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| # Fallback check for well-known SPIFFE path (agent identity) | ||||||||||||||||||||||||||||
| if ( | ||||||||||||||||||||||||||||
| path.exists(_WELL_KNOWN_SPIFFE_CERT_PATH) | ||||||||||||||||||||||||||||
| and path.getsize(_WELL_KNOWN_SPIFFE_CERT_PATH) > 0 | ||||||||||||||||||||||||||||
| ): | ||||||||||||||||||||||||||||
| return True | ||||||||||||||||||||||||||||
|
Comment on lines
+523
to
+527
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. Using
Suggested change
|
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| return False | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| def check_parameters_for_unauthorized_response(cached_cert): | ||||||||||||||||||||||||||||
| """Returns the cached and current cert fingerprint for reconfiguring mTLS. | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
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.
If
GOOGLE_API_USE_CLIENT_CERTIFICATEis set to an invalid value (e.g.,'foo'),check_use_client_cert()will treat it asFalse(disabled). However, the current check only returnsNoneif it is explicitly'false'. To ensure perfect alignment withcheck_use_client_cert(), we should treat any value other than'true'as an opt-out/disabled state when the environment variable is set.