Monter un serveur d’authentification multifacteur 1/3

YubiKey-4-EdgeLe simple mot de passe est de plus en plus souvent pointé du doigt comme ne présentant plus les garanties requises pour l'authentification. L'alliance FIDO montre d'ailleurs l'exemple en assurant le développement et la promotion d'une infrastructure grand public d'authentification double facteur.

Quels outils pour l'authentification double facteur ?

Il est néanmoins des cas (nombreux) où FIDO et son U2F ne fonctionneront pas pour vous : pour toutes les authentifications qui ne passent pas par un navigateur web.

Pour tout le reste, et en particulier ce qui passe par PAM sur les UNIX et Linux, il existe quelques outils. Google Authenticator fonctionne bien, mais il nécessite deux choses importantes : que la machine à laquelle vous vous connectez accède librement à internet, et que vous utilisiez un client Google Authenticator, en général sur téléphone malin. Pour quelqu'un qui va s'authentifier rarement, disons 1 à 5 fois par semaine, c'est supportable : initier la connexion, sortir et déverrouiller son téléphone, lancer l'application, recopier le code temporaire (OTP), fermer l'application.
Sur un an je m'authentifie en ssh ou dans sudo environ 4000 fois. C'est à dire en moyenne plus de 11 fois par jour, pour des semaines de 7 jours.
En réalité je travaille toujours un peu moins le week end. Il en résulte qu'une journée de travail peut voir des pics autour de 60 authentifications.
Utiliser une authentification double facteur basée sur une appli mobile ou tout autre appareil affichant un code à recopier serait un échec dans un tel contexte.

C'est là que la Yubikey entre en jeu. Cette petite clé USB programmable est vue comme un clavier par votre ordinateur, et sur pression du doigt elle va taper pour vous l'OTP nécessaire à l'auth double facteur.
Par défaut, comme Google Authenticator, elle s'appuie sur des serveurs externes pour valider votre OTP. Le jour où pour une raison X ou Y votre réseau n'accède plus à ces serveurs, vous ne pourrez donc plus vous authentifier. C'est particulièrement problématique si vous tentez justement de reprendre la main sur votre firewall qui a décidé de bloquer l'accès à internet.
Heureusement, Yubico met à disposition de la communauté les outils nécessaires à la création d'un serveur de validation et d'un serveur de stockage des clés, permettant ainsi aux plus exigeants d'entre-nous de monter leur propre infrastructure d'authentification double facteur interne.

Ces outils sont bruts de fonderie, un peu mal dégrossis, et les instructions pour leur mise en place sont parfois approximatives. Le sujet devient encore plus épineux quand on tente de faire l'installation sur FreeBSD. Voici donc quelques instructions basées sur les notes prises lors de mes errances.

Monter des serveurs d'authentification Yubico sur FreeBSD

L'infrastructure d'authentification double facteur Yubico est composée de deux services : le service de validation (YK-VAL), c'est à dire celui auquel votre OTP va être envoyé par PAM, et qui donnera la réponse à PAM, et le service de stockage des clés (YK-KSM) sur lequel les clés des utilisateurs sont enregistrées.
Pour des raisons évidentes, il est recommandé de sécuriser au maximum ces deux services. L'idéal est donc de les installer sur deux machines différentes, et d'appliquer de la sécurité en profondeur dessus : firewall en entrée et sortie, si possible un réseau privé entre les deux serveurs, HTTPS partout, etc.

proposition d'infrastructure YK-VAL YK-KSM

Prérequis : pour aller au bout du process, il vous faudra acheter au moins une clé Yubikey, n'importe laquelle supportant "Yubico OTP".

Pour les besoins de cette maquette, j'ai créé 3 machines virtuelles FreeBSD 10.3. Une d'entre elle sera le serveur de validation ykval.patpro.net, une autre sera le serveur de clé yksm.patpro.net, et la dernière sera un serveur sshd sur lequel je souhaite activer l'authentification double facteur.

Serveur de validation YK-VAL

Sur une installation relativement vierge de FreeBSD 10.3, j'installe les composants suivants :

pkg install apache24 mod_php56 php56-xml php56-curl php56-hash bash sudo mysql57-server php56-pdo_mysql ykclient

Attention, pour une raison que je n'ai plus en tête, l'installation du serveur YK-KSM n'est pas compatible avec mysql57-server. Si vous installez YK-VAL et YK-KSM sur la même machine, choisissez mysql56-server !

Je récupère aussi les sources du serveur de validation Yubico :

fetch https://github.com/Yubico/yubikey-val/archive/master.zip
unzip master.zip

Et je tente de suivre les instructions présentées sur la documentation officielle Yubico. Je vous encourage à lire cette documentation en parallèle avec ce blog, car je ne reprends pas ici l'intégralité des explications.

Premier souci, le Makefile n'est pas fonctionnel sur FreeBSD, et j'ai du faire l'installation manuellement. J'ai tenté de scripter les choses un tout petit peu en créant automatiquement les répertoires manquants :

for DIR in $(awk '/prefix = / {print $3}' Makefile); do
	[ -d $DIR ] || mkdir -p $DIR
done

Puis en créant une série de variables qu'il faut injecter dans bash :

awk '/prefix = / {print $1"="$3}' Makefile
etcprefix=/etc/yubico/val
sbinprefix=/usr/sbin
phpprefix=/usr/share/yubikey-val
docprefix=/usr/share/doc/yubikey-val
manprefix=/usr/share/man/man1
muninprefix=/usr/share/munin/plugins
wwwprefix=/var/www/wsapi

Ensuite on peut copier les fichiers vers leur destination :

cp ykval-verify.php ${phpprefix}/ykval-verify.php
cp ykval-common.php ${phpprefix}/ykval-common.php
cp ykval-synclib.php ${phpprefix}/ykval-synclib.php
cp ykval-sync.php ${phpprefix}/ykval-sync.php
cp ykval-resync.php ${phpprefix}/ykval-resync.php
cp ykval-db.php ${phpprefix}/ykval-db.php
cp ykval-db-pdo.php ${phpprefix}/ykval-db-pdo.php
cp ykval-db-oci.php ${phpprefix}/ykval-db-oci.php
cp ykval-log.php ${phpprefix}/ykval-log.php
cp ykval-queue ${sbinprefix}/ykval-queue
cp ykval-synchronize ${sbinprefix}/ykval-synchronize
cp ykval-export ${sbinprefix}/ykval-export
cp ykval-import ${sbinprefix}/ykval-import
cp ykval-gen-clients ${sbinprefix}/ykval-gen-clients
cp ykval-export-clients ${sbinprefix}/ykval-export-clients
cp ykval-import-clients ${sbinprefix}/ykval-import-clients
cp ykval-checksum-clients ${sbinprefix}/ykval-checksum-clients
cp ykval-checksum-deactivated ${sbinprefix}/ykval-checksum-deactivated
cp ykval-nagios-queuelength.php ${sbinprefix}/ykval-nagios-queuelength
cp ykval-queue.1 ${manprefix}/ykval-queue.1
cp ykval-synchronize.1 ${manprefix}/ykval-synchronize.1
cp ykval-import.1 ${manprefix}/ykval-import.1
cp ykval-export.1 ${manprefix}/ykval-export.1
cp ykval-gen-clients.1 ${manprefix}/ykval-gen-clients.1
cp ykval-import-clients.1 ${manprefix}/ykval-import-clients.1
cp ykval-export-clients.1 ${manprefix}/ykval-export-clients.1
cp ykval-checksum-clients.1 ${manprefix}/ykval-checksum-clients.1
cp ykval-checksum-deactivated.1 ${manprefix}/ykval-checksum-deactivated.1
cp ykval-munin-ksmlatency.php ${muninprefix}/ykval_ksmlatency
cp ykval-munin-vallatency.php ${muninprefix}/ykval_vallatency
cp ykval-munin-queuelength.php ${muninprefix}/ykval_queuelength
cp ykval-munin-responses.pl ${muninprefix}/ykval_responses
cp ykval-munin-ksmresponses.pl ${muninprefix}/ykval_ksmresponses
cp ykval-munin-yubikeystats.php ${muninprefix}/ykval_yubikeystats
cp ykval-db.sql ${docprefix}/ykval-db.sql
cp ykval-db.oracle.sql ${docprefix}/ykval-db.oracle.sql

À ce stade on a installé différents scripts php dont le shebang est réglé sur un chemin "linux". On pourrait modifier tous les shebangs, mais il est plus simple de créer un lien symbolique de php au bon endroit :

ln -s /usr/local/bin/php /usr/bin/

Ensuite on s'attaque à la configuration de la base de données. J'ai opté pour MySQL, avec lequel je suis plus à l'aise.
Les étapes de la documentation officielle fonctionnent assez bien, mais les droits alloués à l'utilisateur ykval_verifier sont insuffisants. Il faut en effet lui donner les droits d'ajouter des clients dans la table ykval.clients. J'ai donc remplacé la ligne :

GRANT SELECT(id, secret, active) ON ykval.clients TO 'ykval_verifier'@'localhost';

par :

GRANT SELECT,INSERT,UPDATE ON ykval.clients TO 'ykval_verifier'@'localhost';

L'étape 4 de la doc. se résume à ces trois commandes :

mkdir /var/www/wsapi/2.0
ln -sf /usr/share/yubikey-val/ykval-verify.php /var/www/wsapi/2.0/verify.php
ln -sf /usr/share/yubikey-val/ykval-sync.php /var/www/wsapi/2.0/sync.php

L'étape 5 consiste à créer l'endroit où seront stockées les préférences de l'application, et à s'assurer qu'elles seront lisibles par Apache.

mkdir /etc/default
vi /etc/default/ykval-queue
ajouter la ligne : DAEMON_ARGS="/etc/yubico/val:/usr/share/yubikey-val"
vi /var/www/wsapi/2.0/.htaccess
ajouter les lignes suivantes : 
	RewriteEngine on
	RewriteRule ^([^/\.\?]+)(\?.*)?$ $1.php$2 [L]
	<IfModule mod_php5.c>
	  php_value include_path ".:/etc/yubico/val:/usr/share/yubikey-val"
	</IfModule>
cd /var/www/wsapi
ln -s 2.0/.htaccess /var/www/wsapi/.htaccess

À l'étape 6, il faut récupérer l'exemple de fichier de configuration ykval-config.php fourni avec le code de l'application, et le copier au bon endroit :

cp ykval-config.php /etc/yubico/val/

On édite ce fichier pour renseigner le mot de passe MySQL.

L'étape 7 permet de configurer Apache. Les instructions présentées sont valides pour apache 2.2, mais en version 2.4 il y a quelques modifications à faire. Notamment, autoriser l'accès se fait par la formule Require all granted. Il faut aussi activer le mod_rewrite qui n'est pas actif par défaut sur FreeBSD.

C'est l'occasion de configurer proprement le virtual host, d'activer SSL, etc.

Pour configurer syslogd lors de l'étape 8, il suffit d'ajouter la ligne suivante à /etc/syslog.conf :

local0.*              /var/log/ykval.log

et de faire un touch /var/log/ykval.log && service syslogd restart.

La rotation automatique pourra être gérée dans /etc/newsyslog.conf.

J'ai sauté l'étape 9, car je n'ai pas souhaité mettre en place de synchronisation entre différents YK-VAL pour cette maquette. C'est néanmoins une étape à valider si vous souhaitez mettre en place une infrastructure utilisable en production (ie. redondante).

À ce stade vous pourrez passer à l'étape 11. Cette dernière donnera sans doute lieu à de nombreux petits ajustements : config Apache, droits de fichiers, etc. Pas de panique, les logs fournissent normalement pas mal de pistes (notamment /var/log/ykval.log), et vous ne seriez pas ici si vous n'étiez pas capable de vous en sortir.

curl 'http://ykval.patpro.net/wsapi/2.0/verify?id=1&nonce=asdmalksdmlkasmdlkasmdlakmsdaasklmdlak&otp=dteffujehknhfjbrjnlnldnhcujvddbikngjrtgh'
h=CqcOcOH1yokoXcDimKDnElyP+f8=
t=2016-02-18T17:47:56Z0853
status=NO_SUCH_CLIENT

Lire la suite : la création du serveur YK-KSM.

Un shell taillé pour le roaming, la 3G et les cages d’ascenseur

Les fans de la ligne de commande, ou ceux qui sont obligés de s'en servir, le savent : garder un shell distant ouvert et réactif peut parfois être très compliqué, voire impossible. Vous lancez une manip un peu longue mais après quelques temps vous devez changer de réseau. Vous devez intervenir d'urgence via une connexion mobile de très mauvaise qualité. Vous êtes coincé dans un ascenseur avec quelques kbps de bande passante sur un WIFI intermittent. Autant de raisons qui vous feront aimer mosh.

passer-sur-mosh
Mosh pour Mobile Shell, est un outil client et serveur qui supporte le roaming, et qui fonctionne particulièrement bien sur les connexions de mauvaise qualité.
L'architecture de mosh est bien pensée : elle ne nécessite pas d'intervention lourde sur vos serveurs pour en permettre l'utilisation et elle s'appuie sur un serveur ssh existant pour gérer l'initialisation de la connexion, dont la phase d'authentification.

Vous devez simplement installer mosh sur votre poste et sur votre serveur. À partir de là, la connexion fonctionne selon ce principe :
- vous lancez mosh en pointant sur votre login@serveur
- mosh ouvre une connexion ssh login@serveur ce qui permet de procéder à votre authentification
- dès que vous êtes authentifié, mosh lance mosh-server sur le serveur, sous votre UID
- mosh ferme la connexion ssh, et passe la main au mosh-client
- mosh-client se connecte en UDP sur votre mosh-server distant.

Bien sûr, les paquets UDP sont chiffrés, garantissant la sécurité de la connexion. Mosh dispose aussi d'une fonction local-echo très utile sur les liens à forte latence : vous voyez ce que vous tapez immédiatement, contrairement à ce qu'il se passe sur une connexion ssh, où votre entrée devra arriver sur le serveur, être traitée, être renvoyée à votre terminal puis enfin affichée. Cela ajoute un agrément très appréciable sur les lignes très lente ou de mauvaise qualité.

Aussi, l'utilisation de l'UDP, un protocole non-connecté, autorise une implémentation simple de l'itinérance. Plus de problème quand votre portable change de réseau Wifi, passe de l'ethernet à la 4G, etc. Votre connexion avec le serveur reste ouverte, et utilisable dès que le réseau est disponible.

J'encourage vivement les curieux à approfondir les aspects techniques, c'est intéressant et innovant.
Attention cependant, mosh ne supporte pas les utilisations non-intéractives de ssh : scp, les tunnels, X-Forwarding… C'est vraiment fait - actuellement - pour remplacer la bonne vieille session interactive.

Les binaires sont disponibles pour de nombreuses plateformes, même pour Mac OS X 10.5... vous n'avez plus aucune excuse.

Mise à jour FreeBSD : impact sur SSL/TLS

Dans le cadre de mon travail, j'administre quelques serveurs de messagerie à fort trafic. Certains tournent sous FreeBSD. Toute modification de ces serveurs, clés de voûte de notre infrastructure de messagerie, doit se faire avec prudence et en comparant leur fonctionnement avant et après le changement. Je suis aidé en cela par Splunk, un collecteur de logs qui me permet de faire des analyses poussées.
Splunk est notamment très doué pour afficher des tendances, des évolutions, et permettre de représenter quelques secondes ou minutes vos données sous forme graphique. Notamment, à chaque mise à jour de l'antispam, de l'antivirus, de Perl ou d'autres composants critiques, il me permet de vérifier en quelques instants que le temps de traitement des messages dans les différents filtres n'est pas dégradé par la mise à jour.
La montée de version de FreeBSD 9.x vers FreeBSD 10.1 m'a notamment permis de bénéficier d'une version d'OpenSSL plus récente, et supportant TLS 1.2. Ce changement n'est pas anodin. SSL est mort, TLS 1.0 suit le même chemin, il faut donc pouvoir utiliser TLS 1.2.
Voyons l'impact de cette mise à jour sur les échanges chiffrés avec les autres serveurs de messagerie (ici, uniquement les messages sortants) :

Évolution de la version de TLS dans les échanges suite à la mise à jour du système

Évolution de la version de TLS dans les échanges suite à la mise à jour du système

Évolution des algorithmes de chiffrement dans les échanges suite à la mise à jour du système

Évolution des algorithmes de chiffrement dans les échanges suite à la mise à jour du système

Cette rapide analyse ne porte que sur les échanges chiffrés : les échanges de mails avec les serveurs ne permettant pas le chiffrement TLS ne sont pas représentés. La passerelle de messagerie dont il est question ici fait tourner trois instances postfix en "postfix-multi", et voit transiter trois à cinq millions de messages par mois selon la période de l'année.

ZFS primary cache is good

Last year I've written a post about ZFS primarycache setting, showing how it's not a good idea to mess with it. Here is a new example based on real world application.
Recently, my server crashed, and at launch-time Splunk decided it was a good idea to re-index a huge apache log file. Apart from exploding my daily index quota, this misbehavior filed the index with duplicated data. Getting rid of 1284408 events in Splunk can be a little bit resource-intensive. I won't detail the Splunk part of the operation: I've ended up having 1285 batches of delete commands that I've launched with a simple for/do/done bash loop. After a while, I noticed that the process was slow and was making lots of disk IOs. Annoying. So I checked:

# zfs get primarycache zdata/splunk
NAME          PROPERTY      VALUE         SOURCE
zdata/splunk  primarycache  metadata      local

Uncool. This setting was set locally so that my toy (Splunk) would not harvest all ARC from the server, hurting production. For efficiency's sake, I've switched back the primary cache to all:

# zfs set primarycache=all zdata/splunk

Effect was almost instantaneous: ARC filled with Splunk data and disk IOs plummeted.

primarycache # of deletes per second
metadata 10.06
all 22.08

A x2.2 speedup on a very long operation (~20 hours here) is a very good argument in favor of primarycache=all for any ZFS user.

acceleration of a repetitive splunk operation thanks to ZFS primarycache setting

Acceleration of a repetitive splunk operation thanks to ZFS primarycache setting

Rotation des clés DKIM

La signature des emails par clé DKIM est une composante importante d'un système de messagerie sain et bien géré. Comme tout système cryptographique fonctionnant sur la base d'un couple clé publique / clé privée, il est nécessaire de renouveler les jeux de clés régulièrement.
En effet, plus longtemps une même clé est utilisée, plus elle a de chance d'être cassée. De la même manière, plus elle est petite, plus elle sera facilement cassée.
Comme toujours, avec la correspondance par email, rien n'est instantané. Contrairement à un serveur web par exemple qui pourrait changer de certificat SSL en quelques minutes et faire immédiatement disparaître l'ancien de la circulation, les clés DKIM doivent changer selon un roulement tuilé : quand la nouvelle clé remplace l'ancienne, cette dernière doit rester disponible encore quelques temps pour permettre la validation des signatures présentes dans des messages encore en cours de livraison.
Ainsi, le besoin de faire tourner régulièrement les clés DKIM tout en garantissant un tuilage, ajouté à la complexité (relative) d'une architecture mixte à base de serveur DNS, serveur de mail, logiciel de signature DKIM, rendent nécessaire la mise en place d'une procédure claire, fiable et automatique.

Évolution de l'utilisation de DKIM mesurée au niveau de l'antispam en entrée de mon serveur de mails

Évolution de l'utilisation de DKIM mesurée au niveau de l'antispam en entrée de mon serveur de mails, entre fin 2005 et début 2015

Pour mes propres besoins, j'ai conçu un script shell utilisable pour faire tourner les clés d'un domaine donné. Ce script s'appuie sur un serveur Bind local : la modification des zones DNS se fait par modification directe des fichiers. Il utilise l'implémentation DKIM de OpenDKIM.
Il serait tout à fait possible de concevoir un script qui utilise un serveur DNS distant ou local via la commande nsupdate(8), mais cette approche laisse la main totalement à Bind pour l'écriture des fichiers de zones. Il fait alors disparaître tous les commentaires, il change l'ordre des enregistrements, etc. Ce n'était pas souhaitable pour moi qui stocke des informations en commentaire dans mes fichiers de zones.

Prérequis pour utiliser ce script

- Être root sur le serveur ;
- Disposer d'un serveur DNS Bind, ou compatible (c'est OpenDKIM qui écrit les enregistrements DNS), maître pour les zones concernées par la rotation de clés ;
- Disposer sur la même machine d'une installation OpenDKIM fonctionnelle ;
- Avoir une version de sed qui supporte l'option -i (in place).

Comportement

Le premier argument obligatoire du script est l'action : renouvellement ("new") ou nettoyage ("clean"). Le second argument obligatoire est le domaine cible et le troisième argument (facultatif) est la longueur des nouvelles clés.
Pour l'action "new" et un domaine donné, le script renouvelle l'ensemble des clés DKIM. Par défaut, il génère des clés de la même longueur que celles qu'il remplace, et les sélecteurs sont nommés en `date "+%Y%m"` suivi d'un tiret et des 8 premiers caractères retournés par la commande uuidgen(1). Si une longueur de clé est imposée en argument, l'ensemble des clés du domaine utilisera cette nouvelle valeur. Pour l'action "clean" et un domaine donné, le script vidange les anciennes clés.
Pour assurer un tuilage d'une semaine par exemple, il faut donc le jour J lancer le script avec "new", et à J+7 lancer le script avec "clean" sur le même domaine. Ainsi, avant le jour J, la clé N est utilisée pour signer les messages. À partir du jour J, la clé N+1 est utilisée pour signer les messages, mais la clé N est encore disponible pour vérifier de vieux messages qui seraient encore en circulation. À partir de J+7, l'ancienne clé N disparaît.

Syntaxe des fichiers de zone DNS

Deux contraintes ici :
- Le numéro de série de la zone doit être seul sur sa ligne et suivi du commentaire " ; Serial" pour que le script puisse facilement le localiser et l'incrémenter.
- Les enregistrements DNS des clés DKIM doivent être en inclusion dans le fichier de la zone et résider respectivement dans le fichier dkim-active.${DOMAIN} pour les clés actives et dans le fichier dkim-retired.${DOMAIN} pour les clés inactives. Cela donne pour le fichier de zone patpro.net :

...
$INCLUDE "/etc/namedb/master/dkim-active.patpro.net"
$INCLUDE "/etc/namedb/master/dkim-retired.patpro.net"
...

Ainsi, le script peut très facilement déplacer les clés actives vers le fichier dkim-retired quand de nouvelles clés sont fabriquées et intégrées à dkim-active.

OpenDKIM

Les clés doivent être rangées par domaine, dans des répertoires qui portent le nom du domaine. Cela donne chez moi :

opendkim/patpro.net/201501-62e79011.private
opendkim/patpro.net/201501-62e79011.txt
opendkim/patpro.net/201501-82a3edc5.private
opendkim/patpro.net/201501-82a3edc5.txt
opendkim/proniewski.net/201502-e47af46c.private
opendkim/proniewski.net/201502-e47af46c.txt

Les clés utilisées doivent être référencées dans un fichier KeyTable unique que le script analyse pour retrouver les sélecteurs à remplacer, et réécrit avec les nouveaux sélecteurs.

Utilisation

crontab pour renouvellement automatique mensuel :

0 0 1 * *    /usr/local/sbin/rotate-dkim.sh new patpro.net
0 0 8 * *    /usr/local/sbin/rotate-dkim.sh clean patpro.net

ligne de commande pour changer la taille des clés d'un domaine :

rotate-dkim.sh new proniewski.net 2048

Corpus Scripti

#!/usr/local/bin/bash
# rotate DKIM keys
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
# settings
ACTION=${1}
DOMAIN=${2}
NBITS=${3}; NBITS=$(( $NBITS+0 )) # force number
CHROOT=/var/db/opendkim
GENKEY=/usr/local/sbin/opendkim-genkey
KeyTable=/usr/local/etc/opendkim/KeyTable
ZONEROOT=/etc/namedb/master
USERGROUP=root:opendkim
ARCHIVE=/tmp/
MAXKEY=4096
MINKEY=512

onerror() {
case "$1" in
	"args")
		echo "usage: $(basename $0) new|clean domain.name [bits]"; exit 1
		;;
	"chroot")
		echo "\$CHROOT must exist and be a directory"; exit 1
		;;
	"genkey")
		echo "opendkim-genkey not found or not executable"; exit 1
		;;
	"ktable")
		echo "KeyTable not found"; exit 1
		;;
	"zoneroot")
		echo "\$ZONEROOT must exist and be a directory"; exit 1
		;;
	"usergroup")
		echo "User and group must exist"; exit 1
		;;
	"archive")
		echo "\$ARCHIVE must exist and be a directory"; exit 1
		;;
	"nbits")
		echo "Bits length must be greater than $MINKEY, and should not be greater than $MAXKEY"; exit 1
		;;
esac
}

# Checks
[ $# -ge 2 ] || onerror "args"
[ ${ACTION} = "new" -o ${ACTION} = "clean" ] || onerror "args"
[ -d ${CHROOT} ] || onerror "chroot"
[ -d ${ZONEROOT} ] || onerror "zoneroot"
[ -d ${ARCHIVE} ] || onerror "archive"
[ -x ${GENKEY} ] || onerror "genkey"
[ -f ${KeyTable} ] || onerror "ktable"
pw usershow ${USERGROUP/:*/} >/dev/null 2>&1 || onerror "usergroup"
pw groupshow ${USERGROUP/*:/} >/dev/null 2>&1 || onerror "usergroup"
if [ $NBITS -ne 0 ]; then
	[ $NBITS -gt $MINKEY ] || onerror "nbits"
	[ $NBITS -le $MAXKEY ] || onerror "nbits"
fi


archive() {
	echo "Putting away files for old DKIM selector ${1}"
	mv -v ${CHROOT}/${DOMAIN}/${1}.* ${ARCHIVE}
}

if [ ${ACTION} = "new" ]; then
	# Keytable: update SELECTOR
	# structure: 
	# arbitrary-name signing-domain:selector:keypath
	# for each line matching DOMAIN: 
	# - get SELECTOR. 
	# - key path derived from CHROOT/SELECTOR

	# get SELECTOR for DOMAIN:
	SELECTORLIST=( $(awk '/^[^#].* '${DOMAIN}':/ {split($2,a,":"); print a[2]}' ${KeyTable}) )

	# for each SELECTOR we must:
	# - destroy/archive private key 
	# - create a new SELECTOR + key + DNS
	# - rewrite KeyTable
	echo ""
	echo "Selector list for domain $DOMAIN has ${#SELECTORLIST[*]} member-s, lets renew each one:"
	for OLDSELECTOR in ${SELECTORLIST[@]}; do
		if [ $NBITS -ne 0 ]; then
			echo "Bits length set on command line to ${NBITS}, overriding current private key bits length"
			BITS=${NBITS}
		else
			echo -n "Get bits length for private key ${OLDSELECTOR}.private: "
			BITS=$(openssl rsa -in ${CHROOT}/${DOMAIN}/${OLDSELECTOR}.private -text -noout | awk '/Private-Key/ {gsub(/[^0-9]/,"",$2); print $2}')
			echo "$BITS"
		fi		
		echo -n "Create new selector: "
		UUID=$(uuidgen)
		SELECTOR="$(date "+%Y%m")-${UUID/-*/}"
		echo "${SELECTOR}"
		echo "Create new private key with ${SELECTOR}:"
		${GENKEY} -v -b ${BITS} -d ${DOMAIN} -D ${CHROOT}/${DOMAIN} -r -s ${SELECTOR}
		# permission adjustments:
		chown ${USERGROUP} ${CHROOT}/${DOMAIN}/${SELECTOR}.*
		chmod u=r,g=r ${CHROOT}/${DOMAIN}/${SELECTOR}.*
		ls -l ${CHROOT}/${DOMAIN}/${SELECTOR}.*
		echo -n "Update KeyTable: "
		sed -i .bkp -e 's/'$OLDSELECTOR'/'${SELECTOR}'/g' ${KeyTable}
		[ $? -eq 0 ] && echo "OK"
		archive ${OLDSELECTOR}
	done
	echo ""
	echo "End of selector renewal."
	# every SELECTOR for DOMAIN is updated, now deal with DNS
fi

echo ""
# calculate new serial for zone $DOMAIN
echo -n "Get old DNS zone serial for ${DOMAIN} and increment: "
NEWSERIAL=$(date +%Y%m%d%I)
OLDSERIAL=$(awk '/ ; Serial/ {print $1}' ${ZONEROOT}/${DOMAIN})
while [ $OLDSERIAL -ge $NEWSERIAL ]; do
	let NEWSERIAL=(NEWSERIAL+1)
done
echo "$OLDSERIAL -> $NEWSERIAL"
#
# Update SERIAL of DNS zone.
echo -n "Change serial in zone file: "
sed -i .bkp -e 's/'$OLDSERIAL' ; Serial/'${NEWSERIAL}' ; Serial/g' ${ZONEROOT}/${DOMAIN}
[ $? -eq 0 ] && echo "OK"

if [ ${ACTION} = "new" ]; then
	echo -n "Move old public keys to dkim-retired.${DOMAIN}: "
	cat ${ZONEROOT}/dkim-active.${DOMAIN} > ${ZONEROOT}/dkim-retired.${DOMAIN}
	[ $? -eq 0 ] && echo "OK"
	echo -n "Copy new public keys to dkim-active.${DOMAIN}: "
	cat ${CHROOT}/${DOMAIN}/*.txt > ${ZONEROOT}/dkim-active.${DOMAIN}
	[ $? -eq 0 ] && echo "OK"
elif [ ${ACTION} = "clean" ]; then
	echo -n "Clean old public keys of dkim-retired.${DOMAIN}: "
	echo "" > ${ZONEROOT}/dkim-retired.${DOMAIN}
	[ $? -eq 0 ] && echo "OK"
fi

echo ""
echo "Everything looks good, lets refresh world:"
named-checkzone ${DOMAIN} ${ZONEROOT}/${DOMAIN}
service named restart
[ ${ACTION} = "new" ] && service milter-opendkim restart

Les commentaires dans le code sont suffisants pour expliquer chaque étape du déroulement du script.

Bibliographie

DKIM
M3AAWG DKIM Key Rotation Best Common Practices (PDF)

Création de mots de passe en ligne de commande

keychainLa création de nouveaux mots de passe est une activité assez peu passionnante, d'autant que dans certains contextes on peut être amené à répéter l'opération plusieurs fois par jour. Plutôt que d'avoir à réfléchir - et donc de produire un résultat influencé par les travers de l'esprit humain - il est préférable de s'en remettre totalement à la machine.
L'aléa est bien meilleur, et le résultat est obtenu bien plus facilement. Il est bien sûr question ici de vrais mots de passe : large palette de caractères, longueur correcte, totalement aléatoires.

Voici deux fonctions assez simples qui trouveront leur place tout naturellement dans votre .bashrc ou .bash_profile sur *BSD (FreeBSD, Mac OS X, etc.). La seule condition étant de disposer des commandes jot et rs déjà présentées dans les articles "jot et seq, créer des séquences en ligne de commande" et surtout "Les bons mots de passe…".

Cette première fonction crée 5 mots de passe aléatoires dont les caractères sont pris entre les codes ASCII 32 (espace) et 126 (tilde). Par défaut, ces mots de passe font 25 caractères de long, mais la commande génère en fait un tirage de 500 caractères, ce qui permet de fabriquer 5 mots de passe de 100 chiffres, lettres, symboles en indiquant la longueur souhaitée comme paramètre. L'astuce ici tient dans l'utilisation de la tabulation comme séparateur en sortie de jot, ce qui permet d'avoir l'espace présent dans les mots de passe en sortie.

mdp-complexe() {
jot -s $'\t' -r -c 500 32 126 | rs -c$'\t' -g0 0 ${1-25} | head -5
}

Exemple d'utilisation :

$ mdp-complexe 
fnc\Y}02PSc6}{;0`T}LOeN?\
5W Y1Y8!o<VlTA2vuE}SU)g?]
'kGhRa=u|hJW#;6aDd[A&UR_/
n|$_f3S?[`pO2":+0e<;aTi\v
.nU\i.PhbPE5TfuXY)g+DiN~g

En imposant la longueur :

$ mdp-complexe 50
9Tu1StsL $Gk@K<X\R)xKv{JXYe|g>L^2T;|*GMASee@HX.Epj
T%2 N;VvLFVi]s>_~xpo^~vD8b2.Fb$02ay9sLgjW#,TQ>8JAO
7EyWUC)Cqm2F,72mIf$#vh3<sTx%j>*v't{Fump=Fb5cJeZir$
RfS638d~a.T#StF.t3%Z*`RG)/?#; H?liH2DielVvs#4IBp0a
G`OyIOzpYtt5Sh!92/zdp7^C98r.OtIV8Fk_ilYCvVI6~e@17T

Cette seconde fonction est basée sur la première, mais un filtrage est imposé à la sortie de jot pour ne garder qu'un sous-ensemble des caractères générés. Ici, les caractères a-z, A-Z, 0-9, _, / et - sont conservés. L'approche est relativement simple : il suffit d'ajouter à la liste entre [ ] les caractères que l'on souhaite conserver dans les mots de passe :

mdp-simple() {
jot -s $'\t' -r -c 500 32 126 | sed "s,[^a-zA-Z0-9_/-]"$'\t'",,g"|\
 rs -c$'\t' -g0 0 ${1-25} | head -5
}

Ce qui donne :

$ mdp-simple 
y0q/EKoZKN9ZqGGhr1triaB0l
VMFUNYNpopAu9NleSv0BxuuTh
W07eE4ef1A99yAl0UmP6EHo6_
9X4CWx7qgML6qxn9Y0nxhONpr
lw_eY4DYY414mrllNxe/FSUfM

Stockez ensuite le mot de passe choisi dans votre trousseau de mots de passe sécurisé.

Créer un trou noir DNS

Même si la question a déjà été largement traitée, je livre ici ma propre version d'un trou noir DNS. Le DNS blackhole, en anglais dans le texte, permet par exemple de s'assurer (dans une certaine mesure) que les utilisateurs de votre réseau ne peuvent pas se connecter trop facilement à des sites web vecteurs d'infection.
Le fonctionnement est simple : vous avez un réseau local, chez vous ou dans votre entreprise, et vous mettez à disposition de vos utilisateurs un serveur DHCP pour que les équipements qui se connectent puissent obtenir une adresse IP. Je pars du principe que sur ce réseau local, vous disposez aussi d'un serveur DNS. Un utilisateur se connecte avec votre réseau : le serveur DHCP lui donne une adresse IP pour que la nouvelle machine puisse parler sur le réseau, et fourni aussi l'adresse de votre serveur DNS. C'est donc par ce serveur que vous maitrisez que la machine de l'utilisateur va convertir les noms de domaine (www.patpro.net) en adresse IP (193.30.227.216).
Comme vous avez la maîtrise du serveur DNS, vous avez la maîtrise de la résolution des noms de domaine. Vous pouvez donc décider de bloquer la résolution de certains noms de sites web qui posent problème (gros pourvoyeurs de malware, régies publicitaires, facebook, etc.).

Je vais me baser ici sur la liste de malwaredomains.com qui regroupe environ 20000 noms de domaine qui sont, ou ont été, impliqués dans la distribution de malware (en général en infectant le visiteur imprudent). Il s'agit donc pour moi de défendre mon réseau en protégeant les utilisateurs.

Les informations sont largement inspirées - voire pompées - de la prose anglophone de Paul.

Pré-requis : un serveur DNS BIND que vous maîtrisez, et un réseau avec serveur DHCP qui indique l'adresse IP du DNS, de sorte que les clients qui se connectent utiliseront par défaut le serveur DNS sus-mentionné. Il faut bash, curl, et jot (sur linux il faut remplacer jot par seq).

Obtenir la liste des zones DNS à mettre en trou noir.

Préparer le lieu de stockage :

mkdir /etc/namedb/blackhole

J'utilise le script suivant, à lancer en root via une crontab (une fois par semaine suffit) :

#!/usr/local/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
ZONEFILE=/etc/namedb/blackhole/spywaredomains.zones
MIRRORS[1]="http://mirror1.malwaredomains.com/files/spywaredomains.zones"
MIRRORS[2]="http://mirror2.malwaredomains.com/files/spywaredomains.zones"
MIRRORS[3]="http://dns-bh.sagadc.org/spywaredomains.zones"

# fonctionne sur BSD uniquement, pour linux il faut
# remplacer par `seq`
CHOOSE=$(jot -r 1 1 ${#MIRRORS[@]})

if [[ "$(/usr/local/bin/curl ${MIRRORS[CHOOSE]} -z ${ZONEFILE} -o ${ZONEFILE}_new -s -L -w %{http_code})" == "200" ]]; then
	rm -f ${ZONEFILE}_old
	mv ${ZONEFILE} ${ZONEFILE}_old
	mv ${ZONEFILE}_new ${ZONEFILE}
	named-checkconf -z | grep -v ": loaded serial"
	if [ ${PIPESTATUS[0]} -eq 0 ]; then
		service named restart
	else
		echo "Error loading new zone file, reversing..."
		mv ${ZONEFILE} ${ZONEFILE}_invalid_$(date "+%Y%m%d")
		mv ${ZONEFILE}_old ${ZONEFILE}
	fi
fi

C'est loin d'être foolproof, mais ça fait le travail. Les options de curl permettent de ne télécharger le fichier que si il a été modifié depuis votre dernière mise à jour. le script choisit un miroir au hasard au moment de faire le comparatif ce qui évite de charger toujours le même serveur.

Configurer BIND pour gérer les zones téléchargées.

Il faut ajouter dans la configuration du serveur DNS une directive d'inclusion du fichier téléchargé ci-dessus. Il suffit pour cela d'ajouter la ligne suivante à la fin du fichier /etc/named/named.conf :

 include "/etc/namedb/blackhole/spywaredomains.zones";

Le fichier spywaredomains.zones fait pointer chaque zone vers un unique fichier de définition qu'il convient de créer et de renseigner : /etc/namedb/blockeddomain.hosts.
Ce fichier contient une définition minimaliste de zone DNS BIND. Faites-la pointer vers un serveur web existant, cela vous permettra d'afficher à vos utilisateurs une page web d'explication :

$TTL    86400   ; one day
@       IN     SOA    NOM-DE-VOTRE-SERV-DNS. NOM-DE-VOTRE-SERV-DNS. (
	1
	28800   ; refresh  8 hours
	7200    ; retry    2 hours
	864000  ; expire  10 days
	86400 ) ; min ttl  1 day

	NS     NOM-DE-VOTRE-SERV-DNS.

	       A       IP-DE-VOTRE-SERVEUR-WEB
*	IN     A       IP-DE-VOTRE-SERVEUR-WEB

Si vous ne souhaitez pas informer vos utilisateurs, remplacez simplement IP-DE-VOTRE-SERVEUR-WEB par 127.0.0.1. Mais je recommande tout de même la publication d'une page web d'information sur la quelle les utilisateurs pourront arriver si ils tentent de joindre un domaine présent dans la liste noire.

Avec ça, votre serveur DNS est transformé en trou noir pour l'ensemble des domaines répertoriés chez malwaredomains.com. En analysant les logs de votre serveur web, vous pourrez aussi déterminer rapidement qui sur votre réseau est infecté ou risque de l'être.

Références.

edit : correction d'une coquille dans le script

Chiffrer un répertoire sur FreeBSD avec PEFS

La sécurité des données numériques est au cœur des enjeux actuels, et pas seulement pour les professionnels. Pouvoir chiffrer ses propres données pour les rendre inutilisables pour un voleur éventuel, est donc une nécessité. Sous FreeBSD il est possible de faire du chiffrement sur l'ensemble du disque avec geli(8) ou gbde(4), ou sur un répertoire uniquement avec PEFS. Les deux premières solutions opèrent un chiffrement au niveau bloc. Elles travaillent sur des devices de taille fixe, et ne protègent pas vos données si un pirate accède à la machine quand elle est allumée et que les disques chiffrés sont montés.
PEFS permet de travailler sur un répertoire donc sans avoir à fixer un volume arbitraire au moment de l'initialisation. Le chiffrement se fait au niveau fichier avec une ou plusieurs clés autorisant la hiérarchisation des accès.

fichiers-chiffres-avec-PEFS

Installer PEFS

PEFS est disponible dans les ports et dans les packages. Il est recommandé de faire l'installation par les ports. En effet, l'outil contient un module pour le kernel, et un trop grand écart entre la version de votre noyau et celle qui a servi à la compilation du package peut générer un panic et geler votre système. Aussi, avec un processeur récent vous pourrez activer AES-NI au moment de la compilation du port (accélération matérielle pour le chiffrement AES).

$ cd /usr/ports/sysutils/pefs-kmod
$ sudo make install clean

Pour charger le module automatiquement au redémarrage, ajouter cette ligne dans votre /boot/loader.conf :

pefs_load="YES"

Utiliser PEFS

PEFS s'utilise assez simplement. Il faut commencer par créer un répertoire vide pour stocker les fichiers chiffrés. On peut créer un second répertoire qui servira de point de montage pour la version déchiffrée, mais le montage peut aussi se faire sur le premier répertoire.

$ mkdir ~/coffre.pefs ~/coffre

Ensuite on monte le répertoire (soit vers lui-même, soit comme ici vers un autre pour des raisons de clarté) :

$ sudo pefs mount ~/coffre.pefs ~/coffre

À ce stade, le répertoire ~/coffre est en lecture seule, car nous n'avons pas encore fourni de clé de chiffrement.

$ touch ~/coffre/toto
touch: coffre/toto: Read-only file system

L'ajout d'une clé est une étape simple, mais la documentation officielle manque légèrement d'explications. Il ne s'agit pas ici de fournir une clé une fois pour toute, mais de dire à PEFS de travailler pour la session courante avec cette clé. Si le répertoire est démonté, ou si les clés sont vidangées (flushkeys) il faudra fournir la même clé à nouveau pour accéder aux fichiers.

$ pefs addkey ~/coffre
Enter passphrase: (mot de passe pour (dé)chiffrer vos fichiers)
$ touch ~/coffre/toto
$ ls -A1 ~/coffre*
/home/me/coffre:
toto                               <- version en clair

/home/me/coffre.pefs:
.SfoQ93cCWWT+NnBB7EYE2ZK402YDSwsT  <- version chiffrée

Pour interdire l'accès à vos données vous pouvez simplement utiliser la commande flushkeys, ou aller jusqu'au démontage du répertoire:

$ pefs flushkeys ~/coffre
$ ls -A1 ~/coffre*
/home/me/coffre:
.SfoQ93cCWWT+NnBB7EYE2ZK402YDSwsT  <- version chiffrée

/home/me/coffre.pefs:
.SfoQ93cCWWT+NnBB7EYE2ZK402YDSwsT  <- version chiffrée
$ sudo pefs unmount ~/coffre
$ ls -A1 ~/coffre*
/home/me/coffre:
(vide)

/home/me/coffre.pefs:
.SfoQ93cCWWT+NnBB7EYE2ZK402YDSwsT  <- version chiffrée

Le démontage est facultatif, mais si vous utilisez PEFS dans des scripts, c'est sans doute plus propre. Aussi, faites bien attention, il est possible de monter plusieurs fois le même répertoire au même endroit ce qui peut déboucher sur des situations tout à fait inconfortables. En cas d'utilisation automatisée mettez les bouchées doubles sur les contrôles avant et après chaque commande.

Après le flushkeys, les données sont présentées uniquement dans leur forme chiffrée, mais vous y avez toujours accès ce qui vous permet par exemple de les exporter vers un cloud de sauvegarde (Amazon Glacier, par exemple) en toute sérénité.

Pour déchiffrer vos données, et en admettant que le répertoire soit déjà monté, il vous suffit de reproduire l'étape addkey avec la même clé :

$ pefs addkey ~/coffre
Enter passphrase:

Si à cette étape vous changez la valeur de la clé (volontairement ou par faute de frappe), la nouvelle clé sera acceptée par PEFS. Cette nouvelle clé ne vous donnera pas accès aux anciennes données bien évidemment. Par contre elle vous permettra de stocker de nouveaux fichiers, qui seront chiffrés grâce à cette nouvelle clé. C'est à la fois dangereux et pratique. Si la faute de frappe est votre ennemi vous pouvez jouer la prudence et utiliser un trousseau de clé. Le trousseau se trouve dans le fichier ~/coffre.pefs/.pefs.db et il stocke la ou les clés "officielles" de votre choix. L'initialisation du trousseau se fait avec la commande addchain :

$ pefs addchain -f -Z ~/coffre.pefs

Ensuite, il vous suffira lorsque vous fournirez votre clé de le faire avec l'option -c pour que votre saisie soit comparée au contenu du trousseau :

$ pefs addkey -c ~/coffre

Cela offre une sécurité intéressante mais impose aussi la contrainte d'avoir un fichier dont il faudra prendre soin (attention aux rsync qui pourraient le supprimer si vous utilisez PEFS pour stocker des sauvegardes dans un répertoire chiffrés).

Il existe d'autres possibilités au logiciel PEFS que je ne détaille pas ici, notamment celle de stocker dans le trousseau .pefs.db des chaînes de clés avec héritage parent-enfant, ou encore le module PAM pam_pefs.so qui permet de fournir le montage et le déchiffrement automatique du ~/ d'un utilisateur au moment où il s'authentifie.

Un peu de lecture

Wiki FreeBSD au sujet de PEFS
Tutoriel PEFS
Audit de sécurité rapide sur PEFS