feat: MDS connections use mTLS (#1856) · googleapis/google-auth-library-python@0387bb9
@@ -24,31 +24,72 @@
2424import os
2525from urllib.parse import urljoin
262627+import requests
28+2729from google.auth import _helpers
2830from google.auth import environment_vars
2931from google.auth import exceptions
3032from google.auth import metrics
3133from google.auth import transport
3234from google.auth._exponential_backoff import ExponentialBackoff
35+from google.auth.compute_engine import _mtls
36+33373438_LOGGER = logging.getLogger(__name__)
353940+_GCE_DEFAULT_MDS_IP = "169.254.169.254"
41+_GCE_DEFAULT_HOST = "metadata.google.internal"
42+_GCE_DEFAULT_MDS_HOSTS = [_GCE_DEFAULT_HOST, _GCE_DEFAULT_MDS_IP]
43+3644# Environment variable GCE_METADATA_HOST is originally named
3745# GCE_METADATA_ROOT. For compatibility reasons, here it checks
3846# the new variable first; if not set, the system falls back
3947# to the old variable.
4048_GCE_METADATA_HOST = os.getenv(environment_vars.GCE_METADATA_HOST, None)
4149if not _GCE_METADATA_HOST:
4250_GCE_METADATA_HOST = os.getenv(
43-environment_vars.GCE_METADATA_ROOT, "metadata.google.internal"
51+environment_vars.GCE_METADATA_ROOT, _GCE_DEFAULT_HOST
52+ )
53+54+55+def _validate_gce_mds_configured_environment():
56+"""Validates the GCE metadata server environment configuration for mTLS.
57+58+ mTLS is only supported when connecting to the default metadata server hosts.
59+ If we are in strict mode (which requires mTLS), ensure that the metadata host
60+ has not been overridden to a custom value (which means mTLS will fail).
61+62+ Raises:
63+ google.auth.exceptions.MutualTLSChannelError: if the environment
64+ configuration is invalid for mTLS.
65+ """
66+mode = _mtls._parse_mds_mode()
67+if mode == _mtls.MdsMtlsMode.STRICT:
68+# mTLS is only supported when connecting to the default metadata host.
69+# Raise an exception if we are in strict mode (which requires mTLS)
70+# but the metadata host has been overridden to a custom MDS. (which means mTLS will fail)
71+if _GCE_METADATA_HOST not in _GCE_DEFAULT_MDS_HOSTS:
72+raise exceptions.MutualTLSChannelError(
73+"Mutual TLS is required, but the metadata host has been overridden. "
74+"mTLS is only supported when connecting to the default metadata host."
75+ )
76+77+78+def _get_metadata_root(use_mtls: bool):
79+"""Returns the metadata server root URL."""
80+81+scheme = "https" if use_mtls else "http"
82+return "{}://{}/computeMetadata/v1/".format(scheme, _GCE_METADATA_HOST)
83+84+85+def _get_metadata_ip_root(use_mtls: bool):
86+"""Returns the metadata server IP root URL."""
87+scheme = "https" if use_mtls else "http"
88+return "{}://{}".format(
89+scheme, os.getenv(environment_vars.GCE_METADATA_IP, _GCE_DEFAULT_MDS_IP)
4490 )
45-_METADATA_ROOT = "http://{}/computeMetadata/v1/".format(_GCE_METADATA_HOST)
469147-# This is used to ping the metadata server, it avoids the cost of a DNS
48-# lookup.
49-_METADATA_IP_ROOT = "http://{}".format(
50-os.getenv(environment_vars.GCE_METADATA_IP, "169.254.169.254")
51-)
92+5293_METADATA_FLAVOR_HEADER = "metadata-flavor"
5394_METADATA_FLAVOR_VALUE = "Google"
5495_METADATA_HEADERS = {_METADATA_FLAVOR_HEADER: _METADATA_FLAVOR_VALUE}
@@ -102,6 +143,33 @@ def detect_gce_residency_linux():
102143return content.startswith(_GOOGLE)
103144104145146+def _prepare_request_for_mds(request, use_mtls=False) -> None:
147+"""Prepares a request for the metadata server.
148+149+ This will check if mTLS should be used and mount the mTLS adapter if needed.
150+151+ Args:
152+ request (google.auth.transport.Request): A callable used to make
153+ HTTP requests.
154+ use_mtls (bool): Whether to use mTLS for the request.
155+156+ Returns:
157+ google.auth.transport.Request: A request object to use.
158+ If mTLS is enabled, the request will have the mTLS adapter mounted.
159+ Otherwise, the original request will be returned unchanged.
160+ """
161+# Only modify the request if mTLS is enabled.
162+if use_mtls:
163+# Ensure the request has a session to mount the adapter to.
164+if not request.session:
165+request.session = requests.Session()
166+167+adapter = _mtls.MdsMtlsAdapter()
168+# Mount the adapter for all default GCE metadata hosts.
169+for host in _GCE_DEFAULT_MDS_HOSTS:
170+request.session.mount(f"https://{host}/", adapter)
171+172+105173def ping(request, timeout=_METADATA_DEFAULT_TIMEOUT, retry_count=3):
106174"""Checks to see if the metadata server is available.
107175@@ -115,6 +183,8 @@ def ping(request, timeout=_METADATA_DEFAULT_TIMEOUT, retry_count=3):
115183 Returns:
116184 bool: True if the metadata server is reachable, False otherwise.
117185 """
186+use_mtls = _mtls.should_use_mds_mtls()
187+_prepare_request_for_mds(request, use_mtls=use_mtls)
118188# NOTE: The explicit ``timeout`` is a workaround. The underlying
119189# issue is that resolving an unknown host on some networks will take
120190# 20-30 seconds; making this timeout short fixes the issue, but
@@ -129,7 +199,10 @@ def ping(request, timeout=_METADATA_DEFAULT_TIMEOUT, retry_count=3):
129199for attempt in backoff:
130200try:
131201response = request(
132-url=_METADATA_IP_ROOT, method="GET", headers=headers, timeout=timeout
202+url=_get_metadata_ip_root(use_mtls),
203+method="GET",
204+headers=headers,
205+timeout=timeout,
133206 )
134207135208metadata_flavor = response.headers.get(_METADATA_FLAVOR_HEADER)
@@ -153,7 +226,7 @@ def ping(request, timeout=_METADATA_DEFAULT_TIMEOUT, retry_count=3):
153226def get(
154227request,
155228path,
156-root=_METADATA_ROOT,
229+root=None,
157230params=None,
158231recursive=False,
159232retry_count=5,
@@ -168,7 +241,8 @@ def get(
168241 HTTP requests.
169242 path (str): The resource to retrieve. For example,
170243 ``'instance/service-accounts/default'``.
171- root (str): The full path to the metadata server root.
244+ root (Optional[str]): The full path to the metadata server root. If not
245+ provided, the default root will be used.
172246 params (Optional[Mapping[str, str]]): A mapping of query parameter
173247 keys to values.
174248 recursive (bool): Whether to do a recursive query of metadata. See
@@ -189,7 +263,24 @@ def get(
189263 Raises:
190264 google.auth.exceptions.TransportError: if an error occurred while
191265 retrieving metadata.
266+ google.auth.exceptions.MutualTLSChannelError: if using mtls and the environment
267+ configuration is invalid for mTLS (for example, the metadata host
268+ has been overridden in strict mTLS mode).
269+192270 """
271+use_mtls = _mtls.should_use_mds_mtls()
272+# Prepare the request object for mTLS if needed.
273+# This will create a new request object with the mTLS session.
274+_prepare_request_for_mds(request, use_mtls=use_mtls)
275+276+if root is None:
277+root = _get_metadata_root(use_mtls)
278+279+# mTLS is only supported when connecting to the default metadata host.
280+# If we are in strict mode (which requires mTLS), ensure that the metadata host
281+# has not been overridden to a non-default host value (which means mTLS will fail).
282+_validate_gce_mds_configured_environment()
283+193284base_url = urljoin(root, path)
194285query_params = {} if params is None else params
195286