Configurer HTTPS avec Nginx, Let's Encrypt et Docker

Comme il s'agit d'une tâche assez commune, j'ai écrit ce post pour vous guider à travers un pas à pas pour protéger votre site internet (et vos utilisateurs) en utilisant HTTPS. La spécificité est que l'on va le faire dans un environnement Docker.

Dans ce post, je vais utiliser Docker Compose pour simplifier le tutoriel et aussi parce que j'aime le concept d'Infrastructure as Code.

Nginx comme serveur

Pour pouvoir utiliser nginx avec Docker comme serveur pour un projet quel qu'il soit, il faut lui créer un conteneur et/ou un service Docker Compose. Docker va gérer le téléchargement de l'image correspondante et toutes les tâches que l'on fait manuellement sans Docker.

version: '3'

services:
  webserver:
    image: nginx:latest
    ports:
      - 80:80
      - 443:443

À n'importe quel moment pendant ce tutoriel, vous pouvez faire un docker compose up pour lancer l'environnement et voir si tout se passe bien.

Nous avons maintenant une installation vierge de nginx qui écoute le port 80 pour HTTP et le port 443 pour HTTPS.

version: '3'

services:
  webserver:
    image: nginx:latest
    ports:
      - 80:80
      - 443:443
    restart: always

Puisque je veux que le serveur soit toujours en marche, je dis à Docker qu'il doit se charger de relancer le service "webserver" s'il venait à s'éteindre inopinément.

version: '3'

services:
  webserver:
    image: nginx:latest
    ports:
      - 80:80
      - 443:443
    restart: always
    volumes:
      - ./nginx/conf/:/etc/nginx/conf.d/:ro

Le but ultime de cette installation n'est pas de servir uniquement la page de bienvenue de nginx. On doit donc mettre à jour la configuration nginx et déclarer notre site internet.

Pour faire cela, on utilise la fonctionnalité de volumes de Docker. Cela veut dire que que l'on relie le dossier localisé à /etc/nginx/conf.d/ du conteneur docker au dossier localisé à ./nginx/conf/ sur notre machine locale. Chaque fichier que nous ajoutons, enlevons ou mettons à jour dans ce dossier local sera mis à jour dans le conteneur aussi.

Vous noterez que j'ai ajouté un :ro à la fin de la déclaration du volume. ro veut dire "read-only" ou "lecture seule" en français. Le conteneur n'aura jamais le droit de mettre à jour un fichier dans ce dossier. Ce n'est pas capital, mais c'est une bonne pratique. Cela permet d'éviter des heures de débogage inutile.

Maintenant, on met à jour le fichier Docker Compose comme suit :

version: '3'

services:
  webserver:
    image: nginx:latest
    ports:
      - 80:80
      - 443:443
    restart: always
    volumes:
      - ./nginx/conf/:/etc/nginx/conf.d/:ro
      - ./certbot/www:/var/www/certbot/:ro

Puis, on va ajouter le fichier de configuration suivant à notre dossier local ./nginx/conf/. N'oubliez pas de le mettre à jour en utilisant vos propres données.

server {
    listen 80;
    listen [::]:80;

    server_name example.org www.example.org;
    server_tokens off;

    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }

    location / {
        return 301 https://example.org$request_uri;
    }
}

La configuration est simple. On explique à nginx qu'il doit écouter le port 80 (soit en IPv4 ou IPv6) pour le nom de domaine spécifique example.org.

Par défaut, on veut rediriger quelqu'un qui arrive sur le port 80 vers la même route sur le port 443. C'est ce qu'on fait avec le bloc location /.

La spécificité ici, c'est l'autre bloc location. Il sert les fichiers dont Certbot à besoin pour authentifier notre serveur et lui créer un certificat HTTPS.

En gros, on dit "redirige toujours vers HTTPS sauf pour la route /.well-know/acme-challenge/".

On peut maintenant recharger nginx en faisant un docker compose restart ou si vous voulez éviter une interruption de service (même quelques secondes), rechargez-le directement dans le conteneur avec docker compose exec webserver nginx -s reload.

Créer le certificat en utilisant Certbot

Pour l'instant, rien ne sera affiché parce que nginx continue à rediriger vers le port 443 qui n'est pas encore géré par nginx. Mais c'est normal. On veut seulement que Certbot puisse authentifier notre serveur.

Pour ce faire, on va utiliser l'image docker de Certbot et l'ajouter comme un service à notre projet Docker Compose.

version: '3'

services:
  webserver:
    image: nginx:latest
    ports:
      - 80:80
      - 443:443
    restart: always
    volumes:
      - ./nginx/conf/:/etc/nginx/conf.d/:ro
      - ./certbot/www:/var/www/certbot/:ro
  certbot:
    image: certbot/certbot:latest
    volumes:
      - ./certbot/www/:/var/www/certbot/:rw

Maintenant on a deux services, un pour nginx et un pour Certbot. Vous avez peut-être remarqué qu'ils ont tous les deux déclaré le même volume. Cela leur permettra de communiquer ensemble.

Certbot va écrire ses fichiers dans ./certbot/www/ et nginx les servira sur le port 80 à chaque utilisateur demandant /.well-know/acme-challenge/. C'est ainsi que le Certbot peut authentifier notre serveur.

Notez que pour le Certbot on a utilisé :rw qui veut dire "read and write" à la fin de la déclaration de volume. Si vous utilisez :ro à la place, il ne pourra pas écrire dans le dossier et l'authentification échouera.

Vous pouvez maintenant tester que tout fonctionne comme il faut en lançant docker compose run --rm certbot certonly --webroot --webroot-path /var/www/certbot/ --dry-run -d example.org. Si vous avez un message comme "The dry run was successful", c'est que tout fonctionne.

Maintenant on peut créer des certificats pour le serveur, on veut qu'ils utilisent nginx pour gérer la sécurité des connexions avec les navigateurs des utilisateurs.

Certbot crée les certificats dans le dossier /etc/letsencrypt/. Même principe que pour le webroot, on va utiliser les volumes pour partager les fichiers entre les conteneurs.

version: '3'

services:
  webserver:
    image: nginx:latest
    ports:
      - 80:80
      - 443:443
    restart: always
    volumes:
      - ./nginx/conf/:/etc/nginx/conf.d/:ro
      - ./certbot/www:/var/www/certbot/:ro
      - ./certbot/conf/:/etc/nginx/ssl/:ro
  certbot:
    image: certbot/certbot:latest
    volumes:
      - ./certbot/www/:/var/www/certbot/:rw
      - ./certbot/conf/:/etc/letsencrypt/:rw

Relancez votre conteneur en utilisant docker compose restart. Nginx devrait maintenant avoir accès au dossier dans lequel Certbot crée les certificats.

Cependant, ce dossier est vide pour le moment. Relancez Certbot sans le flag --dry-run pour remplir le dossier de certificats :

$ docker compose run --rm  certbot certonly --webroot --webroot-path /var/www/certbot/ -d example.org

Maintenant qu'on a les certificats, il nous reste à configurer le port 443 de nginx.

server {
    listen 80;
    listen [::]:80;

    server_name example.org www.example.org;
    server_tokens off;

    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }

    location / {
        return 301 https://example.org$request_uri;
    }
}

server {
    listen 443 default_server ssl http2;
    listen [::]:443 ssl http2;

    server_name example.org;

    ssl_certificate /etc/nginx/ssl/live/example.org/fullchain.pem;
    ssl_certificate_key /etc/nginx/ssl/live/example.org/privkey.pem;
    
    location / {
    	# ...
    }
}

En rechargeant le serveur nginx maintenant, on va sécuriser les connexions en utilisant HTTPS. Nginx utilise les certificats et clés privées des volumes de Certbot.

Renouveler les certificats

Un petit problème que l'on peut avoir avec le Certbot et Let's Encrypt c'est que les certificats ne durent que trois mois. Il faut régulièrement les renouveler si vous ne voulez pas que vos utilisateurs se retrouvent bloqués devant un message effrayant de leur navigateur.

Mais, puisqu’on a cet environnement Docker en place, c'est plus facile de renouveler les certificats de Let's Encrypt !

$ docker compose run --rm certbot renew

Cette petite commande "renew" est suffisante pour que le système continue de fonctionner comme prévu. Vous n'avez besoin d'y penser qu'une fois tous les trois mois. Vous pourriez même automatiser ce process...