Skip to content

Commit 9513d26

Browse files
committed
Address review comments and improve auth
Auth now chooses between cert (#1) and bearer token (#2) instead of sending both. Also renamed ssl --> tls for correctness
1 parent dbf5b46 commit 9513d26

File tree

4 files changed

+95
-99
lines changed

4 files changed

+95
-99
lines changed

checks.d/kubernetes.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ def _perform_kubelet_checks(self, url):
103103
service_check_base = NAMESPACE + '.kubelet.check'
104104
is_ok = True
105105
try:
106-
req = self.kubeutil.retrieve_kubelet_url(url)
106+
req = self.kubeutil.perform_kubelet_query(url)
107107
for line in req.iter_lines():
108108

109109
# avoid noise; this check is expected to fail since we override the container hostname

conf.d/kubernetes.yaml.example

+12-9
Original file line numberDiff line numberDiff line change
@@ -66,29 +66,32 @@ instances:
6666
# kubelet port. It needs to be set if you are not using a default one (10250 or 10255)
6767
# kubelet_port: 10255
6868
#
69-
# SSL configuration for querying kubelet.
69+
# TLS configuration for querying kubelet.
7070
#
7171
# Available options are:
72-
# - simple SSL validation (with readily available certificates)
73-
# - disabling SSL validation
72+
# (server auth)
73+
# - simple TLS validation (with readily available certificates)
74+
# - disabling TLS validation
7475
# - providing the server certificate
76+
# (client auth)
7577
# - providing a client cert/key pair
78+
# - using a service account bearer token
7679
#
77-
# The default is to try and use the read-only port that doesn't require SSL
78-
# And to fall back to the HTTPS API with simple SSL validation.
80+
# The default is to try and use the read-only port that doesn't require TLS
81+
# And to fall back to the HTTPS API with simple TLS validation.
7982
#
80-
# Explicitly disabling ssl_verify should be used with caution:
83+
# Explicitly disabling tls_verify should be used with caution:
8184
# if an attacker sniffs the agent requests they will see the agent's service account bearer token.
8285
#
83-
# Providing a server cert enables SSL validation.
86+
# Providing a server cert enables TLS validation.
8487
#
8588
# It is possible to providing a client cert/key pair for client auth.
86-
# The agent also uses its pod bearer token (if available) when querying kubelet and the apiserver.
89+
# If no such cert is passed, the agent authenticates using its pod bearer token (if available) when querying kubelet and the apiserver.
8790
#
8891
# The recommended way to pass certificates and keys to the agent is by using
8992
# "Secret" Kubernetes object (and to mount them as volumes).
9093
#
91-
# kubelet_ssl_verify: True
94+
# kubelet_tls_verify: True
9295
# kubelet_cert: /path/to/ca.pem
9396
# kubelet_client_crt: /path/to/client.crt
9497
# kubelet_client_key: /path/to/client.key

tests/checks/mock/test_kubernetes.py

+45-56
Original file line numberDiff line numberDiff line change
@@ -385,22 +385,31 @@ class TestKubeutil(unittest.TestCase):
385385
def setUp(self, _locate_kubelet):
386386
self.kubeutil = KubeUtil()
387387

388-
def test_init_ssl_settings(self):
388+
@mock.patch('os.path.exists', return_value=True)
389+
@mock.patch('utils.kubernetes.kubeutil.KubeUtil.get_auth_token', return_value=None)
390+
def test_init_tls_settings(self, *args):
391+
# kubelet
389392
instances = [
390393
# (instance, expected_result)
391-
({}, {'verify': True}),
392-
({'kubelet_ssl_verify': False}, {'verify': False}),
393-
({'kubelet_ssl_verify': True}, {'verify': True}),
394-
({'kubelet_ssl_verify': 'foo.pem'}, {'verify': 'foo.pem'}),
395-
({'kubelet_cert': 'foo.pem'}, {'verify': 'foo.pem'}),
394+
({}, {'kubelet_verify': True}),
395+
({'kubelet_tls_verify': False}, {'kubelet_verify': False}),
396+
({'kubelet_tls_verify': True}, {'kubelet_verify': True}),
397+
({'kubelet_tls_verify': 'foo.pem'}, {'kubelet_verify': 'foo.pem'}),
398+
({'kubelet_cert': 'foo.pem'}, {'kubelet_verify': 'foo.pem'}),
396399
({'kubelet_client_crt': 'client.crt', 'kubelet_client_key': 'client.key'},
397-
{'verify': True, 'kubelet_client_cert': ('client.crt', 'client.key')}),
398-
({'kubelet_ssl_verify': True, 'kubelet_client_crt': 'client.crt'}, {'verify': True}),
399-
({'kubelet_client_crt': 'client.crt'}, {'verify': True})
400+
{'kubelet_verify': True, 'kubelet_client_cert': ('client.crt', 'client.key')}),
401+
({'kubelet_tls_verify': True, 'kubelet_client_crt': 'client.crt'}, {'kubelet_verify': True}),
402+
({'kubelet_client_crt': 'client.crt'}, {'kubelet_verify': True})
400403
]
401404
for instance, result in instances:
402-
self.assertEqual(self.kubeutil._init_ssl_settings(instance), result)
405+
self.assertEqual(self.kubeutil._init_tls_settings(instance), result)
403406

407+
# apiserver
408+
instance = {'apiserver_client_crt': 'foo.crt', 'apiserver_client_key': 'foo.key'}
409+
expected_res = {'apiserver_client_cert': ('foo.crt', 'foo.key'), 'kubelet_verify': True}
410+
self.assertEqual(self.kubeutil._init_tls_settings(instance), expected_res)
411+
with mock.patch('utils.kubernetes.kubeutil.os.path.exists', return_value=False):
412+
self.assertEqual(self.kubeutil._init_tls_settings(instance), {'kubelet_verify': True})
404413

405414
##### Test _locate_kubelet #####
406415

@@ -419,7 +428,7 @@ def test_locate_kubelet_no_auth_no_ssl(self, _get_hostname):
419428
({'kubelet_port': '1337'}, 'http://test_docker_host:1337'),
420429
({'host': 'test_explicit_host', 'kubelet_port': '1337'}, 'http://test_explicit_host:1337')
421430
]
422-
with mock.patch('utils.kubernetes.kubeutil.KubeUtil.retrieve_kubelet_url', return_value=True):
431+
with mock.patch('utils.kubernetes.kubeutil.KubeUtil.perform_kubelet_query', return_value=True):
423432
for instance, result in no_auth_no_ssl_instances:
424433
self.assertEqual(self.kubeutil._locate_kubelet(instance), result)
425434

@@ -434,13 +443,13 @@ def test_locate_kubelet_no_auth_no_verify(self, _get_hostname):
434443
]
435444

436445
def side_effect(url):
437-
"""Mock KubeUtil.retrieve_kubelet_url"""
446+
"""Mock KubeUtil.perform_kubelet_query"""
438447
if url.startswith('https://'):
439448
return True
440449
else:
441450
raise Exception()
442451

443-
with mock.patch('utils.kubernetes.kubeutil.KubeUtil.retrieve_kubelet_url', side_effect=side_effect):
452+
with mock.patch('utils.kubernetes.kubeutil.KubeUtil.perform_kubelet_query', side_effect=side_effect):
444453
for instance, result in no_auth_no_verify_instances:
445454
self.assertEqual(self.kubeutil._locate_kubelet(instance), result)
446455

@@ -449,85 +458,65 @@ def side_effect(url):
449458
@mock.patch('utils.kubernetes.kubeutil.KubeUtil.get_auth_token', return_value='foo')
450459
def test_locate_kubelet_verify_and_auth(self, *args):
451460
"""
452-
Test kubelet connection with SSL. Also look for auth token.
461+
Test kubelet connection with TLS. Also look for auth token.
453462
"""
454463
no_auth_instances = [
455-
# instance, ssl_settings, expected_result
456-
({}, {'verify': True}, 'https://test_k8s_host:10250'),
457-
({'kubelet_port': '1337'}, {'verify': 'test.pem'}, 'https://test_k8s_host:1337'),
464+
# instance, tls_settings, expected_result
465+
({}, {'kubelet_verify': True}, 'https://test_k8s_host:10250'),
466+
({'kubelet_port': '1337'}, {'kubelet_verify': 'test.pem'}, 'https://test_k8s_host:1337'),
458467
(
459468
{'host': 'test_explicit_host'},
460-
{'verify': True, 'kubelet_client_cert': ('client.crt', 'client.key')},
469+
{'kubelet_verify': True, 'kubelet_client_cert': ('client.crt', 'client.key')},
461470
'https://test_explicit_host:10250'
462471
),
463472
(
464473
{'host': 'test_explicit_host', 'kubelet_port': '1337'},
465-
{'verify': True},
474+
{'kubelet_verify': True},
466475
'https://test_explicit_host:1337'
467476
),
468477
]
469478

470479
def side_effect(url, **kwargs):
471-
"""Mock KubeUtil.retrieve_kubelet_url"""
480+
"""Mock KubeUtil.perform_kubelet_query"""
472481
if url.startswith('https://') and '10255' not in url:
473482
return True
474483
else:
475484
raise Exception()
476485

477-
# no auth / SSL with verify
478-
for instance, ssl_settings, result in no_auth_instances:
486+
# no auth / TLS with verify
487+
for instance, tls_settings, result in no_auth_instances:
479488
with mock.patch('utils.kubernetes.kubeutil.requests') as req:
480489
req.get = mock.MagicMock(side_effect=side_effect)
481-
self.kubeutil.ssl_settings = ssl_settings
490+
self.kubeutil.tls_settings = tls_settings
482491
self.assertEqual(self.kubeutil._locate_kubelet(instance), result)
483492
req.get.assert_called_with(result + '/healthz', # test endpoint
484493
timeout=10,
485-
verify=ssl_settings.get('verify', False),
486-
cert=ssl_settings.get('kubelet_client_cert'),
487-
headers={'Authorization': 'Bearer foo'}, # auth
494+
verify=tls_settings.get('kubelet_verify', True),
495+
headers={'Authorization': 'Bearer foo'} if 'kubelet_client_cert' not in tls_settings else None,
496+
cert=tls_settings.get('kubelet_client_cert'),
488497
params={'verbose': True}
489498
)
490499

491500
@mock.patch('utils.kubernetes.kubeutil.KubeUtil.get_auth_token', return_value='foo')
492501
def test_get_node_hostname(self, _get_auth_tkn):
493502
node_lists = [
494503
(json.loads(Fixtures.read_file('filtered_node_list_1_4.json', string_escape=False)), 'ip-10-0-0-179'),
495-
({'items': [{'foo': 'bar'}]}, None)
496-
]
497-
498-
exception_node_lists = [
499-
{'items': []},
500-
{'items': [{'foo': 'bar'}, {'bar': 'foo'}]}
504+
({'items': [{'foo': 'bar'}]}, None),
505+
({'items': []}, None),
506+
({'items': [{'foo': 'bar'}, {'bar': 'foo'}]}, None)
501507
]
502508

503509
for node_list, expected_result in node_lists:
504510
with mock.patch('utils.kubernetes.kubeutil.KubeUtil.retrieve_json_auth', return_value=node_list):
505511
self.assertEqual(self.kubeutil.get_node_hostname('ip-10-0-0-179'), expected_result)
506512

507-
for node_list in exception_node_lists:
508-
with mock.patch('utils.kubernetes.kubeutil.KubeUtil.retrieve_json_auth', return_value=node_list):
509-
self.assertRaises(Exception, self.kubeutil.get_node_hostname, 'ip-10-0-0-179')
510-
511513
@mock.patch('utils.kubernetes.KubeUtil.retrieve_pods_list', side_effect=['foo'])
512514
@mock.patch('utils.kubernetes.KubeUtil.extract_kube_labels')
513515
def test_get_kube_labels(self, extract_kube_labels, retrieve_pods_list):
514516
self.kubeutil.get_kube_labels(excluded_keys='bar')
515517
retrieve_pods_list.assert_called_once()
516518
extract_kube_labels.assert_called_once_with('foo', excluded_keys='bar')
517519

518-
@mock.patch('utils.kubernetes.kubeutil.KubeUtil.get_auth_token', return_value=None)
519-
def test_init_tls_settings(self, get_tkn):
520-
self.assertEqual(self.kubeutil._init_tls_settings({}), {})
521-
self.assertEqual(self.kubeutil._init_tls_settings({'apiserver_client_crt': 'foo.crt'}), {})
522-
523-
instance = {'apiserver_client_crt': 'foo.crt', 'apiserver_client_key': 'foo.key'}
524-
with mock.patch('utils.kubernetes.kubeutil.os.path.exists', return_value=True):
525-
expected_res = {'apiserver_client_cert': ('foo.crt', 'foo.key')}
526-
self.assertEqual(self.kubeutil._init_tls_settings(instance), expected_res)
527-
528-
with mock.patch('utils.kubernetes.kubeutil.os.path.exists', return_value=False):
529-
self.assertEqual(self.kubeutil._init_tls_settings(instance), {})
530-
531520
def test_extract_kube_labels(self):
532521
"""
533522
Test with both 1.1 and 1.2 version payloads
@@ -551,7 +540,7 @@ def test_extract_kube_labels(self):
551540
labels = set(inn for out in res.values() for inn in out)
552541
self.assertEqual(len(labels), 3)
553542

554-
@mock.patch('utils.kubernetes.kubeutil.KubeUtil.retrieve_kubelet_url')
543+
@mock.patch('utils.kubernetes.kubeutil.KubeUtil.perform_kubelet_query')
555544
def test_retrieve_pods_list(self, retrieve_url):
556545
self.kubeutil.retrieve_pods_list()
557546
retrieve_url.assert_called_twice_with(self.kubeutil.pods_list_url, verbose=True, timeout=10)
@@ -568,7 +557,7 @@ def test_retrieve_metrics(self, retrieve_json):
568557

569558
@mock.patch('utils.kubernetes.kubeutil.KubeUtil.get_auth_token', return_value='foo')
570559
@mock.patch('utils.kubernetes.kubeutil.requests')
571-
def test_retrieve_kubelet_url(self, req, _get_auth_tkn):
560+
def test_perform_kubelet_query(self, req, _get_auth_tkn):
572561
base_params = {'timeout': 10, 'verify': False,
573562
'params': {'verbose': True}, 'cert': None, 'headers': None}
574563

@@ -580,15 +569,15 @@ def test_retrieve_kubelet_url(self, req, _get_auth_tkn):
580569
instances = [
581570
('http://test.com', {}, dict(base_params.items() + verify_true.items())),
582571
('https://test.com', {}, dict(base_params.items() + verify_true.items() + auth_token_header.items())),
583-
('https://test.com', {'verify': True}, dict(base_params.items() + verify_true.items() + auth_token_header.items())),
584-
('https://test.com', {'verify': 'kubelet.pem'}, dict(base_params.items() + verify_cert.items() + auth_token_header.items())),
572+
('https://test.com', {'kubelet_verify': True}, dict(base_params.items() + verify_true.items() + auth_token_header.items())),
573+
('https://test.com', {'kubelet_verify': 'kubelet.pem'}, dict(base_params.items() + verify_cert.items() + auth_token_header.items())),
585574
('https://test.com', {'kubelet_client_cert': ('client.crt', 'client.key')},
586-
dict(base_params.items() + verify_true.items() + client_cert.items() + auth_token_header.items())),
575+
dict(base_params.items() + verify_true.items() + client_cert.items())),
587576
]
588577
for url, ssl_context, expected_params in instances:
589578
req.get.reset_mock()
590-
self.kubeutil.ssl_settings = ssl_context
591-
self.kubeutil.retrieve_kubelet_url(url)
579+
self.kubeutil.tls_settings = ssl_context
580+
self.kubeutil.perform_kubelet_query(url)
592581
req.get.assert_called_with(url, **expected_params)
593582

594583
@mock.patch('utils.kubernetes.kubeutil.requests')

0 commit comments

Comments
 (0)