MySQL is a popular open-source relational-database management system. One of its core features is replication, and in this article I will be showing how to configure a master and slave MySQL instance, and then configure replication from master to slave over SSL. Encryption will help protect the replication from snooping. This type of replication has many uses, for example: disaster-recovery scenarios whereby the slave can be switched to a master role in the case of a master outage, for performance where all reads can take place on the slave with writes and updates occurring on the master, and so on. Replication can be configured without encryption, but encrypting with SSL is preferred as part of a defence-in-depth strategy - it’s an extra layer of security.
This article already presumes a good working knowledge of MySQL. The master server is centosa with IP address 10.1.1.150, and is running a minimal installation of CentOS 6.4 x86_64. The slave, centosb, is running the same OS and has IP address 10.1.1.151. MySQL will be installed from the latest current stable RPMs available at dev.mysql.com, rather than using the upstream versions. The latest stable version available at the time of writing is 5.6.14.
This article will cover the configuration of an SSL-encrypted replicated environment from scratch - it does not cover the migration of an existing replicated configuration to an SSL-encrypted replicated configuration, or the migration of any existing data to a new slave.
MySQL Installation
Perform all installation steps on both servers. I downloaded the RPM bundle from dev.mysql.com: MySQL-5.6.14-1.el6.x86_64.rpm-bundle.tar. Let’s first take a look at the contents of the tar file:
|
1 2 3 4 5 6 7 8 |
# tar tvf /var/tmp/MySQL-5.6.14-1.el6.x86_64.rpm-bundle.tar -rw-r--r-- bteam/staff 1936928 2013-09-11 13:26 MySQL-shared-5.6.14-1.el6.x86_64.rpm -rw-r--r-- bteam/staff 3329604 2013-09-11 13:24 MySQL-devel-5.6.14-1.el6.x86_64.rpm -rw-r--r-- bteam/staff 85034772 2013-09-11 13:24 MySQL-embedded-5.6.14-1.el6.x86_64.rpm -rw-r--r-- bteam/staff 18534880 2013-09-11 13:24 MySQL-client-5.6.14-1.el6.x86_64.rpm -rw-r--r-- bteam/staff 49586908 2013-09-11 13:26 MySQL-test-5.6.14-1.el6.x86_64.rpm -rw-r--r-- bteam/staff 57625168 2013-09-11 13:25 MySQL-server-5.6.14-1.el6.x86_64.rpm -rw-r--r-- bteam/staff 3969816 2013-09-11 13:26 MySQL-shared-compat-5.6.14-1.el6.x86_64.rpm |
The RPM files that we will install are as follows:
| RPM File | Description |
|---|---|
| MySQL-shared-5.6.14-1.el6.x86_64.rpm | MySQL Shared Libraries |
| MySQL-devel-5.6.14-1.el6.x86_64.rpm | MySQL Development Files (headers etc.) |
| MySQL-client-5.6.14-1.el6.x86_64.rpm | MySQL Client |
| MySQL-server-5.6.14-1.el6.x86_64.rpm | MySQL Server |
| MySQL-shared-compat-5.6.14-1.el6.x86_64.rpm | MySQL Compatibility Libraries |
I will not install the embedded or test RPMs. Unbundle the tar file to /usr/local/src:
|
1 |
# tar xf /var/tmp/MySQL-5.6.14-1.el6.x86_64.rpm-bundle.tar -C /usr/local/src |
Install the following pre-requisite packages and their dependencies with yum:
|
1 |
# yum install libaio libaio-devel openssl-devel zlib-devel perl |
If you attempt to install now, you’ll see a bunch of RPM conflicts during the preparation phase:
|
1 2 3 4 5 6 7 |
# rpm -ivh * Preparing... ########################################### [100%] file /usr/share/mysql/charsets/Index.xml from install of MySQL-server-5.6.14-1.el6.x86_64 conflicts with file from package mysql-libs-5.1.66-2.el6_3.x86_64 file /usr/share/mysql/charsets/latin1.xml from install of MySQL-server-5.6.14-1.el6.x86_64 conflicts with file from package mysql-libs-5.1.66-2.el6_3.x86_64 file /usr/share/mysql/czech/errmsg.sys from install of MySQL-server-5.6.14-1.el6.x86_64 conflicts with file from package mysql-libs-5.1.66-2.el6_3.x86_64 file /usr/share/mysql/danish/errmsg.sys from install of MySQL-server-5.6.14-1.el6.x86_64 conflicts with file from package mysql-libs-5.1.66-2.el6_3.x86_64 ... |
The conflicts are with the stock mysql-libs package. A great deal of software is linked against the MySQL libraries, for example:
|
1 2 |
# ldd /usr/libexec/postfix/smtpd | grep mysql libmysqlclient.so.16 => /usr/lib64/mysql/libmysqlclient.so.16 (0x00007f6655651000) |
hence there are a few important dependencies if we try to remove it:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
# yum erase mysql-libs ... Dependencies Resolved ========================================================================================== Package Arch Version Repository Size ========================================================================================== Removing: mysql-libs x86_64 5.1.66-2.el6_3 @anaconda-CentOS-201303020151.x86_64/6.4 4.0 M Removing for dependencies: cronie x86_64 1.4.4-7.el6 @anaconda-CentOS-201303020151.x86_64/6.4 166 k cronie-anacron x86_64 1.4.4-7.el6 @anaconda-CentOS-201303020151.x86_64/6.4 43 k crontabs noarch 1.10-33.el6 @anaconda-CentOS-201303020151.x86_64/6.4 2.4 k postfix x86_64 2:2.6.6-2.2.el6_1 @anaconda-CentOS-201303020151.x86_64/6.4 9.7 M Transaction Summary ========================================================================================== Remove 5 Package(s) Installed size: 14 M Is this ok [y/N]: N Exiting on user Command |
To resolve this, first force removal of the mysql-libs package using the dreaded --nodeps rpm option:
|
1 |
# rpm -e mysql-libs --nodeps |
Next, install the MySQL shared libraries RPMs:
|
1 2 3 4 5 |
# cd /usr/local/src # rpm -ivh MySQL-shared-5.6.14-1.el6.x86_64.rpm MySQL-shared-compat-5.6.14-1.el6.x86_64.rpm Preparing... ########################################### [100%] 1:MySQL-shared-compat ########################################### [ 50%] 2:MySQL-shared ########################################### [100%] |
Check that dynamic linking hasn’t been broken:
|
1 2 3 4 |
# ldd /usr/libexec/postfix/smtpd | grep mysql libmysqlclient.so.16 => /usr/lib64/libmysqlclient.so.16 (0x00007f45178dc000) # rpm -qf /usr/lib64/libmysqlclient.so.16 MySQL-shared-compat-5.6.14-1.el6.x86_64 |
You can see we are now using the correct version of the library, and so can proceed with the installation of the remaining RPMs:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
# rpm -ivh MySQL-server-5.6.14-1.el6.x86_64.rpm MySQL-client-5.6.14-1.el6.x86_64.rpm MySQL-devel-5.6.14-1.el6.x86_64.rpm Preparing... ########################################### [100%] 1:MySQL-devel ########################################### [ 33%] 2:MySQL-client ########################################### [ 67%] 3:MySQL-server ########################################### [100%] 2013-12-03 01:28:24 0 [Warning] TIMESTAMP with implicit DEFAULT value is deprecated. Please use --explicit_defaults_for_timestamp server option (see documentation for more details). 2013-12-03 01:28:24 2334 [Note] InnoDB: The InnoDB memory heap is disabled 2013-12-03 01:28:24 2334 [Note] InnoDB: Mutexes and rw_locks use GCC atomic builtins 2013-12-03 01:28:24 2334 [Note] InnoDB: Compressed tables use zlib 1.2.3 2013-12-03 01:28:24 2334 [Note] InnoDB: Using Linux native AIO 2013-12-03 01:28:24 2334 [Note] InnoDB: Not using CPU crc32 instructions 2013-12-03 01:28:24 2334 [Note] InnoDB: Initializing buffer pool, size = 128.0M 2013-12-03 01:28:24 2334 [Note] InnoDB: Completed initialization of buffer pool 2013-12-03 01:28:24 2334 [Note] InnoDB: The first specified data file ./ibdata1 did not exist: a new database to be created! 2013-12-03 01:28:24 2334 [Note] InnoDB: Setting file ./ibdata1 size to 12 MB 2013-12-03 01:28:24 2334 [Note] InnoDB: Database physically writes the file full: wait... 2013-12-03 01:28:24 2334 [Note] InnoDB: Setting log file ./ib_logfile101 size to 48 MB 2013-12-03 01:28:24 2334 [Note] InnoDB: Setting log file ./ib_logfile1 size to 48 MB 2013-12-03 01:28:24 2334 [Note] InnoDB: Renaming log file ./ib_logfile101 to ./ib_logfile0 2013-12-03 01:28:24 2334 [Warning] InnoDB: New log files created, LSN=45781 2013-12-03 01:28:24 2334 [Note] InnoDB: Doublewrite buffer not found: creating new 2013-12-03 01:28:24 2334 [Note] InnoDB: Doublewrite buffer created 2013-12-03 01:28:24 2334 [Note] InnoDB: 128 rollback segment(s) are active. 2013-12-03 01:28:25 2334 [Warning] InnoDB: Creating foreign key constraint system tables. 2013-12-03 01:28:25 2334 [Note] InnoDB: Foreign key constraint system tables created 2013-12-03 01:28:25 2334 [Note] InnoDB: Creating tablespace and datafile system tables. 2013-12-03 01:28:25 2334 [Note] InnoDB: Tablespace and datafile system tables created. 2013-12-03 01:28:25 2334 [Note] InnoDB: Waiting for purge to start 2013-12-03 01:28:25 2334 [Note] InnoDB: 5.6.14 started; log sequence number 0 A random root password has been set. You will find it in '/root/.mysql_secret'. 2013-12-03 01:28:25 2334 [Note] Binlog end 2013-12-03 01:28:25 2334 [Note] InnoDB: FTS optimize thread exiting. 2013-12-03 01:28:25 2334 [Note] InnoDB: Starting shutdown... 2013-12-03 01:28:26 2334 [Note] InnoDB: Shutdown completed; log sequence number 1625977 2013-12-03 01:28:26 0 [Warning] TIMESTAMP with implicit DEFAULT value is deprecated. Please use --explicit_defaults_for_timestamp server option (see documentation for more details). 2013-12-03 01:28:27 2358 [Note] InnoDB: The InnoDB memory heap is disabled 2013-12-03 01:28:27 2358 [Note] InnoDB: Mutexes and rw_locks use GCC atomic builtins 2013-12-03 01:28:27 2358 [Note] InnoDB: Compressed tables use zlib 1.2.3 2013-12-03 01:28:27 2358 [Note] InnoDB: Using Linux native AIO 2013-12-03 01:28:27 2358 [Note] InnoDB: Not using CPU crc32 instructions 2013-12-03 01:28:27 2358 [Note] InnoDB: Initializing buffer pool, size = 128.0M 2013-12-03 01:28:27 2358 [Note] InnoDB: Completed initialization of buffer pool 2013-12-03 01:28:27 2358 [Note] InnoDB: Highest supported file format is Barracuda. 2013-12-03 01:28:27 2358 [Note] InnoDB: 128 rollback segment(s) are active. 2013-12-03 01:28:27 2358 [Note] InnoDB: Waiting for purge to start 2013-12-03 01:28:27 2358 [Note] InnoDB: 5.6.14 started; log sequence number 1625977 2013-12-03 01:28:27 2358 [Note] Binlog end 2013-12-03 01:28:27 2358 [Note] InnoDB: FTS optimize thread exiting. 2013-12-03 01:28:27 2358 [Note] InnoDB: Starting shutdown... 2013-12-03 01:28:29 2358 [Note] InnoDB: Shutdown completed; log sequence number 1625987 A RANDOM PASSWORD HAS BEEN SET FOR THE MySQL root USER ! You will find that password in '/root/.mysql_secret'. You must change that password on your first connect, no other statement but 'SET PASSWORD' will be accepted. See the manual for the semantics of the 'password expired' flag. Also, the account for the anonymous user has been removed. In addition, you can run: /usr/bin/mysql_secure_installation which will also give you the option of removing the test database. This is strongly recommended for production servers. See the manual for more instructions. Please report any problems with the /usr/bin/mysqlbug script! The latest information about MySQL is available on the web at http://www.mysql.com Support MySQL by buying support/licenses at http://shop.mysql.com New default config file was created as /usr/my.cnf and will be used by default by the server when you start it. You may edit this file to change server settings |
As well as InnoDB initialisation (under /var/lib/mysql), there are some other important things to note. First, a random MySQL root user password is set and saved to /root/.mysql_secret on each node. This allows us to connect, and as indicated above, issue a SET PASSWORD command to update the root password. We should also run mysql_secure_installation to secure the MySQL installation. Let’s start by updating the root password. Start MySQL server on both machines:
|
1 2 |
# service mysql start Starting MySQL... SUCCESS! |
Connect to the MySQL instance using the temporary password:
|
1 |
# mysql -u root -p$( awk 'FNR == 1 { print $NF }' /root/.mysql_secret ) |
Update the password for root@localhost:
|
1 2 3 4 5 |
mysql> SET PASSWORD FOR 'root'@'localhost' = PASSWORD('securepass'); Query OK, 0 rows affected (0.01 sec) mysql> exit Bye |
Now we can run mysql_secure_installation:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
# mysql_secure_installation NOTE: RUNNING ALL PARTS OF THIS SCRIPT IS RECOMMENDED FOR ALL MySQL SERVERS IN PRODUCTION USE! PLEASE READ EACH STEP CAREFULLY! In order to log into MySQL to secure it, we'll need the current password for the root user. If you've just installed MySQL, and you haven't set the root password yet, the password will be blank, so you should just press enter here. Enter current password for root (enter for none): OK, successfully used password, moving on... Setting the root password ensures that nobody can log into the MySQL root user without the proper authorisation. You already have a root password set, so you can safely answer 'n'. Change the root password? [Y/n] n ... skipping. By default, a MySQL installation has an anonymous user, allowing anyone to log into MySQL without having to have a user account created for them. This is intended only for testing, and to make the installation go a bit smoother. You should remove them before moving into a production environment. Remove anonymous users? [Y/n] Y ... Success! Normally, root should only be allowed to connect from 'localhost'. This ensures that someone cannot guess at the root password from the network. Disallow root login remotely? [Y/n] Y ... Success! By default, MySQL comes with a database named 'test' that anyone can access. This is also intended only for testing, and should be removed before moving into a production environment. Remove test database and access to it? [Y/n] Y - Dropping test database... ... Success! - Removing privileges on test database... ... Success! Reloading the privilege tables will ensure that all changes made so far will take effect immediately. Reload privilege tables now? [Y/n] Y ... Success! All done! If you've completed all of the above steps, your MySQL installation should now be secure. Thanks for using MySQL! Cleaning up... |
I skipped changing the root password as we have just done that. However, all other security measures are allowed to complete, including the removal of anonymous users, disallowing root-login remotely, and dropping the test database. With the privilege tables reloaded, we are good to continue with the configuration of the MySQL master and slave.
Before proceeding with the configuration of replication and the creation of our SSL certificates, let’s check the MySQL installation. You will see that, when installed in this way via official RPMs, a mysql user and group are created:
|
1 2 3 4 |
# getent passwd mysql mysql:x:498:499:MySQL server:/var/lib/mysql:/bin/bash # getent group mysql mysql:x:499: |
A quick check of the process table, and we can see that the MySQL datadir is /var/lib/mysql, and the error log is at /var/lib/mysql/<hostname>.err, which are sensible defaults:
|
1 2 3 |
# ps -ef | grep '[m]ysql' root 2413 1 0 01:31 pts/0 00:00:00 /bin/sh /usr/bin/mysqld_safe --datadir=/var/lib/mysql --pid-file=/var/lib/mysql/centosa.pid mysql 2518 2413 0 01:31 pts/0 00:00:03 /usr/sbin/mysqld --basedir=/usr --datadir=/var/lib/mysql --plugin-dir=/usr/lib64/mysql/plugin --user=mysql --log-error=/var/lib/mysql/centosa.err --pid-file=/var/lib/mysql/centosa.pid |
These defaults are inherited from /etc/init.d/mysql, and can be overridden in my.cnf, which by default will be located at /etc/my.cnf.
MySQL SSL Support
The first thing to verify is whether our MySQL installation has SSL support. Perform the following query:
|
1 2 3 4 5 6 7 |
# mysql -u root -e 'SHOW VARIABLES LIKE "have_ssl";' -p Enter password: +---------------+----------+ | Variable_name | Value | +---------------+----------+ | have_ssl | DISABLED | +---------------+----------+ |
As you can see, SSL support is compiled in, but disabled. Let’s create a basic /etc/my.cnf to enable this:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
# vi /etc/my.cnf # cat /etc/my.cnf [mysqld] ssl # service mysql restart Shutting down MySQL.. SUCCESS! Starting MySQL.... SUCCESS! # mysql -u root -e 'SHOW VARIABLES LIKE "have_ssl";' -p Enter password: +---------------+-------+ | Variable_name | Value | +---------------+-------+ | have_ssl | YES | +---------------+-------+ |
Excellent - we now have SSL support available.
In a Production environment, we’d look at obtaining SSL certificates from a trusted authority. For the purposes of this tutorial, we’ll create our own CA and generate self-signed certificates. We need two certificates, one for the MySQL server, and one for the MySQL client (in our case, the replication user will connect as a client).
Generating SSL Certificates
On both servers, create a directory structure to hold the certificates, and ensure that it is locked down appropriately:
|
1 2 |
# mkdir -m 700 -p /etc/mysql/{ssl.crt,ssl.key,ssl.csr} # chown -R mysql:mysql /etc/mysql |
Before starting, you’ll need to install the latest OpenSSL 0.9.8 version - at the time of writing that’s 0.9.8y. The 1.0.* versions have issues (or, MySQL has issues with them) and you’ll see plenty of “ERROR 2026 (HY000): SSL connection error: protocol version mismatch” messages and lose a lot of hair.
Download the latest 0.9.8 version from the OpenSSL website and install gcc if you don’t already have it:
|
1 |
# yum -y install gcc |
Extract the tarball:
|
1 |
# tar xzf /var/tmp/openssl-0.9.8y.tar.gz -C /usr/local/src |
And configure. Note that the configure script is named config - not the standard configure:
|
1 2 |
# cd /usr/local/src/openssl-0.9.8y # ./config --prefix=/usr/local/openssl-0.9.8y |
Then make and make install:
|
1 |
# make && make install |
Let’s check it worked:
|
1 2 |
# /usr/local/openssl-0.9.8y/bin/openssl version OpenSSL 0.9.8y 5 Feb 2013 |
So that we make sure the correct version is used for certificate generation, alias openssl to the new binary:
|
1 |
# alias openssl=/usr/local/openssl-0.9.8y/bin/openssl |
Now, we can proceed.
First, we’ll create our CA. Generate a new key and certificate, valid for 9999 days (again - only do this in a TEST environment!):
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
# openssl req -x509 -new -days 9999 -newkey rsa:2048 -nodes \ > -keyout /etc/mysql/ssl.key/ca.mysqlreplication.tokiwinter.com.key \ > -out /etc/mysql/ssl.crt/ca.mysqlreplication.tokiwinter.com.crt Generating a 2048 bit RSA private key ..................................................+++ ..............................................................+++ writing new private key to '/etc/mysql/ssl.key/ca.mysqlreplication.tokiwinter.com.key' ----- You are about to be asked to enter information that will be incorporated into your certificate request. What you are about to enter is what is called a Distinguished Name or a DN. There are quite a few fields but you can leave some blank For some fields there will be a default value, If you enter '.', the field will be left blank. ----- Country Name (2 letter code) [XX]:AU State or Province Name (full name) []:Victoria Locality Name (eg, city) [Default City]:Melbourne Organization Name (eg, company) [Default Company Ltd]:Toki Winter Organizational Unit Name (eg, section) []: Common Name (eg, your name or your server's hostname) []:ca.mysqlreplication.tokiwinter.com Email Address []: |
Verify:
|
1 2 3 4 5 |
# openssl x509 -in /etc/mysql/ssl.crt/ca.mysqlreplication.tokiwinter.com.crt \ > -noout -subject -dates subject= /C=AU/ST=Victoria/L=Melbourne/O=Toki Winter/CN=ca.mysqlreplication.tokiwinter.com notBefore=Dec 2 15:29:17 2013 GMT notAfter=Apr 18 15:29:17 2041 GMT |
Next, we need to create the server certificate. This will be the certificate used by the master MySQL instance. Start with the CSR:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
# openssl req -new -newkey rsa:2048 -nodes \ > -keyout /etc/mysql/ssl.key/server.mysqlreplication.tokiwinter.com.key \ > -out /etc/mysql/ssl.csr/server.mysqlreplication.tokiwinter.com.csr Generating a 2048 bit RSA private key .................................+++ .................+++ writing new private key to '/etc/mysql/ssl.key/server.mysqlreplication.tokiwinter.com.key' ----- You are about to be asked to enter information that will be incorporated into your certificate request. What you are about to enter is what is called a Distinguished Name or a DN. There are quite a few fields but you can leave some blank For some fields there will be a default value, If you enter '.', the field will be left blank. ----- Country Name (2 letter code) [XX]:AU State or Province Name (full name) []:Victoria Locality Name (eg, city) [Default City]:Melbourne Organization Name (eg, company) [Default Company Ltd]:Toki Winter Organizational Unit Name (eg, section) []: Common Name (eg, your name or your server's hostname) []:server.mysqlreplication.tokiwinter.com Email Address []: Please enter the following 'extra' attributes to be sent with your certificate request A challenge password []: An optional company name []: |
Sign the CSR, and create the certificate:
|
1 2 3 4 5 6 7 8 9 |
# openssl x509 -req -days 9999 \ > -CA /etc/mysql/ssl.crt/ca.mysqlreplication.tokiwinter.com.crt \ > -CAkey /etc/mysql/ssl.key/ca.mysqlreplication.tokiwinter.com.key \ > -CAcreateserial -CAserial /etc/mysql/ca-serial.txt \ > -in /etc/mysql/ssl.csr/server.mysqlreplication.tokiwinter.com.csr \ > -out /etc/mysql/ssl.crt/server.mysqlreplication.tokiwinter.com.crt Signature ok subject=/C=AU/ST=Victoria/L=Melbourne/O=Toki Winter/CN=server.mysqlreplication.tokiwinter.com Getting CA Private Key |
Verify:
|
1 2 3 4 5 |
# openssl x509 -in /etc/mysql/ssl.crt/server.mysqlreplication.tokiwinter.com.crt \ > -noout -dates -subject notBefore=Dec 2 15:34:44 2013 GMT notAfter=Apr 18 15:34:44 2041 GMT subject= /C=AU/ST=Victoria/L=Melbourne/O=Toki Winter/CN=server.mysqlreplication.tokiwinter.com |
Finally, repeat the process for the client certificate:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
# openssl req -new -newkey rsa:2048 -nodes \ > -keyout /etc/mysql/ssl.key/client.mysqlreplication.tokiwinter.com.key \ > -out /etc/mysql/ssl.csr/client.mysqlreplication.tokiwinter.com.csr Generating a 2048 bit RSA private key .....+++ .................................................................+++ writing new private key to '/etc/mysql/ssl.key/client.mysqlreplication.tokiwinter.com.key' ----- You are about to be asked to enter information that will be incorporated into your certificate request. What you are about to enter is what is called a Distinguished Name or a DN. There are quite a few fields but you can leave some blank For some fields there will be a default value, If you enter '.', the field will be left blank. ----- Country Name (2 letter code) [XX]:AU State or Province Name (full name) []:Victoria Locality Name (eg, city) [Default City]:Melbourne Organization Name (eg, company) [Default Company Ltd]:Toki Winter Organizational Unit Name (eg, section) []: Common Name (eg, your name or your server's hostname) []:client.mysqlreplication.tokiwinter.com Email Address []: Please enter the following 'extra' attributes to be sent with your certificate request A challenge password []: An optional company name []: # openssl x509 -req -days 9999 \ > -CA /etc/mysql/ssl.crt/ca.mysqlreplication.tokiwinter.com.crt \ > -CAkey /etc/mysql/ssl.key/ca.mysqlreplication.tokiwinter.com.key \ > -CAserial /etc/mysql/ca-serial.txt \ > -in /etc/mysql/ssl.csr/client.mysqlreplication.tokiwinter.com.csr \ > -out /etc/mysql/ssl.crt/client.mysqlreplication.tokiwinter.com.crt Signature ok subject=/C=AU/ST=Victoria/L=Melbourne/O=Toki Winter/CN=client.mysqlreplication.tokiwinter.com Getting CA Private Key # openssl x509 -in /etc/mysql/ssl.crt/client.mysqlreplication.tokiwinter.com.crt \ > -noout -dates -subject notBefore=Dec 2 15:37:44 2013 GMT notAfter=Apr 18 15:37:44 2041 GMT subject= /C=AU/ST=Victoria/L=Melbourne/O=Toki Winter/CN=client.mysqlreplication.tokiwinter.com |
We are left with the following files:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
# ls -lR /etc/mysql /etc/mysql: total 16 -rw-r--r--. 1 root root 17 Dec 3 02:37 ca-serial.txt drwx------. 2 mysql mysql 4096 Dec 3 02:37 ssl.crt drwx------. 2 mysql mysql 4096 Dec 3 02:37 ssl.csr drwx------. 2 mysql mysql 4096 Dec 3 02:36 ssl.key /etc/mysql/ssl.crt: total 12 -rw-r--r--. 1 root root 1363 Dec 3 02:29 ca.mysqlreplication.tokiwinter.com.crt -rw-r--r--. 1 root root 1249 Dec 3 02:37 client.mysqlreplication.tokiwinter.com.crt -rw-r--r--. 1 root root 1249 Dec 3 02:34 server.mysqlreplication.tokiwinter.com.crt /etc/mysql/ssl.csr: total 8 -rw-r--r--. 1 root root 1029 Dec 3 02:37 client.mysqlreplication.tokiwinter.com.csr -rw-r--r--. 1 root root 1029 Dec 3 02:32 server.mysqlreplication.tokiwinter.com.csr /etc/mysql/ssl.key: total 12 -rw-r--r--. 1 root root 1704 Dec 3 02:29 ca.mysqlreplication.tokiwinter.com.key -rw-r--r--. 1 root root 1704 Dec 3 02:37 client.mysqlreplication.tokiwinter.com.key -rw-r--r--. 1 root root 1704 Dec 3 02:32 server.mysqlreplication.tokiwinter.com.key |
Fix the ownership and permissions on the files:
|
1 2 |
# chown -R mysql:mysql /etc/mysql # find /etc/mysql -type f -exec chmod 600 {} \; |
As per this post, change all occurrences of:
|
1 |
-----BEGIN PRIVATE KEY----- |
to
|
1 |
-----BEGIN RSA PRIVATE KEY----- |
And
|
1 |
-----END PRIVATE KEY----- |
to
|
1 |
-----END RSA PRIVATE KEY----- |
In the *.key files, else MySQL will not recognise them as valid keys and you’ll see errors such as
|
1 |
mysql [Warning] SSL error: Unable to get private key |
in the MySQL error log.
Finally, transfer the following files to the slave:
/etc/mysql/ssl.crt/ca.mysqlreplication.tokiwinter.com.crt/etc/mysql/ssl.crt/client.mysqlreplication.tokiwinter.com.crt/etc/mysql/ssl.crt/server.mysqlreplication.tokiwinter.com.crt/etc/mysql/ssl.key/client.mysqlreplication.tokiwinter.com.key/etc/mysql/ssl.key/server.mysqlreplication.tokiwinter.com.key
And ensure that ownership and permissions are set as above.
Firewall
At this point, it’s prudent to make sure that the slave MySQL instance can reach the master instance on the appropriate port, 3306/tcp by default. The default CentOS 6.4 iptables ruleset is:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
# iptables -L -n --line-numbers Chain INPUT (policy ACCEPT) num target prot opt source destination 1 ACCEPT all -- 0.0.0.0/0 0.0.0.0/0 state RELATED,ESTABLISHED 2 ACCEPT icmp -- 0.0.0.0/0 0.0.0.0/0 3 ACCEPT all -- 0.0.0.0/0 0.0.0.0/0 4 ACCEPT tcp -- 0.0.0.0/0 0.0.0.0/0 state NEW tcp dpt:22 5 REJECT all -- 0.0.0.0/0 0.0.0.0/0 reject-with icmp-host-prohibited Chain FORWARD (policy ACCEPT) num target prot opt source destination 1 REJECT all -- 0.0.0.0/0 0.0.0.0/0 reject-with icmp-host-prohibited Chain OUTPUT (policy ACCEPT) num target prot opt source destination |
Let’s grant our slave access:
|
1 |
# iptables -I INPUT 5 -m state --state NEW -p tcp --dport 3306 -s 10.1.1.151 -j ACCEPT |
Test access from the slave:
|
1 2 3 4 5 6 |
# yum -y install telnet # telnet 10.1.1.150 3306 Trying 10.1.1.150... Connected to 10.1.1.150. Escape character is '^]'. Host '10.1.1.151' is not allowed to connect to this MySQL serverConnection closed by foreign host. |
The “Host … is not allowed to connect to this MySQL server” message is expected as we haven’t GRANTed access yet. It does prove that our iptables rule is working, so save the config on the master:
|
1 2 |
# service iptables save iptables: Saving firewall rules to /etc/sysconfig/iptables:[ OK ] |
MySQL Configuration
On the master server, add the following to the [mysqld] section of /etc/my.cnf:
|
1 2 3 |
ssl-ca=/etc/mysql/ssl.crt/ca.mysqlreplication.tokiwinter.com.crt ssl-key=/etc/mysql/ssl.key/server.mysqlreplication.tokiwinter.com.key ssl-cert=/etc/mysql/ssl.crt/server.mysqlreplication.tokiwinter.com.crt |
Restart MySQL:
|
1 2 3 |
# service mysql restart Shutting down MySQL.. SUCCESS! Starting MySQL.... SUCCESS! |
Check the SSL variables for correctness:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
# mysql -u root -p -e "show variables like '%ssl%';" Enter password: +---------------+---------------------------------------------------------------+ | Variable_name | Value | +---------------+---------------------------------------------------------------+ | have_openssl | YES | | have_ssl | YES | | ssl_ca | /etc/mysql/ssl.crt/ca.mysqlreplication.tokiwinter.com.crt | | ssl_capath | | | ssl_cert | /etc/mysql/ssl.crt/server.mysqlreplication.tokiwinter.com.crt | | ssl_cipher | | | ssl_crl | | | ssl_crlpath | | | ssl_key | /etc/mysql/ssl.key/server.mysqlreplication.tokiwinter.com.key | +---------------+---------------------------------------------------------------+ |
On the slave server, append the following to /etc/my.cnf:
|
1 2 3 4 |
[client] ssl-ca=/etc/mysql/ssl.crt/ca.mysqlreplication.tokiwinter.com.crt ssl-key=/etc/mysql/ssl.key/client.mysqlreplication.tokiwinter.com.key ssl-cert=/etc/mysql/ssl.crt/client.mysqlreplication.tokiwinter.com.crt |
And add the following to the [mysqld] section:
|
1 2 3 |
ssl-ca=/etc/mysql/ssl.crt/ca.mysqlreplication.tokiwinter.com.crt ssl-key=/etc/mysql/ssl.key/server.mysqlreplication.tokiwinter.com.key ssl-cert=/etc/mysql/ssl.crt/server.mysqlreplication.tokiwinter.com.crt |
Restart MySQL:
|
1 2 3 |
# service mysql restart Shutting down MySQL.. SUCCESS! Starting MySQL.. SUCCESS! |
Back on the master, add the following to the [mysqld] section:
|
1 2 |
server-id=1 log-bin=mysql-bin.log |
We could specify further parameters here, such as binlog-do-db if we were only interested in replicating a subset of the databases, along with replicate-do-db on the slave.
Restart MySQL once again:
|
1 2 3 |
# service mysql restart Shutting down MySQL.. SUCCESS! Starting MySQL.. SUCCESS! |
Verify that this server is now keeping a binary log:
|
1 2 3 4 5 6 7 8 |
# mysql -u root -p -e 'SHOW MASTER STATUS\G' Enter password: *************************** 1. row *************************** File: mysql-bin.000001 Position: 120 Binlog_Do_DB: Binlog_Ignore_DB: Executed_Gtid_Set: |
We can now create a database on the master server:
|
1 2 |
# mysql -u root -p -e 'CREATE DATABASE testdb;' Enter password: |
On the slave, set server-id to 2 and restart:
|
1 2 3 4 5 6 7 |
# vi /etc/my.cnf [mysqld] ... server-id=2 # service mysql restart Shutting down MySQL.. SUCCESS! Starting MySQL..... SUCCESS! |
Next, back on the master, GRANT REPLICATION SLAVE privileges to a new “replicate” user. Note that the subject and issuer fields from the client.mysqlreplication.tokiwinter.com certificate are required by the grant, and will be checked upon connection:
|
1 2 3 |
# mysql -u root -p mysql> GRANT REPLICATION SLAVE ON *.* TO 'replicate'@'10.1.1.151' IDENTIFIED BY 'reppass' REQUIRE SSL; Query OK, 0 rows affected (0.00 sec) |
In this example, we only REQUIRE SSL - you can also REQUIRE SUBJECT and REQUIRE ISSUER if desired. On the slave, use CHANGE MASTER TO to connect to the master. Use the values obtained from the earlier invocation of SHOW MASTER STATUS\G on the master to populate the statement.
|
1 2 3 4 5 6 7 8 9 |
mysql> CHANGE MASTER TO MASTER_HOST='10.1.1.150', -> MASTER_USER='replicate', -> MASTER_PASSWORD='reppass', -> MASTER_LOG_FILE='mysql-bin.000001', -> MASTER_LOG_POS=120, -> MASTER_SSL=1, -> MASTER_SSL_CA='/etc/mysql/ssl.crt/ca.mysqlreplication.tokiwinter.com.crt', -> MASTER_SSL_CERT='/etc/mysql/ssl.crt/client.mysqlreplication.tokiwinter.com.crt', -> MASTER_SSL_KEY='/etc/mysql/ssl.key/client.mysqlreplication.tokiwinter.com.key'; |
If you run a SHOW SLAVE STATUS\G on the slave, you should now see the slave operating as expected, with both the IO and SQL threads active:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
mysql> show slave status\G *************************** 1. row *************************** Slave_IO_State: Waiting for master to send event Master_Host: 10.1.1.150 Master_User: replicate Master_Port: 3306 Connect_Retry: 60 Master_Log_File: mysql-bin.000007 Read_Master_Log_Pos: 120 Relay_Log_File: centosb-relay-bin.000010 Relay_Log_Pos: 283 Relay_Master_Log_File: mysql-bin.000007 Slave_IO_Running: Yes Slave_SQL_Running: Yes Replicate_Do_DB: Replicate_Ignore_DB: Replicate_Do_Table: Replicate_Ignore_Table: Replicate_Wild_Do_Table: Replicate_Wild_Ignore_Table: Last_Errno: 0 Last_Error: Skip_Counter: 0 Exec_Master_Log_Pos: 120 Relay_Log_Space: 621 Until_Condition: None Until_Log_File: Until_Log_Pos: 0 Master_SSL_Allowed: Yes Master_SSL_CA_File: /etc/mysql/ssl.crt/ca.mysqlreplication.tokiwinter.com.crt Master_SSL_CA_Path: Master_SSL_Cert: /etc/mysql/ssl.crt/client.mysqlreplication.tokiwinter.com.crt Master_SSL_Cipher: Master_SSL_Key: /etc/mysql/ssl.key/client.mysqlreplication.tokiwinter.com.key Seconds_Behind_Master: 0 Master_SSL_Verify_Server_Cert: No Last_IO_Errno: 0 Last_IO_Error: Last_SQL_Errno: 0 Last_SQL_Error: Replicate_Ignore_Server_Ids: Master_Server_Id: 1 Master_UUID: 7c1232f8-5b5e-11e3-96a9-000c29e87a84 Master_Info_File: /var/lib/mysql/master.info SQL_Delay: 0 SQL_Remaining_Delay: NULL Slave_SQL_Running_State: Slave has read all relay log; waiting for the slave I/O thread to update it Master_Retry_Count: 86400 Master_Bind: Last_IO_Error_Timestamp: Last_SQL_Error_Timestamp: Master_SSL_Crl: /etc/mysql/ssl.crt/ca.mysqlreplication.tokiwinter.com.crt Master_SSL_Crlpath: Retrieved_Gtid_Set: Executed_Gtid_Set: Auto_Position: 0 1 row in set (0.00 sec) |
Create a database on the master:
|
1 2 |
mysql> create database foodb; Query OK, 1 row affected (0.01 sec) |
On the slave, you’ll see the database created:
|
1 2 3 4 5 6 7 8 9 10 |
mysql> show databases; +--------------------+ | Database | +--------------------+ | information_schema | | foodb | | mysql | | performance_schema | +--------------------+ 5 rows in set (0.00 sec) |
MySQL replication over SSL is now completed. Check that the service is set to start automatically at boot:
|
1 2 |
# chkconfig --list mysql mysql 0:off 1:off 2:on 3:on 4:on 5:on 6:off |
Troubleshooting
Be careful when transporting the certificates between servers as SELinux contexts may change depending on how you do it. If they end up being tmp_t or similar, change them back with chcon:
|
1 |
# chcon -R -t etc_t /etc/mysql/* |