369 lines
17 KiB
Markdown
369 lines
17 KiB
Markdown
|
---
|
||
|
title: "Comment faire du HTTPS chez soi (quand son infrastructure est privée)"
|
||
|
date: 2024-07-02T21:00:50+02:00
|
||
|
draft: true
|
||
|
toc: true
|
||
|
images:
|
||
|
tags:
|
||
|
- self-hosting
|
||
|
- sysadmin
|
||
|
---
|
||
|
|
||
|
## Le problème quand on a une infrastructure chez soi
|
||
|
|
||
|
Cela fait plusieurs années que je maintiens ma propre infrastructure chez
|
||
|
moi, mais l'un des problèmes les plus pénibles lorsque l'on commence ce
|
||
|
type de projet est la fameuse page **Attention: risque de sécurité** qui
|
||
|
apparaît lorsque l'on utilise un certificat autosigné, ou lorsque l'on essaye
|
||
|
d'utiliser un mot de passe sur un site web ou une application servie uniquement
|
||
|
via HTTP.
|
||
|
|
||
|
![Une capture d'écran de la page Firefox indiquant que le site web auquel on essaye d'accéder n'est pas sécurisé.](/images/dns_article_firefox_warning.png)
|
||
|
|
||
|
Si on peut accepter cela si on est seul·e sur son infrastructure, ou son
|
||
|
environnement de dev, cela commence à poser des problèmes dans d'autres contextes :
|
||
|
|
||
|
- Ce n'est pas acceptable d'exposer publiquement un site web présentant ce problème
|
||
|
- Cela paraît douteux de conseiller à ses ami·es ou fanille "t'inquiète, je sais que
|
||
|
ton navigateur te montre un gros avertissement en rouge là, mais tu peux accepter".
|
||
|
C'est juste une très mauvaise habitude à avoir
|
||
|
- Au bout d'un moment c'est vraiment pénible de voir cette page à chaque fois
|
||
|
|
||
|
Heureusement, il y a une solution pour cela. Vous la connaissez sûrement déjà, et cela
|
||
|
fait maintenant presque dix (10) ans que cela existe, c'est
|
||
|
[Let's Encrypt, et le protocole ACME](https://letsencrypt.org/).
|
||
|
|
||
|
{{< callout type="note" >}}
|
||
|
Je vous jure que ce n'est pas encore un nouveau tuto Let's Encrypt. Enfin, en un sens
|
||
|
c'est le cas, mais c'est un peu plus spécifique ici.
|
||
|
{{< /callout >}}
|
||
|
|
||
|
## La solution Let's Encrypt
|
||
|
|
||
|
### Let's Encrypt, c'est quoi?
|
||
|
|
||
|
[Let's Encrypt](https://letsencrypt.org/) est une autorité de certification non-lucrative
|
||
|
fondée en novembre 2014. Son objectif principal était de proposer une façon facile, et
|
||
|
gratuite d'obtenir un certificat TLS afin de rendre facile le HTTPS partout sur le Web.
|
||
|
|
||
|
Le [protocole ACME](https://letsencrypt.org/docs/client-options/), développé par Let's
|
||
|
Encrypt, est un système de vérification automatique visant à faire la chose suivante :
|
||
|
|
||
|
- vérifier que l'on possède le domaine pour lequel on veut obtenir un certificat
|
||
|
- créer et enregistrer ce certificat
|
||
|
- délivrer le certificat à l'utilisateur
|
||
|
|
||
|
La majorité des implémentations client de ce protocole proposent également un système
|
||
|
automatisé de renouvellement, réduisant davantage la charge de travail pour les
|
||
|
administrateur·ices système.
|
||
|
|
||
|
Les spécifications actuelles du protocol ACME offrent le choix entre deux (2) types de
|
||
|
challenge afin de prouver le contrôle sur le domaine : [HTTP-01](https://letsencrypt.org/docs/challenge-types/#http-01-challenge) et [DNS-01](https://letsencrypt.org/docs/challenge-types/#dns-01-challenge).
|
||
|
|
||
|
{{< callout type=note >}}
|
||
|
En vérité, il existe deux (2) autres types de challenges : [TLS-SNI-01](https://letsencrypt.org/docs/challenge-types/#tls-sni-01) aujourd'hui déprécié et désactivé, ainsi que [TLS-ALPN-01](https://letsencrypt.org/docs/challenge-types/#tls-alpn-01).
|
||
|
Ce dernier s'adresse à un public très spécifique, et nous allons donc complètement l'ignorer
|
||
|
pour le moment.
|
||
|
{{< /callout >}}
|
||
|
|
||
|
### La solution habituelle : le challenge HTTP
|
||
|
|
||
|
Le challenge [HTTP-01](https://letsencrypt.org/docs/challenge-types/#http-01-challenge)
|
||
|
est le type de challenge ACME le plus courant. Il est largement suffisant pour la majorité
|
||
|
des cas d'usage.
|
||
|
|
||
|
![Un schema décrivant le fonctionnement du challenge HTTP challenge pour le protocole ACME et les interactions entre le serveur applicatif, Let's Encrypt, et le serveur DNS, tous publics.](/images/dns_article_http_challenge.svg)
|
||
|
|
||
|
Pour ce challenge, on a besoin des éléments suivants :
|
||
|
|
||
|
- Un nom de domaine et un enregistrement pour ce domaine dans un serveur DNS public
|
||
|
(cela peut être un serveur DNS self-hosted, celui du provider DNS, etc)
|
||
|
- Un accès à un serveur avec une adresse IP publiquement accessible
|
||
|
|
||
|
Ce type de challenge se déroule de la façon suivante (de façon schématisée et simplifiée)
|
||
|
|
||
|
1. Le client ACME contacte l'API Let's Encrypt afin de commencer le challenge
|
||
|
2. Il obtient un token
|
||
|
3. Ensuite, il va démarrer un serveur dédié, ou éditer la configuration du serveur Web actuel
|
||
|
(nginx, apache, etc) afin de serveur un fichier contenant le token et l'empreinte de la clé
|
||
|
de notre compte Let's Encrypt
|
||
|
4. Let's Encrypt va ensuite essayer de résoudre notre domaine `test.example.com`
|
||
|
5. Si la résolution fonctionne, Let's Encrypt va accéder à l'URL `http://test.example.com/.well-known/acme-challange/<TOKEN>` et vérifier que le fichier généré à l'étape 3 est bien servi avec le
|
||
|
contenu attendu
|
||
|
|
||
|
Si tout se déroule comme prévu, alors le client ACME peut télécharger le certificat et sa clé,
|
||
|
et on peut configurer notre reverse-proxy ou serveur pour utiliser ce certificat, et c'est bon.
|
||
|
|
||
|
{{< callout type=help >}}
|
||
|
Ok, super, mais mon app c'est l'interface de management de mon serveur Proxmox, et franchement
|
||
|
je ne veux vraiment pas la rendre accessible publiquement, du coup ça marchera comment ?
|
||
|
{{< /callout >}}
|
||
|
|
||
|
Dans ce cas, ce type de challenge ne fonctionnera tout simplement pas. En effet, pour ce type
|
||
|
de challenge, le serveur applicatif **doit** être public. Ce type de challenge cherche à montrer
|
||
|
que l'on a le contrôle sur la destination ciblée par le domaine (même si on n'a pas le contrôle
|
||
|
sur le domaine lui-même). En revanche, le challenge DNS-01 permet de répondre à ce besoin.
|
||
|
|
||
|
### Si cela ne suffit pas : le challenge DNS
|
||
|
|
||
|
Comme on l'a dit précédemment, parfois, pour diverses raisons, le serveur applicatif se trouve
|
||
|
dans une zone privée. Il ne doit être accessible que depuis un réseau privée, mais on aimerait
|
||
|
quand même pouvoir avoir recours à un certificat gratuit Let's Encrypt
|
||
|
|
||
|
Dans cet objectif, il existe le challenge [DNS-01](https://letsencrypt.org/docs/challenge-types/#dns-01-challenge), dont l'objectif est de prouver le contrôle du **serveur DNS** (ou
|
||
|
plutôt de la zone DNS), et non du serveur applicatif.
|
||
|
|
||
|
![Un schema décrivant le fonctionnement du challenge DNS pour le protocole ACME ainsi que les interactions entre Let's Encrypt, le serveur DNS public et le serveur applicatif privé](/images/dns_article_dns_challenge_1.svg)
|
||
|
|
||
|
Pour ce type de challenge, les éléments suivants sont nécessaires :
|
||
|
|
||
|
- Un serveur DNS public dont on a le contrôle (ici encore, il peut s'agit d'un serveur self-hosted,
|
||
|
de l'interface de notre provider, etc)
|
||
|
- Un client ACME qui n'a pas besoin d'être sur une machine publiquement accessible
|
||
|
|
||
|
Ensuite, le challenge se déroule de la façon suivante :
|
||
|
|
||
|
1. Le client ACME demande à l'API Let's Encrypt de démarrer le challenge
|
||
|
2. Le client obtient un token en retour
|
||
|
3. Le client crée ensuite un enregistrement `TXT` sur `_acme-challenge.test.example.com`, dont
|
||
|
la valeur est dérivée du token et de la clé utilisateur Let's Encrypt
|
||
|
4. Let's Encrypt essaye de résoudre l'enregistrement `TXT` en question, et vérifie que le contenu
|
||
|
est correct.
|
||
|
|
||
|
Si la vérification se déroule correctement, il est ensuite possible de télécharger le certificat
|
||
|
et la clé associée, comme pour le type de challenge précédent.
|
||
|
|
||
|
Il faut noter que **à aucun moment Let's Encrypt n'a eu besoin d'avoir accès au serveur de l'application**
|
||
|
car ce challenge a pour objectif de montrer que l'on contrôle le domaine, pas que l'on contrôle
|
||
|
la destination de ce domaine.
|
||
|
|
||
|
Si je veux obtenir un certificat valide et vérifiable pour mon interface Proxmox, c'est ce type
|
||
|
de challenge que je vais vouloir utiliser. En effet, cela me permet d'obtenir mon certificat
|
||
|
valide sans pour autant rendre public mon serveur Proxmox. Voyons-voir comment cela fonctionne
|
||
|
en pratique.
|
||
|
|
||
|
## Comment faire un challenge DNS en pratique
|
||
|
|
||
|
Dans le cadre de cet example, je vais essayer d'obtenir un certificat pour mon propre domaine
|
||
|
`test.internal.example.com`. Comme ce nom le suggère, il s'agit d'un domaine interne qui ne
|
||
|
devrait pas être publiquement accessible, ce qui signifie que je vais utiliser un challenge
|
||
|
DNS. Je ne veux pas vraiment utiliser l'API de mon provider DNS pour cela, et je vais donc
|
||
|
me reposer sur un serveur [bind](https://www.isc.org/bind/) self-hosted.
|
||
|
|
||
|
### Configuration du serveur DNS
|
||
|
|
||
|
La première étape est de configurer un serveur DNS. Pour cela, je vais simplement utiliser
|
||
|
un server [bind](https://bind9.readthedocs.io/en/v9.18.27/) installé depuis mon gestionnaire
|
||
|
de paquets habituel.
|
||
|
|
||
|
```bash
|
||
|
# exemple pour Debian 12
|
||
|
sudo apt install bind9
|
||
|
```
|
||
|
|
||
|
La majorité de la configuration se produit dans le dossier `/etc/bind/`, et principalement
|
||
|
dans le fichier `/etc/bind/named.conf.local`
|
||
|
|
||
|
```shell
|
||
|
root@dns-server: ls /etc/bind/
|
||
|
bind.keys db.127 db.empty named.conf named.conf.local rndc.key
|
||
|
db.0 db.255 db.local named.conf.default-zones named.conf.options zones.rfc1918
|
||
|
```
|
||
|
|
||
|
Commençons par déclarer une première zone pour le domaine `internal.example.com`. On ajoute
|
||
|
la configuration suivante au fichier `/etc/bind/named.conf.local`
|
||
|
|
||
|
```text
|
||
|
zone "internal.example.com." IN {
|
||
|
type master;
|
||
|
file "/var/lib/bind/internal.example.com.zone";
|
||
|
```
|
||
|
|
||
|
Cela permet de déclarer une nouvelle zone dont le contenu est décrit dans le fichier
|
||
|
`/var/lib/bind/internal.example.com.zone`.
|
||
|
|
||
|
À présent, on va créer la zone elle-même. Une zone DNS a une structure de base qu'il faut suivre.
|
||
|
|
||
|
```dns
|
||
|
$ORIGIN .
|
||
|
$TTL 7200 ; 2 hours
|
||
|
internal.example.com IN SOA ns.internal.example.com. admin.example.com. (
|
||
|
2024070301 ; serial
|
||
|
3600 ; refresh (1 hour)
|
||
|
600 ; retry (10 minutes)
|
||
|
86400 ; expire (1 day)
|
||
|
600 ; minimum (10 minutes)
|
||
|
)
|
||
|
NS ns.internal.example.com.
|
||
|
|
||
|
$ORIGIN internal.example.com.
|
||
|
ns A 1.2.3.4
|
||
|
test A 192.168.1.2
|
||
|
```
|
||
|
|
||
|
Ce fichier déclare une zone `internal.example.com`, dont le serveur maître a pour nom
|
||
|
`ns.internal.example.com`. On définit également une série de paramètres (durée de vie
|
||
|
des enregistrements, numéro de série de la zone, etc).
|
||
|
|
||
|
Enfin, on crée deux (2) enregistrements `A` afin d'associer le nom `ns.internal.example.com`
|
||
|
à l'adresse IP `1.2.3.4`, et `test.internal.example.com` (qui est le domaine pour lequel on
|
||
|
voudra un certificat) à une adresse IP locale `192.168.1.2`.
|
||
|
|
||
|
Enfin, un simple `systemctl restart bind9` (sur Debian) permettrait d'appliquer directement
|
||
|
ces modifications. Mais il nous reste encore une chose à configurer avant : permettre la
|
||
|
modification de cette zone à distance.
|
||
|
|
||
|
### Activation des modifications de la zone à distance
|
||
|
|
||
|
Afin de permettre de modifier notre zone DNS à distance, nous allons utiliser
|
||
|
[TSIG](https://www.ibm.com/docs/en/aix/7.3?topic=ssw_aix_73/network/bind9_tsig.htm), ce
|
||
|
qui signifie **Transaction signature**. C'est une façon de sécuriser des opérations
|
||
|
entre serveurs afin d'éditer une zone DNS, et elle est préférée par rapport à une simple
|
||
|
sécurisation basée sur l'adresse IP des clients par exemple.
|
||
|
|
||
|
Commençons par créer une clé en utilisant la commande `tsig-keygen <keyname>`.
|
||
|
|
||
|
```shell
|
||
|
➜ tsig-keygen letsencrypt
|
||
|
key "letsencrypt" {
|
||
|
algorithm hmac-sha256;
|
||
|
secret "oK6SqKRvGNXHyNyIEy3hijQ1pclreZw4Vn5v+Q4rTLs=";
|
||
|
};
|
||
|
```
|
||
|
|
||
|
Cela permet de créer une clé qui a le nom donné en paramètre en utilisant l'algorithme
|
||
|
par défaut (ici il s'agit de `hmac-sha256`). L'intégralité de la sortie de cette commande
|
||
|
est en fait un bloc de configuration que l'on peut ajouter au reste de la configuration
|
||
|
de `bind`.
|
||
|
|
||
|
Enfin, en utilisant la directive `update-policy`, on peut permettre à cette clé d'autoriser
|
||
|
des modifications à distance de notre zone.
|
||
|
|
||
|
```text
|
||
|
update-policy {
|
||
|
grant letsencrypt. zonesub txt;
|
||
|
};
|
||
|
```
|
||
|
|
||
|
{{< callout type=note >}}
|
||
|
En faisant cela, nous autorisons les utilisateur·ices à modifier l'intégralité de cette
|
||
|
zone en utilisant cette clé. En fait, ici nous n'aurions besoin de modifier que l'enregistrement
|
||
|
`TXT` `_acme-challenge.test.internal.example.com`, comme ce qui est spécifié pour le challenge
|
||
|
DNS.
|
||
|
|
||
|
Si on veut une meilleure restriction, on peut utiliser cette configuration à la place,
|
||
|
et dans ce cas n'autoriser que la modification d'un enregistrement spécifique.
|
||
|
|
||
|
```text
|
||
|
update-policy {
|
||
|
grant letsencrypt. name _acme-challenge.test.internal.example.com. txt;
|
||
|
};
|
||
|
```
|
||
|
|
||
|
{{< /callout >}}
|
||
|
|
||
|
Cela veut dire que le contenu de notre fichier `named.conf.local` devient
|
||
|
|
||
|
```text
|
||
|
key "letsencrypt" {
|
||
|
algorithm hmac-sha256;
|
||
|
secret "oK6SqKRvGNXHyNyIEy3hijQ1pclreZw4Vn5v+Q4rTLs=";
|
||
|
};
|
||
|
|
||
|
zone "internal.example.com." IN {
|
||
|
type master;
|
||
|
file "/var/lib/bind/internal.example.com.zone";
|
||
|
update-policy {
|
||
|
grant letsencrypt. zonesub txt;
|
||
|
};
|
||
|
};
|
||
|
```
|
||
|
|
||
|
{{< callout type="warning" >}}
|
||
|
Il faut faire **très attention** au `.` à la fin du nom de la zone ainsi que du nom de la clé,
|
||
|
c'est très facile de les oublier, ce qui causerait des problèmes difficiles à détecter.
|
||
|
{{< /callout >}}
|
||
|
|
||
|
### Réalisation du challenge
|
||
|
|
||
|
On commence par installer le certbot avec le plugin **RFC 2136** (qui nous permet de réaliser
|
||
|
les challenges DNS).
|
||
|
|
||
|
```shell
|
||
|
apt install python3-certbot-dns-rfc2136
|
||
|
```
|
||
|
|
||
|
Le certbot se configure via un fichier de configuration au format `.ini`, on va le placer
|
||
|
dans le fichier `/etc/certbot/credentials.ini`.
|
||
|
|
||
|
```ini
|
||
|
dns_rfc2136_server = <you_dns_ip>
|
||
|
dns_rfc2136_port = 53
|
||
|
dns_rfc2136_name = letsencrypt.
|
||
|
dns_rfc2136_secret = oK6SqKRvGNXHyNyIEy3hijQ1pclreZw4Vn5v+Q4rTLs=
|
||
|
dns_rfc2136_algorithm = HMAC-SHA512
|
||
|
```
|
||
|
|
||
|
Enfin, on peut lancer le challenge en utilisant le certbot (si c'est la première fois que
|
||
|
le bot est utilisé sur notre machine, on nous demandera d'accepter les conditions d'utilisation
|
||
|
et de donner une adresse email pour la gestion administrative des certificats et les notifications
|
||
|
de renouvellement, c'est normal.)
|
||
|
|
||
|
```shell
|
||
|
root@toolbox:~: certbot certonly --dns-rfc2136 --dns-rfc2136-credentials /etc/certbot/credentials.ini -d 'test.internal.example.com'
|
||
|
|
||
|
Saving debug log to /var/log/letsencrypt/letsencrypt.log
|
||
|
Requesting a certificate for test.internal.example.com
|
||
|
Waiting 60 seconds for DNS changes to propagate
|
||
|
|
||
|
Successfully received certificate.
|
||
|
Certificate is saved at: /etc/letsencrypt/live/test.internal.example.com/fullchain.pem
|
||
|
Key is saved at: /etc/letsencrypt/live/test.internal.example.com/privkey.pem
|
||
|
This certificate expires on 2024-09-30.
|
||
|
These files will be updated when the certificate renews.
|
||
|
Certbot has set up a scheduled task to automatically renew this certificate in the background.
|
||
|
|
||
|
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
|
If you like Certbot, please consider supporting our work by:
|
||
|
* Donating to ISRG / Let's Encrypt: https://letsencrypt.org/donate
|
||
|
* Donating to EFF: https://eff.org/donate-le
|
||
|
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||
|
```
|
||
|
|
||
|
Et c'est bon, on a un certificat, et à aucun moment nous n'avons eu besoin d'exposer une
|
||
|
application au monde extérieur.
|
||
|
|
||
|
{{< callout type="warning" >}}
|
||
|
On a utilisé ici le mode `standalone` pour le certbot, ce qui signifie que lorsque les
|
||
|
certificats sont renouvelés, aucune action supplémentaire n'est exécutée. Si on utilise
|
||
|
un reverse proxy comme `nginx`, il faut également redémarrer le serveur (ou le recharger)
|
||
|
afin de charger les nouveaux certificats après renouvellement car le `certbot` ne le fait
|
||
|
pas de lui-même en mode `standalone`.
|
||
|
{{< /callout >}}
|
||
|
|
||
|
Maintenant, comme j'aime aller beaucoup trop loin dans tout ce que je fais. on peut ajouter
|
||
|
deux (2) améliorations à notre setup :
|
||
|
|
||
|
- Utiliser des ACL (Contrôle d'Accès) en plus des clés TSIG pour sécuriser les opérations sur
|
||
|
notre serveur DNS
|
||
|
- Utiliser un second serveur DNS accessible uniquement localement contenant nos enregistrements
|
||
|
privés, et utiliser le serveur public uniquement pour réaliser les challenges
|
||
|
|
||
|
## Bonus 1 : ajouter une couche d'authentification pour se connecter au DNS
|
||
|
|
||
|
Dans notre setup, on a utilisé **TSIG** afin de sécuriser l'accès au serveur DNS, ce qui
|
||
|
signifie que la possession de la clé est nécessaire pour effectuer les opérations. Si vous
|
||
|
êtes paranoïaque, ou que vous voulez juste en faire un peu plus, il est possible d'ajouter
|
||
|
une seconde couche d'authentification en utilisant des [Listes de Contrôle d'Accès (ACL)](https://bind9.readthedocs.io/en/v9.18.1/security.html).
|
||
|
|
||
|
Les **ACL** nous permettent de filtrer les opérations autorisées en se basant sur plusieurs
|
||
|
caractéristiques, par exemple l'adresse IP, une clé TSIG, etc. Dans notre cas, nous allons
|
||
|
utiliser un sous-réseau IPV4 qui se situe à l'intérieur d'un tunnel Wireguard entre notre
|
||
|
serveur applicatif (client DNS qui veut réaliser le challenge) et le serveur DNS. Cela peut
|
||
|
être n'importe quel type de tunnel, mais Wireguard est facile à configurer et est parfait pour
|
||
|
notre cas d'usage.
|
||
|
|
||
|
### Configuration de Wireguard
|
||
|
|
||
|
Commençons par créer le tunnel [Wireguard](https://www.wireguard.com/quickstart/)
|