Docker en opération
Pour ce cours, nous avons opté pour une solution self-hosting. Si on voulait mettre en production notre SGBDR, il y a quelques considérations :
L'emplacement physique de nos données
La sécurité de nos données at rest
La sécurité de nos communications avec le SGBDR
Les options de configuration de MariaDB
Pour suivre ces cours, vous allez créer une instance chez votre fournisseur cloud (e.g. Scaleway)
Installer Docker sur votre Serveur
Nous faisons un déploiement de MariaDB avec Docker. Jusqu'au présent, nous avons installé Docker sur un PC type Desktop, en utilisant Docker Desktop. Vous trouverez un guide d'installation ici.
Pour installer docker sur une instance Linux, il y aura quelques démarches à faire. Vous pouvez consulter la documentation officielle ici : https://docs.docker.com/engine/install/
Par exemple, sur Ubuntu, les étapes sont les suivantes (attention, à faire avec sudo ou en tant que root) :
Une fois installé, vous pouvez vérifier que le processus fonctionne correctement avec :
Accorder accès au groupe Docker
Certains de vos utilisateurs Linux auront le droit à contrôler les processus Docker, sans passer par sudo
.
Pour leur donner ce droit, ajoutez les utilisateurs concernés au groupe docker
:
Attention !
Seulement les utilisateurs privilégiés devront posséder ce droit.
L'emplacement des données
Lorsqu'on crée un Container pour tourner MariaDB, Docker crée par défaut un volume de stockage associé au Container. En revanche, ce volume sera supprimé avec le Container.
Il faudrait provisionner un emplacement fixe pour les données de MariaDB :
qui est bien connu
qui sera toujours présent, même si l'instance redémarre
facilement montable sur une autre instance, si jamais la première disparaît
idéalement, le volume sera crypté
Nous avons déjà vu comment provisionner un volume qui répond aux critères 1, 2 et 3 en montant un volume sur notre instance Ubuntu.
En revanche, comment faire une sorte que les données soient cryptées sur le disque dur ? Il y a trois niveaux de cryptage possibles :
Au niveau du disque dur
Par table par MariaDB
Via votre API
Ici, nous regardons le premier niveau de cryptage, avec l'extension LUKS (Linux Unified Key Setup). Nous allons provisionner un volume qui est crypté par défaut.
D'abord, il faut provisionner un volume chez votre fournisseur, identifier le volume avec lsblk
, et formater le volume :
Nous installons LUKS avec la commande cryptsetup
:
Nous avons déjà vu que quand on parle de la cryptographie, il faudrait nécessairement un (ou plusieurs) clé(s) qui permettent de crypter et décrypter des données. Avec LUKS, nous allons créer et gérer ces clés.
Nous commençons par générer une clé forte, en utilisant l'outil openssh
par exemple. Plus la clé est longue, plus elle est sécurisée :
Nous avons formaté le volume crypté. Ce volume ne peut pas être lu comme un volume normal. Il faudrait fournir la clé de cryptage. Donc avant de le monter, il faut configurer le périphérique :
Nous disposons maintenant d'un nouveau périphérique "virtuel" qui se trouve à /dev/mapper/mariadbdata
. Ce périphérique peut être formaté et monté comme n'importe quel autre périphérique :
Nous pouvons donc naviguer à /mnt/mariadbdata
et créer et modifier les fichiers. Nous pouvons aussi préciser ce chemin pour le stockage de nos données MariaDB dans notre docker-compose.yml
par exemple.
Mais, avant de le faire, il y a un problème : le volume crypté n'existera uniquement tant que l'instance ne redémarre pas. Au redémarrage, il faudrait fournir de nouveau le mot de passe de cryptage.
L'avantage de LUKS est qu'on peut ajouter plusieurs clés en parallèle. Nous allons créer une autre clé qui sera plutôt stocké dans un fichier sur un autre volume. Au démarrage, nous allons utiliser ce fichier pour déverrouiller notre volume.
Nous ajoutons cette clé parmi les clés possibles pour LUKS :
A tout moment, nous pouvons visionner l'état de nos clés :
Testons cette nouvelle clé. D'abord, fermons le volume qui a été ouvert via la clé initiale :
Ensuite, montons le volume en précisant plutôt le fichier comme clé :
C'est super. Maintenant, on peut déverrouiller un volume sans fournir manuellement un mot de passe. Comment le faire au démarrage ?
Vous vous souvenez du fichier /etc/fstab
? Nous avons l'équivalent pour les volumes cryptés : /etc/crypttab
, qui prend le format suivant :
Les options sont :
target name
: le nom de notre volume, pour nousmariadbdata
source device
: le UUID de notre volume. On peut trouver le UUID avecblkid /dev/sda
. Dans mon exemple, j'ai trouvé le UUID9096ac61-20bf-4c88-a770-35e6b71897b7
.key-file
: le chemin absolu du fichier avec la clé, pour nous/etc/.luks-mariadbdata-key
options
: les options du système de cryptographie. Pout nousluks
Nous ajoutons alors la ligne suivante :
Cette procédure assurera que le périphérique virtuel est créé (le volume crypté est ouvert). En revanche, il faut quand même monter le volume au démarrage, comme nous avons fait avec un volume normal. On modifie donc /etc/fstab
:
Vérifiez bien que vous votre volume monte correctement :
Ensuite, redémarrez votre instance pour tester.
La rotation des clés
Vous avez sûrement remarqué qu'on peut ajouter plusieurs clés pour décrypter notre volume. Comment cela fonctionne ?
Il y a deux niveaux de clé :
Une clé pour nous, l'utilisateur
Une clé "interne" connu et utilisé uniquement par LUKS
Les données sur le disque sont cryptées avec la deuxième clé.
Notre clé à nous sert uniquement à décrypter la clé interne.
Cela veut dire qu'on peut crypter plusieurs fois la clé interne avec différentes clés "externe", sans crypter à nouveau toutes les données sur le volume !
L'autre avantage est qu'on peut implémenter de la rotation des clés. Tous les X jours/semaines/mois, nous ajoutons une nouvelle clé, et on supprime l'ancienne. L'idée est d'encore minimiser les dégâts si une des clés est divulguée, car on rend obsolète les clés précédentes.
Le stockage des clés
Pour le moment, nous avons 2 clés :
La clé initiale qu'on a mémorisé ou stocké sur notre machine locale
La clé dans
/etc/.luks-mariadbdata-key
qui est stocké sur un autre volume
Vous remarquez qu'aucune clé n'est stockée sur le même volume que les données. Donc si quelqu'un pénètre la data-center et vol le disque dur avec nos données, il ne pourra pas décrypter nos données.
Références:
docker-compose.yml
Nous allons créer un sous-répertoire sur notre volume crypté pour stocker les données de notre SGBDR:
Nous sommes prêts maintenant à rédiger une docker-compose.yml
pour production.
Avant, nous avons marqué le mot de passe pour l'utilisateur root
directement dans le docker-compose.yml
. Ceci est une très mauvaise idée en production, car on pourrait récupérer le mot de passe en lisant le fichier docker-compose.yml
.
Nous allons plutôt créer des variables d'environnement dans notre shell (qui persisteront uniquement la durée de notre SHELL) :
Dans le docker-compose.yml
, nous utilisons ces variables avec ${ NOM_DE_VARIABLE }
.
On lance notre DB avec :
Testez la connexion à votre base de données déployée (attention, ici, on utilise le port 7100
) !
Obliger une connexion sécurisée
Actuellement, toute communication avec notre SGBDR sera non crypté. Nous ne l'avons pas obligé, et en production, ceci est une très mauvaise pratique ! N'importe qui pourrait sniffer nos communications et dériver nos mots de passes ou nos données secrètes.
Idéalement, on sécurise des communications avec TLS. C'est l'équivalent de la connexion SSL qu'on voit dans les navigateurs. Non seulement on établit des connexions cryptées, mais aussi, on établit un niveau de confiance auprès de notre base de données.
En revanche, créer des certificats exige un nom de domaine, qu'on ne va pas créer pendant ce cours. Je vous encourage quand même à suivre le guide marque dessus.
Pour ce cours, nous nous contenterons à obliger des connexions via SSH exclusivement. C'est-à-dire, nous devons d'abord établir un tunnel SSH vers l'instance hôte, et ensuite envoyer des commandes via ce tunnel crypté.
Ceci est facile avec Docker ! Nous modifions notre docker-compose.yml
, en ajoutant 127.0.0.1
devant le port qu'on a ouvert :
En ajoutant 127.0.0.1
, nous signalons à Docker qu'il faut accepter uniquement les connexions provenant de l'hôte même, et pas du monde extérieur.
Essayer : modifiez vous-même votre docker-compose.yml
, redémarrez votre service, et essayez de vous connecter via votre client local.
La seule façon d'établir une connexion du monde extérieur est d’établir d'abord une connexion SSH à l'instance. Dans votre client, vous auriez l'option de configurer la connexion SSH et fournir la clé privée nécessaire pour la connexion.
Connexions par SSH
Cette stratégie implique que même vos APIs doivent se connecter via SSH, qui n'est pas forcément voulu, surtout s'ils existent sur le réseau local. À ce moment-là, on n'ouvre pas de port du tout (on enlève la partie ports
du docker-compose.yml
). Seulement les services qui existent sans le réseau docker auront accès (mais en non sécurisé).
Sinon, on peut aussi créer des tunnels SSH dans nos APIs, si nécessaire avec des librairies tierces (ex. ssh2 pour nodejs). En revanche, nous créons une dépendance sur une tech basée sur l'architecture de prod qui n'est pas idéal.
Enfin, il sera peut-être prudent de suivre les démarches de mise en place des certificats TLS. Cette solution est effectivement plus souple dans le long terme.
Sécuriser MariaDB
MySQL (et MariaDB) contient par défaut un script qui permet d’affecter des règles de sécurité de base
Désactiver les connexions anonymes
Assurer des utilisateurs admin/root ne peuvent connexion uniquement par la machine locale
Assurer que l’utilisateur
root
a un mot de passe (et qu’il est suffisamment sécurisé)Supprimer la base de données de test (parfois installé par défaut)
Options de production MariaDB
Parfois, il est pertinent de modifier la configuration de MariaDB pour mieux répondre à la charge sur le SGBDR. Par exemple :
Le nombre de connexions concurrentes autorisées
La durée de vie des connexions avant de les fermer
Par défaut, un fichier de configuration se trouve déjà dans l'image MariaDB à /etc/mysql/mariadb.cnf
.
Nous pouvons remplacer ce fichier, simplement en le recréant sur notre instance locale (à côté de docker-compose.yml
) :
Nous ajoutons les deux dernières lignes :
max_connections
: fixer le nombre de connexions total à 1000wait_timeout
: fermez les connexions après 130 secondes
Afin d'appliquer ces réglages, il suffit d'ajouter ce fichier dans la partie volumes
de docker-compose.yml
:
Il faut redémarrer votre service pour que la modification soit prise en compte.
À tout moment, on peut récupérer la liste de sous-processus en train de traiter des requêtes, avec (en tant que root
) :
Dernière mise à jour