diff --git a/service/files/ca.pem.j2 b/service/files/ca.pem.j2 new file mode 100644 index 0000000..d52069b --- /dev/null +++ b/service/files/ca.pem.j2 @@ -0,0 +1 @@ +{{ security.tls.ca_cert }} diff --git a/service/files/defaults.yaml b/service/files/defaults.yaml index 6deee0e..aa21508 100644 --- a/service/files/defaults.yaml +++ b/service/files/defaults.yaml @@ -16,6 +16,8 @@ configs: node: null port: cont: 3306 + tls: + enabled: true url: percona: debian: diff --git a/service/files/dhparams.pem.j2 b/service/files/dhparams.pem.j2 new file mode 100644 index 0000000..1d807c7 --- /dev/null +++ b/service/files/dhparams.pem.j2 @@ -0,0 +1 @@ +{{ security.tls.dhparam }} diff --git a/service/files/galera_checker.py b/service/files/galera_checker.py index e3134a8..9cc2ddc 100644 --- a/service/files/galera_checker.py +++ b/service/files/galera_checker.py @@ -33,11 +33,16 @@ PID_FILE = os.path.join(DATADIR, "mysqld.pid") HOSTNAME = socket.getfqdn() IPADDR = socket.gethostbyname(HOSTNAME) +CA_CERT = '/opt/ccp/etc/tls/ca.pem' +SERVER_CERT = '/opt/ccp/etc/tls/server-cert.pem' +SERVER_KEY = '/opt/ccp/etc/tls/server-key.pem' + MONITOR_PASSWORD = None CLUSTER_NAME = None ETCD_PATH = None ETCD_HOST = None ETCD_PORT = None +ETCD_TLS = None def retry(f): @@ -64,11 +69,22 @@ def retry(f): def get_etcd_client(): - etcd_client = etcd.Client(host=ETCD_HOST, - port=ETCD_PORT, - allow_reconnect=True, - read_timeout=2) - return etcd_client + if ETCD_TLS: + protocol = 'https' + cert = (SERVER_CERT, SERVER_KEY) + ca_cert = CA_CERT + else: + protocol = 'http' + cert = None + ca_cert = None + + return etcd.Client(host=ETCD_HOST, + port=ETCD_PORT, + allow_reconnect=True, + protocol=protocol, + cert=cert, + ca_cert=ca_cert, + read_timeout=2) @retry @@ -297,7 +313,7 @@ def set_globals(): config = get_config() global MONITOR_PASSWORD, CLUSTER_NAME - global ETCD_PATH, ETCD_HOST, ETCD_PORT + global ETCD_PATH, ETCD_HOST, ETCD_PORT, ETCD_TLS CLUSTER_NAME = config['percona']['cluster_name'] MONITOR_PASSWORD = config['percona']['monitor_password'] @@ -305,6 +321,7 @@ def set_globals(): ETCD_HOST = "etcd.%s.svc.%s" % (config['namespace'], config['cluster_domain']) ETCD_PORT = int(config['etcd']['client_port']['cont']) + ETCD_TLS = config['etcd']['tls']['enabled'] if __name__ == "__main__": diff --git a/service/files/haproxy_entrypoint.py b/service/files/haproxy_entrypoint.py index afc3744..2d6f21e 100644 --- a/service/files/haproxy_entrypoint.py +++ b/service/files/haproxy_entrypoint.py @@ -18,6 +18,10 @@ BACKEND_NAME = "galera-cluster" SERVER_NAME = "primary" GLOBALS_PATH = '/etc/ccp/globals/globals.json' +CA_CERT = '/opt/ccp/etc/tls/ca.pem' +SERVER_CERT = '/opt/ccp/etc/tls/server-cert.pem' +SERVER_KEY = '/opt/ccp/etc/tls/server-key.pem' + LOG_DATEFMT = "%Y-%m-%d %H:%M:%S" LOG_FORMAT = "%(asctime)s.%(msecs)03d - %(levelname)s - %(message)s" logging.basicConfig(format=LOG_FORMAT, datefmt=LOG_DATEFMT) @@ -29,6 +33,7 @@ CONNECTION_DELAY = None ETCD_PATH = None ETCD_HOST = None ETCD_PORT = None +ETCD_TLS = None # Haproxy constant for health checks SRV_STATE_RUNNING = 2 @@ -68,7 +73,7 @@ def set_globals(): config = get_config() global CONNECTION_ATTEMPTS, CONNECTION_DELAY - global ETCD_PATH, ETCD_HOST, ETCD_PORT + global ETCD_PATH, ETCD_HOST, ETCD_PORT, ETCD_TLS CONNECTION_ATTEMPTS = config['etcd']['connection_attempts'] CONNECTION_DELAY = config['etcd']['connection_delay'] @@ -76,13 +81,26 @@ def set_globals(): ETCD_HOST = "etcd.%s.svc.%s" % (config['namespace'], config['cluster_domain']) ETCD_PORT = int(config['etcd']['client_port']['cont']) + ETCD_TLS = config['etcd']['tls']['enabled'] def get_etcd_client(): + if ETCD_TLS: + protocol = 'https' + cert = (SERVER_CERT, SERVER_KEY) + ca_cert = CA_CERT + else: + protocol = 'http' + cert = None + ca_cert = None + return etcd.Client(host=ETCD_HOST, port=ETCD_PORT, allow_reconnect=True, + protocol=protocol, + cert=cert, + ca_cert=ca_cert, read_timeout=2) diff --git a/service/files/my.cnf.j2 b/service/files/my.cnf.j2 index ebfc3ce..973e991 100644 --- a/service/files/my.cnf.j2 +++ b/service/files/my.cnf.j2 @@ -35,4 +35,16 @@ wsrep_provider = /usr/lib/galera3/libgalera_smm.so wsrep_cluster_name = {{ percona.cluster_name }} wsrep_sst_method = xtrabackup-v2 wsrep_sst_auth = "xtrabackup:{{ percona.xtrabackup_password }}" -wsrep_provider_options = "gcache.size={{ percona.gcache_size }};gcache.recover=yes" +wsrep_provider_options = "gcache.size={{ percona.gcache_size }};gcache.recover=yes{% if percona.tls.enabled %};socket.ssl=yes;socket.ssl_key=/opt/ccp/etc/tls/server-key.pem;socket.ssl_cert=/opt/ccp/etc/tls/server-cert.pem;socket.ssl_ca=/opt/ccp/etc/tls/ca.pem"{% endif %} + +{% if percona.tls.enabled %} +ssl-ca = /opt/ccp/etc/tls/ca.pem +ssl-cert = /opt/ccp/etc/tls/server-cert.pem +ssl-key = /opt/ccp/etc/tls/server-key.pem + +[sst] +encrypt = 4 +ssl-ca = /opt/ccp/etc/tls/ca.pem +ssl-cert = /opt/ccp/etc/tls/server-cert.pem +ssl-key = /opt/ccp/etc/tls/server-key.pem +{% endif %} diff --git a/service/files/percona_entrypoint.py b/service/files/percona_entrypoint.py index 5a1f73d..8dd20dc 100644 --- a/service/files/percona_entrypoint.py +++ b/service/files/percona_entrypoint.py @@ -24,8 +24,13 @@ INIT_FILE = os.path.join(DATADIR, 'init.ok') PID_FILE = os.path.join(DATADIR, "mysqld.pid") GRASTATE_FILE = os.path.join(DATADIR, 'grastate.dat') SST_FLAG = os.path.join(DATADIR, "sst_in_progress") +DHPARAM = os.path.join(DATADIR, "dhparams.pem") GLOBALS_PATH = '/etc/ccp/globals/globals.json' +CA_CERT = '/opt/ccp/etc/tls/ca.pem' +SERVER_CERT = '/opt/ccp/etc/tls/server-cert.pem' +SERVER_KEY = '/opt/ccp/etc/tls/server-key.pem' + LOG_DATEFMT = "%Y-%m-%d %H:%M:%S" LOG_FORMAT = "%(asctime)s.%(msecs)03d - %(levelname)s - %(message)s" logging.basicConfig(format=LOG_FORMAT, datefmt=LOG_DATEFMT) @@ -44,6 +49,8 @@ CONNECTION_DELAY = None ETCD_PATH = None ETCD_HOST = None ETCD_PORT = None +ETCD_TLS = None +DHPARAM_CERT = None class ProcessException(Exception): @@ -76,7 +83,8 @@ def get_config(): variables = {} with open(GLOBALS_PATH) as f: global_conf = json.load(f) - for key in ['percona', 'db', 'etcd', 'namespace', 'cluster_domain']: + for key in ['percona', 'db', 'etcd', 'namespace', 'cluster_domain', + 'security']: variables[key] = global_conf[key] LOG.debug(variables) return variables @@ -88,7 +96,7 @@ def set_globals(): global MYSQL_ROOT_PASSWORD, CLUSTER_NAME, XTRABACKUP_PASSWORD global MONITOR_PASSWORD, CONNECTION_ATTEMPTS, CONNECTION_DELAY global ETCD_PATH, ETCD_HOST, ETCD_PORT, EXPECTED_NODES - global FORCE_BOOTSTRAP, FORCE_BOOTSTRAP_NODE + global FORCE_BOOTSTRAP, FORCE_BOOTSTRAP_NODE, ETCD_TLS, DHPARAM_CERT FORCE_BOOTSTRAP = config['percona']['force_bootstrap']['enabled'] FORCE_BOOTSTRAP_NODE = config['percona']['force_bootstrap']['node'] @@ -103,6 +111,8 @@ def set_globals(): ETCD_HOST = "etcd.%s.svc.%s" % (config['namespace'], config['cluster_domain']) ETCD_PORT = int(config['etcd']['client_port']['cont']) + ETCD_TLS = config['etcd']['tls']['enabled'] + DHPARAM_CERT = config['security']['tls']['dhparam'] def get_mysql_client(insecure=False): @@ -118,9 +128,21 @@ def get_mysql_client(insecure=False): def get_etcd_client(): + if ETCD_TLS: + protocol = 'https' + cert = (SERVER_CERT, SERVER_KEY) + ca_cert = CA_CERT + else: + protocol = 'http' + cert = None + ca_cert = None + return etcd.Client(host=ETCD_HOST, port=ETCD_PORT, allow_reconnect=True, + protocol=protocol, + cert=cert, + ca_cert=ca_cert, read_timeout=2) @@ -134,6 +156,15 @@ def datadir_cleanup(path): os.remove(fullpath) +def create_dhparam(): + if not os.path.isfile(DHPARAM): + with open(DHPARAM, 'w') as f: + f.write(DHPARAM_CERT) + LOG.info("dhparam cert created in %s", DHPARAM) + else: + LOG.info("%s exists, not overriding it", DHPARAM) + + def create_init_flag(): if not os.path.isfile(INIT_FILE): @@ -156,6 +187,7 @@ def run_cmd(cmd, check_result=False): def run_mysqld(available_nodes, donors_list, etcd_client, lock): + create_dhparam() cmd = ("mysqld --user=mysql --wsrep_cluster_name=%s" " --wsrep_cluster_address=%s" " --wsrep_sst_method=xtrabackup-v2" @@ -476,7 +508,9 @@ def wait_for_mysqld(proc): def wait_for_mysqld_to_start(proc, insecure): LOG.info("Waiting mysql to start...") - time.sleep(5) + # Sometimes initial mysql start could take some time, especialy with SSL + # enabled. FIXME - replace sleep with some additional checks. + time.sleep(30) while True: if check_if_sst_running(): LOG.debug("SST sync detected, waiting...") diff --git a/service/files/server-cert.pem.j2 b/service/files/server-cert.pem.j2 new file mode 100644 index 0000000..8abc152 --- /dev/null +++ b/service/files/server-cert.pem.j2 @@ -0,0 +1 @@ +{{ security.tls.server_cert }} diff --git a/service/files/server-key.pem.j2 b/service/files/server-key.pem.j2 new file mode 100644 index 0000000..70cf751 --- /dev/null +++ b/service/files/server-key.pem.j2 @@ -0,0 +1 @@ +{{ security.tls.server_key }} diff --git a/service/galera.yaml b/service/galera.yaml index 1cf0438..db7b066 100644 --- a/service/galera.yaml +++ b/service/galera.yaml @@ -15,6 +15,11 @@ service: daemon: files: - galera-checker + # {% if percona.tls.enabled %} + - ca.pem + - server-key.pem + - server-cert.pem + # {% endif %} dependencies: - etcd command: "/opt/ccp/bin/galera_checker.py" @@ -31,6 +36,11 @@ service: files: - haproxy-conf - haproxy_entrypoint + # {% if percona.tls.enabled %} + - ca.pem + - server-key.pem + - server-cert.pem + # {% endif %} dependencies: - etcd command: "/opt/ccp/bin/haproxy_entrypoint.py daemon" @@ -67,6 +77,11 @@ service: - entrypoint - mycnf - galera-checker + # {% if percona.tls.enabled %} + - ca.pem + - server-key.pem + - server-cert.pem + # {% endif %} dependencies: - etcd command: /opt/ccp/bin/entrypoint.py @@ -90,3 +105,22 @@ files: path: /opt/ccp/bin/haproxy_entrypoint.py content: haproxy_entrypoint.py perm: "0755" +# {% if percona.tls.enabled %} + ca.pem: + path: /opt/ccp/etc/tls/ca.pem + content: ca.pem.j2 + perm: "0400" + server-key.pem: + path: /opt/ccp/etc/tls/server-key.pem + content: server-key.pem.j2 + perm: "0400" + server-cert.pem: + path: /opt/ccp/etc/tls/server-cert.pem + content: server-cert.pem.j2 + perm: "0400" + # Cant use it right now, 'cos of the file creation order + dhparams.pem: + path: /var/lib/mysql/dhparams.pem + content: dhparams.pem.j2 + perm: "0400" +# {% endif %}