714 lines
29 KiB
Markdown
714 lines
29 KiB
Markdown
|
---
|
||
|
title: "Comment faire du HTTPS chez soi (quand son infrastructure est privée)"
|
||
|
date: 2024-09-08T15:18:00+02:00
|
||
|
draft: false
|
||
|
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 propose é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, ici, 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.
|
||
|
|
||
|
{{< callout type="note" >}}
|
||
|
Pour le reste de ce "guide", je vais me baser sur un déploiement pour un serveur DNS
|
||
|
`bind9`. Tout est adaptable pour n'importe quel autre type de déploiement, mais tous
|
||
|
les exemples de configurations sont spécifiques à `bind9`. Let's Encrypt propose
|
||
|
de la [documentation adaptée](https://community.letsencrypt.org/t/dns-providers-who-easily-integrate-with-lets-encrypt-dns-validation/86438) à d'autres
|
||
|
types de déploiements.
|
||
|
{{< /callout >}}
|
||
|
|
||
|
### 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/)
|
||
|
|
||
|
On commence par créer deux paires de clé Wireguard, cela peut se faire de la façon suivante
|
||
|
|
||
|
```shell
|
||
|
# Installation des outils Wireguard
|
||
|
apt install wireguard-tools
|
||
|
|
||
|
# Création de la paire de clés
|
||
|
wg genkey | tee privatekey | wg pubkey > publickey
|
||
|
```
|
||
|
|
||
|
La clé privée se trouve dans le fichier `privatekey`, et la clé publique dans le fichier
|
||
|
`publickey`.
|
||
|
|
||
|
Ensuite, on peut créer la configuration du serveur. Commençons par placer un fichier
|
||
|
`/etc/wg/wg0.conf` dans le serveur DNS avec le contenu suivant
|
||
|
|
||
|
```ini
|
||
|
[Interface]
|
||
|
PrivateKey = <server_private_key>
|
||
|
Address = 192.168.42.1/24
|
||
|
ListenPort = 51820
|
||
|
|
||
|
[Peer]
|
||
|
PublicKey = <client_public_key>
|
||
|
AllowedIPs = 192.168.42.0/24
|
||
|
```
|
||
|
|
||
|
Ensuite, du côté de notre client, on peut faire la même chose
|
||
|
|
||
|
```ini
|
||
|
[Interface]
|
||
|
PrivateKey = <client_private_key>
|
||
|
Address = 192.168.42.2/24
|
||
|
|
||
|
[Peer]
|
||
|
PublicKey = <server_public_key>
|
||
|
Endpoint = <dns_public_ip>:51820
|
||
|
AllowedIPs = 192.168.42.1/32
|
||
|
```
|
||
|
|
||
|
Enfin, on démarre le tunnel des deux cotés en utilisant la commande `wg-quick up wg0`. Un
|
||
|
ping nous permet de vérifier que le tunnel est opérationnel et que le client peut bien
|
||
|
atteindre le serveur.
|
||
|
|
||
|
```shell
|
||
|
root@toolbox:~ ping 192.168.42.1
|
||
|
PING 192.168.42.1 (192.168.42.1) 56(84) bytes of data.
|
||
|
64 bytes from 192.168.42.1: icmp_seq=1 ttl=64 time=19.2 ms
|
||
|
64 bytes from 192.168.42.1: icmp_seq=2 ttl=64 time=8.25 ms
|
||
|
```
|
||
|
|
||
|
Pour résumer, on vient de créer un nouveau réseau privé `192.168.42.0/24` qui lie le serveur
|
||
|
DNS et notre client, et nous pouvons restreindre les modifications à la zone DNS afin de ne
|
||
|
les autoriser que depuis ce réseau privé virtuel, et non de partout comme c'était le cas
|
||
|
auparavant.
|
||
|
|
||
|
{{< callout type="note" >}}
|
||
|
Les Contrôles d'Accès que nous allons utiliser ici ont plusieurs autres usages possibles, tels
|
||
|
que cacher certains domaines, ou servir différentes versions d'une zone en fonction de
|
||
|
l'origine du client. Ça n'est pas vraiment l'objet de ce post cependant.
|
||
|
{{< /callout >}}
|
||
|
|
||
|
### Configuration du serveur DNS
|
||
|
|
||
|
En utilisant des ACLs, nous allons séparer la zone DNS en différentes [vues](https://kb.isc.org/docs/aa-00851) basées sur l'adresse IP source. Notre objectif est donc que :
|
||
|
|
||
|
- les utilisateur·ices venant de notre réseau Wiregaurd `192.168.42.0/24` puissent modifier
|
||
|
les enregistrements DNS en utilisant la clé TSIG définie précédemment
|
||
|
- les utilisateur·ices venant de toute autre adresse IP puissent lire notre zone DNS, mais
|
||
|
sans pouvoir la modifier, même avec la bonne clé TSIG
|
||
|
|
||
|
Les ACL se définissent dans `named.conf.local` en utilisant la syntaxe suivante
|
||
|
|
||
|
```text
|
||
|
acl local {
|
||
|
127.0.0.0/8;
|
||
|
192.168.42.0/24;
|
||
|
};
|
||
|
```
|
||
|
|
||
|
Cela permet de considérer que les adresses locales, ainsi que les adresses de notre réseau
|
||
|
Wireguard sont dans un groupe nommé `local`, et on peut utiliser ce groupe pour les
|
||
|
référencer dans le reste de notre configuration.
|
||
|
|
||
|
Ensuite, une vue se crée de la façon suivante :
|
||
|
|
||
|
```text
|
||
|
view "internal" {
|
||
|
match-clients { local; };
|
||
|
zone "internal.example.com." IN {
|
||
|
type master;
|
||
|
file "/var/lib/bind/internal.example.com.zone";
|
||
|
update-policy {
|
||
|
grant letsencrypt. zonesub txt;
|
||
|
};
|
||
|
};
|
||
|
};
|
||
|
```
|
||
|
|
||
|
Globalement, cela signifie que notre vue `internal` n'est utilisable que pour les clients
|
||
|
qui correspondent bien à notre liste d'accès `local` que nous venons de définir. Dans cette
|
||
|
vue, nous définissons la zone `internal.example.com` comme dans les sections précédentes.
|
||
|
|
||
|
Nous devons également déclarer la zone pour les utilisateurs non-locaux qui ne vont dont
|
||
|
pas correspondre à l'ACL `local`. Il est important de souligner que **l'on ne peut pas
|
||
|
utiliser le même fichier de zone pour deux zones différentes**, donc nous ne pouvons
|
||
|
pas définir la vue publique de la même façon que notre vue privée. Nous allons avoir
|
||
|
recours à la syntaxe suivante :
|
||
|
|
||
|
```text
|
||
|
view "public" {
|
||
|
zone "internal.example.com." IN {
|
||
|
in-view internal;
|
||
|
};
|
||
|
};
|
||
|
```
|
||
|
|
||
|
Avec cette syntaxe, dans la vue `public`, nous définissons la zone `internal.example.com`
|
||
|
en indiquant que cette zone se trouve dans la vue `internal`. De cette façon nous servons
|
||
|
exactement la même zone DNS quelle que soit l'origine du client, mais la **politique de mise
|
||
|
à jour** ne s'applique pour les utilisateurs avec une adresse locale, qui seront donc les
|
||
|
seuls à avoir le droit de mettre à jour la zone.
|
||
|
|
||
|
En résumé, notre fichier `named.conf.local` devrait ressembler à cela
|
||
|
|
||
|
```text
|
||
|
acl local {
|
||
|
127.0.0.0/8;
|
||
|
192.168.42.0/24;
|
||
|
};
|
||
|
|
||
|
key "letsencrypt." {
|
||
|
algorithm hmac-sha512;
|
||
|
secret "oK6SqKRvGNXHyNyIEy3hijQ1pclreZw4Vn5v+Q4rTLs=";
|
||
|
};
|
||
|
|
||
|
view "internal" {
|
||
|
match-clients { local; };
|
||
|
zone "internal.example.com." IN {
|
||
|
type master;
|
||
|
file "/var/lib/bind/internal.example.com.zone";
|
||
|
update-policy {
|
||
|
grant letsencrypt. zonesub txt;
|
||
|
};
|
||
|
};
|
||
|
};
|
||
|
|
||
|
view "public" {
|
||
|
zone "internal.example.com." IN {
|
||
|
in-view internal;
|
||
|
};
|
||
|
};
|
||
|
```
|
||
|
|
||
|
## Bonus 2 : complètement cacher les adresses privées
|
||
|
|
||
|
Dans cet article, nous avons implémenté notre propre serveur DNS (ou utilisé celui de notre
|
||
|
provider) afin de résoudre des domaines privés internes et effectuer des challenges DNS pour
|
||
|
ces hôtes dans le but d'obtenir des certificats SSL. Cependant il reste un élément peu satisfaisant.
|
||
|
|
||
|
Par exemple, prenons cet enregistrement dans notre zone DNS
|
||
|
|
||
|
```text
|
||
|
test A 192.168.1.2
|
||
|
```
|
||
|
|
||
|
Cela signifie qu'en utilisant la commande `host test.internal.example.com` (ou dig, ou
|
||
|
n'importe quel autre outil client DNS) va renvoyer l'adresse `192.168.1.2`, que l'on
|
||
|
utilise notre serveur DNS, celui de Google, etc. C'est un peu dommage : cette adresse
|
||
|
est privée, elle n'a aucun sens en dehors de sa propre infrastructure, et, même si l'impact
|
||
|
est limité, donner des informations sur la nature de notre infrastructure publique est
|
||
|
généralement non-désirable.
|
||
|
|
||
|
Pour corriger ce problème, nous pourrions utiliser deux (2) serveurs DNS ayant chacun leur
|
||
|
propre rôle :
|
||
|
|
||
|
- Un serveur à l'intérieur du réseau privé qui servira à résoudre ces hôtes privés
|
||
|
- Un serveur à l'extérieur du réseau public qui servira uniquement pour les challenges DNS
|
||
|
|
||
|
En effet, à l'intérieur de notre réseau, nous n'avons pas besoin d'être publiquement accessible,
|
||
|
mais nous avons besoin d'une résolution de nos noms locaux. De même, Let's Encrypt n'a besoin
|
||
|
d'aucun enregistrement `A` pour effectuer le challenge DNS, nous n'avons besoin que d'un
|
||
|
unique enregistrement `TXT`, donc chaque serveur a son propre rôle.
|
||
|
|
||
|
![Un schéma décrivant le fonctionnement du challenge DNS dans le protocole ACME avec une séparation entre un serveur DNS privé et public et les interactions entre Let's Encrypt le serveur DNS public d'un côté, et le serveur DNS privé, l'utilisateur, de l'autre coté](/images/dns_article_dns_challenge_2.svg)
|
||
|
|
||
|
Globalement, nous avons besoin des éléments suivants :
|
||
|
|
||
|
- un serveur DNS publiquement accessible (concrètement celui des précédentes parties de cet
|
||
|
article) qui aura :
|
||
|
- uniquement son propre enregistrement `NS`
|
||
|
- la clé TSIG et les règles permettant la mise à jour de sa zone
|
||
|
- le tunnel VPN optionnel
|
||
|
- l'enregistrement `TXT` permettant la réalisation des challenges DNS
|
||
|
- un serveur DNS privé dans l'infrastructure locale qui aura :
|
||
|
- tous les enregistrements `A` (et autres) de l'infrastructure privée
|
||
|
|
||
|
Nous allons donc séparer la configuration précédente (je vais utiliser la configuration
|
||
|
finale obtenue dans la section [Bonus 1](#bonus-1--ajouter-une-couche-dauthentification-pour-se-connecter-au-dns)
|
||
|
en guise d'exemple)
|
||
|
|
||
|
### Serveur DNS privé
|
||
|
|
||
|
Du côté du serveur privé, nous n'avons besoin que de définir notre zone locale
|
||
|
`internal.example.com`. Donc notre fichier `named.conf.local` n'a besoin que
|
||
|
de la configuration suivante
|
||
|
|
||
|
```text
|
||
|
zone "internal.example.com" IN {
|
||
|
type master;
|
||
|
file "/var/lib/bind/internal.example.com.zone";
|
||
|
allow-update { none; };
|
||
|
};
|
||
|
```
|
||
|
|
||
|
Et la définition de notre zone ressemblerait à ceci
|
||
|
|
||
|
```text
|
||
|
$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 192.168.1.1
|
||
|
test A 192.168.1.2
|
||
|
```
|
||
|
|
||
|
Ce serveur devrait être configuré en tant que serveur DNS principal dans notre configuration
|
||
|
DHCP (ou en configuration statique le cas échéant).
|
||
|
|
||
|
### Serveur DNS public
|
||
|
|
||
|
Pour le serveur DNS public, nous n'avons plus besoin des enregistrements `A` précédents,
|
||
|
mais uniquement de la configuration nécessaire afin de pouvoir mettre à jour la zone
|
||
|
à distance. Notre fichier `named.conf.local` devrait donc ressembler à ceci (il s'agit
|
||
|
de la même configuration qu'avant en vérité)
|
||
|
|
||
|
```text
|
||
|
acl local {
|
||
|
127.0.0.0/8;
|
||
|
192.168.42.0/24;
|
||
|
};
|
||
|
|
||
|
key "letsencrypt." {
|
||
|
algorithm hmac-sha512;
|
||
|
secret "oK6SqKRvGNXHyNyIEy3hijQ1pclreZw4Vn5v+Q4rTLs=";
|
||
|
};
|
||
|
|
||
|
view "internal" {
|
||
|
match-clients { local; };
|
||
|
zone "internal.example.com." IN {
|
||
|
type master;
|
||
|
file "/var/lib/bind/internal.example.com.zone";
|
||
|
update-policy {
|
||
|
grant letsencrypt. zonesub txt;
|
||
|
};
|
||
|
};
|
||
|
};
|
||
|
|
||
|
view "public" {
|
||
|
zone "internal.example.com." IN {
|
||
|
in-view internal;
|
||
|
};
|
||
|
};
|
||
|
```
|
||
|
|
||
|
Le fichier de zone devrait ressembler à ceci (nous avons enlevé l'enregistrement `A` qui n'a
|
||
|
plus sa place dans la zone publique)
|
||
|
|
||
|
```text
|
||
|
$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
|
||
|
```
|
||
|
|
||
|
### Tester la configuration
|
||
|
|
||
|
Une fois que les deux serveurs sont opérationnels, et que tout est correctement configuré,
|
||
|
nous pouvons tester que tout fonctionne comme attendu. Il suffit d'effectuer une requête DNS
|
||
|
en utilisant `host`, `dig`, ... sur notre enregistrement privé afin de vérifier que
|
||
|
le comportement attendu a bien lieu.
|
||
|
|
||
|
```shell
|
||
|
# Nous pouvons bien atteindre notre domaine privé depuis notre infrastructure
|
||
|
~ …
|
||
|
➜ host test.internal.example.com
|
||
|
Using domain server:
|
||
|
Name: 192.168.1.1
|
||
|
Address: 192.168.1.11#53
|
||
|
Aliases:
|
||
|
|
||
|
test.internal.example.com has address 192.168.1.2
|
||
|
|
||
|
# Utiliser un serveur DNS public (ici 8.8.8.8) ne fonctionne pas
|
||
|
# car cet enregistrement n'existe pas en dehors de notre réseau
|
||
|
~ …
|
||
|
➜ host test.internal.example.com 8.8.8.8
|
||
|
Using domain server:
|
||
|
Name: 8.8.8.8
|
||
|
Address: 8.8.8.8#53
|
||
|
Aliases:
|
||
|
|
||
|
Host test.internal.example.com not found: 3(NXDOMAIN)
|
||
|
```
|
||
|
|
||
|
## Mot de la fin
|
||
|
|
||
|
Cette méthode (en considérant également les améliorations proposées) est plus exigeante
|
||
|
que simplement ignorer le problème et n'utiliser que des challenges HTTP. Mais une fois que
|
||
|
l'infrastructure est en place, cela devient très simple à mettre en place, et permet
|
||
|
d'obtenir une infrastructure très propre et facile à maintenir.
|
||
|
|
||
|
Il s'agit également de la seule façon possible d'obtenir un certificat wildcard (par exemple
|
||
|
`*.internal.example.com`) qui permettrait d'utiliser un unique certificat pour la totalité de
|
||
|
notre infrastructure.
|
||
|
|
||
|
Je considère que ce type de setup est particulièrement adapté à un homelab, ou a un business
|
||
|
de petite taille possédant une infrastructure privée, mais ne souhaitant pas s'infliger
|
||
|
la création d'une infrastructure PKI (Infrastructure de Clé Privée) complète.
|