MySQL-Replikation über SSL absichern

Diesen Beitrag schrieb ich 9 Jahre und 2 Monate zuvor; die nachfolgenden Ausführungen müssen heute weder genau so nach wie vor funktionieren, noch meiner heutigen Meinung entsprechen. Behalte das beim Lesen (und vor allem: beim Nachmachen!) bitte stets im Hinterkopf.

Geschätzte Lesezeit: 4 Minuten

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 CA ca-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 zu slave1-private.pem kompatible Key vom Typ PEM 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!

Alle Bilder dieser Seite: © Marianne Spiller – Alle Rechte vorbehalten
Hintergrundbild: 2448x 2448px, Bild genauer anschauen – © Marianne Spiller – Alle Rechte vorbehalten

Eure Gedanken zu „MySQL-Replikation über SSL absichern“

Ich freue mich über jeden Kommentar, es sei denn, er ist blöd. Deshalb behalte ich mir auch vor, die richtig blöden kurzerhand wieder zu löschen. Die Kommentarfunktion ist über GitHub realisiert, weshalb ihr euch zunächst dort einloggen und „utterances“ bestätigen müsst. Die Kommentare selbst werden im Issue-Tracker und mit dem Label „✨💬✨ comment“ erfasst – jeder Blogartikel ist ein eigenes Issue. Über GitHub könnt ihr eure Kommentare somit jederzeit bearbeiten oder löschen.