Ceci est une ancienne révision du document !
Chiffrement des données Medshake EHR/EDC
Préambule
Proof of concept !
La procédure indiqué ici est à l'état de prototype. Elle n'est pour le moment pas ni mature, ni validé et demande du travail pour être finalisé. Ne l'appliquez en aucuns 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 dure du serveur sur le quel l'instance est hébergée afin protéger les données des patients en cas de vol du disque dure. Un solution efficace et commode et d'utiliser LUKS et de chiffré la totalité des données du disque dure, 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émarrage, ce qui peut s'avérer problématique si il s'agit d'une machine dédié utilisé par plusieurs utilisateurs (n'ayant pas forcément les compétence 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 la page normal d'accès à l'instance MedShake avec une page offrant un formulaire permettant de saisir une clé de déchiffrement des données si on détecte que celle-ci ne sont pas déchiffré. La validation de ce formulaire permet l'exécution d'un script chargé de déchiffrer et monter la partition de donnée si la clé saisis est correcte, puis de rendre la main à Medshake une fois l'opération réussi.
En gros cela cela ce passe comme ceci :
Dans ce cas nous ne chiffrons pas l'entièreté du disque dure mais uniquement une partition qui contiendra toutes les données utilisées par MedShake comprenant :
- Les fichier de l'installation Medshake (on inclus tout : le
homeDirectory
,webDirectory
,storageLocation
,workingDirectory
, le code, etc…). - Les fichier de la base de donné Mysql (le
datadir
).
Pour être réellement efficace il ne faut pas utiliser de partition swap sur le disque dure avec la méthode décrite ici. En effet celle ci peut éventuellement contenir des information 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 serai bien pour 1 à 5 utilisateurs) on peut aussi de jeter un œil du coté 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 des fichier actuel du /tmp pour ne pas qu'il reste présent sur le DD reboot # redémarre l'ordinateur
D'autres approche du problèmes existent comme celle ci : https://opensource.com/article/20/11/nbde-linux qui est plus lourdes et complexe à mettre en place. De plus elle demande une configuration particulière sur les clients. Cette solution se veux une alternative plus simple à mettre en place et plus facilement adaptable (elle ne nécessite pas l'utilisation de d'outils particulière et repose sur une configuration Apache et des script bash et php qui peuvent facilement êtres adaptés au besoins). 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 au documentation déjà existantes :
On considère aussi les point suivant comme étant valide
- 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 peut plus bas).- Les modules Apache2 suivant sont activé (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é dans le
/etc/crypttab
Point à travailler
- Mise en route du compagnon Apicrypt ?
- Service et donné Orthanc ?
- Actuellement les logs Apache ne sont pas sur la partition chiffré ? 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 seul application présente sur le serveur qui utilise la base de donnée MariaDB est MedShake. Pour des raison de simplicité de mise en place nous modifions le datadir
du service “de base” pour le placer sur notre volume chiffré, comme ce 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 éventuel applications.
Cette solution ne fonctionne pas avec le mod_php d'Apache. Quant le script php chargé de proposé la validation de la saisis 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 appel le script bash chargé de monté la partition de données chiffré (/usr/local/lib/medshake/uncrypt.sh
), à l’intérieur de l’environnement du script bash semble tout semble normal (le volume chiffré est bien monté et on peut accéder au données qu'il contient). Par contre pour le reste du système le volume ne semble pas monté (pas d'entré dans le /etc/mtab
et l'emplacement du point de montage reste vide). Je ne trouve pour le moment pas 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 deux petit script :
- Un script php chargé de présenté un formulaire web pour demander aux utilisateurs tentant de se connecter à l'instance MedShake la saisis 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 monté le volume LUKS ainsi que d'effectuer d'autres opération comme démarrer le service MariaDB une fois le datadir accessible.
Commençons par créer un endroit adapté pour y placer nos script :
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" # Non 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 !!! # Non du volume LUKS une fois déverrouillé UNCRYPTED_DEV="meduncrypted" # Emplacement pour le montage de la partition des données déchiffrés UNCRYPTED_MNT_PT="/srv/medshake" # Option de montage supplémentaire pour le périphérique déchiffré UNCRYPTED_MNT_OPT="" # Non du service pour lancer la base de donnée 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'éxiste 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: monte /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émare)" 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érer et placer 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é 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é 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 action 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 essai d'appeler le script alors que les données sont déjà accessible // 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 action suvantes si on fournis la clé de déchiffrage des données if (!empty($_POST['uncryptkey'])) { // Appel le script de bash d'ouverture du contenuer LUKS de montage de montage du volume avec sudo // Requiet un entrées sudoers adapté 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épalcer le ''datadir'' MariaDB
Vu que nous voulons que les fichier de la base de données MariaDB se trouvent sur la partition chiffré 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 fichier de la base de donnée dans le nouvel emplacement :
cp -a /var/lib/mysql/ /srv/medshake/db/
Si l'opération est effectué sur un serveur qui contiens déjà des donné de patient 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 fichier de la base de donnée situé dans /srv/medshake/db/
ne seront pas accessibles au démarrage vu que le volume chiffré est monté après la saisis 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émarer 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ée 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ée MedShake n'ont pas été déchiffrés # On redirige donc vers la page qui permet la saisis 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] # Pass tous les ficher 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 destination ou 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" # Fix 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é et empéche un retrour 304 # quant 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 du supprimer des ancienne données de manière sécurisé après leur déplacements.
srm /home/EHR/
**Ne pas oublier de modifier le fichier de configuration pour MedShake (/srv/medshake/EHR/config/config.yml
) affin 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 Final
Création du fichier ''/srv/medshake/.uncrypted''
Le fichier “témoin” /srv/medshake/.uncrypted̉
est utilisé pour indiqué à Apache si les données chiffrés sont bien accessibles ou non.
echo -e '!!! NE PAS SUPRIMER !!!\nCe fichier est utilisé pour vérifier que les données MedShake sont bien déchiffré.' > /srv/medshake/.uncrypted chmod 444 /srv/medshake/.uncrypted
Substitue ''/srv/medshake/EHR/public_html'' quant les données ne sont pas déchiffrés
Au moment ou le serveur démarre la partition contentant les données chiffrés n'est pas encore monté 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 vrais avant que les données chiffrés ne soient monté.
Pour commencer il faut démonté la partitions des données chiffrés :
# Stoper ces service évite que des fichier présent sur la partition soient ouvert ce qui empêcherai de 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'a 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 saisis l'instance MedShake devrait être accessible.