From 68fe8eea41ad46b5d0b40b9388520c5be00d6730 Mon Sep 17 00:00:00 2001 From: Tiago Silva Date: Tue, 16 May 2023 15:59:06 +0100 Subject: [PATCH] Adds support for custom Server Name Indication (SNI) --- kubernetes/base/config/kube_config.py | 4 ++- kubernetes/base/config/kube_config_test.py | 39 ++++++++++++++++++++-- kubernetes/base/stream/ws_client.py | 2 ++ kubernetes/client/configuration.py | 4 +++ kubernetes/client/rest.py | 3 ++ scripts/rest_sni_patch.diff | 29 ++++++++++++++++ scripts/update-client.sh | 7 +++- 7 files changed, 83 insertions(+), 5 deletions(-) create mode 100644 scripts/rest_sni_patch.diff diff --git a/kubernetes/base/config/kube_config.py b/kubernetes/base/config/kube_config.py index a62df4105..d8c63a826 100644 --- a/kubernetes/base/config/kube_config.py +++ b/kubernetes/base/config/kube_config.py @@ -567,6 +567,8 @@ class KubeConfigLoader(object): temp_file_path=self._temp_file_path).as_file() if 'insecure-skip-tls-verify' in self._cluster: self.verify_ssl = not self._cluster['insecure-skip-tls-verify'] + if 'tls-server-name' in self._cluster: + self.tls_server_name = self._cluster['tls-server-name'] def _set_config(self, client_configuration): if 'token' in self.__dict__: @@ -578,7 +580,7 @@ class KubeConfigLoader(object): self._set_config(client_configuration) client_configuration.refresh_api_key_hook = _refresh_api_key # copy these keys directly from self to configuration object - keys = ['host', 'ssl_ca_cert', 'cert_file', 'key_file', 'verify_ssl'] + keys = ['host', 'ssl_ca_cert', 'cert_file', 'key_file', 'verify_ssl','tls_server_name'] for key in keys: if key in self.__dict__: setattr(client_configuration, key, getattr(self, key)) diff --git a/kubernetes/base/config/kube_config_test.py b/kubernetes/base/config/kube_config_test.py index da0d2f35c..b41549203 100644 --- a/kubernetes/base/config/kube_config_test.py +++ b/kubernetes/base/config/kube_config_test.py @@ -102,7 +102,7 @@ TEST_CLIENT_KEY = "client-key" TEST_CLIENT_KEY_BASE64 = _base64(TEST_CLIENT_KEY) TEST_CLIENT_CERT = "client-cert" TEST_CLIENT_CERT_BASE64 = _base64(TEST_CLIENT_CERT) - +TEST_TLS_SERVER_NAME = "kubernetes.io" TEST_OIDC_TOKEN = "test-oidc-token" TEST_OIDC_INFO = "{\"name\": \"test\"}" @@ -592,7 +592,14 @@ class TestKubeConfigLoader(BaseTestCase): "cluster": "clustertestcmdpath", "user": "usertestcmdpathscope" } - } + }, + { + "name": "tls-server-name", + "context": { + "cluster": "tls-server-name", + "user": "ssl" + } + }, ], "clusters": [ { @@ -634,7 +641,17 @@ class TestKubeConfigLoader(BaseTestCase): { "name": "clustertestcmdpath", "cluster": {} - } + }, + { + "name": "tls-server-name", + "cluster": { + "server": TEST_SSL_HOST, + "certificate-authority-data": + TEST_CERTIFICATE_AUTH_BASE64, + "insecure-skip-tls-verify": False, + "tls-server-name": TEST_TLS_SERVER_NAME, + } + }, ], "users": [ { @@ -1251,6 +1268,22 @@ class TestKubeConfigLoader(BaseTestCase): active_context="no_ssl_verification").load_and_set(actual) self.assertEqual(expected, actual) + def test_tls_server_name(self): + expected = FakeConfig( + host=TEST_SSL_HOST, + token=BEARER_TOKEN_FORMAT % TEST_DATA_BASE64, + cert_file=self._create_temp_file(TEST_CLIENT_CERT), + key_file=self._create_temp_file(TEST_CLIENT_KEY), + ssl_ca_cert=self._create_temp_file(TEST_CERTIFICATE_AUTH), + verify_ssl=True, + tls_server_name=TEST_TLS_SERVER_NAME + ) + actual = FakeConfig() + KubeConfigLoader( + config_dict=self.TEST_KUBE_CONFIG, + active_context="tls-server-name").load_and_set(actual) + self.assertEqual(expected, actual) + def test_list_contexts(self): loader = KubeConfigLoader( config_dict=self.TEST_KUBE_CONFIG, diff --git a/kubernetes/base/stream/ws_client.py b/kubernetes/base/stream/ws_client.py index a1fe80949..5ec8e7d4a 100644 --- a/kubernetes/base/stream/ws_client.py +++ b/kubernetes/base/stream/ws_client.py @@ -475,6 +475,8 @@ def create_websocket(configuration, url, headers=None): ssl_opts['certfile'] = configuration.cert_file if configuration.key_file: ssl_opts['keyfile'] = configuration.key_file + if configuration.tls_server_name: + ssl_opts['server_hostname'] = configuration.tls_server_name websocket = WebSocket(sslopt=ssl_opts, skip_utf8_validation=False) connect_opt = { diff --git a/kubernetes/client/configuration.py b/kubernetes/client/configuration.py index cd1ce61d6..51727214c 100644 --- a/kubernetes/client/configuration.py +++ b/kubernetes/client/configuration.py @@ -144,6 +144,10 @@ class Configuration(object): self.assert_hostname = None """Set this to True/False to enable/disable SSL hostname verification. """ + self.tls_server_name = None + """SSL/TLS Server Name Indication (SNI) + Set this to the SNI value expected by Kubernetes API. + """ self.connection_pool_maxsize = multiprocessing.cpu_count() * 5 """urllib3 connection pool's maximum number of connections saved diff --git a/kubernetes/client/rest.py b/kubernetes/client/rest.py index 9c703da3b..debf32e58 100644 --- a/kubernetes/client/rest.py +++ b/kubernetes/client/rest.py @@ -77,6 +77,9 @@ class RESTClientObject(object): if configuration.retries is not None: addition_pool_args['retries'] = configuration.retries + if configuration.tls_server_name: + addition_pool_args['server_hostname'] = configuration.tls_server_name + if maxsize is None: if configuration.connection_pool_maxsize is not None: maxsize = configuration.connection_pool_maxsize diff --git a/scripts/rest_sni_patch.diff b/scripts/rest_sni_patch.diff new file mode 100644 index 000000000..cdd516e49 --- /dev/null +++ b/scripts/rest_sni_patch.diff @@ -0,0 +1,29 @@ +diff --git a/kubernetes/client/configuration.py b/kubernetes/client/configuration.py +index 2b9dd96a50..ac5a18bf8a 100644 +--- a/kubernetes/client/configuration.py ++++ b/kubernetes/client/configuration.py +@@ -144,6 +144,10 @@ def __init__(self, host="http://localhost", + self.assert_hostname = None + """Set this to True/False to enable/disable SSL hostname verification. + """ ++ self.tls_server_name = None ++ """SSL/TLS Server Name Indication (SNI) ++ Set this to the SNI value expected by the server. ++ """ + + self.connection_pool_maxsize = multiprocessing.cpu_count() * 5 + """urllib3 connection pool's maximum number of connections saved +diff --git a/kubernetes/client/rest.py b/kubernetes/client/rest.py +index 48cd2b7752..4f04251bbf 100644 +--- a/kubernetes/client/rest.py ++++ b/kubernetes/client/rest.py +@@ -77,6 +77,9 @@ def __init__(self, configuration, pools_size=4, maxsize=None): + if configuration.retries is not None: + addition_pool_args['retries'] = configuration.retries + ++ if configuration.tls_server_name: ++ addition_pool_args['server_hostname'] = configuration.tls_server_name ++ + if maxsize is None: + if configuration.connection_pool_maxsize is not None: + maxsize = configuration.connection_pool_maxsize diff --git a/scripts/update-client.sh b/scripts/update-client.sh index 2348f39b8..87e00ed40 100755 --- a/scripts/update-client.sh +++ b/scripts/update-client.sh @@ -59,7 +59,7 @@ else fi echo ">>> Running python generator from the gen repo" -"${GEN_ROOT}/openapi/python.sh" "${CLIENT_ROOT}" "${SETTING_FILE}" +"${GEN_ROOT}/openapi/python.sh" "${CLIENT_ROOT}" "${SETTING_FILE}" mv "${CLIENT_ROOT}/swagger.json" "${SCRIPT_ROOT}/swagger.json" echo ">>> updating version information..." @@ -73,6 +73,11 @@ sed -i'' "s,^DEVELOPMENT_STATUS = .*,DEVELOPMENT_STATUS = \\\"${DEVELOPMENT_STAT # second, this should be ported to swagger-codegen echo ">>> patching client..." git apply "${SCRIPT_ROOT}/rest_client_patch.diff" +# The fix this patch is trying to make is already in the upstream swagger-codegen +# repo but it's not in the version we're using. We can remove this patch +# once we upgrade to a version of swagger-codegen that includes it (version>= 6.6.0). +# See https://github.com/OpenAPITools/openapi-generator/pull/15283 +git apply "${SCRIPT_ROOT}/rest_sni_patch.diff" echo ">>> generating docs..." pushd "${DOC_ROOT}" > /dev/null