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) :
# Désinstaller les anciennes versions de docker
sudo apt-get remove docker docker-engine docker.io containerd runc
# Mettre à jour les indexes des packages
sudo apt-get update
# Installer les packages tiers nécessaires pour ajouter Docker parmi nos indexes
sudo apt-get install \
ca-certificates \
curl \
gnupg \
lsb-release
# Télécharger la clé publique de Docker qui va vérifier l'authenticité
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
# Installer le dépot Docker parmi les sources connues de notre distribution
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
$(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo chmod a+r /etc/apt/keyrings/docker.gpg
# Remettre à jour nos indexes
sudo apt-get update
# Installer Docker
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-compose-plugin
Une fois installé, vous pouvez vérifier que le processus fonctionne correctement avec :
systemctl status docker

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
:
usermod -aG docker [nom d'utilisateur]
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 :
# Lister les volumes attachés à l'instance
lsblk
# Formater le périphérique à /dev/sda avec le format "ext4"
mkfs.ext4 /dev/sda
Nous installons LUKS avec la commande cryptsetup
:
# Installer
sudo apt install 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 :
# Générer un mot de passe bien unique
openssl rand -base64 32
# Formater le disque pour cryptage LUKS
cryptsetup -c aes-xts-plain64 -v luksFormat /dev/sda
# On vous demande de fournir un mot de passe fort.
# Utilisez le mot de passe généré par openssl juste avant
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 :
# Configurer le peripherique
# On précise :
# - l'opération: luksOpen (ouvrir un volume)
# - le péripherique physique concerné : /dev/sda
# - l'alias pour ce volume dans LUKS : mariadbdata
cryptsetup luksOpen /dev/sda mariadbdata
# On vous demande de fournir le mot de passe utilisé à la création du volume
# Le nouveau péripherique devrait être présent
ls /dev/mapper
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 :
# Formater le nouveau volume
mkfs.ext4 /dev/mapper/mariadbdata
# Si pas déjà fait, créer le point de montage /mnt/mariadbdata
mkdir /mnt/mariadbdata
# Monter
mount /dev/mapper/mariadbdata /mnt/mariadbdata
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.
# Créer uns chaîne de caractères aléatoire, et les sauvegarder dans /etc/.luks-mariadbdata-key
dd if=/dev/random of=/etc/.luks-mariadbdata-key bs=32 count=1
# Regarder ce fichier :
cat /etc/.luks-mariadbdata-key
Nous ajoutons cette clé parmi les clés possibles pour LUKS :
# Ajouter la clé /etc/.luks-mariadbdata-key parmi le trousseau pour /dev/sda
cryptsetup luksAddKey /dev/sda /etc/.luks-mariadbdata-key
# Attention: il va vous demander le mot de passe précédemment crée.
# C'est normale ! Il faut connaître déjà un mot de passe avant d'en ajouter un autre !
A tout moment, nous pouvons visionner l'état de nos clés :
cryptsetup luksDump /dev/sda
Testons cette nouvelle clé. D'abord, fermons le volume qui a été ouvert via la clé initiale :
# Démonter le volume
umount /mnt/mariadbdata
# Fermer le périphérique
cryptsetup luksClose mariadbdata
Ensuite, montons le volume en précisant plutôt le fichier comme clé :
cryptsetup -v luksOpen /dev/sda mariadbdata --key-file /etc/.luks-mariadbdata-key
# Key slot 1 unlocked.
# Command successful.
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 :
<target name> <source device> <key-file> <options>
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 :
# Ajouter cette ligne à /etc/crypttab, ex:
# nano /etc/crypttab
mariadbdata UUID="9096ac61-20bf-4c88-a770-35e6b71897b7" /etc/.luks-mariadbdata-key luks
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
:
# Ajouter cette ligne à /etc/fstab, ex:
# nano /etc/fstab
/dev/mapper/mariadbdata /mnt/mariadbdata ext4 defaults,nofail 0 0
Vérifiez bien que vous votre volume monte correctement :
umount /mnt/mariadbdata
mount /mnt/mariadbdata/
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:
mkdir /mnt/mariadbdata/data
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) :
export MYSQL_ROOT_PASSWORD=abcd1234
export MYSQL_DATABASE=mydbprod
Dans le docker-compose.yml
, nous utilisons ces variables avec ${ NOM_DE_VARIABLE }
.
version: '3.9'
services:
mon_sgbdr:
image: mariadb
restart: always
# port-externe (hôte) : port-interne (container)
ports:
- "7100:3306"
# variables d'environnement à créer dans le container
environment:
- MYSQL_ALLOW_EMPTY_PASSWORD=false
- MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
- MYSQL_DATABASE=${MYSQL_DATABASE}
# options de lancement (encodage)
command: [
"--character-set-server=utf8mb4",
"--collation-server=utf8mb4_unicode_ci",
]
volumes:
# stocker les données sur le volume cryptée : /mnt/mariadbdata/data
- /mnt/mariadbdata/data:/var/lib/mysql
networks:
# Ce service existera sur le réseau virtuel suivant
- sgbdr-network-prod
networks:
sgbdr-network-prod:
driver: bridge
name: sgbdr-network-prod
On lance notre DB avec :
docker compose up
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 :
version: '3.9'
services:
mon_sgbdr:
image: mariadb
restart: always
# port-externe (hôte) : port-interne (container)
ports:
- "127.0.0.1:7100:3306"
...
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)
docker exec -it [Container ID] mysql_secure_installation
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
) :
# The MariaDB configuration file
#
# The MariaDB/MySQL tools read configuration files in the following order:
# 0. "/etc/mysql/my.cnf" symlinks to this file, reason why all the rest is read.
# 1. "/etc/mysql/mariadb.cnf" (this file) to set global defaults,
# 2. "/etc/mysql/conf.d/*.cnf" to set global options.
# 3. "/etc/mysql/mariadb.conf.d/*.cnf" to set MariaDB-only options.
# 4. "~/.my.cnf" to set user-specific options.
#
# If the same option is defined multiple times, the last one will apply.
#
# One can use all long options that the program supports.
# Run program with --help to get a list of available options and with
# --print-defaults to see which it would actually understand and use.
#
# If you are new to MariaDB, check out https://mariadb.com/kb/en/basic-mariadb-articles/
#
# This group is read both by the client and the server
# use it for options that affect everything
#
[client-server]
# Port or socket location where to connect
# port = 3306
socket = /run/mysqld/mysqld.sock
# Import all .cnf files from configuration directory
[mariadbd]
skip-host-cache
skip-name-resolve
max_connections=1000
wait_timeout=130
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
:
volumes:
# stocker les données sur le volume cryptée : /mnt/mariadbdata/data
- /mnt/mariadbdata/data:/var/lib/mysql
# remplacer le fichier de configuration avec notre propre version
- ./mariadb.cnf:/etc/mysql/mariadb.cnf
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
) :
SHOW PROCESSLIST;
Dernière mise à jour