Mettre en place un service DNS complet à la maison…🔗

Posted by Médéric Ribreux 🗓 In blog/ Sysadmin/

#auto-hébergement #sysadmin #sécurité #security

Introduction

Le DNS, c'est la base de tout Internet aujourd'hui. On ne s'en rend même plus compte mais sans DNS, pas d'Internet facile à utiliser, pas de noms sur le réseau, juste des adresses IP à taper à la main. Si écrire une adresse IPv4 reste possible, il n'en est pas de même pour une adresse IPv6 ! Le DNS est donc obligatoire. Pour ceux qui l'ignorent, le Domain Name System est une méthode permettant de faire le lien entre un nom de machine sous forme quasi-humaine en adresse IP, utilisable par un système d'exploitation. La forme quasi-humaine est une syntaxe de parcours d'arbre. Chaque branche possède un nom et le délimiteur est le caractère point ('.').

Ainsi, medspx.homenet.org indique qu'on veut accéder à la machine nommée medspx, située dans le domaine homenet, lui-même situé dans le domaine général org. Je vous invite à lire la page d'introduction de Wikipedia sur le sujet pour une introduction plus complète.

Cet article tentera de montrer comment monter son propre serveur DNS en auto-hébergement pour permettre de nommer des machines sur le réseau local ainsi que pour servir de résolveur DNS global. Au cours de ces étapes, il nous faudra faire une petite introduction sur le fonctionnement du DNS. Pour la mise en œuvre, nous allons utiliser le vénérable serveur Bind dans la configuration de Debian Wheezy. Il existe d'autres serveurs avec des fonctionnalités différentes mais Bind reste l'implémentation de référence (pas forcément la plus performante) et vous trouverez pléthore de documentation sur son utilisation.

Avant de commencer, sachez que cet article n'a pas vocation à servir de référence. Il illustre juste la mise en place d'une configuration adaptée à un réseau local avec des règles de sécurité bien définies.

Les différents modes du DNS

Pour notre cas d'illustration, nous allons étudier deux modes de fonctionnement du DNS:

Ces deux modes sont bien différents. Commençons par le plus simple à expliquer: le serveur faisant autorité. Comme son nom l'indique, un serveur qui fait autorité est un serveur capable de faire la correspondance entre des noms complets (FQDN) et des adresses IP. Le terme d'autorité indique que le serveur est la source de référence de la correspondance. En tant que référence, il peut donc être requêté par d'autres machines pour lui demander de faire la correspondance et de renvoyer l'adresse IP. Prenons le domaine example.com. Le serveur faisant autorité est la machine capable de donner l'adresse IP de la machine a.example.com.

Le système DNS est donc composé d'un tas de serveurs qui font autorité. La "base de données" des noms/adresses IP est donc distribuée sur ces serveurs faisant autorité. Mais, en plus d'être distribué, le DNS est également hiérarchique. En effet, il faut un moyen de contacter les serveurs faisant autorité. Ce moyen se situe au niveau des domaines supérieurs. Dans example.com, com est un domaine supérieur. Le serveur qui gère le domaine com (qui fait autorité sur le domaine) connaît donc l'adresse IP du serveur DNS faisant autorité pour le domaine example.com. C'est assez simple à gérer et on voit bien l'importance de la filiation pour la résolution de noms.

Lorsque vous avez un nom de domaine (example.com) et que vous avez un serveur faisant autorité dessus, cela signifie que vous pourrez définir autant de noms de machines situées sous ce domaine. Votre serveur DNS stockera ces informations de référence et les mettra à disposition des machines sur le réseau. Si vous êtes connectés à Internet, cela signifie que potentiellement, d'autres machines pourront vous poser des questions.

À l'inverse, un résolveur est un mode de fonctionnement bien particulier qui permet de regrouper les demandes de résolution de nom en un point central. Concrètement, chaque client du réseau va demander au résolveur de faire la requête DNS pour son compte.

Pour mieux comprendre le fonctionnement d'un résolveur, étudions rapidement ce qui se passe quand on fait une requête DNS complète. À la base, la machine doit disposer d'un minimum d'informations. Ces informations de base sont les adresses IP des serveurs d'autorité de la racine. La racine, c'est le point culminant du système mondial distribué du DNS. Les serveurs (car ils sont plusieurs) de la racine connaissent les adresses IP des domaines TLD (Top Level Domain) comme .com, .org, .net, etc. Ces serveurs de TLD connaissent ensuite les correspondances pour les domaines du type linuxfr.org, homenet.org, duckduckgo.com, etc. La racine n'a pas de nom, c'est juste le caractère '.'. Une adresse DNS complète est du type 'medspx.homenet.org.'. Mais l'usage veut qu'on supprime le point terminal.

Donc le résolveur doit connaître les adresses IP de la racine. Ensuite, une résolution complète prend la forme qui suit:

Voilà comment fonctionne la résolution DNS. On le voit, à chaque fois, on interroge la source faisant autorité. Aucune machine ne fait autorité pour tout le DNS. La distribution de l'autorité permet de distribuer les données sur de nombreuses branches d'un arbre. L'intérêt est que lorsqu'une branche est coupée, l'arbre reste vivant et on peut continuer à interroger les autres branches (d'un niveau égal ou supérieur au domaine coupé). C'est donc un modèle relativement résilient.

L'exemple typique d'un résolveur DNS est le serveur DNS de votre FAI: il fait la requête DNS complète pour vous. L'intérêt du résolveur c'est qu'il peut mettre en cache les données. On peut imaginer qu'au sein du réseau d'un FAI, ou même à l'échelle d'un réseau local, de nombreux clients fassent la même requête. Genre, quelle est l'adresse IP de debian.org ou de google.fr ? Au lieu de faire la requête complète qui interroge la racine, le domaine TLD et le domaine final, on ne fait qu'un seul passage. C'est un peu plus performant.

Toutefois, sachez que les résolveurs DNS des FAI ne sont pas parfaits. D'abord, ils peuvent tomber en panne. Ça été le cas pour le FAI Free sur le réseau duquel j'ai pu mesurer plus d'une dizaine de fois en quelques années des coupures de service DNS pendant plus de 10 minutes. Ce qui se passe est assez sidérant: plus rien de ce qui peut aller sur le réseau ne fonctionne. Mais si vous connaissez une adresse IP, vous pouvez y accéder sans problème. Autre point plus inquiétant et plus en lien avec l'actualité, les résolveurs DNS de votre FAI peuvent mentir ! Ce comportement est loin d'être anodin. En effet, comme je l'avais dit en introduction, sans DNS, pas grand-chose de ce qui doit aller sur le réseau ne fonctionne ! Si votre résolveur DNS dit qu'un nom de machine n'existe pas alors qu'il existe quand même, vous n'y avez tout simplement pas accès. Pire, le résolveur DNS peut indiquer une autre adresse IP et vous diriger vers une mauvaise machine.

Le mensonge DNS est généralement la technique utilisée pour censurer des sites web: le gouvernement décide qu'un site ne doit pas être consulté et indique aux FAI l'adresse (le FQDN) à filtrer. Les résolveurs DNS des FAI renvoient une fausse adresse IP à leurs clients. Le site web disparaît. Si vous avez votre propre résolveur ce n'est pas possible (sauf en faisant du DPI mais c'est un autre sujet).

N'importe qui peut néanmoins mettre en place un résolveur complet sur sa propre machine. Il suffit de connaître les adresses IP des serveurs de la racine (la liste est publique) et le tour est joué, avec le bon logiciel. Si vous avez un serveur chez vous, vous pouvez mettre en place un service de résolution centralisée. Ce dernier fonctionnera techniquement (à peu près) comme je l'ai indiqué et ne sera pas sujet à d'éventuels mensonges.

À propos des fichiers de zone

Les fichiers de zone sont les fichiers de données du serveur. En effet, comme tout programme démon sous Unix, un serveur DNS dispose de fichiers de configuration permettant de déterminer son comportement. Mais comme nous l'avons déjà vu auparavant, le DNS sert essentiellement à faire la correspondance entre des noms et des adresses IP et il faut bien que ces données soient stockées quelque part.

Ces données sont stockées dans des fichiers de zone qu'on nomme également "resource records" (RR). Ces fichiers ont un format bien particulier qu'il convient de présenter.

Avec ces 5 champs, on peut définir de nombreuses choses.

Prenons un premier exemple qui nous permettra de présenter différents types d'information (le quatrième champ):

server1	   	   IN	   A   192.168.0.2

Dans cet exemple:

Étudions maintenant le type CNAME:

server2                IN        CNAME        server1

Ici, nous définissons que la machine server2 sera un alias de server1. Toute résolution de nom de server2 renverra comme réponse que server2 est un alias de server1 (si on veut l'ip de server2, il faudra faire une requête DNS pour l'IP de server1).

Enfin, il existe un type bien particulier: le type SOA. Ce dernier permet d'indiquer qu'on définit un serveur faisant autorité (master). Étudions un exemple de déclaration SOA:

@IN	SOA	dns.example.org. admin.example.org. (
  201401071		; Numéro de série du fichier de zone
     604800		; Refresh
      86400		; Retry
    2419200		; Expire
     604800 )	; Negative Cache TTL

Notre cahier des charges

Comme d'habitude, avant de mettre en place un service, il faut étudier nos besoins.

Configuration du résolveur Bind sous Debian Wheezy

Installation du paquet

Le service Bind s'installe avec peu de paquets:

# aptitude install bind9

Répartition de la configuration

Sous Debian, la configuration de Bind se retrouve dans le répertoire /etc/bind/. Elle se répartit entre les fichiers de zones et les fichiers de configuration du serveur. Ces derniers sont distribués. Pour ma part, j'ai choisi de concentrer la configuration du serveur en un seul fichier nommé named.conf. Les fichiers de zone seront créés selon le code: un fichier de zone par zone !

Une fois que la configuration a été modifiée, il faut relancer le service pour la prendre en compte via:

# service bind9 restart

Configuration du serveur

Voici la configuration du serveur que j'héberge sur mon réseau local. Nous allons la commenter ensemble pour mieux la comprendre. Pour des raisons de simplification, j'ai concentré ma configuration dans un seul fichier qui reste modeste. Je procède toujours ainsi pour les configurations qui doivent rester simple car cela permet de ne pas chercher pendant des heures quel fichier modifier.

Vous pouvez bien sûr lire la documentation de référence de Bind pour connaître la signification et la syntaxe de chaque directive.

// This is the primary configuration file for the BIND DNS server named.
//
// Please read /usr/share/doc/bind9/README.Debian.gz for information on the 
// structure of BIND configuration files in Debian, *BEFORE* you customize 
// this configuration file.
//
// If you are just adding zones, please do that in /etc/bind/named.conf.local

// On commence pas définir une acl qui permet de définir notre réseau local
// cette acl est nommée reseau_local et elle concerne le réseau 192.168.0.0/24, un réseau IPv4.
acl reseau_local { 192.168.0.0/24; };

// la directive controls permet d'indiquer comment configurer le démon
// de contrôle de Bind. Pour ma part, je ne souhaite pas que ce démon soit présent
// pour des questions de simplicité et de sécurité
controls { };

// Ici, on inclue les clefs de DNSSEC. Je ne vais pas revenir dessus car je pourrais en parler pendant des heures.
// Sachez juste que DNSSEC est une tentative de sécurisation du système DNS.
include "/etc/bind/bind.keys";

// Voici les options générales du service Bind.
options {
// On lui indique d'écouter sur localhost (pour la résolution interne de la machine qui héberge le résolveur, eat your own dog food)
// ainsi que sur l'ip de la machine qui écoute sur le réseau local.
// Bien entendu, on écoute sur le port 53 car c'est la norme de base du DNS.
	listen-on { 127.0.0.1; 192.168.0.x; };
	port 53;
// répertoire de travail du serveur
directory "/var/cache/bind";

// un minimum de configuration de DNSSEC
dnssec-validation auto;

// Le serveur 
auth-nxdomain no;    # conform to RFC1035

// on écoute sur le port localhost IPv6. Conclusion, le service DNS ne sera pas disponible par IPv6.
listen-on-v6 { ::1; };

// cette simple directive permet de dire qu'on souhaite mettre en place un résolveur.
// Pas très compliqué !
recursion yes;
};

// Configuration des journaux systèmes (les logs)
// Attention, cette configuration est en mode paranoïaque: tout est loggué, y compris les requêtes DNS des clients
// Les logs peuvent donc être très volumineux.
logging {
 // on commence à définir des "canaux" de logs: ce sont des définitions d'emplacement de fichiers
	 channel security_warning {
	 // Les avertissements de sécurité iront dans /var/log/bind/security.log
	 // on garde 3 versions du fichier et on créé un nouveau fichier tous les 100ko
			 file "/var/log/bind/security.log" versions 3 size 100k;
			 severity warning;
			 print-severity  yes;
			 print-time      yes;
	 };

	 channel client_info {
			 file "/var/log/bind/requests.log" versions 2 size 10m;
			 severity info;
			 print-severity  yes;
			 print-time      yes;
	 };

	 channel bind_log {
			 file "/var/log/bind/bind.log" versions 3 size 1m;
			 severity info;
			 print-category  yes;
			 print-severity  yes;
			 print-time      yes;
	 };

 // ensuite, on fait la répartition vers les fichiers en fonction de la catégorie
	 category default { bind_log; };
 category client { client_info; };
 category resolver { client_info; };
 category queries { client_info; };
	 category lame-servers { null; };
	 category update { null; };
	 category update-security { null; };
	 category security { security_warning; }; 
};

// Définition des zones
// c'est notre domaine local
zone "ici" {
	   // le serveur fait autorité, il est de type master.
	   type master;
   // le fichier de zone est stocké dans /etc/bind/db.ici
   file "/etc/bind/db.ici";
   // localhost et les machines définies dans l'acl "reseau_local" (voir plus haut) peuvent faire des requêtes DNS sur ce serveur
   allow-query { localhost; reseau_local; };
   // On ne peut pas mettre à jour les données de ce serveur autrement que par la configuration.
   allow-update { none; };
};

// Cette zone permet de configurer la résolution DNS inverse (on donne une IP et on obtient le nom en échange).
zone "0.168.192.in-addr.arpa" {
	type master;
file "/etc/bind/db.192.168.0";
allow-query { localhost; reseau_local; };
allow-update { none; };
};

// C'est ici qu'on indique le fichier contenant les IP des serveur racine.
// consultez ce fichier, vous verrez les adresses !
zone "." {
 type hint;
 file "/etc/bind/db.root";
};

// Cette "zone" permet aux clients de merde (MS-Windows) qui font des
// requêtes sur localhost, d'avoir une réponse censée (127.0.0.1 ou ::1)
// Dans le cas normal, un client normal ne fait PAS de demande de résolution de localhost
// Il sait par défaut que ça correspond à 127.0.0.1 ou ::1.
zone "localhost" {
 type master;
 file "/etc/bind/db.local";
};

// C'est la même chose que pour localhost mais en résolution inverse
zone "127.in-addr.arpa" {
 type master;
 file "/etc/bind/db.127";
};

zone "0.in-addr.arpa" {
 type master;
 file "/etc/bind/db.0";
};

zone "255.in-addr.arpa" {
 type master;
 file "/etc/bind/db.255";
};

// Include empty zones for RFC 1918 DNS queries (from fucked clients)
include "/etc/bind/zones.rfc1918";
};

On le voit, cette configuration est assez simple. Tout tient dans un seul fichier et nous avons peu de zones à servir. Intéressons-nous maintenant aux données des fichiers de zone.

Fichiers de zones

Voici quelques fichiers de zone précédemment référencés dans la configuration du serveur DNS. Je ne vais pas indiquer les fichiers de zone de base (racine, localhost, reverse localhost) mais bien les fichiers de notre configuration

/etc/bind/db.ici:

;
; Fichier de zone du réseau local ici.
;
; Le TTL global sera de 1 journée
$TTL	1d
; Notre premier enregistrement est un type SOA pour indiquer
; que le fichier gère un serveur faisant autorité.
; le nom du serveur de nom qui fait autorité est debianplug.ici.
; l'adresse email est du type email.nom_de_domaine (on remplace @ par un .).
@	IN	SOA	serveur.ici. admin.ici. (
	  201401071		; Numéro de série du fichier de zone
		 604800		; Refresh
		  86400		; Retry
		2419200		; Expire
		 604800 )	; TTL
; Indique que le nom du serveur faisant autorité pour la zone est serveur.ici.
; Si on a une configuration avec des esclaves, on peut mettre plusieurs enregistrements NS
@	IN	NS	serveur.ici.
; un enregistrement A (adresse IPv4) pour le nom "serveur"
serveur		IN	A	192.168.0.1
; un enregistrement AAAA (adresse IPv6) pour le nom "serveur"
serveur		IN	AAAA	2a21:2e35:8c57:24ef:bad:cafe:bad:caca
machinea	IN	A	192.168.0.2
machineb	IN	A	192.168.0.3
machinec	IN	A	192.168.0.4
machinec	IN	AAAA	2a21:2e35:8a57:24ef:babe:babe:babe:babe
; un alias de machined vers machinec
machined	IN	CNAME	machinec

Voici la zone inverse, stockée dans /etc/bind/db.192.168.0:

;
; Fichier de configuration de la zone DNS inverse locale
;
; Le TTL sera de 3 jours
$TTL 3d

; la zone est un peu spéciale. le "réseau inversé".in-addr.arpa.
; est une zone dédiée à la résolution inverse.
; le serveur d'autorité sera toujours notre serveur DNS.
0.168.192.in-addr.arpa.       IN      SOA     serveur.ici. admin.ici.     (
			  2014013101 ;Serial Number
					  8H ;refresh
					  2H ;retry
					  4W ;expire
					  1d)
; Le nom du serveur désservant la zone sera bien serveur.ici
@	  IN	NS      serveur.ici.
; ensuite on trouve les enregistrements de type PTR (pointeur)
1         IN      PTR     serveur.ici.
2         IN      PTR     machinea.ici.
3         IN      PTR     machineb.ici.
4         IN      PTR     machinec.ici.

Sécurité

Une partie de la sécurité est gérée au niveau de fichier de configuration de Bind. En effet, on a indiqué qu'on souhaitait que le serveur soit uniquement accédé par les adresses IPv4 du réseau local 192.168.0.0/24. Néanmoins, il faut bien avoir à l'esprit que si Bind est une implémentation de référence, elle a un lourd historique de failles de sécurité.

Il serait donc vraiment indispensable de fermer le port 53 à toute machine non autorisée. Voici quelques règles pour iptables au format iptables-save:

-A INPUT -s 192.168.0.0/24 -i eth0 -p tcp -m tcp --dport 53 -j ACCEPT
-A INPUT -s 192.168.0.0/24 -i eth0 -p udp -m udp --dport 53 -j ACCEPT
-A OUTPUT -o eth0 -p udp -m udp --dport 53 -j ACCEPT
-A OUTPUT -o eth0 -p tcp -m tcp --dport 53 -j ACCEPT

Et voici celles pour ip6tables:

-A INPUT -s 2a21:2e35:8c57:24ef::/64 -i eth0 -p tcp -m tcp --dport 53 -j ACCEPT
-A INPUT -s 2a21:2e35:8c57:24ef::/64 -i eth0 -p udp -m udp --dport 53 -j ACCEPT
-A OUTPUT -o eth0 -p udp -m udp --dport 53 -j ACCEPT
-A OUTPUT -o eth0 -p tcp -m tcp --dport 53 -j ACCEPT

Globalement, on ouvre le port 53 en entrée uniquement pour les machines du réseau local. Par contre on permet à toute requête DNS vers le port 53 de sortir. En effet, notre machine est également un résolveur DNS qui doit pouvoir interroger les autres serveurs DNS de référence.

Tests de fonctionnement

Maintenant que notre configuration est prête, il faut réaliser quelques tests:

Vous pouvez utiliser la commande dig issue du paquet dnsutils:

$ dig @ip_du_serveur_dns_à_tester nom_de_domaine_complet

Dans notre cas, voici quelques exemples:

$ dig @192.168.0.1 machinea.ici
; <<>> DiG 9.9.5-9-Debian <<>> @192.168.0.1 machinea.ici
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 43547
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 1, ADDITIONAL: 3

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;machinea.ici.		IN	A

;; ANSWER SECTION:
machinea.ici.		86400	IN	A	192.168.0.2

;; AUTHORITY SECTION:
ici.			86400	IN	NS	serveur.ici.

;; ADDITIONAL SECTION:
serveur.ici.		86400	IN	A	192.168.0.1
serveur.ici.		86400	IN	AAAA	2a21:2e35:8c57:24ef:bad:cafe:bad:caca

;; Query time: 2 msec
;; SERVER: 192.168.0.1#53(192.168.0.1)
;; WHEN: Thu Mar 26 20:32:47 CET 2015
;; MSG SIZE  rcvd: 123

On voit que la réponse est correcte: on obtient une section ANSWER avec l'adresse IP de la machine. On voit également que les renseignements sur la partie du serveur faisant autorité sont corrects. Vous pouvez également utiliser la commande host qui affiche moins de détails pour vérifier votre configuration effective de serveur DNS.

Configuration des machines clientes du réseau local

Une fois que le service DNS fonctionne correctement, il faut faire en sorte que les clients du réseau local l'utilisent. Deux méthodes sont disponibles:

Pour la méthode manuelle, je ne parlerai que de la distribution Debian. Vous pouvez utiliser le fichier /etc/resolv.conf pour paramétrer manuellement le service DNS. En voici un exemple:

domain ici
search ici
nameserver 192.168.0.1

Ici, le domaine auquel appartient la machine se nomme ici. Par défaut, la recherche DNS ajoutera l'extension .ici lors de la recherche des noms courts. Enfin, le serveur de nom aura pour adresse IP 192.168.0.1

Cette configuration manuelle peut également être répercutée dans le fichier /etc/network/interfaces à l'aide des quelques lignes à ajouter à la configuration d'une interface:

iface eth0 inet static
	address 192.168.0.10
	netmask 255.255.255.0
	network 192.168.0.0
	broadcast 192.168.0.255
dns-nameservers 192.168.0.1
dns-domain ici
dns-search ici

Mais on peut aller plus loin et indiquer au serveur DHCP de fournir cette information aux clients qui demandent une adresse IP. Il suffit d'ajouter les lignes suivantes dans la configuration du serveur DHCP (/etc/dhcp/dhcpd.conf):

# Options communes à tous les sous-réseaux gérés par le serveur DHCP.
## Définition de la résolution de nom
option domain-name "ici";
option domain-name-servers 192.168.0.4;

Conclusion

Avec relativement peu d'effort de configuration, on peut mettre en place un service DNS efficace pour un réseau chez soi. Même si le serveur DNS Bind peut (presque) tout faire, on voit bien qu'on peut limiter son périmètre pour gérer un seul serveur DNS centralisé qui fait tout. Cette configuration est adaptée pour les petits réseaux locaux, typiquement les réseaux qu'on retrouve dans la majorité des foyers de ce pays (et d'autres).

Utiliser un vrai service de cache de résolution DNS présente l'intérêt de limiter les requêtes DNS qui partent sur Internet.

Autre intérêt, le DNS étant un élément de base, conserver les logs du DNS permet de savoir à peu près ce que font les clients du réseau local. Quand on regarde les statistiques, on voit que ça n'est pas brillant: les clients font des requêtes DNS toutes les 5 secondes pour le même domaine: il n'y a pas de cache local. Un résolveur est donc quasi-indispensable sur un réseau local.

De plus, notre configuration permet d'avoir de vraies réponses DNS (genuine): le mensonge DNS n'est pas possible (sauf avec des moyens assez complexes à mettre en œuvre).

L'inconvénient de notre installation est qu'elle concentre en un seul point la gestion du DNS ce qui peut poser de sérieux problèmes lorsque le service devient inopérant (le serveur est cassé ou la configuration est perdue). En règle générale, pour les réseaux un peu plus sérieux, on délègue le service DNS à plusieurs machines. C'est expressément prévu dans Bind avec la notion de serveurs esclaves qui viennent régulièrement interroger le serveur maître.

Si vous souhaitez installer un serveur qui fait autorité sur Internet et non plus uniquement sur votre petit réseau local, sachez que vous DEVEZ disposer d'au moins un serveur de secours toujours disponible.

En attendant, vous pouvez vous faire la main avec la configuration de Bind en local à peu de frais…