hugo-relie/content/fr/posts/dns-challenge.md

369 lines
17 KiB
Markdown
Raw Normal View History

2024-07-02 21:16:04 +00:00
---
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/)