From d82aebcd82d02b7fb178a3095c13b7136fe33edf Mon Sep 17 00:00:00 2001 From: Aleksandr Mogylchenko Date: Wed, 18 Jan 2017 17:23:01 +0100 Subject: [PATCH] Add support for TLS-enabled etcd With this change CA certificate is saved to /opt/ccp/etc/tls/ca.pem, if that file does not exist. Should be useful to avoid mounting CA certificate to each job, pod, etc. Change-Id: I574d64082e77f49024f49aa7b30c4f2f6cc044ac Depends-On: Ib4b3ea4da7c1f641b9ab0223226348de5eac94df --- fuel_ccp_entrypoint/start_script.py | 36 ++++++++++++-- .../tests/test_fuel_ccp_entrypoint.py | 48 ++++++++++++++++++- 2 files changed, 78 insertions(+), 6 deletions(-) diff --git a/fuel_ccp_entrypoint/start_script.py b/fuel_ccp_entrypoint/start_script.py index b374ddb..0f32aa1 100644 --- a/fuel_ccp_entrypoint/start_script.py +++ b/fuel_ccp_entrypoint/start_script.py @@ -24,6 +24,7 @@ import six VARIABLES = {} GLOBALS_PATH = '/etc/ccp/globals/globals.json' META_FILE = "/etc/ccp/meta/meta.json" +CACERT = "/opt/ccp/etc/tls/ca.pem" WORKFLOW_PATH_TEMPLATE = '/etc/ccp/role/%s.json' FILES_DIR = '/etc/ccp/files' EXPORTS_DIR = '/etc/ccp/exports' @@ -291,24 +292,39 @@ def create_files(files): @retry def get_etcd_client(): + if VARIABLES["security"]["tls"]["enabled"]: + LOG.debug("TLS is enabled for etcd, using encrypted connectivity") + scheme = "https" + ca_cert = CACERT + else: + scheme = "http" + ca_cert = None + etcd_machines = [] # if it's etcd container use local address because container is not # accessible via service due failed readiness check if VARIABLES["role_name"] in ["etcd", "etcd-leader-elector", "etcd-watcher"]: + if VARIABLES["security"]["tls"]["enabled"]: + # If it's etcd container, connectivity goes over IP address, thus + # TLS connection will fail. Need to reuse non-TLS + # https://github.com/coreos/etcd/issues/4311 + scheme = "http" + ca_cert = None + etcd_address = '127.0.0.1' + else: + etcd_address = VARIABLES["network_topology"]["private"]["address"] etcd_machines.append( - (VARIABLES["network_topology"]["private"]["address"], - VARIABLES["etcd"]["client_port"]['cont'])) + (etcd_address, VARIABLES["etcd"]["client_port"]['cont'])) else: etcd_machines.append( (address('etcd'), VARIABLES["etcd"]["client_port"]['cont']) ) - etcd_machines_str = " ".join(["%s:%d" % (h, p) for h, p in etcd_machines]) LOG.debug("Using the following etcd urls: \"%s\"", etcd_machines_str) return etcd.Client(host=tuple(etcd_machines), allow_reconnect=True, - read_timeout=2) + read_timeout=2, protocol=scheme, ca_cert=ca_cert) def check_dependence(dep, etcd_client): @@ -465,6 +481,16 @@ def get_variables(role_name): return variables +def _get_ca_certificate(): + name = CACERT + if not os.path.isfile(name): + with open(CACERT, 'w') as f: + f.write(VARIABLES['security']['tls']['ca_cert']) + LOG.info("CA certificated saved to %s", CACERT) + else: + LOG.info("CA file exists, not overwriting it") + + def main(): action_parser = argparse.ArgumentParser(add_help=False) action_parser.add_argument("action") @@ -476,6 +502,8 @@ def main(): VARIABLES = get_variables(args.role) LOG.debug('Global variables:\n%s', VARIABLES) + if VARIABLES["security"]["tls"]["enabled"]: + _get_ca_certificate() if args.action == "provision": do_provision(args.role) elif args.action == "status": diff --git a/fuel_ccp_entrypoint/tests/test_fuel_ccp_entrypoint.py b/fuel_ccp_entrypoint/tests/test_fuel_ccp_entrypoint.py index 51d8f63..e117579 100644 --- a/fuel_ccp_entrypoint/tests/test_fuel_ccp_entrypoint.py +++ b/fuel_ccp_entrypoint/tests/test_fuel_ccp_entrypoint.py @@ -142,6 +142,11 @@ class TestGetETCDClient(base.TestCase): "private": { "address": "192.0.2.1" } + }, + "security": { + "tls": { + "enabled": False + } } } with mock.patch("etcd.Client") as m_etcd: @@ -152,7 +157,9 @@ class TestGetETCDClient(base.TestCase): m_etcd.assert_called_once_with( host=(("192.0.2.1", 10042),), allow_reconnect=True, - read_timeout=2) + read_timeout=2, + protocol='http', + ca_cert=None) def test_get_etcd_client(self): start_script.VARIABLES = { @@ -166,6 +173,11 @@ class TestGetETCDClient(base.TestCase): "connection_attempts": 3, "connection_delay": 0, }, + "security": { + "tls": { + "enabled": False + } + } } with mock.patch("etcd.Client") as m_etcd: expected_value = object() @@ -175,7 +187,39 @@ class TestGetETCDClient(base.TestCase): m_etcd.assert_called_once_with( host=(('etcd.ccp.svc.cluster.local', 1234),), allow_reconnect=True, - read_timeout=2) + read_timeout=2, + protocol='http', + ca_cert=None) + + def test_get_secured_etcd_client(self): + start_script.VARIABLES = { + "role_name": "banana", + "namespace": "ccp", + "cluster_domain": 'cluster.local', + "etcd": { + "client_port": { + "cont": 1234 + }, + "connection_attempts": 3, + "connection_delay": 0, + }, + "security": { + "tls": { + "enabled": True + } + } + } + with mock.patch("etcd.Client") as m_etcd: + expected_value = object() + m_etcd.return_value = expected_value + etcd_client = start_script.get_etcd_client() + self.assertIs(expected_value, etcd_client) + m_etcd.assert_called_once_with( + host=(('etcd.ccp.svc.cluster.local', 1234),), + allow_reconnect=True, + read_timeout=2, + protocol='https', + ca_cert='/opt/ccp/etc/tls/ca.pem') def test_get_etcd_client_wrong(self): start_script.VARIABLES = {