Chiffrement des données Medshake EHR/EDC
Préambule
Proof of concept !
La procédure indiquée ici est à l'état de prototype. Elle n'est pour le moment pas ni mature, ni validée et demande du travail pour être finalisée. Ne l'appliquez en aucun cas sur votre instance de production dans savoir ce que vous faites !
Comme recommandé sur la documentation officiel MedShake EHR/EDC sur la mise en production d'une instance, il faudrait chiffrer l'intégralité des données stockées par Medshake sur le disque dur du serveur sur lequel l'instance est hébergée afin protéger les données des patients en cas de vol du disque dur. Une solution efficace et commode est d'utiliser LUKS et de chiffrer la totalité des données du disque dur, cependant cette solution possède un problème assez gênant : devoir aller saisir la clé de déchiffrement sur le serveur à chacun de ses redémarrages, ce qui peut s'avérer problématique si il s'agit d'une machine dédiée utilisée par plusieurs utilisateurs (n'ayant pas forcément les compétences pour déchiffrer la partition) et que la machine ne possède pas d'écran et de clavier branché en permanence.
Cette article propose une ébauche de solution pour ce problème en substituant la page normale d'accès à l'instance MedShake par une page offrant un formulaire permettant de saisir une clé de déchiffrement des données si on détecte que celles-ci ne sont pas déchiffrées. La validation de ce formulaire permet l'exécution d'un script chargé de déchiffrer et monter la partition de données si la clé saisie est correcte, puis de rendre la main à Medshake une fois l'opération réussie.
En gros cela se passe comme ceci :
Dans ce cas nous ne chiffrons pas l'entièreté du disque dur mais uniquement une partition qui contiendra toutes les données utilisées par MedShake comprenant :
- Les fichiers de l'installation Medshake (on inclut tout : le
homeDirectory
,webDirectory
,storageLocation
,workingDirectory
, le code, etc…). - Les fichiers de la base de données Mysql (le
datadir
).
Pour être réellement efficace il ne faut pas utiliser de partition swap sur le disque dur avec la méthode décrite ici. En effet celle-ci peut éventuellement contenir des informations provenant de la mémoire vive de l'ordinateur, y compris la clé de déchiffrage LUKS. Il est donc nécessaire de disposer d'un serveur avec suffisamment de mémoire vive (4GB serait bien pour 1 à 5 utilisateurs) on peut aussi jeter un œil du côté de ZRam (voir aussi le paquet Debian zram-tools
).
Il faudrait aussi placer le /tmp
en mémoire vive comme ceci :
cp -a /usr/share/systemd/tmp.mount /etc/systemd/system/ systemctl enable tmp.mount rm -rf /tmp/* # Purge les fichiers actuels du /tmp pour ne pas qu'ils restent présents sur le DD reboot # redémarre l'ordinateur
D'autres approches du problèmes existent comme celle-ci : https://opensource.com/article/20/11/nbde-linux qui est plus lourde et complexe à mettre en place. De plus elle demande une configuration particulière sur les clients. Cette solution se veut une alternative plus simple à mettre en place et plus facilement adaptable (elle ne nécessite pas l'utilisation d'outils particuliers et repose sur une configuration Apache et des scripts bash et php qui peuvent facilement êtres adaptés au besoin). Elle ne nécessite pas non plus d'opération particulière sur les clients.
Pré-requis et contrainte
Disposer d'un volume LUKS
Le cas de la création du volume LUKS n'est pas traité ici. Il peut être crée par l'installateur Debian au moment de l'installation du système, sinon je vous renvois aux documentations déjà existantes :
On considère aussi les points suivants comme étant valides
- Nous utilisons un OS Debian 10
cryptsetup
est présent sur le systèmesudo
est présent sur le systèmephp-fpm
doit être installé et doit être utilisé à la place du *mod_php* d'Apache (raison expliquée un peu plus bas).- Les modules Apache2 suivants sont activés (pour pouvoir fonctionner avec *php-fpm*) :
proxy
(a2enmod proxy
)proxy_fcgi
(a2enmod proxy_fcgi
)
- Ici le volume LUKS une fois ouvert est identifié comme
/dev/mapper/vg0-cypted
.- Nous démarrons avec celui-ci monté dans
/srv/medshake/
- Il ne doit pas y avoir d'entrée dans le
/etc/crypttab
Point à travailler
- Mise en route du compagnon Apicrypt ?
- Service et données Orthanc ?
- Actuellement les logs Apache ne sont pas sur la partition chiffrée ? Est-t'il utile de les y placer ?
- D’autres cas de figures à traiter ?
Sujet abordé sur le forum.
Contrainte
Pour le moment on considère que la seule application présente sur le serveur qui utilise la base de données MariaDB est MedShake. Pour des raisons de simplicité de mise en place nous modifions le datadir
du service “de base” pour le placer sur notre volume chiffré, comme ça le volume chiffré ne sera pas accessible tout de suite au démarrage du système; le service MariaDB ne démarrera pas automatiquement avec le système. Cela le rendra inaccessible pour d'autres éventuelles applications.
Cette solution ne fonctionne pas avec le mod_php d'Apache. Quant le script php, chargé de proposé la validation de la saisie de la clé de déchiffrement des données (/usr/local/lib/medshake/www/index.php
) est exécuté par le mod_php d'Apache et que celui-ci appelle le script bash chargé de monter la partition de données chiffrées (/usr/local/lib/medshake/uncrypt.sh
), à l’intérieur de l’environnement du script bash tout semble normal (le volume chiffré est bien monté et on peut accéder aux données qu'il contient). Par contre pour le reste du système, le volume ne semble pas monté (pas d'entrée dans le /etc/mtab
et l'emplacement du point de montage reste vide). Je ne trouve pas, pour le moment, d'explication à ce comportement étrange… Par contre aucun problème si on exécute le script php avec php-fpm.
Étape 1 : Récupération et mise en place des scripts additionnels
Cette méthode requiert l'utilisation deux petits scripts :
- Un script php chargé de présenter un formulaire web pour demander aux utilisateurs tentant de se connecter à l'instance MedShake la saisie de la clé de déchiffrement des données si le volume LUKS n'est pas encore déverrouillé et monté au bon endroit.
- Un script bash appelé par le script php à la validation du formulaire et lancé avec les droits root et dont le but est d'ouvrir et de monter le volume LUKS ainsi que d'effectuer d'autres opérations comme démarrer le service MariaDB une fois le datadir accessible.
Commençons par créer un endroit adapté pour y placer nos scripts :
mkdir -p /usr/local/lib/medshake/www/
Le script bash de déchiffrage des données
- /usr/local/lib/medshake/uncrypt.sh
#!/bin/sh # # Script bash utilisé pour déchiffrer les données MedShake # # Adapter la configuration suivante au besoin # Paramètre supplémentaire pour la commande 'cryptsetup' CRYPT_SETUP_OPEN_MORE_PARAM="--type luks" # Nom du fichier périphérique contenant le volume LUKS chiffré CRYPTED_DEV="/dev/sdXY" # /!\ MODIFIER /dev/sdXY PAR LE NOM DU PÉRIPHÉRIQUE SUR VOTRE SYSTÈME !!! # Nom du volume LUKS une fois déverrouillé UNCRYPTED_DEV="meduncrypted" # Emplacement pour le montage de la partition des données déchiffrées UNCRYPTED_MNT_PT="/srv/medshake" # Option de montage supplémentaire pour le périphérique déchiffré UNCRYPTED_MNT_OPT="" # Nom du service pour lancer la base de données MariaDB MYSQLD_SERVICE="medshakedb" uncrypt_pass="${1}" if [ $(id -u) -gt 0 ] then echo "ERR: Vous n'êtes pas root" exit 65 fi if [ -z "${uncrypt_pass}" ] then echo "ERR: Clé de déchiffrement vide" exit 66 fi if [ ! -b "${CRYPTED_DEV}" ] then echo "ERR: Périphérique chiffré non trouvable ${CRYPTED_DEV}" exit 67 fi if [ ! -d "${UNCRYPTED_MNT_PT}" ] then echo "ERR: Le point de montage pour (${UNCRYPTED_MNT_PT}) n'existe pas" exit 68 fi if [ -z $(which cryptsetup) ] then echo "ERR: l'utilitaire 'cryptsetup' n'est pas installé" exit 69 fi if ! cryptsetup status "${UNCRYPTED_DEV}" > /dev/null then echo "${uncrypt_pass}" | cryptsetup open ${CRYPT_SETUP_OPEN_MORE_PARAM} "${CRYPTED_DEV}" "${UNCRYPTED_DEV}" if [ $? -gt 0 ] then echo "ERR: échec lors de l'ouverture du volume LUKS" exit 70 else echo "INF: Volume LUKS ouvert ${CRYPTED_DEV} -> ${UNCRYPTED_DEV}" fi else echo "WARN: ${CRYPTED_DEV} est déjà ouvert" fi if ! grep -q "^/dev/mapper/${UNCRYPTED_DEV} ${UNCRYPTED_MNT_PT}" /etc/mtab then echo mount -o "${UNCRYPTED_MNT_OPT}" "/dev/mapper/${UNCRYPTED_DEV}" "${UNCRYPTED_MNT_PT}" mount -o "${UNCRYPTED_MNT_OPT}" "/dev/mapper/${UNCRYPTED_DEV}" "${UNCRYPTED_MNT_PT}" 2>&1 if [ $? -gt 0 ] then echo "ERR: Échec au montage de /dev/mapper/${UNCRYPTED_DEV} sur ${UNCRYPTED_MNT_PT}" exit 71 else echo "INF: monter /dev/mapper/${UNCRYPTED_DEV} sur ${UNCRYPTED_MNT_PT}" fi else echo "WARN: /dev/mapper/${UNCRYPTED_DEV} est déjà monté sur ${UNCRYPTED_MNT_PT}" fi if [ -f "/var/run/mysqld/${MYSQLD_SERVICE}.pid" ] then echo "WARN: ${MYSQLD_SERVICE} est déjà lancé (le redémarrer)" systemctl restart "${MYSQLD_SERVICE}.service" if [ $? -gt 0 ] then echo "ERR: Échec du redémarrage de ${MYSQLD_SERVICE}" exit 72 fi echo "INF: service ${MYSQLD_SERVICE} redémarré" else systemctl start "${MYSQLD_SERVICE}.service" if [ $? -gt 0 ] then echo "ERR: Échec du démarrage du service ${MYSQLD_SERVICE}" exit 72 fi echo "INF: service ${MYSQLD_SERVICE} démarré" fi
Une fois récupéré et placé dans /usr/local/lib/medshake/uncrypt.sh
le rendre exécutable :
chmode 755 /usr/local/lib/medshake/uncrypt.sh
Dans notre cas, le script sera appelé par l'utilisateur www-data
et obtiendra les droits root via sudo
. Il faut donc disposer d'une entrée sudoers adaptée pour ne pas avoir de demande de mot de passe par sudo
:
www-data ALL=(ALL) NOPASSWD:/usr/local/lib/medshake/uncrypt.sh
Le fichier php pour afficher le formulaire de déchiffrage des données
- /usr/local/lib/medshake/www/index.php
<?php setlocale(LC_ALL, "fr_FR.UTF-8"); /********************************************************************** * * Page web de déchiffrage des données MedShake * *********************************************************************/ /* * Configuration */ // Emplacement où doivent êtres montées les données déchiffrés de MedShake define('MEDPATH', '/srv/medshake/'); // Script bash des opérations de déchiffrage des données MedShake define('UNCRYPT_SCRIPT', '/usr/local/lib/medshake/uncrypt.sh'); // Emplacement vers l’exécutable sudo define('SUDO_PATH', '/usr/bin/sudo'); // Log pour les actions du script // VOIR ICI EN CAS DE PB D’EXÉCUTION DU SCRIPT define('UNCRYPT_SCRIPT_LOG', '/tmp/medshake_unlock_script.log'); /* * Action */ // Si on essait d'appeler le script alors que les données sont déjà accessibles // on redirige vers l'url principale if (file_exists(MEDPATH.'.uncrypted')) { preg_match('#^(.*)(/uncrypt/.*)$#', $_SERVER['SCRIPT_URI'], $base_url); header('Location: '.$base_url[1]); exit(); } $error = 0; // Exécute les actions suivantes si on fournit la clé de déchiffrage des données if (!empty($_POST['uncryptkey'])) { // Appelle le script de bash d'ouverture du conteneur LUKS de montage du volume avec sudo // Requière une entrée sudoers adaptée pour ne pas avoir la demande de mot de passe exec(SUDO_PATH.' '.UNCRYPT_SCRIPT.' '.escapeshellarg($_POST['uncryptkey']), $res, $res_code); $error = $res_code; $f_res = ''; $log_content = '--- Last extuted at : '.date('Y-m-d H:i:s')."\n".implode("\n", $res)."\n"; file_put_contents(UNCRYPT_SCRIPT_LOG, $log_content); foreach ($res as $r) { $f_res .= '<li>'.$r.'</li>'."\n"; } if (! $error) { preg_match('#^(.*)(/uncrypt/.*)$#', $_SERVER['SCRIPT_URI'], $base_url); header('Location: '.$base_url[1]); exit(); } } /* * Vue */ ?> <html> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Déchiffrement des données Medshake EHR/EDC</title> <style> body { font-family: sans-serif; background-color: #17a2b8; } #main-block { max-width: 500px; margin: 100px auto; padding: 25px; background-color: #f8f9fa; border-radius: 5px; } #block-info { font-style: italic; margin: 20px; font-size: 0.9em; color: darkred; } #main-block h1 { font-size: 1.5em; } #uncrypkey-label { text-decoration: underline; display: block; margin-bottom: 5px; } #uncryptkey-field { width: 100%; } #submit { margin-top: 15px; display: flex; justify-content: flex-end; } #errors { padding: 15px; background-color: #F002; border-radius: 5px; } #errors h2 { font-size: 1.2em; } </style> </head> <body> <div id="main-block"> <h1>Données Medshake EHR/EDC chiffrées</h1> <div id="block-info"> Les données Medshake EHR/EDC sont actuellement chiffrés. Pour accéder à l'instance veuillez fournir la clé de déchiffrement. </div> <div> <form action="/uncrypt/index.php" method="post"> <div id="uncryptkey-group"> <label id="uncrypkey-label" for="uncrytpkey">Clé de déchiffrement :</label> <input type="password" name="uncryptkey" id="uncryptkey-field" required> </div> <div id="submit"> <button type="submit">Déchiffrer</button> </div> </form> </div> <?php if (!empty($f_res)): ?> <div id="errors"> <h2>Erreur</h2> <ul> <?php print $f_res; ?> </ul> </div> <?php endif; ?> </div> </body> </html>
Étape 2 : Configurer déplacer le ''datadir'' MariaDB
Vu que nous voulons que les fichiers de la base de données MariaDB se trouvent sur la partition chiffrée il va falloir adapter la configuration du service. Pour cela il faut modifier le fichier /etc/mysql/mariadb.conf.d/50-server.cnf
et pour le paramètre datadir
remplacer /var/lib/mysql/
par /srv/medshake/db/
:
sed -i 's#^\(datadir[[:space:]]*=\).*#\1 /srv/medshake/db/#' /etc/mysql/conf.d/medshakedb.cnf
Puis stopper le service :
systemctl stop mariadb.service
Et copier les fichiers de la base de données dans le nouvel emplacement :
cp -a /var/lib/mysql/ /srv/medshake/db/
Si l'opération est effectuée sur un serveur qui contient déjà des données de patients, il est fortement recommandé d'effacer proprement des données avec un outil tel que secure-delete
:
apt install secure-delete srm /var/lib/mysql/*
Pour finir il faut désactiver le démarrage automatique du service :
systemctl disable mariadb.service
Le service mariadb.service
ne doit pas être démarré automatiquement car les fichiers de la base de données situés dans /srv/medshake/db/
ne seront pas accessibles au démarrage vu que le volume chiffré est monté après la saisie de la clé de déchiffrement par l'utilisateur.
Étape 3 : Configuration de l'accès web
Configuration de *php-fpm*
Pool php-fpm dédié pour l'application MedShake :
- /etc/php/7.3/fpm/pool.d/medshake.conf
[medshake] user = www-data group = www-data chdir = /srv/medshake/EHR/ listen = /run/php/php-fpm-medshake.sock listen.owner = www-data listen.group = www-data pm = dynamic pm.max_children = 5 pm.start_servers = 2 pm.min_spare_servers = 1 pm.max_spare_servers = 3 php_admin_value[max_input_vars] = 20000 php_admin_value[upload_max_filesize] = 40M php_admin_value[post_max_size] = 40M php_admin_value[max_execution_time] = 300 env[MEDSHAKEEHRPATH] = '/srv/medshake/EHR/' env[MEDSHAKEEHRLOGFILE] = '/var/log/apache2/medshake.log
Redémarrer le service *php-fpm* :
systemctl restart php7.3-fpm.service
Configuration *Apache*
Nous avons besoin d'utiliser une configuration Apache alternative pour MedShake utilisant php-fpm et renvoyant vers la page de déchiffrage du mot de passe si la partition de données pour MedShake n'est pas accessible :
- /etc/apache2/sites-enabled/medshake.conf
# Redirige automatiquement tout le trafic http vers https # (Ne pas autoriser le trafic en claire) <VirtualHost *:80> # /!\ Remplacer "mon-medshake.fr" par le domaine utilisé ServerName mon-medshake.fr ServerAdmin admin@mon-medshake.fr RedirectMatch permanent ^(.*)$ https://mon-medshake.fr/$1 </VirtualHost> # Trafic https <VirtualHost *:443> # /!\ Remplacer "mon-medshake.fr" par le domaine utilisé ServerName mon-medshake.fr ServerAdmin admin@mon-medshake.fr # # Configuration SSL # # Charge le Module SSL SSLEngine On # /!\ Adapter en fonction du cetificat utilisé SSLCertificateFile /etc/ssl/certs/ssl-cert-snakeoil.pem SSLCertificateKeyFile /etc/ssl/private/ssl-cert-snakeoil.key # Ajoute l'entête http pour le HSTS Header always set Strict-Transport-Security "max-age=0; includeSubDomains" # Fichier de log dédié avec un format compatible avec le lecteur de log intégré # à Medshake CookieName apacheLogUserID CookieDomain .mon-medshake.fr Define MEDSHAKEEHRLOGFILE /var/log/apache2/medshake.log SetEnv MEDSHAKEEHRLOGFILE ${MEDSHAKEEHRLOGFILE} LogFormat "%{%Y-%m-%d %H:%M:%S}t %{c}a %r %{Cookie}n" usertrack SetEnvIf Request_URI "\.png$|\.gif$|\.jpg$|\.svg$|\.js$|\.css$|\.map$|\.ico$|\.woff2" do_not_log CustomLog ${MEDSHAKEEHRLOGFILE} usertrack env=!do_not_log # Charge le module de réécriture de d'url RewriteEngine On DocumentRoot /srv/medshake/EHR/public_html/ DirectoryIndex index.php <Directory /srv/medshake/EHR/public_html/> # uniquement les clients du réseau local (modfier 192.168.1.0/24 en fonction # de la configuration de votre réseau local) Require ip 192.168.1.0/24 Require local </Directory> # # Si le fichier /srv/medshake/.uncrypted n'existe pas c'est que les données # MedShake n'ont pas été déchiffrés # On redirige donc vers la page qui permet la saisie de la clé de déchiffrement # RewriteCond /srv/medshake/.uncrypted !-f RewriteRule ^(.*)$ unix:/run/php/php-fpm-medshake.sock|fcgi://localhost/usr/local/lib/medshake/www/index.php [P,L] # Passe tous les fichers php existants à php-fpm RewriteCond /srv/medshake/.uncrypted -f RewriteCond %{DOCUMENT_ROOT}/%{REQUEST_URI} -f RewriteRule ^(.+\.php)$ unix:/run/php/php-fpm-medshake.sock|fcgi://localhost/%{DOCUMENT_ROOT}/$1 [P] # Route /public/ au script public.php RewriteCond /srv/medshake/.uncrypted -f RewriteCond %{DOCUMENT_ROOT}/%{REQUEST_URI} !-f RewriteRule ^(/public/)(.*)$ unix:/run/php/php-fpm-medshake.sock|fcgi://localhost/%{DOCUMENT_ROOT}/public.php [P] # Route /phonecapture/ à phonecapture.php RewriteCond /srv/medshake/.uncrypted -f RewriteCond %{DOCUMENT_ROOT}/%{REQUEST_URI} !-f RewriteRule ^(/phonecapture/)(.*)$ unix:/run/php/php-fpm-medshake.sock|fcgi://localhost/%{DOCUMENT_ROOT}/phonecapture.php [P] # Route toutes les autres destinations où le fichier n'existe pas à index.php RewriteCond /srv/medshake/.uncrypted -f RewriteCond %{DOCUMENT_ROOT}/%{REQUEST_URI} !-f RewriteRule ^(.*)$ unix:/run/php/php-fpm-medshake.sock|fcgi://localhost/%{DOCUMENT_ROOT}/index.php [P] <Location /> # uniquement les clients du réseau local (modifier 192.168.1.0/24 en fonction # de la configuration de votre réseau local) Require ip 192.168.1.0/24 Require local </Location> <Location ~ /(components/|thirdparty/|scss/|img/|js/|favicon.ico|maintenancePublic.html|maintenance.html)> # Ces ressources peuvent êtres mise en cache Header setifempty Cache-Control "must-revalidate" # Fixe un bug présent dans le version de apache utilisé dans Debian 10(2.4.38) # qui gènère une entête http Etag mal formée et empêche un retrour 304 # quand la ressource est en cache. # voire https://bz.apache.org/bugzilla/show_bug.cgi?id=45023#c22 RequestHeader edit "If-None-Match" '^"((.*)-gzip)"$' '"$1", "$2"' # uniquement les clients du réseau local (modfier 192.168.1.0/24 en fonction # de la configuration de votre réseau local) Require ip 192.168.1.0/24 Require local </Location> # Bloque les accès aux fichiers composer (juste c'est plus propre) <Location ~ /composer.(json|lock)> Require all denied </Location> </VirtualHost>
Étape 4 : Mise en place du code MedShake
Deux cas de figures se présentent :
- Le système possède déjà une installation MedShake.
- Nous effectuons une nouvelle installation MedShake.
Déplacer son installation existante
Admettons que notre instance MedShake se trouve actuellement dans /home/EHR/
, nous déplaçons les fichiers sur notre volume chiffré :
cp -a /home/EHR/ /srv/medshake/EHR/
Ne pas oublier de supprimer des ancienne données de manière sécurisée après leurs déplacements.
srm /home/EHR/
**Ne pas oublier de modifier le fichier de configuration pour MedShake (/srv/medshake/EHR/config/config.yml
) afin de mettre à jour les paramètres (webDirectory
, stockageLocation
, backupLocation
, workingDirectory
, templatesFolder
) avec le nouvel emplacement de l'instance.
Procéder à une nouvelle installation
Utiliser la méthode classique mais la faire dans /srv/medshake/EHR/
.
Étape Finale
Création du fichier ''/srv/medshake/.uncrypted''
Le fichier “témoin” /srv/medshake/.uncrypted̉
est utilisé pour indiquer à Apache si les données chiffrées sont bien accessibles ou non.
echo -e '!!! NE PAS SUPPRIMER !!!\nCe fichier est utilisé pour vérifier que les données MedShake sont bien déchiffrées.' > /srv/medshake/.uncrypted chmod 444 /srv/medshake/.uncrypted
Substitue ''/srv/medshake/EHR/public_html'' quand les données ne sont pas déchiffrés
Au moment où le serveur démarre la partition contentant les données chiffrées, elle n'est pas encore montée dans /srv/medshake/
et donc le dossier /srv/medshake/EHR/public_html/
n'est pas présent or il est définit comme DocumentRoot
dans la configuration *Apache* et celui-ci refusera de démarrer si il ne le trouve pas. Il faut donc créer un dossier de substitution à l'emplacement du vrai avant que les données chiffrées ne soient montées.
Pour commencer il faut démonter la partition des données chiffrés :
# Stopper ces services évite que des fichiers présents sur la partition soient # ouverts ce qui empêcherait le démontage de la partition systemctl stop medshakedb.service systemctl stop php7.3-fpm.service systemctl stop apache2.service umount /srv/medshake
Puis créer le dossier de substitution :
mkdir -p /srv/medshale/EHR/public_html/
Il ne reste plus qu'à redémarrer le serveur et à accéder à son instance MedShake via le navigateur web. Normalement la page permettant de déchiffrer les données devrait se substituer à l'instance MedShake. Une fois la bonne clé de déchiffrement saisie l'instance MedShake devrait être accessible.