feat: MDS connections use mTLS (#1856) · googleapis/google-auth-library-python@0387bb9

@@ -24,31 +24,72 @@

2424

import os

2525

from urllib.parse import urljoin

262627+

import requests

28+2729

from google.auth import _helpers

2830

from google.auth import environment_vars

2931

from google.auth import exceptions

3032

from google.auth import metrics

3133

from google.auth import transport

3234

from 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)

4149

if 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():

102143

return 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+105173

def 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):

129199

for attempt in backoff:

130200

try:

131201

response = 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

)

134207135208

metadata_flavor = response.headers.get(_METADATA_FLAVOR_HEADER)

@@ -153,7 +226,7 @@ def ping(request, timeout=_METADATA_DEFAULT_TIMEOUT, retry_count=3):

153226

def get(

154227

request,

155228

path,

156-

root=_METADATA_ROOT,

229+

root=None,

157230

params=None,

158231

recursive=False,

159232

retry_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+193284

base_url = urljoin(root, path)

194285

query_params = {} if params is None else params

195286