Dans tous les emails reçus, OpenDKIM écrit « Authentication-Results [...] dkim=pass reason="XXXX-bit key; unprotected key" [...] ».
« unprotected key » ne signifie pas que la clé cryptographique utilisée est d'une taille insuffisante mais que, soit le domaine de l'émetteur n'est pas signé avec DNSSEC, soit la validation DNSSEC locale a foiré.
Pour rappel, DNSSEC c'est la signature cryptographique des enregistrements DNS. Cela permet d'en garantir l'authenticité et la non-altération (mais ça n'apporte pas la confidentialité, attention !). Dans le cas présent, si le serveur mail de destination veut vérifier la signature DKIM (que le serveur de mails émetteur a généré avec sa clé privée), il faut utiliser la clé publique associée. Comment trouver toutes les clés de tous les serveurs mails du monde ? Dans le DNS. Oui, mais cette clé publique peut être substituée par un tiers lors de sa récupération via le réseau par le serveur de mails destinataire. Ainsi, il est théoriquement possible d'émettre des mails usurpés en indiquant avec DKIM qu'ils proviennent bien du domaine usurpé, c'est-à-dire en masquant l'usurpation.
Bon, faut le dire tout de suite : on est dans une attaque de haut vol, qui cible deux canaux en même temps, etc. On est vraiment dans de l'attaque purement théorique, les spammeurs et autres arnaqueurs-ses par email utilisent des techniques beaucoup plus simples que ça, hein. D'autant plus que DNSSEC est très peu utilisé. D'autant plus que DKIM ne sert pas vraiment à grand-chose : les spammeurs savent en faire pour que leur merde passe partout. Le seul intérêt de faire du DKIM sur son serveur de mails perso est de ne pas se faire envoyer boulet par les géants du mail (Google, Microsoft, Yahoo, etc.) qui l'exigent de plus en plus.
Pour vérifier les signatures DNSSEC, on peut soit faire confiance au récursif DNS qui fait la validation des signatures pour vous, soit le faire en local. Dans le deuxième cas de figure, on peut effectuer la validation soit dans chaque programme, soit mutualiser ça dans une lib. Les devs d'OpenDKIM ont choisi d'utiliser une vérification locale, avec la libunbound (du nom du logiciel serveur récursif DNS Unbound). Le problème d'utiliser un récursif DNS validant est que, s'il n'est pas local, la validation peut être contournée. En crypto, c'est le problème du dernier kilomètre (voir http://www.bortzmeyer.org/ou-valider-dnsssec.html ).
Pour que la libunbound puisse effectuer les vérifications cryptographiques, il faut lui donner la clé publique de la racine DNS. Ainsi, on peut valider toute la chaîne, du domaine jusqu'à la racine. Un-e attaquant-e qui voudrait usurper mon enregistrement DKIM (ou autre, hein, mais je reste dans le sujet de ce shaarli) devrait le signer avec la clé privée associée à mon domaine (guiguishow.info.)… Mais il ne l'a pas. Et s'il signe avec une clé perso, le récursif validant verra que cette clé n'est pas celle que j'ai indiquée à mon domaine parent (exemple : info.) pour mon domaine (guiguishow.info.). Allez, soyons fous, l'attaquant-e refait un faux bout de la zone parente (info.). Même problème, il n'a pas la clé privée. Donc il fait encore une clé perso. Pas de bol, ça ne correspondra pas avec ce qu'il y a dans le domaine parent : la racine, « . ». Allez, il refait une clé bidon et un faux bout de la zone racine et… ça ne fonctionnera pas car le récursif DNS connaît la clé de la racine ICANN. Comment ? Elle était intégrée dans le package qui permet son installation (et OpenPGP garantit son intégrité). Il peut également la récupérer, en HTTPS, sur le site web de l'IANA ( https://data.iana.org/root-anchors/root-anchors.xml ).
Je trouve pénible et non pertinent le choix des devs d'OpenDKIM d'utiliser une lib plutôt qu'un récursif validant : dans n'importe quel système GNU/Linux, un récursif DNS est livré pré-configuré avec la validation DNSSEC activée, un exemplaire de la clé publique de la racine, le processus de rafraîchissement de la clé déjà configuré, etc. En gros : installer un tel serveur sur votre réseau local et vous êtes tranquilles. Alors qu'utiliser la libunbound impose de la configuration en plus, de mettre en place nous-mêmes le processus de rafraîchissement de la clé, etc. Sauf à installer Unbound sur la machine. No-way, je ne veux pas d'un récursif DNS sur chacun de mes serveurs de mails. Un seul sur le réseau de mes serveurs est suffisant !
Unbound et la libunbound ( cf https://www.unbound.net/documentation/libunbound-tutorial-6.html - « ub_ctx_set_option(ctx, "auto-trust-anchor-file:", "keys") (not shown in example) can be used to use auto-updated keys (with RFC5011), the file is read from and written to when the keys change. The probes have to be frequent enough to not lose track, about every 15 days. ») supportent la méthode de rafraîchissement des clés définie dans le RFC 5011. C'est pour cela qu'il est inutile de faire tourner Unbound-anchor en cron. ÉDIT DU 01/07/2016 À 15H00 : Heu, inutile si l'on fait tourner un démon Unbound mais si l'on utilise la libunbound, il faut un cronjob, la lib. FIN DE L'ÉDIT.
À côté de ça, il est conseillé d'utiliser Unbound-anchor (qui, en gros, si la clé livrée avec ne fonctionne pas, tente de récupérer la nouvelle sur le site web de l'IANA). Le manuel (https://www.unbound.net/documentation/howto_anchor.html) explique bien pourquoi : « Unbound uses RFC5011 updates to keep the anchor updated if it is changed while the computer is in operation, but the unbound-anchor tool is used if it is changed while the computer is not in operation. ». Là encore, on est sur un risque théorique qui a une faible probabilité de se produire sur un serveur qui est ON 99 % du temps mais bon, faisons les choses bien.
Allons-y pour la pratique. \o/
On installe unbound-anchor
sudo apt-get install unbound-anchor
On crée une nouvelle unit systemd qui lancera unbound-anchor, /etc/systemd/system/unbound-anchor.service , par exemple, qui contient :
[Unit]
Description=Update of the root trust anchor for DNSSEC validation in libunbound (for OpenDKIM)
Documentation=man:unbound-anchor(8)
[Service]
Type=oneshot
ExecStart=/usr/sbin/unbound-anchor -a /var/lib/dkim/dnssec.root.key
SuccessExitStatus=1
[Install]
WantedBy=multi-user.target
Cette unit est inspirée de https://bugzilla.redhat.com/attachment.cgi?id=983267&action=diff . Le dossier /var/lib/dkim doit exister. Perso, c'est là où je range la clé privée utilisée par OpenDKIM.
« SuccessExitStatus=1 » permet d'indiquer à systemd que, si ce programme quitte avec un code de retour 1, ce n'est pas une erreur. Cela s'ajoute au code de retour 0 habituel qui indique que tout s'est bien déroulé. En gros, Unbound-anchor peut sortir avec un code de retour = 0 ou 1. Et c'est conforme avec le manuel d'Unbound-anchor (http://linux.die.net/man/8/unbound-anchor ) : « This tool exits with value 1 if the root anchor was updated using the certificate or if the builtin root-anchor was used. It exits with code 0 if no update was necessary, if the update was possible with RFC5011 tracking, or if an error occurred. ». Bon, par contre utiliser le code de retour 0 pour dire à la fois que ça va bien et qu'il y a une erreur, c'est crade, vraiment. :(
On active la nouvelle unit :
sudo systemctl daemon-reload
sudo systemctl enable unbound-anchor.service
On lance la nouvelle unit et l'on vérifie que ça a bien créé le fichier /var/lib/dkim/dnssec.root.key :
sudo systemctl start unbound-anchor
On modifie l'initscript d'OpenDKIM pour qu'il se lance après Unbound-anchor : dans /etc/init.d/opendkim, on ajoute « unbound-anchor » à la fin de la ligne « Required-Start: ».
ÉDIT DU 15/01/2017 À 16H : L'idée est bonne mais lors d'une mise à jour d'opendkim, le script postinst d'opendkim (/var/lib/dpkg/info/opendkim.postinst) invoquera update-rc.d qui sortira en erreur :
insserv: Service unbound-anchor has to be enabled to start service opendkim
insserv: exiting now!
update-rc.d: error: insserv rejected the script header
Hé oui, il n'existe pas de script d'init au format sysvinit équivalent à notre unit systemd pour unbound-anchor. Soit on décide de remettre le script sysvinit d'opendkim dans son état d'origine le temps de faire un dpkg --configure -a
, soit on modifie le script postinst d'opendkim soit on créer un bête script sysvinit /etc/init.d/unbound-anchor … Perso, j'ai décidé de modifier le script postinst (et d'ajouter un divert dpkg) pour commenter la ligne update-rc.d opendkim defaults >/dev/null
.
FIN DE L'ÉDIT.
On indique à systemctl de prendre en compte la modif :
sudo systemctl daemon-reload
On vérifie que la modification a bien eu lieu :
systemctl show opendkim.service | grep -i after
« unbound-anchor.service » doit apparaître.
Pour que cette modification ne disparaisse pas lors d'une mise à jour d'OpenDKIM :
sudo dpkg-divert --add --rename --divert /etc/init.d/opendkim.dpkg-dist /etc/init.d/opendkim
sudo cp /etc/init.d/opendkim.dpkg-dist /etc/init.d/opendkim
sudo systemctl daemon-reload
On crée un cronjob dans /etc/cron.daily/unbound-anchor, par exemple avec le contenu suivant :
#!/bin/bash
logger -p user.notice -t unbound-anchor-cron "Updating DNS root key..."
/usr/sbin/unbound-anchor -a /var/lib/dkim/dnssec.root.key
exit 0
Ça ne sert à rien de vérifier le code de retour vu que 0 et 1 signifient une réussite et 0 peut signifier une réussite ou un échec, dixit le man… Grâce à la priorité (-p user.notice
), syslog enverra les logs de cette tâche cron dans /var/log/user.log.
On n'oublie pas de rendre ce script exécutable :
chmod +x /etc/cron.daily/unbound-anchor
On ajoute ce qui suit à la configuration d'OpenDKIM (/etc/opendkim.conf) :
# DNSSEC validation
ResolverConfiguration /etc/opendkim.libunbound.conf
On crée le fichier /etc/opendkim.libunbound.conf avec le contenu suivant :
server:
auto-trust-anchor-file: /var/lib/dkim/dnssec.root.key
On redémarre OpenDKIM :
sudo systemctl restart opendkim
Maintenant, OpenDKIM fait de la validation DNSSEC lorsqu'il récupère les clés publiques nécessaires à la validation des signatures DKIM. Dans le source des emails, il écrira : « Authentication-Results: [...] dkim=pass reason="XXXX-bit key; secure key" » à condition, évidemment, que le domaine de l'émetteur utilise DNSSEC.