0

Quelles solutions de chiffrement de données pour MySQL / MariaDB

twitterlinkedinmail

C’est un sujet d’actualité, d’ailleurs début mars l’ANSSI a réévalué son estimation de la menace cyber au regard des évènements en Ukraine, et invité les entreprises françaises à renforcer leur posture de sécurité.

Le vol de données est un des risques auxquels toutes les entreprises s’exposent, et ce risque existe tout autant à l’intérieur qu’à l’extérieur d’une organisation. Brevets, résultats de recherche, données financières, données personnelles et RGPD… Chiffrer les données sensibles, ou même toutes les données tant qu’à faire, peut être une réponse rassurante mais elle entraine son lot de coûts cachés : options ou outils payants, impact sur la performance, la volumétrie, la manageabilité et et le maintien en conditions opérationnelles.

Les questions à se poser sont principalement : contre quoi dois-je me protéger ? quelles sont mes données à caractère sensible ?

Nous allons voir dans cet article (assez long finalement), l’éventail de solutions de chiffrement proposées pour les bases de données de type MySQL / MariaDB.

Solutions principales en théorie:

On peut distinguer deux grandes manières d’adresser la question du chiffrement de données avec MySQL:

Le chiffrement Data-at-Rest :
Le principe est de chiffrer les données sur disque, pour se prémunir du vol de fichiers, à travers la solution InnoDB Data-at-rest Encryption en édition communautaire, ou Transparent Data Encryption (aka TDE) en édition Enterprise. La fonctionnalité existe depuis plusieurs années, mais je recommande de migrer au minimum vers les dernières versions de MySQL et MariaDB pour éviter les écueils des premières implémentations.

Que ce soit InnoDB Data-at-rest ou TDE, seul le tablespace est chiffré. Lorsqu’il est lu depuis le disque, MySQL le déchiffre et ensuite son contenu est visible en clair :
– Depuis le serveur MySQL lui-même.
– Sur le réseau entre le client et le serveur.
– Dans l’application.

On comprend donc que l’effet de TDE est limité : il faudra en plus songer à gérer le droit d’en connaître des utilisateurs, chiffrer la communication entre le client et le serveur (idéalement via un tunnel sinon gare aux problèmes de performance), et aussi chiffrer les extractions, les dumps, etc… A ce niveau seuls les backups physiques sont compatibles avec TDE, les fichiers de dump type mysqldump / mysqlpump seront lisibles en clair.

L’avantage de TDE est, comme son nom l’indique, de rester transparent vis-à-vis des applications. Nul besoin de modifier le code applicatif pour décrypter les données, c’est MySQL qui s’en charge. La granularité en revanche sera le tablespace. Si votre déploiement est au format innodb_file_per_table=1 qui est le cas par défaut depuis la version 5.6.6, chaque table peut être chiffrée ou non séparément.

TDE utilise une infrastructure de chiffrement composée d’une InnoDB MASTER KEY, qui elle-même chiffre des TABLESPACE KEYS permettant d’encrypter les fichiers sur disque. Il existe également une BINLOG KEY qui permet de chiffrer le contenu des journaux binaires. Ces clés doivent être gérées : stockées, sauvegardées, et doivent être changées régulièrement comme préconisé par les normes de sécurité type PCI-DSS. Nous allons voir plus loin spécifiquement comment le faire.

Le chiffrement applicatif :
Il consiste à ne chiffrer que les colonnes qui sont éligibles. Plus chirurgical, mais ce ne sera pas la solution adaptée s’il faut chiffrer toute la base. Dans ce cas, la colonne est stockée chiffrée sur disque, et ne pourra être déchiffrée qu’en fournissant la clé qui a permis l’encryption. Sans ce sésame, un fouillis de chiffres et lettres illisibles.
Cette solution couvre donc plus de cas de figure que TDE car sans clé, les données restent chiffrées de bout en bout. Il suffit donc de ne donner la clé qu’aux applications / personnes autorisées:

Parmi les inconvénients principaux, le manque de transparence vis-à-vis de l’application. Le code devra être modifié pour manipuler la ou les clés de chiffrement, ce qui exclue la solution pour nombre de progiciels qui ne l’ont pas prévu. L’impact sur la performance et la volumétrie est également très dissuasif, comme nous allons le voir plus loin.

Transparent Data Encryption:

Dans le cas de TDE, les MASTER KEY et BINLOG KEY sont stockées dans un keyring, qui peut être soit porté par une infrastructure KMS type Oracle OKV, Amazon KMS, Hashicorp Vault, etc… ou un keyring local sous la forme d’un fichier crypté (édition Enterprise) ou non (édition communautaire):

Dans l’exemple nous allons utiliser un keyring local crypté. Dans un premier temps il faut créer un répertoire sous lequel notre keyring sera stocké :

$ mkdir /var/lib/mysql/mysql_keyring_encrfile
$ chown -R mysql:mysql /var/lib/mysql/mysql_keyring_encrfile
$ chmod 750 /var/lib/mysql/mysql_keyring_encrfile

Puis modifier le fichier de configuration de MySQL pour déclarer le keyring:

$ vi /etc/mysql/mysql.conf.d/mysqld.cnf
(...)
# KMIP KEYRING_ENCRYPTED_FILE
early-plugin-load=keyring_encrypted_file.so
keyring_encrypted_file_data=/var/lib/mysql/mysql_keyring_encrfile/keyring-encrypted
keyring_encrypted_file_password=************************

Noter le premier problème ici, la nécessité de stocker le mot de passe de déchiffrement du keyring dans la config de MySQL, ce qui fait porter tout le poids de la sécurisation sur les droits et comptes autorisés à lire le fichier (évidemment il faut déjà pouvoir accéder à la machine locale). Dans les implémentations de production, on lui préfèrera nettement un KMS qui est un outil fait pour ça.

On redémarre le service MySQL pour pouvoir vérifier si le plugin est bien disponible et prêt à être utilisé:

$ chmod og-rwx /etc/mysql/mysql.conf.d/mysqld.cnf
$ systemctl restart mysql
$ mysql --login-path=local \
	--execute="select PLUGIN_NAME, PLUGIN_VERSION, PLUGIN_STATUS 
->  from information_schema.plugins 
->  where PLUGIN_NAME = 'keyring_encrypted_file’;"

+------------------------+----------------+---------------+
| PLUGIN_NAME            | PLUGIN_VERSION | PLUGIN_STATUS |
+------------------------+----------------+---------------+
| keyring_encrypted_file | 1.0            | ACTIVE        |
+------------------------+----------------+---------------+

Il ne reste plus qu’à tester le chiffrement d’une table et voir ce que ça donne sur disque:

mysql> alter table payment_method_encrypted ENCRYPTION='Y' ;
Query OK, 122 rows affected (0.06 sec)
Records: 122  Duplicates: 0  Warnings: 0

mysql> select * from payment_method_encrypted ;
+-------------+-----------------+---------------------+-----+
| customer_id | credit_card     | expiration_date     | CCV |
+-------------+-----------------+---------------------+-----+
|         103 | 586792408103868 | 2020-09-13 19:39:13 |  62 |
|         112 | 191648252486855 | 2020-09-13 19:39:13 |  13 |
|         114 | 775979657448809 | 2020-09-13 19:39:13 |  56 |
|         119 | 927510594881615 | 2020-09-13 19:39:13 |  78 |
|         121 | 226470357949352 | 2020-09-13 19:39:13 |  36 |
(...)

OK, les données sont bien lues en clair dans le client MySQL, voyons dans le fichier IBD:

$ strings payment_method_encrypted.ibd | more
cffb97d4-1474-11ec-b390-00163e74e536
T&xNn
U\v4
>xS{& 
sMPrP
Bz|&_;Rl
#?d:
,i&xr_
'B' y
(...)

Le fichier est bien chiffré correctement, les données sont illisibles.

Chiffrement applicatif avec clés symétriques / asymétriques:

Nous avons vu que le chiffrement applicatif consiste à chiffrer une ou plusieurs colonnes éligibles, non la table ou la base en entier. Deux cas sont possibles:
Chiffrement symétrique : on chiffre et déchiffre avec la même clé. Moins sécurisé, mais l’impact en performance est maîtrisé.
Chiffrement asymétrique : le bon vieux principe des clés privées / publiques, plus sécurisé, par contre avec un impact en performance / volumétrie qui fait réfléchir (voir plus loin).

Dans les 2 cas, il faudra commencer par installer les fonctions nécessaires de génération de clés, chiffrement, déchiffrement etc… livrées dans les plugins keyring_udf.so et openssl_udf.so :

INSTALL PLUGIN keyring_udf SONAME 'keyring_udf.so';
CREATE FUNCTION keyring_key_generate RETURNS INTEGER SONAME 'keyring_udf.so';
CREATE FUNCTION keyring_key_fetch RETURNS STRING  SONAME 'keyring_udf.so';
CREATE FUNCTION keyring_key_length_fetch RETURNS INTEGER  SONAME 'keyring_udf.so';
CREATE FUNCTION keyring_key_type_fetch RETURNS STRING  SONAME 'keyring_udf.so';
CREATE FUNCTION keyring_key_store RETURNS INTEGER  SONAME 'keyring_udf.so';
CREATE FUNCTION keyring_key_remove RETURNS INTEGER  SONAME 'keyring_udf.so’;
--
CREATE FUNCTION asymmetric_decrypt  RETURNS STRING SONAME 'openssl_udf.so';
CREATE FUNCTION asymmetric_derive  RETURNS STRING SONAME 'openssl_udf.so';
CREATE FUNCTION asymmetric_encrypt  RETURNS STRING SONAME 'openssl_udf.so';
CREATE FUNCTION asymmetric_sign  RETURNS STRING SONAME 'openssl_udf.so';
CREATE FUNCTION asymmetric_verify  RETURNS INTEGER SONAME 'openssl_udf.so';
CREATE FUNCTION create_asymmetric_priv_key  RETURNS STRING SONAME 'openssl_udf.so';
CREATE FUNCTION create_asymmetric_pub_key  RETURNS STRING SONAME 'openssl_udf.so';
CREATE FUNCTION create_dh_parameters  RETURNS STRING SONAME 'openssl_udf.so';
CREATE FUNCTION create_digest   RETURNS STRING SONAME 'openssl_udf.so';

Dans le cas d’un chiffrement symétrique:
On ne peut pas utiliser de keyring ni local ni sous forme de KMS, donc il faut trouver le moyen de stocker la clé quelque part, encore un problème de plus à gérer… On utilisera aes_encrypt() / aes_decrypt() pour chiffrer / déchiffrer dans notre exemple:

mysql> desc payment_method ;
+-----------------+----------+------+-----+---------+-------+
| Field           | Type     | Null | Key | Default | Extra |
+-----------------+----------+------+-----+---------+-------+
| customer_id     | int      | NO   | PRI | NULL    |       |
| credit_card     | char(16) | NO   | PRI | NULL    |       |
| expiration_date | datetime | NO   |     | NULL    |       |
| CCV             | smallint | NO   |     | NULL    |       |
+-----------------+----------+------+-----+---------+-------+

mysql> -- -- Stocker la clé qq part !!
mysql> set @key_str = sha2('***************************************',512) ;
Query OK, 0 rows affected (0.00 sec)

mysql> set @init_vector = random_bytes(16) ;
Query OK, 0 rows affected (0.00 sec)

mysql> create table payment_method_cconly_encrypted_sym  
	as select customer_id 
	, aes_encrypt(credit_card,@key_str,@init_vector) as credit_card
	, expiration_date 
	,CCV 
	from payment_method;
Query OK, 122 rows affected, 122 warnings (0.03 sec)
Records: 122  Duplicates: 0  Warnings: 122

mysql> desc payment_method_cconly_encrypted_sym ;
+-----------------+---------------+------+-----+---------+-------+
| Field           | Type          | Null | Key | Default | Extra |
+-----------------+---------------+------+-----+---------+-------+
| customer_id     | int           | NO   |     | NULL    |       |
| credit_card     | varbinary(32) | YES  |     | NULL    |       |
| expiration_date | datetime      | NO   |     | NULL    |       |
| CCV             | smallint      | NO   |     | NULL    |       |
+-----------------+---------------+------+-----+---------+-------+

Déjà première remarque, notre colonne char(16) est devenue un varbinary(32) au passage, mais on le verra plus loin cela n’aura que peu d’effets sur la volumétrie. Voyons ce que révèlent nos données avec et sans clé de chiffrement:

mysql> select * from payment_method_cconly_encrypted_sym ;
+-------------+------------------------------------+---------------------+-----+
| customer_id | credit_card                        | expiration_date     | CCV |
+-------------+------------------------------------+---------------------+-----+
|         103 | 0xA7D64A3EF3E7D099552B964722DFA851 | 2020-09-13 19:39:13 |  62 |
|         112 | 0xFB07124853DFD549FA813DE8702C7993 | 2020-09-13 19:39:13 |  13 |
|         114 | 0xC3462527596C47371FEF7EDB29CF5E8F | 2020-09-13 19:39:13 |  56 |
(...)

mysql> select customer_id
	, cast(aes_decrypt(credit_card, @key_str, @init_vector) as char(16))
	, expiration_date
	, CCV from payment_method_cconly_encrypted_sym;
+-------------+--------------------------------------------------------------------+---------------------+-----+
| customer_id | cast(aes_decrypt(credit_card, @key_str, @init_vector) as char(16)) | expiration_date     | CCV |
+-------------+--------------------------------------------------------------------+---------------------+-----+
|         103 | 58679240810386                                                     | 2020-09-13 19:39:13 |  62 |
|         112 | 191648252486855                                                    | 2020-09-13 19:39:13 |  13 |
|         114 | 775979657448809                                                    | 2020-09-13 19:39:13 |  56 |
|         119 | 927510594881615                                                    | 2020-09-13 19:39:13 |  78 |
|         121 | 226470357949352                                                    | 2020-09-13 19:39:13 |  36 |
(...)

On voit bien le manque de transparence de cette solution pour l’applicatif, qui devra modifier tous ses accès en lecture ou DML aux données pour passer la clé de chiffrement.

Regardons le contenu du fichier sur disque, ou dans un mysqldump, pour voir si notre colonne reste bien illisible de bout en bout:

$ strings /var/lib/mysql/classicmodels/payment_method_cconly_encrypted_sym.ibd | more
infimum
supremum
_Zu;
f2>Zt
^pzvK
?!'0uz9
\rKrU+
pn<g
E3k&
T#}(H0
Nd(M
Z!\jad
(…)

$ mysqldump --login-path=local classicmodels > classicmodels.dmp
$ more classicmodels.dmp
(…)
/*!40000 ALTER TABLE `payment_method_cconly_encrypted_sym` DISABLE KEYS */;
INSERT INTO `payment_method_cconly_encrypted_sym` VALUES (103,_binary '�\�J>\�\�ЙU+�G\"ߨQ','2020-09-13 19:39:13',62),(112,_binary '�HS\�\�I��=\�p,y�','2020-09-13 19:39:13',13),(114,
_binary '\�F%\'YlG7\�~\�)\�^�','2020-09-13 19:39:13',56),(…)

Dans le cas d’un chiffrement asymétrique:
Pour le chiffrement asymétrique, nous pouvons utiliser soit une clé comme pour l’exemple précédent, soit un keyring, ce qui semble être un plus mais comme il faut quand même pouvoir retenir la valeur de la clé qui permet d’ouvrir le keyring, le problème est simplement déplacé. Ci-dessous nous créons la paire de clés privée / publique, et chiffrons la table avec la clé publique.

mysql> SELECT keyring_key_store('*************************************', 
-> 'SECRET', create_asymmetric_priv_key('RSA', 2048)) as res;
+----------+
| res	   |
+----------+
|        1 |
+----------+
mysql> set @ppk = keyring_key_fetch('*************************************') ;
mysql> set @pubkey = CREATE_ASYMMETRIC_PUB_KEY('RSA',@ppk) ;
mysql> create table payment_method_cconly_encrypted_asym_keyr
    -> as
    -> select customer_id
    -> ,asymmetric_encrypt('RSA',credit_card,@ppk) as credit_card
    -> ,expiration_date
    -> ,CCV
    -> from payment_method ;
Query OK, 122 rows affected (0.24 sec)
Records: 122  Duplicates: 0  Warnings: 0

Les données sont correctement cryptées:

mysql> select * from payment_method_cconly_encrypted_asym limit 3 \G
*************************** 1. row ***************************
    customer_id: 103
    credit_card: 0x7CE7DE6D248AECB4136499F8886C45EFB4C9FA2916BFAB4C4947AE2296A26A48DBF5963CCF4543751D48AFB41A6643CE0BE8A42DE39AFE591428C721E8A2B413224A1CB5D2CFE690FB5FB56D683BEC435A4065192900849114E8E42CE9C884BC597A94DCDB48FD7D460833147AABBC88D1077EB4BE3A48751462E5E12007E76C
expiration_date: 2020-09-13 19:39:13
            CCV: 62
*************************** 2. row ***************************
    customer_id: 112
    credit_card: 0x65C0361273A511E367ABEF1BB84A5CEB3AEE0E85C6243AF2BC8E8D096DFA073058CF59A972DEF3BE94257EADDC8D237941167F263E77FC637BF0522A0FB987CC605D6680BF7592BC4488ECE48A476E62018043C26ADF8EB0DC50200C03D56B06A7681E06DB9DC891ED97C5C035863A17A3B9C8BD1ACEF4A2D634354EDE84135D
expiration_date: 2020-09-13 19:39:13
            CCV: 13
*************************** 3. row ***************************
    customer_id: 114
    credit_card: 0x34CA32F38C722E8AF1BCAA9C7D60412678C4959702577705FFB98FA346A6BF897A3558CA13BD94B4B3BC039DAB036642EBE4660813E1F2FD366DC654C4334BCDB8AC0C9B641B0F1C9F5C3D576C791107F3C20C1CE3427768B386EA5741E338141E60A858B6B4EB883DB614150607BBA299938AA0A584FC09AE896F952E7B853B
expiration_date: 2020-09-13 19:39:13
            CCV: 56
3 rows in set (0.00 sec)

La colonne peut être ensuite déchiffrée avec la clé publique. Peu importe l’ordre ici il ne s’agit pas d’un échange de clé type RSA entre Alice et Bob, le but est simplement d’éviter d’utiliser la même clé pour faire les deux opérations.

mysql> desc payment_method_cconly_encrypted_asym ;
+-----------------+----------+------+-----+---------+-------+
| Field           | Type     | Null | Key | Default | Extra |
+-----------------+----------+------+-----+---------+-------+
| customer_id     | int      | NO   |     | NULL    |       |
| credit_card     | blob     | NO   |     | NULL    |       |
| expiration_date | datetime | NO   |     | NULL    |       |
| CCV             | smallint | NO   |     | NULL    |       |
+-----------------+----------+------+-----+---------+-------+
4 rows in set (0.00 sec)

mysql> select customer_id
    -> , cast(asymmetric_decrypt('RSA',credit_card, @pubkey) as char(16)) as credit_card
    -> , expiration_date
    -> , CCV from payment_method_cconly_encrypted_asym;
+-------------+-----------------+---------------------+-----+
| customer_id | credit_card     | expiration_date     | CCV |
+-------------+-----------------+---------------------+-----+
|         103 | 58679240810386  | 2020-09-13 19:39:13 |  62 |
|         112 | 191648252486855 | 2020-09-13 19:39:13 |  13 |
|         114 | 775979657448809 | 2020-09-13 19:39:13 |  56 |
(...)

A noter que cette fois le type de la colonne credit_card est passée de char(16) à blob directement, ce qui aura un impact non négligeable sur la volumétrie, comme on le verra plus loin.

Enfin, pour éviter d’avoir à passer la clé d’ouverture du keyring partout dans le code, il est possible de la centraliser dans une fonction :

mysql> CREATE DEFINER = 'root'@'localhost' FUNCTION `appdecrypt`() 
    -> RETURNS TEXT(500) deterministic SQL SECURITY DEFINER 
    -> RETURN RTRIM(CREATE_ASYMMETRIC_PUB_KEY('RSA',
    -> keyring_key_fetch('****************************************')));

mysql> grant execute on function appdecrypt to 'appuser'@'localhost' ;
mysql> flush privileges ;

$ mysql --user=appuser --socket=/var/run/mysqld/mysqld.sock \
  --password=****** classicmodels

mysql> set @pubkey = appdecrypt() ;
Query OK, 0 rows affected (0.00 sec)

mysql> select customer_id , cast(asymmetric_decrypt('RSA',credit_card,@pubkey) as char(16)) 
-> as credit_card ,expiration_date ,CCV from payment_method_cconly_encrypted_asym_keyr limit 1 ;
+-------------+-----------------+---------------------+-----+
| customer_id | credit_card     | expiration_date     | CCV |
+-------------+-----------------+---------------------+-----+
|         103 | 58679240810386  | 2020-09-13 19:39:13 |  62 |
+-------------+-----------------+---------------------+-----+

Mais là encore le problème de stocker le secret pour déverrouiller le keyring est seulement déplacé ailleurs…

Autres sources de données à chiffrer:

Jusqu’ici, nous nous sommes intéressé aux fichiers de données, mais des informations sensibles peuvent se trouver aussi dans d’autres fichiers, comme les fichiers de sauvegardes, les fichiers de log (log d’erreur, log de requêtes lentes), les journaux binaires …

Les journaux binaires:
Les journaux binaires peuvent contenir soit les requêtes de modification en mode STATEMENT ou MIXED, ou les lignes au format base64 qui peuvent être décodées par l’option –decode-rows de mysqlbinlog, donc ils doivent être considérés comme une source potentielle à protéger. C’est chose possible mais seulement depuis les versions récentes de MySQL.

Comme pour TDE, le chiffrement de journaux binaires est basé sur une hiérarchie de clés avec une BINLOG MASTER KEY qui chiffre les binlogs, ce qui nécessite la mise en place d’un keyring et d’activer l’option binlog_encryption:

$ vi /etc/mysql/mysql.conf.d/mysqld.cnf 
(...)
early-plugin-load=keyring_encrypted_file.so
keyring_encrypted_file_data=/var/lib/mysql/mysql_keyring_encrfile/keyring-encrypted
keyring_encrypted_file_password=*********
binlog_encryption=on

$ systemctl restart mysql.service
mysql> show binary logs ;
+---------------+-----------+-----------+
| Log_name      | File_size | Encrypted |
+---------------+-----------+-----------+
| binlog.000001 |       504 | No        |
| binlog.000002 |    154604 | No        |
| binlog.000003 |       179 | No        |
(…)
| binlog.000021 |     64159 | Yes       |
+---------------+-----------+-----------+

Le dernier binlog généré est bien encrypté. La lecture d’un binlog n’est plus possible directement via mysqlbinlog:

$ mysqlbinlog binlog.000021 | more
ERROR: Reading encrypted log files directly is not supported.
# The proper term is pseudo_replica_mode, but we use this compatibility alias
# to make the statement usable on server versions 8.0.24 and older.
/*!50530 SET @@SESSION.PSEUDO_SLAVE_MODE=1*/;
/*!50003 SET @OLD_COMPLETION_TYPE=@@COMPLETION_TYPE,COMPLETION_TYPE=0*/;
DELIMITER /*!*/;
SET @@SESSION.GTID_NEXT= 'AUTOMATIC' /* added by mysqlbinlog */ /*!*/;
DELIMITER ;
# End of log file

Pour pouvoir le décoder, il faudra forcer son interprétation par le serveur : il devra lire le binlog, le décoder et renvoyer le résultat, en détournant l’utilisation de l’option –read-on-remote-server sur le serveur local:

$ mysqlbinlog --login-path=local  \
 --read-from-remote-server binlog.000021
(…)
# at 319
#210915  8:10:48 server id 1  end_log_pos 604 CRC32 0x9e86fd59 	
Query	thread_id=9	exec_time=0	error_code=0
use `classicmodels`/*!*/;
SET TIMESTAMP=1631693448/*!*/;
/*!80013 SET @@session.sql_require_primary_key=0*//*!*/;
CREATE TABLE `payment_method_tde_plus_encr` (
  `customer_id` int NOT NULL,
  `credit_card` varbinary(32) DEFAULT NULL,
  `expiration_date` datetime NOT NULL,
  `CCV` smallint NOT NULL(…)

Le problème évident que cela pose, c’est qu’on ne pourra lire le contenu d’un binlog sans avoir un serveur démarré à côté, avec le keyring chargé en mémoire. mysqlbinlog dans ce cas devient beaucoup moins souple d’utilisation lorsque les journaux binaires sont chiffrés.

Autres fichiers:
Pour les autres fichiers, il n’existe rien de natif pour chiffrer, donc s’ils sont considérés comme cibles potentielles, il faudra trouver d’autres moyens de les protéger.

Gestion des clés:

La plupart des normes conseillent voire imposent la rotation régulière des clés. Dans le cas de TDE, il faudra effectuer la rotation à la fois sur la INNODB MASTER KEY pour les fichiers de données et la BINLOG MASTER KEY pour les journaux binaires.

ALTER INSTANCE ROTATE MASTER KEY;
ALTER INSTANCE ROTATE BINLOG MASTER KEY; 

Pour des raisons évidentes de performance, la rotation de la master key réencrypte simplement la clé dans l’entête de chaque tablespace, elle ne rechiffre pas tout le tablespace. La rotation de la binlog master key quant à elle permet de réencrypter le fichier de mot de passe associé à chaque binlog. Elle ne réencrypte pas tous les binlogs.

Impact sur la performance et la volumétrie:

En performance:
Concernant TDE, l’impact sera réduit en lecture tant que le buffer pool InnoDB est suffisamment bien dimensionné pour éviter au maximum de faire des lectures disques. En revanche, sur un workload plutôt transactionnel, attention aux effets de bord sur les checkpoints et les transactions. Il n’y a que peu de benches publiés montrant un réel impact à l’heure actuelle, on peut citer toutefois des travaux de recherche de 2 étudiants de l’université de Luleå en Suède:

source: Thèse 2019 Luleå University of Technology (Feidias Moulianitakis, Konstantinos Asimakopoulos)

Le chiffrement applicatif est nettement plus concerné par les impacts, notamment via l’utilisation de clés asymétriques, car il nécessite un déchiffrement à chaque appel, par chaque client, donc l’impact sur le serveur est à balancer avec le volume de données à déchiffrer. Exemple avec déchiffrement asymétrique d’1 ou 2 colonnes, et comparaison avec la même table sans chiffrement:

Une remarque aussi sur les indexes dans le cas du chiffrement applicatif, c’est la valeur chiffrée qui est indexée, donc l’index ne pourra jamais être utilisé pour résoudre un prédicat :

-- Elapsed : 0.03s
mysql> explain select count(1) from TESTLOAD 
  -> where password like '%c72c%' \G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: TESTLOAD
   partitions: NULL
         type: index
possible_keys: NULL
          key: IX_PASSWD
      key_len: 258
          ref: NULL
         rows: 40962
     filtered: 11.11
        Extra: Using where; Using index
1 row in set, 1 warning (0.00 sec)

-- Elapsed : 1.82s
mysql> explain select count(1) from TESTLOAD_ENCRYPTED 
  -> where cast(asymmetric_decrypt('RSA',password,@pubkey) 
  -> as char(256)) like '%c72c%'\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: TESTLOAD_ENCRYPTED
   partitions: NULL
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 39624
     filtered: 100.00
        Extra: Using where
1 row in set, 1 warning (0.00 sec)

Avec TDE en revanche, l’index se base sur la valeur décryptée donc ce la ne posera pas de problème :

-- Elapsed : 0.029s
mysql> explain select count(1) from TESTLOAD_TDE 
  -> where password like '%c72c%'  \G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: TESTLOAD_TDE
   partitions: NULL
         type: index
possible_keys: NULL
          key: IX_PASSWD
      key_len: 258
          ref: NULL
         rows: 41031
     filtered: 11.11
        Extra: Using where; Using index
1 row in set, 1 warning (0.00 sec)

En volumétrie:
Quant à l’impact sur la volumétrie, c’est aussi l’encryption avec clés asymétriques qui sera la plus impactante, en raison de la conversion de type vers blob:

La compression n’est plus du tout efficace et elle ne permet plus de faire gagner de la place. Il faudra donc prévoir de la capacité disque en plus en fonction de la quantité de données à chiffrer…

Impact sur les sauvegardes:

Dans le cas de TDE, nous avons vu plus haut que les données d’un dump sont en clair, et qu’il n’existe pas d’outil natif pour lier la MASTER KEY au fichier dump généré. En revanche dans le cas du chiffrement applicatif, nous avons vu que la donnée est chiffrée de bout en bout, donc dans ce cas le dump est protégé.

Pour ce qui est des backups physiques (MySQL Enterprise Backup, MariaBackup), il faudra utiliser une option pour spécifier de crypter le backup et d’embarquer le keyring avec. Par défaut, cette option –encrypted-password attend un mot de passe sur la ligne de commande, ce qui n’est pas l’idéal, le mieux (le moins pire, en réalité) étant de créer une section [mysqlbackup] dans le fichier de configuration de mysqld et de renseigner la clé dedans:

$ cat /etc/mysql/mysql.conf.d/mysqld.cnf 
(...)
[mysqlbackup]
encrypt-password=*********

$ mysqlbackup --defaults-file=/etc/mysql/mysql.conf.d/mysqld.cnf \
	--login-path=local --backup-image=/var/lib/mysql/backups/mysql.20210914.mbi \
	--backup-dir=/root/backup-tmp backup-to-image
(...)
210914 12:00:37 MAIN     INFO: The server's active keyring is 'keyring_encrypted_file'.
210914 12:00:37 MAIN     INFO: The keyring file is located at '/var/lib/mysql/mysql_keyring_encrfile/keyring-encrypted', size 613.
210914 12:00:37 MAIN     INFO: The keyring file is copied to '/root/backup-tmp/meta/keyring_kef'.
210914 12:00:37 MAIN     INFO: Initialized keyring with 4 keys.
(...)
210914 12:00:37 MAIN     INFO: This backup includes a keyring.
(...)
210914 12:00:38 MAIN     INFO: Full Image Backup operation completed successfully.
210914 12:00:38 MAIN     INFO: Backup image created successfully.
210914 12:00:38 MAIN     INFO: Image Path = /var/lib/mysql/backups/mysql.20210914.mbi
210914 12:00:38 MAIN     INFO: MySQL binlog position: filename binlog.000021, position 412
(…)

Attention si une rotation de clé intervient pendant le backup ou entre 2 backups, les backups antérieurs seront rendus inutilisables par la suite, donc il vaut mieux planifier les rotations de clés juste avant de lancer un backup full…

La solution ultime : chiffrement côté client:

Le top du chiffrement reste encore aujourd’hui pour MySQL seulement un voeu pieux: le chiffrement côté client permet de protéger les données de bout en bout, tout en rendant l’accès à ces données transparent pour l’application. Le meilleur des mondes en quelque sorte:

Dans ce cas, c’est à la charge de la couche cliente de chiffrer les données lorsqu’elles sont injectées en base. C’est un peu comme un chiffrement applicatif, sauf que l’intelligence du déchiffrement réside dans la couche cliente MySQL, et pas directement dans l’application, ce qui implique que l’on n’a plus de code à modifier. Malheureusement, ce système n’existe pas encore pour MySQL. Pour les concurrents on peut citer quelques exemples comme Always Encrypted sur SQL Server, ou Client-Side FLE sur MongoDB. Attention tout comme le chiffrement aplicatif, la granularité s’applique à la colonne, donc il reste peu commode pour chiffrer toute une base.

Tableau Bilan et conclusion:

Si on fait le bilan des avantages et inconvénients de chaque solution, critère par critère:

On l’aura compris, il n’existe donc pas (encore) la solution universelle, chacune ayant ses avantages et inconvénients qu’il faudra bien mesurer avant de se lancer dans une telle aventure.

Si votre objectif est de tout crypter sans vous poser de question, alors TDE semble la bonne approche mais elle ne suffira pas. Il faudra lui adjoindre une bonne politique de gestion des privilèges en appliquant le need to know, un chiffrement des connexions entre client et serveur, et une protection des fichiers type dump, et journaux binaires.

Si vous savez précisément quelles colonnes chiffrer, qu’elles ne sont pas nombreuses, vous pouvez adopter une approche à base de chiffrage applicatif à clé symétrique pour limiter la casse en termes de performance et volumétrie. Tout en ayant à l’esprit qu’il y aura un coût en termes d’adaptation côté applicatif.

Et pour monter une solution vraiment sérieuse, vous ne pourrez pas vous passer d’un KMS, car sans cela, il faudra toujours stocker une clé en clair dans un fichier quelque part. Ce serait comme de mettre un lecteur d’empreinte digitale à l’entrée et laisser la porte du fond ouverte…

En attendant le chiffrement côté client pour les bases on-prem, on pourra se pencher du côté de GCP Cloud SQL qui a développé sa propre couche cliente capable de prendre en charge une telle fonctionnalité. Raison de plus pour passer sur GCP ?

A bientôt !
~David

Continuez votre lecture sur le blog :

twitterlinkedinmail

David Baffaleuf

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.