MySQL-Replikation über SSL absichern
- Warum ich sichern möchte
- Schlüssel für den Master erstellen
- Zertifikat auf dem Master einbinden
- Einen User für die Replikation erstellen
- Die Keys auf dem Slave erstellen
- Replikation per SSL testen
Sobald ich eine MySQL-Replikation fahre, mache ich den Dienst nach außen hin auf: es genügt dann nicht mehr, ein Socket auf localhost
zu haben, der Dienst muss auf einem von außen ansprechbaren Port erreichbar sein. Das kann nun, je nach Setup, auch netzintern schon kritisch sein – denn unverschlüsselt lassen sich recht einfach Daten abgreifen. Um das zu testen habe ich auf dem MySQL-Slave ein tcpdump
gegen den Master gestartet und eine Abfrage auf die Datenbank losgelassen:
Warum ich sichern möchte
$ tcpdump -ns 0 host master and port 3306 -w /tmp/Replication &
$ mysql -u dspam -p'passwort'
mysql> use database dspam;
mysql> select \* from dspam\_stats;
mysql> exit
Nun kann per fg
der Job nach vorne geholt und per ^C
gestoppt werden; eine meiner Zeilen in der Tabelle dspam_stats
beinhaltete die Nummer 1788, und nach der suche ich nun in meinem tcpdump
-Output:
$ grep --text "1788" /tmp/Replication
defdspam
dspam_stats
dspam_statsinnocent_classifiedinnocent_classified
!
?"
21788162331020000?"fc?U??B'?J'A4?Q@@?7?r?p?v
?????h??D?Y
Nicht unbedingt schön, aber halt eindeutig unverschlüsselt – es könnte sich hier ja genauso gut um Adress- oder Kreditkartendaten handeln. Nun – willst du deinen MySQL per SSL absichern, solltest du erstmal prüfen, ob es grundsätzlich möglich ist:
mysql> SHOW VARIABLES LIKE 'have_ssl';
+---------------+----------+
| Variable_name | Value |
+---------------+----------+
| have_ssl | DISABLED |
+---------------+----------+
1 row in set (0.00 sec)
SSL steht auf DISABLED
; das bedeutet, dass es derzeit nicht aktiv ist, aber zugeschaltet werden kann. (Stünde hier statt DISABLED
ein NO
, so unterstützt dein MySQL die Nutzung von SSL generell nicht!) Um eine Replikation über SSL zu realisieren, muss SSL auf allen angeschlossenen Hosts aktiviert werden.
Schlüssel für den Master erstellen
-
master-private.pem
ist der private Schlüssel, der ausschließlich auf dem Master bleiben muss -
master-public.pem
ist der öffentliche Schlüssel - Die Daten müssen in
/etc/mysql
liegen, alles andere funktionierte (bei mir zumindest) nicht.
$ cd /etc/mysql
openssl req -x509 -newkey rsa:4096 \
-keyout master-private.pem \
-out master-public.pem \
-subj '/CN=master' \
-nodes \
-days 3650
Generating a 4096 bit RSA private key
....................................................................................................++
.........++
writing new private key to 'master-private.pem'
-----
$ openssl rsa -in master-private.pem -out master-private-compat.pem
writing RSA key
chmod 0600 *.pem
chown mysql:mysql *.pem
Da ich alle Hosts selbst unter Kontrolle habe, arbeite ich nicht mit einer Trusted CA.
$ cp master-public.pem ca-cert.pem
Zertifikat auf dem Master einbinden
-
ssl-ca
– unser Master wird ausschließlich Zertifikate akzeptieren, die in unserer CAca-cert.pem
enthalten sind. -
ssl-cert
– mit diesem öffentlichen Schlüssel wird der Master sich dem Slave gegenüber ausweisen; der Slave wird diesen Schlüssel dann nutzen, um die Daten zu verschlüsseln -
ssl-key
– das ist der private Schlüssel des Masters, der auf dem Master verbleibt und unter dessen Einsatz der Master die Daten entschlüsselt, die er vom Slave erhält
## file: "/etc/mysql/my.cnf"
[mysqld]
...
##---------------------------------------------------------------------
# SSL for replication
ssl-ca = /etc/mysql/ca-cert.pem
ssl-cert = /etc/mysql/master-public.pem
ssl-key = /etc/mysql/master-private.pem
...
$ service mysql restart
Beim Restart sollte das Logfile des Dienstes – bei mir ist das /var/log/mysql/error.log
– aufmerksam beobachtet werden; so ist es beispielsweise sehr wahrscheinlich, dass diese Meldung auftaucht:
SSL error: Unable to get certificate from '/etc/ssl/mysql/master-public.pem'
150805 21:54:51 [Warning] Failed to setup SSL
150805 21:54:51 [Warning] SSL error: Unable to get certificate
Das Problem liegt darin begründet, dass master-private.pem
vom Typ her ein ASCII-File ist (erkennbar per file
beziehungsweise auch daran, dass die erste Zeile des Files -----BEGIN PRIVATE KEY-----
lautet); MySQL erwartet da aber ein File vom Typ PEM RSA private key
(erkennbar daran, dass die erste Zeile -----BEGIN RSA PRIVATE KEY-----
lautet – und nein, es reicht nicht, dem ASCII-File einfach ein RSA reinzuschreiben!). Um das zu lösen haben wir oben bereits einen kompatiblen Key erstellt, nämlich master-private-compat.pem
– mit ihm müssen wir zukünftig arbeiten. Lässt der Dienst sich fehlerfrei durchstarten, können die Werte abgeprüft werden:
master> show variables like '%ssl%';
+---------------+------------------------------------------+
| Variable_name | Value |
+---------------+------------------------------------------+
| have_openssl | YES |
| have_ssl | YES |
| ssl_ca | /etc/mysql/ca-cert.pem |
| ssl_capath | |
| ssl_cert | /etc/mysql/master-public.pem |
| ssl_cipher | |
| ssl_key | /etc/mysql/master-private-compat.pem |
+---------------+------------------------------------------+
7 rows in set (0.00 sec)
Einen User für die Replikation erstellen
master> GRANT REPLICATION CLIENT ON *.*
-> TO 'ReplOnSlave1'@'%'
-> IDENTIFIED BY 'qwertz';
Query OK, 0 rows affected (0.00 sec)
Nun sorgst du dafür, dass dieser User ausschließlich über SSL arbeiten darf:
master> GRANT USAGE ON *.*
-> TO 'ReplOnSlave1'@'%'
-> REQUIRE SSL;
Query OK, 0 rows affected (0.00 sec)
Hat alles geklappt? Überprüfe es!
master> SHOW GRANTS FOR 'ReplOnSlave1';
+-------------------------------------------------------------------------------------------------------------------------------------+
| Grants for ReplOnSlave1@% |
+-------------------------------------------------------------------------------------------------------------------------------------+
| GRANT REPLICATION SLAVE ON *.* TO 'ReplOnSlave1'@'%' IDENTIFIED BY PASSWORD '*0E16E0C23FE1A73A4C2957D51C4DF5D5FA4E3E91' REQUIRE SSL |
+-------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)
Vom Slave aus kann sich der User ReplOnSlave1
nun nicht mehr verbinden, wenn er kein SSL nutzt:
$ mysql -hmaster -u ReplOnSlave1 -p'qwertz'
ERROR 1045 (28000): Access denied for user 'ReplOnSlave1'@'slave' (using password: YES)
Nun musst du das vorhin erstellte ca-cert.pem
vom Master zum Slave übertragen und kannst hernach einen neuen Verbindungsversuch starten – unter Benutzung des ca-cert.pem
:
$ scp ca-cert.pem root@slave:/etc/mysql
ca-cert.pem 100% 1789 1.8KB/s 00:00
$ mysql -u slave -p'qwertz' -hmaster --ssl-ca /etc/mysql/ca-cert.pem --ssl-verify-server-cert
master> show status like 'ssl_cipher';
+---------------+--------------------+
| Variable_name | Value |
+---------------+--------------------+
| Ssl_cipher | DHE-RSA-AES256-SHA |
+---------------+--------------------+
1 row in set (0.00 sec)
Dein Replikations-User kann sich so also schonmal einloggen; nun ist es an der Zeit, das Setup weiter zu verfeinern. Logge dich hierzu als root
auf dem MySQL-Master ein und bearbeite die Konfiguration des Replikations-Users:
master> GRANT USAGE ON *.*
-> TO 'ReplOnSlave1'@'%'
-> REQUIRE SUBJECT '/CN=slave1';
Query OK, 0 rows affected (0.00 sec)
mysql> SHOW GRANTS FOR 'ReplOnSlave1';
+-------------------------------------------------------------------------------------------------------------------------------------------------+
| Grants for ReplOnSlave1@% |
+-------------------------------------------------------------------------------------------------------------------------------------------------+
| GRANT REPLICATION SLAVE ON *.* TO 'ReplOnSlave1'@'%' IDENTIFIED BY PASSWORD '*0E16E0C23FE1A73A4CC4DF5D5FA4E3E91' REQUIRE SUBJECT '/CN=slave1' |
+-------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)
Die Keys auf dem Slave erstellen
Anschließend musst du auch auf dem Slave die benötigten Keys erstellen; mittels -subj
wird hierbei genau das subject
festgesetzt, dass der Master erwartet – nämlich /CN=slave1
.
-
slave1-private.pem
ist der private Schlüssel, mit welchem der Slave den Datenverkehr entschlüsseln wird – er muss auf dem Slave allein verbleiben -
slave1-private-compat.pem
ist der zuslave1-private.pem
kompatible Key vom TypPEM RSA private key
-
slave1-public.pem
ist der öffentliche Schlüssel den der Master benutzt, um die Identität des Slave festzustellen und den Datenverkehr zu verschlüsseln
$ cd /etc/mysql
$ openssl req -x509 -newkey rsa:4096 \
-keyout slave1-private.pem \
-out chemnitz-public.pem \
-subj '/CN=slave1' \
-nodes \
-days 3650
Generating a 4096 bit RSA private key
................++
....................++
writing new private key to 'slave1-private.pem'
-----
$ openssl rsa -in slave1-private.pem -out slave1-private-compat.pem
writing RSA key
$ chmod 0600 *.pem
$ chown mysql:mysql *.pem
Hänge das öffentliche Zertifikat des Slave an die CA an und kopiere die so entstandene ca-cert.pem
zurück auf den Master:
$ cat slave1-public.pem >> ca-cert.pem
$ scp ca-cert.pem master:/etc/mysql/
Wichtig ist dann, MySQL auf dem Master durchzustarten – denn ca-cert.pem
wird nur beim Start des Dienstes eingelesen, nicht zur Laufzeit. Binde dann, analog zum Master, die Keys in die my.cnf
auf dem Slave ein und starte auch hier MySQL durch. Ist das passiert solltest du testen, ob dein Replikations-User sich fehlerfrei am Master anmelden kann:
$ mysql -u ReplOnSlave1 -p'qwertz' -hmaster \
--ssl-ca /etc/mysql/ca-cert.pem \
--ssl-cert /etc/mysql/slave1-public.pem \
--ssl-key /etc/mysql/slave1-private.pem
SSL error: Unable to get private key from '/etc/mysql/slave1-private.pem'
ERROR 2026 (HY000): SSL connection error: Unable to get private key
Schon wieder vergessen, den kompatiblen Key zu verwenden? Das führt zu obenstehender Fehlermeldung beziehungsweise zu error 2026
in MySQL – also nie vergessen, mit dem richtigen Key zu arbeiten, und dann funktioniert es auch :-)
$ mysql -u ReplOnSlave1 -p'qwertz' -hmaster \
--ssl-ca /etc/mysql/ca-cert.pem \
--ssl-cert /etc/mysql/slave1-public.pem \
--ssl-key /etc/mysql/slave1-private-compat.pem
Welcome to the MySQL monitor. Commands end with ; or \g.
...
master>
Im letzten Schritt wird nun die Konfiguration auf dem Slave so angepasst, dass er den Host master
als Replikations-Master nimmt – mit User, Passwort, Zertifikaten und allem, was sonst so benötigt ist. Auf dem Master sollte noch das aktuelle bin_log
(in meinem Fall mysql-bin.000008
) sowie die Position (bei mir 107) abgegriffen werden.
slave> CHANGE MASTER TO
-> MASTER_HOST='master',
-> MASTER_USER='ReplOnSlave1',
-> MASTER_PASSWORD='qwertz',
-> MASTER_LOG_FILE='mysql-bin.000008',
-> MASTER_LOG_POS=107,
-> MASTER_SSL=1,
-> MASTER_SSL_CA='/etc/mysql/ca-cert.pem',
-> MASTER_SSL_CERT='/etc/mysql/slave1-public.pem',
-> MASTER_SSL_KEY='/etc/mysql/slave1-private.pem',
-> Master_SSL_Verify_Server_Cert = 1;
Query OK, 0 rows affected (0.00 sec)
slave> START SLAVE;
Query OK, 0 rows affected (0.00 sec)
slave> show slave status\G
*************************** 1. row ***************************
Slave_IO_State: Waiting for master to send event
Master_Host: master
Master_User: ReplOnSlave1
Master_Port: 3306
Connect_Retry: 60
Master_Log_File: mysql-bin.000008
Read_Master_Log_Pos: 107
Relay_Log_File: mysqld-relay-bin.000002
Relay_Log_Pos: 253
Relay_Master_Log_File: mysql-bin.000008
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: 107
Relay_Log_Space: 410
Until_Condition: None
Until_Log_File:
Until_Log_Pos: 0
Master_SSL_Allowed: Yes
Master_SSL_CA_File: /etc/mysql/ca-cert.pem
Master_SSL_CA_Path:
Master_SSL_Cert: /etc/mysql/slave1-public.pem
Master_SSL_Cipher:
Master_SSL_Key: /etc/mysql/slave1-private-compat.pem
Seconds_Behind_Master: 0
Master_SSL_Verify_Server_Cert: Yes
Last_IO_Errno: 0
Last_IO_Error:
Last_SQL_Errno: 0
Last_SQL_Error:
Replicate_Ignore_Server_Ids:
Master_Server_Id: 1
1 row in set (0.00 sec)
Replikation per SSL testen
Per tcpdump
lässt sich beobachten, was zwischen Master und Slave so übertragen wird; hierzu startest du eines auf dem Slave – du kannst die Shell einfach offen lassen und tcpdump
live beobachten – und änderst auf dem Master einen Datensatz, fügst einen hinzu oder tust etwas Vergleichbares.
$ tcpdump -s 0 -A -vv host master and port 3306
tcpdump: listening on eth0, link-type EN10MB (Ethernet), capture size 65535 bytes
22:21:11.679841 IP (tos 0x8, ttl 64, id 8946, offset 0, flags [DF], proto TCP (6), length 313)
master.mysql > slave1.56221: Flags [P.], cksum 0x9fcc (correct), seq 2834879594:2834879855, ack 3136708851, win 257, options [nop,nop,TS val 2793481 ecr 2421061], length 261
E..9".@.@......p...r.......j..............
..z...X......f.0x;.l+.[...~SRS......&..B.MD.R..1....OQ.s...{...J..WA..y)...z...+.Ur@.... L1...!...J5...m|4......5.......a7..;.(......%.]...xV....>=;..8a{63......":h....h..m.}Yl...8....;Om.].......<..!ph....j$..(.&..-.x......]f.L.+&qY.U.
22:21:11.679894 IP (tos 0x8, ttl 64, id 42529, offset 0, flags [DF], proto TCP (6), length 52)
E..4.!@.@..h...r...p......`....o...I.Y......%...*.
Somit läuft die Replikation gesichert per SSL; weitere Slaves würdest du analog hierzu einbinden, jeweils eigene Schlüssel erstellen und diese an ca-cert.pem
anhängen. Auch eine Master-Master-Replikation lässt sich so absichern. Viel Spaß beim Ausprobieren!
Hintergrundbild: 2448x 2448px, Bild genauer anschauen – © Marianne Spiller – Alle Rechte vorbehalten