TP6 : Sécurisation du service web public
Introduction
La semaine passée, nous avons mis en place un service web public composé de deux virtual hosts, dont l’un utilise des pages générées dynamiquement en exploitant des données dans une base de données.
Nous n’avons cependant jusque là pas abordé les aspects sécurité, nous allons donc nous en préoccuper dans le cadre de ce labo.
Trois aspects sont à considérer dans la sécurisation du web :
- La sécurisation du serveur (hardening)
- La sécurisation des données
- La sécurisation des communications (HTTPS)
A la fin de ce TP, vous devez être capable de :
- configurer de manière sécurisée un serveur web
- mettre en place du HTTPS
- sécuriser une base de données
Lectures préalables
Les éléments théoriques liés à ce TP sont présentés dans le chapitre 6 : Gestion et sécurisation d’un service web du support de cours.
Pré-requis pour la réalisation du TP
Avant de pouvoir réaliser ce TP, assurez-vous d’avoir :
- sécurisé votre VPS, notamment par la mise en place de l’authentification par clé SSH (TP3)
- mis en place votre zone DNS, notamment via la délégation. (TP4)
- installé et configurer votre serveur web et votre base de données (TP5)
Environnement de travail et organisation
Dans le cadre de ce TP6, nous travaillerons sur les mêmes fichiers que dans le labo précédent. Assurez-vous d’avoir bien “commité” vos configurations web de base avant de poursuivre le travail, afin de disposer d’un backup fonctionnel en cas de souci.
Ce TP va aborder deux “sous-services” : la DB et le serveur NGINX. N’hésitez pas à vous répartir la tâche : une moitié du groupe peut se pencher sur la partie DB (section 3) et l’autre moitié sur le HTTPS (section 4). Assurez-vous de bien documenter vos réalisations sur le wiki de votre repo afin que chaque membre du groupe puisse s’assurer de bien comprendre ce qui a été fait.
1. Infrastructure du TP5
Pour rappel, suite au TP5, voici l’infrastructure que vous devriez avoir configurée :
Votre fichier compose.yml correspondant à cette architecture devrait ressembler à ceci :
services :
nginx:
build: nginx
ports:
- "80:80"
volumes:
- ./html/:/var/www/html/
mariadb:
image: mariadb:11.1
env_file:
- db/root.env
- db.env
volumes:
- ./db/sql/:/docker-entrypoint-initdb.d/ #MariaDB container automatically loads SQL script at startup
php:
build: php
volumes:
- ./html/:/var/www/html/
env_file:
- db.env
Quelques remarques concernant ce fichier Docker Compose :
- Trois services sont définis :
nginx
,mariadb
etphp
. - Seul le service
nginx
est publié sur les interfaces de l’hôte, et donc accessibles depuis l’extérieur. Les autres containers ne doivent être accessibles que depuis, respectivement, le serveur web pour le containerphp
, et le containermariadb
pour la base de données. - Grâce à l’utilisation de Docker Compose, les services sont automatiquement placé sur un réseau Docker dédié à cette infrastructure. Il ne sont donc pas sur le réseau Docker par défaut.
- Comme lors de la configuration manuelle effectuée dans le cadre du TP5, les fichiers HTML et PHP sont partagés entre le container
nginx
et le containerphp
via l’usage d’un Bind Mount. - Au niveau de la DB, deux éléments supplémentaires ont été rajoutés à des fins d’automatisation :
- L’utilisation de fichiers de variables d’environnement, dont le contenu est donné ci-dessous, et qui permettent de configurer les comptes utilisateurs et la connectivité à la DB. Nous utilisons ainsi à présent un utilisateur
woodytoys
, au lieu du compteroot
utilisé dans le TP5. - L’ajout du script SQL dans le répertoire
/docker-entrypoint-initdb.d/
du container gérant la base de données : le container est prévu pour automatiquement exécuter ces scripts au démarrage, ce qui nous permet de remplir la DB avec un contenu initial.
- L’utilisation de fichiers de variables d’environnement, dont le contenu est donné ci-dessous, et qui permettent de configurer les comptes utilisateurs et la connectivité à la DB. Nous utilisons ainsi à présent un utilisateur
Les fichiers .env
contiennent les informations suivantes :
- Fichier
root.env
:MARIADB_ROOT_PASSWORD= <votre mot de passe root MariaDB>
- Fichier
db.env
:MARIADB_HOST=mariadb MARIADB_DATABASE=woodytoys
Quant au fichier PHP, il a été adapté de sorte de pouvoir exploiter ces variables d’environnement pour la configuration de la connexion à la DB :
$dbname = getenv('MARIADB_DATABASE');
$dbhost = getenv('MARIDB_HOST');
Notez que pour le moment, nous utilisons toujours l’utilisateur root
avec le mot de passe en clair dans le fichier PHP. Nous corrigerons cela prochainement.
Assurez-vous d’avoir un docker-compose fonctionnel similaire à celui-ci avant de commencer le TP6.
2. Sécurisation serveur
La première étape consiste à sécuriser le(s) serveur(s) impliqué(s) dans notre service web, via le hardening, afin de diminuer la surface d’attaque.
Pour rappel, cela consiste entre autres à, pour chaque serveur impliqué :
- mettre à jour régulièrement tous les logiciels et dépendances pour disposer des derniers patchs de sécurité,
- supprimer tout logiciel, service et/ou fonctionnalité inutile,
- s’assurer que seuls les services nécessaires sont à l’écoute sur les ports TCP/UDP,
- mettre en place des outils de protection tels que des firewalls ou Fail2Ban.
Dans notre cas de figure, nous utilisons une couche d’abstraction avec la conteneurisation. Il faut donc appliquer la procédure à plusieurs niveaux :
- au plus haut niveau, à savoir le VPS. Cela est normalement fait depuis le TP3.
- au niveau de la couche intermédiaire (Docker Engine) : assurez-vous d’utiliser une version à jour.
- au niveau des containers :
- Assurez-vous d’utiliser des images à jour (mais évitez la version taggée
latest
, qui pourrait manquer de stabilité pour un usage en production). - Vérifiez l’origine des images utilisées (certains outils existent pour en vérifier les vulnérabilités).
- Assurez vous de ne publier de ports qu’en cas d’absolue nécessité. Dans notre cas, seul le serveur
nginx
a besoin d’être contacté depuis l’extérieur. Ladb
et le containerphp
doivent rester isolés.
- Assurez-vous d’utiliser des images à jour (mais évitez la version taggée
- au niveau des configurations des services à l’intérieur des containers : n’utilisez jamais de configuration “à l’aveugle” : vous devez comprendre chaque ligne de configuration utiliée et pouvoir justifier son utilité.
- au niveau du code applicatif : dans le cas du web, de nombreuses vulnérabilités sont liées au code, donnant lieu par exemple aux attaques par injection. Les menaces et contre-mesures spécifiques à cette thématiques seront explorées dans le cours Dev III.
- Vérifiez que la sécurisation de votre VPS est assurée (cfr TP3)
- Vérifiez que vous utilisez des images officielles et exemptes de vulnérabiltiés, en évitant le tag
latest
- Vérifiez via
netstat
,nmap
etdocker ps
que seuls les ports nécessaires sont publiés et accessibles depuis l’extérieur de votre VPS (au stade actuel : uniquement le port TCP 80 pour le service web)
3. Sécurisation des données
Les données sont la plupart du temps un des assets les plus importants d’une entreprise. Leur intégrité, leur confidentialité et leur disponibilité doivent être particulièrement protégées. Notre infrastructure actuelle mérite quelques contre-mesures supplémentaires à ce niveau.
Dans notre cas pratique, nous avons une base de données qui est destinée à recevoir de nombreuses données essentielles pour l’entreprise : l’état des stocks, les commandes des clients, les coordonnées de ces derniers, etc. Cette DB n’a besoin d’être contactée que pour deux raisons :
- par le serveur
php
, pour la récupération des données, - et éventuellement par un administrateur pour des raisons de maintenance.
Le principe du moindre privilège nous dicte donc d’empêcher toute connexion autre que ces deux là. Pour cela, plusieurs options sont possibles :
- Empêcher l’accès réseau par d’autres containers par la ségrégation dans un subnet dédié
- Configurer la base de données pour limiter les connexions à des IPs et utilisateurs spécifiques.
(Notez que ces principes s’appliquent de manière similaire dans une situation non conteneurisée. )
3.1. Isolation de la base de données
Actuellement, nos trois containers sont regroupés dans un même subnet. Cela signifie qu’en cas de compromission du serveur web, un pirate disposerait d’un accès réseau immédiat à la DB. Nous pouvons lui compliquer la tâche en séparant le serveur nginx
de la db
, en les plaçant dans deux subnets différents.
- Créez, dans votre fichier Compose, deux réseaux Docker :
dmz
etdb_net
et connectez-y les containers de manière appropriée. Notez que grâce à la résolution de noms Docker, nous n’avons pas besoin de modifier d’adresse IP dans nos fichiers de configuration!- Vérifiez à l’aide de
ping
la connectivité entre le containernginx
et le containerphp
, celle entre le containerphp
et la db, ainsi que la non-connectivité entre lenginx
et la DB.
3.2. Configuration d’un utilisateur non privilégié
En sécurité, le principe du moindre privilège consiste à ne donner que les accès minimaux requis pour le fonctionnement d’un service. Notre configuration initiale de l’accès Web/DB ne respecte pas ce principe : le script PHP utilise l’utilisateur root
pour se connecter au SGBD, alors qu’il n’a besoin que d’un accès en lecture à la table Products
de la DB woodytoys
! Nous allons à présent travailler avec un autre utilisateur, dont nous ajusterons les privilèges en fonction des besoins.
Pour définir dans le SGBDP MariaDB un nouvel utilisateur et limiter ses accès, on utilisera ces deux instructions ;
CREATE USER 'wt-user'@'php' IDENTIFIED BY 'wt-pwd';
GRANT SELECT ON `woodytoys`.* TO 'wt-user'@'php';
Notez que lors de la création de l’utilisateur, on peut spécifier le nom ou l’IP de l’hôte depuis lequel cet utilisateur peut contacter la DB. Ici, on va s’assurer que cet utilisateur ne peut effectuer de requêtes que depuis le container PHP
.
Attention : pour pouvoir utiliser le nom du container php
, il faudra autoriser la résolution de noms dans la configuration mariadb
:
[mariadb]
disable-skip-name-resolve=1
Au niveau des accès, il ne doit avoir qu’un accès en lecture à la table woodytoys
afin de pouvoir afficher la liste des produits. On ne lui accordera donc que la possibilité d’effectuer des SELECT
.
- Ajoutez la création et la définition des droits d’accès de cet utilisateur dans le script SQL lancé au démarrage du container
mariadb
- Définissez les variables d’environnement
MARIADB_USER
etMARIADB_PASSWORD
dans votre fichierdb.env
, puis modifiez le script PHP pour qu’il utilise ces valeurs au lieu de l’utilisateur et du mot de passeroot
- Réactivez la résolution de nom dans MariaDB en ajoutant les lignes de configuration dans un fichier
my-resolve.cnf
, que vous monterez via un bind mount dans le container sur le fichier/etc/mysql/conf.d/my-resolve.cnf
Nous avons ici utilisé des variables d’environnement pour transmettre les paramètres d’authentification. Néanmoins, ces derniers se retrouvent en clair dans :
- les fichiers
.env
- le fichier contenant la configuration et l’insertion de données dans la DB (
woodytoys.sql
) De plus, les variables d’environnement restent visibles via les commandes Docker. Testez par exempledocker inspect mariadb
, et observez la section Config -> Env : les variables d’environnements sont visibles en clair, en ce compris le mot de passe root de la DB! Pour mitiger ce problème d’informations de configuration sensibles, Docker Compose propose une manière de gérer les secrets, mais nous ne les explorerons pas dans le cadre de ce TP.
3.3. Backup de la DB
La prévention contre l’indisponibilité des données passe par la mise en place d’une solution de backup. Dans le cas de notre infrastructure Docker, on peut mettre deux choses en place :
- Garantir la persistance des données en plaçant les fichiers de la DB dans un volume. Les données resteront donc stockées sur l’hôte même en cas de suppression du container.
- Il est également possible d’exporter les données depuis le container vers un fichier sur l’hôte, à des fins de backup, tel que conseillé dans la documentation de l’image Docker MariaDB :
$ docker exec <container-name> mariadb-dump --all-databases -uroot -p"$MARIADB_ROOT_PASSWORD"' > /some/path/on/your/host/all-databases.sql
- Ajoutez un volume pour le stockage des données MariaBD. Ces données se trouvent dans le répertoire
/var/lib/mysql
du container.- Testez la commande
mariadb-dump
, et examinez le fichier obtenu.
Dans notre cas, le contenu de la base de données n’évolue pas, mais il est évident que dans un cas plus concret, des sauvegardes régulières s’imposent. Vous pouvez tester cela en l’automatisant par un cron job depuis le VPS. Pensez à compresser les fichiers de sauvegarde, et assurez-vous qu’ils ne saturent pas le disque.
Notez qu’il existe également un système de backup basé sur un service parallèle : MariaDB Backup. Une image Docker existe également pour ce service : mariadb-backup
. Néanmoins, ce système sort du cadre de ce cours.
3.4. Logs de la DB
Pour s’assurer du bon fonctionnement de la DB et pouvoir identifier les problèmes en cas de dysfonctionnement, il faut également réfléchir à la manière dont les logs vont être collectés. Grâce à Docker Compose et à l’utilisation de l’image Docker officielle de MariaDB, les logs de la DB sont automatiquement disponible depuis la ligne de commande (stdout et via docker logs
).
4. Sécurisation des communications avec HTTPS
Nous allons à présent nous assurer de la confidentialité des échanges de données entre client et serveur, tout en garantissant l’authentification de notre serveur web auprès des clients. Cela va se faire en configurant le HTTPS et en utilisant des certificats pour valider les clés publiques utilisées pour le chiffrement.
Nous utiliserons l’autorité de certification Let’s Encrypt, via l’outil certbot
, qui est disponible notamment sous forme de container. La documentation correspondante est disponible ici.
4.1. HTTPS via un certificat auto-signé
4.1.1. Génération du certificat auto-signé avec OpenSSL
Dans un premier temps, nous allons générer un certificat que nous allons signer nous-mêmes grâce à notre clé privée. Cela permet de ne pas dépendre d’une autorité de certification.
Pour créer un certificat, trois étapes sont nécessaires :
- Créer une paire de clés cryptographiques (format .key)
- Générer une demande de certificat (format CSR - Certificate Signing Request)
- Faire signer (ou auto-signer dans ce cas-ci) le certificat. Le résultat sera au format CRT (fichier de certificat).
Les étapes 1 et 2 peuvent se réaliser en une fois avec la commande OpenSSL
suivante :
sudo openssl req -nodes -newkey rsa:4096 -keyout certificate/nginx-selfsigned.key -out certificate/nginx-selfsigned.csr
Les options utilisées sont :
req
indique que l’on souhaite obtenir une demande de certificat (CSR)- l’option
nodes
demande de ne pas chiffrer la clé privée avec DES (ce qui évite la configuration d’une passphrase, inutilisable parnginx
) newkey
indique qu’on souhaite créer une nouvelle clé pour cette demande de certificat (on aurait pu créer le certificat au départ d’une paire de clés existante)keyout
permet d’indiquer le nom du fichier qui contiendra la cléout
indique le fichier contenant la demande de certificat.
La commande demande l’encodage des informations nécessaires pour la construction du certificat. L’élément le plus important est bien entendu le FQDN, qui doit correspondre à celui de l’hôte qui sera protégé par le certificat.
Il est possible de visualiser les clés et certificats produits :
sudo openssl rsa -noout -text -in certificate/nginx-selfsigned.key
sudo openssl req -noout -text -in certificate/nginx-selfsigned.csr
Pour auto-signer le certificat, on utilisera la commande suivante :
sudo openssl x509 -signkey certificate/nginx-selfsigned.key -in certificate/nginx-selfsigned.csr -req -days 365 -out certificate/nginx-selfsigned.crt
Et pour visualiser le certificat obtenu :
openssl x509 -noout -text -in certificate/nginx-selfsigned.crt
On peut constater qu’il est effectivement auto-signé, puisque le champ “Issuer” est identique au champ “subject”.
Sur votre VPS, générez avec OpenSSL un certificat auto-signé pour votre site web
www.lx-y.ephec-ti.be.
. Ensuite, testez votre site et vérifiez s’il est accessible en HTTPS.
Que pense votre navigateur du certificat utilisé ? Pourquoi ? Expliquez la problématique qui est ici mise en évidence.
4.1.2. Configuration de Nginx en HTTPS pour le virtualhost www
Trois éléments doivent être configurés pour permettre l’HTTPS pour le virtualhost www
:
- La redirection de tout le trafic du port 80 vers le port 443 :
server { listen 80; server_name www.lx-y.ephec-ti.be.; return 301 https://$host$request_uri; }
- Une seconde configuration pour le virtualhost, cette fois sur le port 443 :
server { listen 443 ssl; server_name www.lx-y.ephec-ti.be.; ... }
- L’emplacement de la clé privée et du certificat au niveau du bloc du port 443 :
ssl_certificate /etc/ssl/certs/nginx-selfsigned.crt; ssl_certificate_key /etc/ssl/private/nginx-selfsigned.key;
Configurez votre serveur NGINX pour qu’il sécurise le traffic sur le site www
avec un certificat auto-signé.
4.2. Obtention d’un certificat Let’s Encrypt pour le site www
La communication vers notre service web est à présent sécurisée, mais le certificat utilisé n’est pas considéré comme fiable par les navigateurs, et à raison. Nous allons corriger ça en faisant signer cette fois notre certificat non pas par notre propre clé privée, mais par la clé privée d’une autorité de certification reconnue, Let’s Encrypt. Ce CA permet l’acquisition (et le renouvellement) automatisée d’un certificat signé grâce au protocole ACME et à l’outil certbot
.
Nous utiliserons l’outil certbot
directement sur notre container nginx
. Une alternative aurait été d’utiliser le container Docker certbot
, mais ce n’est pas la procédure recommandée.
L’outil certbot
va effectuer les étapes suivantes pour générer une demande de certificat et obtenir le certificat signé auprès de l’autorité de certification :
- Générer les clés publique et privée
- Générer la demande de certificat et l’envoyer au CA
- Récupérer une liste de challenges (voir : https://letsencrypt.org/docs/challenge-types/) contenant un token unique
- Répondre à un de ces challenges afin de prouver la légitimité sur le certificat. Ce challenge consiste en général à publier le token fourni par l’autorité de certification sur une URL données du serveur web (challenge
http-01
), ou à insérer un record TXT contenant ce token dans la zone DNS (challengedns-01
). - Une fois le challenge rempli, récupérer le certificat signé et modifier la configuration
nginx
pour que ce dernier en tienne compte.
Configurer votre site en HTTPS avec un certificat signé par Let’s Encrypt en effectuant les opérations suivantes dans votre container
nginx
:
- Installez
certbot
et son modulenginx
:apt install certbot python3-certbot-nginx
- Pour obtenir vos certificats : Lancez la commande
certbot --nginx -d www.lx-y.ephec-ti.be
.
- Examinez les logs dans le fichier
/var/log/letsencrypt/letsencrypt.log
- Trouvez les trois challenges ACME proposés par let’s encrypt, et le token utilisé
- Trouvez la configuration nginx temporaire utilisée par certbot pour répondre au challenge
- Quelle est l’URL où se trouve le token sur votre serveur nginx?
- Voyez-vous le certificat reçu? De combien de parties se compose-t-il?
- Où sont stockés les fichiers du certificat et de la clé privée générées par
certbot
?- Vérifiez votre configuration
nginx
: qu’est ce qui a changé?- Vérifiez si votre site web possède à présent un certificat signé par Let’s Encrypt et s’il est accepté par votre navigateur
- Examinez le certificat reçu avec l’outil OpenSSL, et identifiez les champs indiquant la signature du CA.
- Vérifiez également le statut HTTPS du second Virtual Host :
blog.lx-y.ephec-ti.be
. Que se passe-t-il? Comment pouvez-vous corriger ça ?
Vous avez pu noter qu’un certificat a une date de péremption. Pour le renouveler, on peut utiliser la commande certbot renew
.
Actuellement, nous avons créé nos certificats “à la main”. Nous pourrions optimiser cette procédure pour l’adapter à notre environnement Docker : automatisation de certaines étapes, accès aux certificats générés, …
On pourrait par exemple imaginer automatiser l’obtention des certificats dans le Dockerfile, mais les implications sont complexes, et il faut notamment tenir compte d’un taux limité de requêtes de certificats auprès de Let’s Encrypt. Nous allons donc nous contenter dans ce cours de faire les demandes et renouvellement à la main, grâce à des docker compose exec
. Facilitons nous néanmoins un peu la tâche pour la suite des TPs avec les manipulations suivantes :
- Déplacez les instructions d’installation de
certbot
dans le Dockerfile de votre containernginx
- Assurez-vous d’avoir le répertoire
/etc/letsencrypt/
monté sur un volume (bind mount pour pouvoir examiner les fichiers produits), afin de rendre vos certificats persistants et de pouvoir y accéder en dehors du containernginx
.
4.3. Obtention manuelle d’un certificat pour le domaine
Actuellement, nous avons vu comment obtenir un certificat pour un domaine spécifique, ou plusieurs domaines. Dans certains cas, il peut être intéressant d’obtenir un certificat pour l’ensemble d’un domaine. Dans notre cas, cela signifie qu’on aimerait disposer d’un certificat couvrant *.lx-y.ephec-ti.be
, avec l’astérisque indiquant que n’importe quel nom de domaine est inclut (donc pour nous, les hôtes www
et blog
). On appelle un tel certificat un certificat wildcard.
Pour obtenir un tel certificat avec Let’s Encrypt, il faut utiliser le challenge DNS et non plus le challenge HTTP. En effet, il faut prouver qu’on a bien le contrôle sur le domaine, et plus simplement sur un ou plusieurs hôtes spécifiques. Le challenge consistera donc à insérer un record DNS dans la zone concernée.
L’outil certbot
peut à nouveau nous aider, cette fois en mode manuel : il s’occupera d’aller récupérer le certificat une fois le challenge réussi, mais ne changera pas la configuration nginx
.
- Obtenez votre certificat wildcard en exécutant la commande suivante sur votre container
nginx
:certbot certonly --manual --preferred-challenges=dns --email \<votre email\> --agree-tos -d \*.lx-y.ephec-ti.be
.- Le programme
certbot
s’interrompt après vous avoir donné le token à placer dans le record TXT spécifié (\_acme-challenge.lx-y.ephec-ti.be
). Editez votre fichier de zone DNS pour le rendre disponible pour l’autorité de certification.- Vérifiez la disponibilité du record TXT avec un
dig
. S’il vous permet d’obtenir le token, vous pouvez continuer l’exécution decertbot
. Si le challenge réussit, ce dernier vous indiquera où se trouvent les certificat.- Vérifiez le certificat obtenu avec OpenSSL (cfr plus haut)
- Changez votre configuration
nginx
pour que les certificats soient définis dans le contexte général et plus dans les contextes des Virtual Hosts. Adaptez les noms de fichiers des certificats pour que le certificat wildcard soit utilisé.- Vérifiez que le HTTPS fonctionne bien pour vos deux Virtual Hosts.
4.4. Révocation de certificat
Lorsque des changements interviennent dans la gestion d’une entreprise qui entraînent la modification d’informations du certificat, ou lorsque la clé privée de ce dernier est compromise, il faut pouvoir signaler à l’autorité de certification qu’un certificat n’est plus valide avant la fin de sa période de validité, afin d’éviter toute utilisation frauduleuse. On parle alors de révocation de certificat. L’autorité de certificat ajoute alors le certificat à sa liste de certificats révoqués (CRL).
Dans le cas de certificats Let’s Encrypt, la commande cerbot
nous permet à nouveau d’effectuer facilement l’opération :
certbot revoke --cert-name example.com --reason keycompromise