Article écrit par Wonderfall. Suite à la fermeture de son blog, j'ai souhaité (avec son accord) remettre en ligne ce dernier pour que son contenu reste accessible.

Cet article est adressé principalement aux administrateurs système, ou devrais-je plutôt dire aux administrateurs de système puisque parmi vous il y a non seulement des personnes qui en ont fait leur métier, et d'autres personnes qui en font leur passion (comme moi, peut-être...). Par manque de temps et surtout parce que je parlerai des outils sur lesquels j'ai mis la main, j'utiliserai un seul serveur web au cours de cet article, le bien connu nginx. Cet article reprendra en partie un ancien article de mon précédent blog : Des certificats ECDSA avec Let's Encrypt.

1. Mettons les choses en ordre...

Jetez un coup d'oeil à l'image d'en-tête de l'article. Vous vous y reconnaitrez peut-être. Ou peut-être pas. Certains parmi vous sont complètement perdus, et ces noms ne leurs évoqueront rien. D'autres sont déjà à l'aise, savent grossièrement à quoi cela correspond. Eh bien, j'en fais moi-même partie. Dans un esprit d'humilité, je reconnais ne pas avoir les capacités nécessaires pour comprendre à 100% les notions conceptuelles, mathématiques, et toutes les subtilités derrière les technologies que je mentionnerai. Cela dit, ça ne m'empêchera pas de tenter, au moins, de vulgariser et de montrer des exemples. Ma vulgarisation sera approximative, elle comportera peut-être des erreurs, je l'admets. Mais tant que j'estime ne pas mettre en danger la sécurité des personnes (au contraire...), et tant que vous avez compris ce que je voulais dire dans ce paragraphe, je me sens libre de continuer à écrire cet article.

Ce qui m'a poussé à le faire, c'est que le nombre de demandes d'aide à propos de la configuration HTTPS est devenu croissant. Vous vous souvenez de Mastodon ? Aujourd'hui le nombre d'instances est bien plus important, et plus d'instances, ça veut dire aussi plus de configurations. Le principal site qui recense les différentes instances classe désormais les instances en fonction de leur configuration HTTPS, à travers Cryptcheck et depuis peu l'Observatoire Mozilla (qui comprend Cryptcheck également puisqu'il fait appel à des tests tiers). Évidemment, c'est la folie : tout le monde veut absolument avoir son instance le plus haut possible dans la liste, donc il faut à tout prix mieux sécuriser... si seulement c'était le cas pour les banques aussi, mais passons.

Et puis, certains iraient bien se masturber un coup sur un A+ de Cryptcheck/Qualys. Mais, pourquoi pas ? Ces questions ne me concernent pas. On atteindra cette finalité si c'est ce que vous voulez, vos motivations sont les vôtres. Quand je parle d'une excellente configuration, c'est effectivement une configuration qui vous donnera une note maximale sur ces outils. Dans le titre, je n'ai pas parlé de "comment configurer HTTPS", nuance. Mais il ne faut pas non plus suivre mon tutoriel n'importe comment, parce que premièrement, la compatibilité des clients en prend forcément un coup, et deuxièmement, certains mécanismes tels que HPKP peuvent vous casser la gueule si vous les utilisez n'importe comment. Lisez, puis faites. Pas l'inverse...

D'avance, j'annonce que je ne me prendrai pas la tête avec la compatibilité. Donc ne venez pas me dire que votre site ne fonctionne pas sur X ou Y, parce que je vous aurais prévenu.

2. Choix de la bibliothèque SSL/TLS

Les bibliothèques SSL/TLS fournissent une implémentation d'algorithmes cryptographiques et de protocoles de communication sécurisés (les différentes versions de SSL et TLS : SSLv2, SSLv3, TLS 1.0, TLS 1.1, TLS 1.2, TLS 1.3..). Aujourd'hui, 3 bibliothèques sont compatibles avec nginx :

  • OpenSSL : la plus connue, et la plus présente.
  • LibreSSL : fork d'OpenSSL par OpenBSD.
  • BoringSSL : fork d'OpenSSL par Google.

Il faut dire que Heartbleed a porté en 2014 un coup à la popularité d'OpenSSL. Non pas que ce fut la première faille découverte, loin de là, mais il y a eu une prise de conscience sur la qualité du code d'OpenSSL et des implémentations trop anciennes qui y sont présentes. OpenBSD fidèle à sa réputation, créa son fork LibreSSL dans le but de littéralement nettoyer OpenSSL et de le débarrasser d'algorithmes dépassés et dangereux d'utilisation. Aujourd'hui, il n'y a pas seulement OpenBSD qui utilise LibreSSL à la place d'OpenSSL en tant que bibliothèque par défaut du système. En effet, d'autres distributions ont sauté le pas, y compris macOS.

Quant à Google, ce n'est pas vraiment la même raison qui a conduit à créer BoringSSL. En effet, ils maintenaient déjà une version d'OpenSSL interne avec de nombreux patchs, Heartbleed a seulement été l'élément déclencheur pour repartir d'une page blanche, et c'est bien ce travail qui a été mené. Peu importe si les mises à jour cassent la compatibilité API/ABI avec d'autres logiciels comme nginx, Google met de toute façon en garde :

EN : Although BoringSSL is an open source project, it is not intended for general use, as OpenSSL is. We don't recommend that third parties depend upon it. Doing so is likely to be frustrating because there are no guarantees of API or ABI stability.

FR : Bien que BoringSSL soit un projet open-source, il n'est pas destiné à un usage général, comme OpenSSL l'est. Nous ne recommandons pas que les tierces parties dépendent dessus. Faire ainsi sera probablement source de frustrations puisqu'il n'y aucune garantie de la stabilité de l'API/ABI.

En pratique, BoringSSL fonctionne avec nginx et les développeurs ont accepté de faire quelques efforts pour satisfaire les quelques curieux qui souhaitent l'utiliser de cette façon. En revanche, pas de support de OCSP stapling.

En pratique, qu'est-ce que je dois choisir ?

Utiliser la bibliothèque de son système est une bonne chose car les mises à jour sont simples, donc il n'est pas nécessaire de recompiler nginx à chaque fois. Sur Debian, ça reste pour le moment OpenSSL. La version de Jessie est maintenue, mais elle est figée du point de vue technologique, donc en pratique, on aura pas quelques algorithmes récents (même si on peut s'en passer) et quelques features intéressantes compatibles avec nginx, mais nécessitant une version d'OpenSSL supérieure. La version d'OpenSSL fournie par Stretch (1.1.0) est aujourd'hui la version la plus récente d'OpenSSL.

Si vous êtes prêts à ne pas utiliser une bibliothèque système, je vous recommande sans hésiter LibreSSL, pour les raisons que j'ai énoncées plus haut. OpenBSD est une équipe de confiance, vraiment (et même un peu trop) perfectionniste. BoringSSL est celui qui vous proposera le plus de nouveautés, dont l'utilisation des versions drafts de TLS 1.3, des expérimentations d'algorithmes quantum proofs qui peuvent être retirées du jour au lendemain par Google (comme CECPQ1), etc. mais de mon point de vue, ce n'est vraiment pas quelque chose à utiliser sur une machine en production, il n'y a même pas de versioning, à utiliser plus pour la curiosité donc.

Si vous avez fait le choix d'utiliser une autre bibliothèque que la bibliothèque utilisée par défaut par le système, vous allez devoir recompiler nginx. Inutile que je passe mon temps à faire un tutoriel, il y en a... plein : ici (2015) pour LibreSSL, là (2016) pour BoringSSL (je me fais un peu de publicité, je sais). Mais si vous ne voulez pas vous prendre la tête, je vous propose des scripts voire des images Docker :

  • Angristan/nginx-autoinstall : compile nginx sur Debian, avec le choix entre OpenSSL et LibreSSL et d'autres modules !
  • wonderfall/boring-nginx : une image Docker basée sur Alpine Linux, avec nginx compilé depuis les sources avec BoringSSL.
  • xataz/nginx : une image Docker basée sur Alpine Linux qui installe simplement nginx, mais Alpine Linux utilise LibreSSL par défaut.
  • ajhaydock/BoringNginx : des scripts pour Debian/CentOS et une image Docker basée sur CentOS pour utiliser nginx avec BoringSSL.

3. D'abord, les certificats !

a. Savoir (un peu) de quoi on parle

Inutile de s'attaquer à la configuration d'HTTPS sans passer par la génération de certificats. Mais d'abord, pourquoi parle-t-on de certificats ? Deux mots pour décrire les buts d'HTTPS : chiffrement et authenticité. Ce dernier point repose sur la certification de la clé publique avec un certificat (d'un certain format nommé X.509) signé par une autorité connue (CA pour Certificate Authority). L'ensemble des composantes visant à gérer le cycle de vie des certificats porte le nom de Public Key Infrastructure (PKI). Les certificats définissent les informations d'appartenance de la clé publique, et définissent également les hostnames pour lesquels le certificat est valide. Par exemple, le certificat que j'utilise pour psychedeli.cat est aussi valable pour isso.psychedeli.cat (un sous-domaine). Ces informations, vous pouvez les obtenir très simplement depuis votre navigateur qui lit ces informations depuis le certificat fourni par le serveur web.

LE

Voyez l'illustration comme un arbre, on part de la racine (= root), on passe par les branches ramifiées (certificats intermédiaires) et enfin on atteint les feuilles (= leaf)

Nous allons ici utiliser Let's Encrypt, qui est comme je vous l'ai dit une autorité de certification. Le principe est celui d'une chaîne de confiance : le certificat root (fourni ici par ISRG) est le certificat originel, qui peut signer des certificats intermédiaires (ceux de Let's Encrypt). Ces certificats intermédiaires permettront de signer les certificats que nous utilisons tous les jours, qu'on appelle aussi leaf certificate, ils permettront de signer des clés temporaires dérivées de la paire clé publique/privée qui sera utilisée pour sécuriser une session. Ces certificats sont en fin de chaîne et ce sont eux que nous allons obtenir.

b. Générer des certificats ECDSA (P-384)

Plus précisément, on va le faire d'une façon un peu plus manuelle mais loin d'être difficile. Cela nous permettra :

  • De gérer nous-même le renouvellement de la paire de clés, ce qui nous permet d'utiliser plus sereinement et efficacement HPKP.
  • D'utiliser une autre approche d'algorithme asymétrique que RSA, ECDSA qui repose donc sur la cryptographie sur les courbes elliptiques (ECC).

Les algorithmes de la famille des courbes elliptiques proposent d'utiliser des clés bien plus courtes, pour une sécurité comparable à celle offerte par RSA, et ils permettent également d'économiser des ressources. Voici des tests de performance que j'ai réalisés avec ma machine :

$ openssl speed rsa
                  sign    verify    sign/s verify/s
rsa 2048 bits 0.000549s 0.000025s   1821.4  39636.0
rsa 3072 bits 0.002579s 0.000051s    387.7  19471.4
rsa 4096 bits 0.005829s 0.000089s    171.5  11270.1

$ openssl speed ecdsa
                              sign    verify    sign/s verify/s
 256 bit ecdsa (nistp256)   0.0000s   0.0001s  25425.7  10819.8
 384 bit ecdsa (nistp384)   0.0002s   0.0007s   5749.2   1340.1
 521 bit ecdsa (nistp521)   0.0003s   0.0006s   3007.0   1659.5

ECDSA en soit n'est pas directement comparable à RSA, il doit être associé à des courbes elliptiques, et ici, ce sont les courbes NIST P-256, P-384, et P-521, qui sont utilisées. Le nombre de signatures/secondes est tout simplement bien meilleur avec ECDSA. Quid de la sécurité ? Selon la NSA elle-même, une clé ECC de taille 384 bits offrirait une sécurité équivalente à une clé RSA de taille 7680 bits.

Depuis Février 2016, Let's Encrypt peut émettre des certificats ECDSA (les choix sont P-256 et P-384). Malheureusement, le client officiel certbot ne le permet pas. Alors soit on peut utiliser d'autres clients comme lego qui vont mâcher le travail, soit on peut toujours utiliser certbot mais se débrouiller nous-même pour générer notre clé privée et notre demande de signature de certificat (dans un fichier CSR).

Commençons par générer une clé privée P-384 :

openssl ecparam -genkey -name secp384r1 > privkey.pem

On peut maintenant créer un fichier CSR, qui contient toutes les informations relatives à une demande de certificat. Comme moi, vous aurez peut-être envie de générer un seul certificat pour plusieurs domaines, ceci peut se faire avec l'extension SAN de X.509. Vous devez tout d'abord copier le fichier /etc/ssl/openssl.cnf dans votre répertoire de travail. Par exemple :

cp /etc/ssl/openssl.cnf custom.cnf

On va modifier ce custom.cnf pour bénéficier de SAN et donc d'un certificat valable, en gros, pour plusieurs domaines. Voici comment. Dans la section [ req ], décommettez req_extensions = v3_req. Dans la section [ v3_req ], ajoutez une nouvelle ligne : subjectAltName = @alt_names. Enfin, tout se passe dans [ alt_names ] pour ajouter vos domaines. Cette section n'existe pas, il faut la créer, par exemple tout à la fin du fichier.

# custom.cnf

...

[ req ]
...
req_extensions = v3_req 

...

[ v3_req ]
...
subjectAltName = @alt_names
### OCSP Must-Staple, cf. plus loin
#tlsfeature = status_request             # OpenSSL >=1.1.0
#1.3.6.1.5.5.7.1.24 = DER:30:03:02:01:05 # OpenSSL <=1.0

...

[ alt_names ]
DNS.1 = domain.tld
DNS.2 = sub1.domain.tld
DNS.3 = sub2.domain.tld
...
DNS.n = subn.domain.tld

Pour chaque domaine souhaité, on l'associe à la directive DNS.kk, allant de 1 à n, est incrémenté sur chaque nouvelle ligne. Le SAN que peut signer Let's Encrypt peut atteindre 100 (autrement dit, 100 domaines différents dans un seul certificat X.509), donc n <= 100.

Enregistrez toutes ces modifications. Vous pouvez désormais générer votre CSR avec la commande suivante :

openssl req -new -sha256 -key privkey.pem -out csr.der -config custom.cnf

Rentrez des informations, mais laissez Common Name vide, et ne protégez pas avec un mot de passe. Vous devez, à ce stade, avoir en votre possession un privkey.pem et un csr.der.

Pour demander à Let's Encrypt de nous émettre un certificat à partir de notre demande, il suffit d'utiliser certbot de cette façon, avec l'argument --csr pour fournir le CSR :

certbot certonly --standalone --agree-tos -m admin@domain.tld \
    --csr /path/to/csr.der \
    --cert-path /path/to/cert.pem \
    --chain-path /path/to/chain.pem \
    --fullchain-path /path/to/fullchain.pem

Congrats! Vous avez ainsi obtenu :

  • cert.pem : leaf certificate, contient entre autres la clé publique
  • chain.pem : certificat de la chaîne Let's Encrypt
  • fullchain.pem : concaténation des deux fichiers ci-dessus
  • privkey.pem : clé privée qu'on avait générée tout à l'heure

DNS CAA : N'oubliez pas d'ajouter des enregistrements CAA dans votre zone DNS, ils permettent de définir quel CA est autorisé à émettre un certificat pour le domaine domain.tld. Exemple pour ce domaine avec Let's Encrypt :

domain.tld. CAA 0 issue "letsencrypt.org"
domain.tld. CAA 0 iodef "mailto:admin@domain.tld"

4. Le protocole SSL/TLS

Je vous recommande la lecture de cet article : CryptCheck, vérifiez vos implémentations de TLS.

TLS et son ancêtre SSL sont des protocoles qui sont utilisés pour sécuriser des échanges sur Internet. Par exemple conjointement avec HTTP pour donner HTTPS, le joli cadenas vert que vous avez dans votre barre d'adresse et qui ne veut pas dire grand chose parfois. Ils sont utilisés pour l'intégrité, l'authentification, la confidentialité (chiffrement), etc.

Aujourd'hui, la compatibilité de TLS 1.2 est répandue et il s'agit de la dernière version datant de 2008. TLS 1.0 (quoique...) et 1.1 peuvent être activés en cas de besoin de compatibilité. Pour cet article je ne vais garder que TLS 1.2. Il est par contre hors de question d'utiliser SSL. SSLv3 c'est poubelle (POODLE), SSLv2 c'est poubelle aussi (DROWN). D'ailleurs j'en profite : arrêtez de véhiculer cet abus de langage comme quoi SSL/TLS = SSL. J'aurais d'ailleurs trouvé ça appréciable si LibreSSL s'appelait plutôt LibreTLS, bref.

Donc on configure nginx pour n'accepter que TLS 1.2 :

ssl_protocols TLSv1.2;

TLS 1.3 arrivera incessamment sous peu, à l'heure où j'écrit cet article la 19e draft est encore très récente. OpenSSL 1.1.0 aura droit à TLS 1.3, tout comme LibreSSL qui devrait suivre. BoringSSL propose déjà TLS 1.3, j'ai testé et ça fonctionne même s'il faut l'activer de force. Cela dit je ne peux pas vraiment recommander d'utiliser un protocole au stade de brouillon et dont l'implémentation est encore trop jeune.

Histoire de faire une transition avec la partie suivante : avec TLS, tout commence par un handshake entre le client et le serveur. Le client envoie au serveur un message ClientHello avec une liste de suites cryptographiques supportées et de protocoles (par exemple pour HTTP/2). Le serveur renvoie à son tour un ServerHello avec entre autres, le choix de la suite cryptographique pour la session.

5. La suite cryptographique

Vous auriez déjà dû entendre parler du terme cipher. Cela désigne pour ceux qui ne le savent pas un algorithme qui chiffre et déchiffre, c'est une notion simple et cela existe depuis l'antiquité en fait, puisque ce sont les débuts même de la cryptographie. Mais avant que les 2 parties (ici, le serveur et le client) ne puissent chiffrer/déchiffrer leurs communications (par exemple du chiffrement par bloc avec un algorithme de cryptographie symétrique comme AES), il faut que les deux s'échangent une clé temporaire qui a été dérivée de la paire de clés du certificat, pour une session. Donc la question est de le faire d'une façon sécurisée à travers Internet, et éviter à tout prix les attaques MiTM.

a. L'échange de clés

Pour cela, on préfère l'échange de clés Diffie-Hellman (DH) car il y a la propriété de confidentialité persistante (Forward Secrecy FS ou Perfect Forward Secrecy PFS) : si la clé privée à partir de laquelle les clés de session ont été dérivés est compromise dans le futur, les clés de session utilisées précédemment pour des communications passées ne seront pas compromises. On pourrait en parler mathématiquement, mais là n'est pas vraiment le sujet, des personnes plus compétentes en parleront mieux que moi.

Avec TLS, on utilise DHE (Diffie-Hellman Exchange) ou ECDHE (Elliptic Curve Diffie-Hellman Exchange), ECDHE étant une variante de DHE. Sans rentrer dans les détails, la différence est dans la génération des clés : pour ce faire, DHE repose sur de l'algorithmique modulaire (simple), tandis que ECDHE repose sur le problème mathématique des courbes elliptiques (algébriques). Ces courbes (NIST, Brainpool, X25519...) ont les bénéfices que nous avons en partie évoqué plus haut.

De mon côté, je vous recommande carrément de n'utiliser que ECDHE pour l'échange de clés. Si vous voulez continuer à utiliser DHE, générez au moins une bonne clé de taille 4096 bits. Souvenez-vous des conséquences d'une clé trop petite, je parle bien sûr de Logjam. DHE souffre également d'un autre problème qui consiste en une MiTM assez violente : il est possible de downgrade vers une suite EXPORT dès que le serveur la supporte, et même si le client ne la supporte pas (merci @aeris pour cette remarque). Je persiste, utilisez donc seulement ECDHE, c'est très bien et dans la continuité de cet article. Et il n'y a rien de plus simple pour nginx puisqu'il suffit de jouer avec la directive ssl_ecdh_curve.

On peut ne pas renseigner cette directive, c'est possible, et dans ce cas le client et le serveur se mettront d'accord comme des grands sur la courbe à utiliser. Avec des versions de nginx relativement récentes (1.11+) et OpenSSL 1.1.0+ / LibreSSL / BoringSSL, il est possible de définir, côté serveur, un ordre de préférence. De mon côté, je préfère le faire (et si vous ne pouvez pas préciser plusieurs courbes, ne pas renseigner la directive est plus sage, sinon vous pouvez utiliser P-384 seulement) :

ssl_ecdh_curve X25519:P-521:P-384;
# ssl_ecdh_curve secp521r1:secp384r1; # Autres noms
# ssl_ecdh_curve secp384r1; # Si nginx pas récent

Petite aparté sur X25519 : cette courbe elliptique est le fruit des recherches du Dr. Bernstein. Comme indiqué sur ce site, la sécurité offerte par les courbes NIST n'est pas parfaite et mathématiquement douteuse. X25519 est "meilleur", et en plus, il ne vient pas d'une agence proche de la NSA qui a tout intérêt à rendre la cryptanalyse des courbes NIST plus avantageuse pour elle.

X25519 n'étant disponible que sur des versions récentes de Chromium, il faut accepter d'autres courbes pour supporter par exemple Firefox et d'autres plateformes : P-521 et P-384, c'est très bien aussi. Ajoutez P-256 si la compatibilité est vraiment un problème.

b. Le chiffrement des données

Maintenant que la clé temporaire de session a été communiquée de façon sécurisée grâce à ECDH au client, ce dernier et le serveur peuvent échanger dans un canal chiffré dans une configuration symétrique, d'où l'utilisation d'algorithmes symétriques comme AES. C'est aujourd'hui le plus éprouvé. RC4, DES, 3DES... sont à bannir. Des chatons meurent à chaque fois que ces algorithmes sont utilisées. En 2017, on utilise normalement AES.

AES faisant du chiffrement par bloc, on introduit deux notions :

  • Le mode d'opération des blocs : CCM, CBC, GCM...
  • La taille des clés utilisées : 128, 256...

TLS 1.2 standardise notamment des suites cryptographiques qui reposent sur AES-CBC et AES-GCM, avec des clés 128 et 256 bits. Le mode GCM est nettement préférable à CBC. Pourquoi ? Dans le mode CBC, on utilise la fonction XOR (disjonction exclusive) avec un bloc de données en clair et un vecteur d'initialisation (IV), ensuite la sortie de XOR est chiffrée avec notre clé symétrique. Le mode GCM diffère de par son fonctionnement : un compteur pour chaque bloc est chiffré par la clé symétrique (dans le Corps de Galois), puis on utilise la fonction XOR avec la sortie obtenue et le bloc de données en clair : le bloc chiffré est formé ainsi. Pour ne pas aller plus loin dans les détails même si ce sont de grands mots qui peuvent faire peur, le fonctionnement inhérent de GCM est plus sécurisé que celui de CBC. Il est aussi très rapide. CBC est vulnérable aux attaques padding oracle.

Ne faites pas une erreur tentante : Qualys ne vous mettra pas du 100/100 partout si vous avez le malheur d'utiliser AES avec une clé 128 bits, même AES GCM. Donc certains vont forcer AES avec une clé 256 bits, mode GCM et CBC, parce que AES-256-GCM n'est pas forcément très bien supporté partout encore. C'est une fausse bonne idée ! AES-128-GCM est très bien, et quoiqu'il arrive, il faut privilégier GCM à CBC. C'est le comportement de la suite EECDH+AES par défaut, inutile d'en préciser plus. Pour utiliser GCM seulement, ce que je fais, vous pouvez utiliser EECDH+AESGCM.

ChaCha20 est un autre algorithme symétrique, qui a un fonctionnement intrinsèquement différent puisqu'il fonctionne par flot et non par blocs (comme RC4). C'est une alternative à AES proposée par le Dr. Bernstein encore une fois. Bien que la cryptanalyse faite sur ChaCha20 ne soit pas aussi importante qu'avec AES, on le considère comme sûr, et de toute façon, avoir une alternative est toujours une bonne chose (et le monopole cryptographique n'étant pas idéal de toute façon...). Il est moins rapide que AES sur des CPU qui disposent des instructions AES-NI, c'est-à-dire presque tous les CPU x86_64 actuels si je ne me trompe pas. Mais ChaCha20 offre des performances remarquables et se défend bien, si bien qu'il est préféré à AES quand on est sur des SoC ARM par exemple.

ChaCha20 est combiné à Poly1305, un type de code d'authentification de message (MAC) assurant l'intégrité des données, alors qu'on combine AES-GCM à HMAC-SHA2 par exemple. La qualité du HMAC dépendant de la fonction de hashage utilisée, je préfère ne pas utiliser SHA1 (et il est hors de question d'utiliser MD5), mais s'en séparer convient de faire une croix sur la compatibilité avec certains clients encore une fois. Pour continuer l'analogie entre ChaCha20 et AES, est-ce que Poly1305 est mieux que HMAC-SHA2 ? C'est différent, et c'est bien d'avoir le choix. Il y a certainement des subtilités qui feront pencher les cryptographes d'un côté plus que l'autre... De mon côté, je préfère même mettre ChaCha20+Poly1305 en tête de liste parce que je préfère Docteur Bernstein. Mais il ne faut pas basher AES non plus : bien qu'il ait vu le jour lors du concours NIST au début des années 2000, les critères autour de sa création sont rigoureusement transparents. Tout ce dont on peut être sûr c'est que la NSA par exemple a eu le temps de faire sa cryptanalyse.

Donc la configuration que je propose est la suivante :

ssl_ciphers EECDH+CHACHA20:EECDH+AESGCM;

Restrictive, certes. Mais il ne devrait pas y avoir de soucis avec des clients modernes. Chrome et Firefox supportent déjà ChaCha20+Poly1305. AES-GCM fera l'affaire pour Edge, Safari (jusqu'à la version incluse dans Yosemite, mais n'oubliez pas que Mavericks a le droit à des updates majeures de Safari par exemple), et Android jusqu'à KitKat.

Bonus si vous utilisez BoringSSL : vous pouvez définir des groupes de suites cryptographiques équivalentes. Au sein d'un même groupe, vous laissez le client choisir selon son ordre à lui. Cela peut-être utile si vous voulez utiliser AES sur votre PC, mais ChaCha20 sur votre smartphone ARM, par exemple. Le groupe est défini entre crochets [] et avec une barre verticale pipe | entre chaque ciphersuite, ça donnerait ceci :

ssl_ciphers [EECDH+CHACHA20|EECDH+AESGCM];

6. HTTP Strict Transport Security

HSTS est un mécanisme de sécurité qui consiste à indiquer par un header envoyé au client qu'il doit communiquer, pour un domaine donné, avec le serveur web par une connexion sécurisée HTTPS uniquement. L'intérêt est évident, car si HSTS est actif, le client est protégé de diverses attaques réseau et d'une MiTM. Mais je n'ai pas mis le précédent "si" en gras pour rien, en effet si HSTS n'a jamais été actif ou ne l'est plus, HSTS ne peut plus plus offrir cette sécurité. C'est pour cela qu'a été inventé le mécanisme de HSTS Preloading, qui consiste en des listes préchargées et intégrées aux navigateurs pour indiquer la politique HSTS utilisée pour un domaine ; en quelque sorte, HSTS est déjà actif avant même que vous n'ayez visité le site.

Pour utiliser HSTS, il faut utiliser :

  • Le header Strict-Transport-Security.
  • max-age : durée d'activité une fois actif (recommandé : 6 mois+)
  • includeSubDomains : protéger tous les sous-domaines.
  • preload : activer HSTS preloading.

De mon côté, je vais mettre max-age à 1 an soit 31536000 secondes. J'activerai HSTS preloading, et j'indiquerai de protéger tous les sous-domaines, soyez sûr que vous souhaitez faire de même :

add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload";

Pour s'inscrire à la liste préchargée, il faut aller inscrire son domaine ici. La propagation peut prendre plusieurs semaines. Attention, HSTS Preload n'est pas là pour remplacer la redirection 301 vers HTTPS depuis HTTP.

7. OCSP Stapling et OCSP Must-Staple

OCSP Stapling est une alternative au protocole OCSP qui consiste à vérifier auprès de l'autorité de certification la validité du certificat. Avec OCSP, c'est le client lui-même qui fait cette démarche de vérification auprès du CA ; tandis qu'avec OCSP Stapling, c'est le serveur qui fait cette tâche et qui envoie la réponse OCSP au début de l'échange TLS. Bien que ce soit discuté d'un point de vue sécurité, les gains apportés par OCSP Stapling pour la vie privée sont non-négligeables, il est donc préférable de l'activer. Voyons comment cela se configure avec nginx, d'abord les paramètres OCSP Stapling à proprement parler :

ssl_stapling on;
ssl_stapling_verify on;
resolver 213.133.98.98 213.133.99.99 valid=300s;
resolver_timeout 5s;

Les deux premières lignes activeront OCSP Stapling. Mais il faut fournir un résolveur DNS à stapling pour qu'il puisse fonctionner. On peut fournir plusieurs IPs ou des FQDN pour pointer vers des résolveurs DNS, IPv4 et IPv6. Il faut également fournir à nginx le certificat de la chaîne Let's Encrypt chain.pem dans chaque configuration :

ssl_trusted_certificate /path/to/chain.pem;

Une fois que c'est fait et nginx redémarré, vous pouvez tester vos paramètres OCSP Stapling sur Qualys SSL. Si OCSP Stapling s'affiche en vert avec Yes, c'est que logiquement ça fonctionne bien.

Quant à OCSP Must-Staple, imaginez tout simplement la même problématique qu'avec HSTS : au départ, le client n'a aucune idée si le serveur supporte HSTS, ou OCSP Stapling. OCSP Must-Staple est la solution pour OCSP Stapling qui a été proposée pour résoudre ce problème. C'est une extension à activer dans le certificat, qui indique au client qu'il doit être livré en même temps qu'une réponse OCSP du serveur, sinon c'est niet (donc vérifiez bien, et régulièrement, que vos paramètres fonctionnent...). Comment faire ? J'en ai déjà parlé plus haut, lors de la génération du certificat, quand vous modifiez custom.cnf dans la section [ v3_req ].

Si vous êtes sur OpenSSL 1.1.0 ou supérieur :

tlsfeature = status_request

Si vous êtes sur OpenSSL 1.0 et inférieur (moins sexy) :

1.3.6.1.5.5.7.1.24 = DER:30:03:02:01:05

Encore une fois vous pouvez vérifier que le flag est bien présent dans votre certificat après un petit tour sur Qualys SSL, ou en regardant les détails du certificat depuis votre navigateur.

Remarque : Firefox est ici le bon élève puisqu'apparemment il supporte bien OCSP Must-Staple. Cependant il se peut que dès le démarrage du serveur web et lors de la première tentative de connexion, Firefox vous colle une belle erreur OCSP : c'est normal. En fait, il faut que nginx remplisse son cache OCSP, et c'est ce que vous avez fait lors de la première connexion. Une idée toute bête pour contourner ce problème est de faire un script qui va faire la connexion. Vous pouvez même l'ajouter dans une entrée cron pour être sûr qu'il n'y ait aucun problème de ce genre sur le long terme.

Problème : le cache OCSP ne se remplit pas automatiquement lors du démarrage d'nginx et il a besoin de se remplir de temps en temps. "Oui, et ?" me diriez-vous. Le problème c'est que quand le cache doit se remplir sur demande d'un client, le client balancera une erreur. Si on rafraîchit la page, hop, l'erreur part. C'est donc une limitation de OCSP stapling que l'on peut contourner avec un script (merci @href pour la piste) à mettre en cron où $domain est le domaine concerné, voici un bout de code pour vous montrer :

/bin/echo "Q" | /usr/bin/openssl s_client -connect $domain:443 -status  > /dev/null 2>&1

pm## 8. HTTP Public Key Pinning (HPKP)
HPKP est un autre mécanisme de sécurité, basé sur un header HTTP, qui vise à faire mémoriser au client une liste sûre de clés. Si le CA est compromis et se met à faire de faux certificats pour votre domaine, HPKP s'il est bien configuré vous protégera d'une MiTM. Vu qu'on utilise Let's Encrypt, et avec la méthode que je vous ai fournie pour gérer vos certificats, nous pouvons directement pin la clé publique du leaf certificate. Pourquoi ? Parce que comme je vous l'ai dit, on peut désormais utiliser nos propres clés privées donc on peut se passer de la régénération automatique de Let's Encrypt qui rend l'usage de HPKP très délicat.

Prudence. Le problème avec HPKP, c'est qu'une fois actif chez le client, il le reste pendant la durée max-age fournie dans le header. Si par malheur la clé privée est compromise mais que vous avez pin un seul certificat, pardonnez-moi le terme, mais vous êtes littéralement baisé (et plus concrètement votre site sera inaccessible chez ceux qui auront l'ancienne version du header active chez eux). Voilà pourquoi il faut pin plusieurs clés :

  • Si la clé privée est compromise, on peut changer de clé.
  • Par mesure de bon sens, il faut parfois rouler les clés.
  • Si l'algo est pété, on peut changer sur une autré clé d'un autre type d'algo.

Concrètement, moi, ce que je vous recommande, c'est de générer 2 clés P-384 et une clé RSA 4096, et de faire le tutoriel pour créer le certificat ECDSA à partir d'une clé P-384 générée.

# Génération des 3 clés : 2x P-384, 1x RSA-4096
openssl ecparam -genkey -name secp384r1 > privkey_ec_1.pem
openssl ecparam -genkey -name secp384r1 > privkey_ec_2.pem
openssl genrsa -out privkey_rsa.pem 4096

# Configurez comme indiqué plus haut le custom.cnf
cp /etc/ssl/openssl.cnf custom.cnf

# Les 3 CSR à générer
openssl req -new -sha256 -key privkey_ec_1.pem -out csr_ec_1.der -config custom.cnf
openssl req -new -sha256 -key privkey_ec_2.pem -out csr_ec_2.der -config custom.cnf
openssl req -new -sha256 -key privkey_rsa.pem -out csr_rsa.der -config custom.cnf

# Récupérer les empreintes
openssl req -pubkey < csr_ec_1.der | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | base64
openssl req -pubkey < csr_ec_2.der | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | base64
openssl req -pubkey < csr_rsa.der | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | base64

# Génération d'un certificat à partir de la première clé
letsencrypt certonly --standalone --agree-tos -m admin@domain.tld \
    --csr /path/to/csr_ec_1.der \
    --cert-path /path/to/cert.pem \
    --chain-path /path/to/chain.pem \
    --fullchain-path /path/to/fullchain.pem

Le header Public-Key-Pins est à renseigner avec les 3 empreintes obtenues et un max-age. Pour l'instant, et pour commencer, je le mettrais à 2 mois (de toute façon, Chrome limite la validité du header à 60 jours...) ; libre à vous de monter plus haut ensuite. Il est également possible d'appliquer HPKP à tous les sous-domaines une fois actif sur un domaine.

add_header Public-Key-Pins 'pin-sha256="empreinte_ec_1"; pin-sha256="empreinte ec_2"; pin-sha256="empreinte_rsa"; max-age=5184000; includeSubdomains';

Il va de soit que les clés ne doivent pas être idéalement sur la même machine, car si celle-ci est compromise, ce n'est pas une mais les trois clés privées que vous allez perdre. N'hésitez pas à rouler les clés, c'est-à-dire passer sur la deuxième clé privée puis en générer une nouvelle et l'ajouter à HPKP, en virant la première et en laissant la deuxième.

9. DANE, une alternative à HPKP ?

DANE pour DNS-based Authentication of Named Entities est un autre mécanisme sécurité, qui a la même finalité que HPKP mais qui repose sur des entrées DNS (des enregistrements TLSA). La condition pour son utilisation est l'activation de DNSSEC, sans quoi DANE n'a aucun intérêt. Je vais éviter la redite parce que mon ami Hardware a rédigé un billet sur le sujet, n'hésitez pas à le consulter.

Les entrées TLSA se présentent sous cette forme :

TLSA

Pour extraire l'empreinte SHA-256 du fullchain.pem, voici comment :

openssl x509 -noout -in fullchain.pem -fingerprint -sha256 | cut -c 20- | sed s/://g | awk '{print tolower($0)}'

Puis il "suffit" d'ajouter une entrée TLSA dans votre zone DNS :

_443._tcp.domain.tld. IN  TLSA  3 0 1  EMPREINTE
# À faire pour chaque sous-domaine.

Du coup si on met le fullchain.pem, il convient de mettre à jour l'entrée TLSA à chaque fois qu'on crée un nouveau certificat. Personnellement j'ai automatisé ça à coups de sed dans mon script de renouvellement, et ça marche bien.

Malheureusement DANE n'est pas présent dans la plupart des clients modernes (même pas Chrome et Firefox), c'est pour cela qu'il vaut mieux garder HPKP à côté. Pour tester si DANE fonctionne correctement, je vous recommande l'utilisation du plugin DNSSEC/TLSA Validator. Vous devriez voir cela :

plugin TLSA

10. Certificate Transparency

Certificate Transparency (CT) est en passe de devenir un standard. L'objectif derrière est de rendre public et transparent par les CAs toutes les informations autour de la génération d'un certificat. Ainsi, c'est une façon de lutter contre l'apparition de certificats frauduleux.

Certificate Transparency

Lors de la création du certificat, le CA envoie des informations au log server qui répond avec un SCT (signed certificate timestamp). Ce SCT est soit intégré au certificat, soit présenté au travers d'autres moyens : une extension TLS, ou OCSP Stapling. Let's Encrypt est déjà compatible avec certificate transparency, mais il ne présente pas encore de SCT (il est prévu que OCSP Stapling s'en occupe d'ici la fin d'année). Pour le moment on peut donc utiliser une extension TLS, il faudra compiler nginx avec le module nginx-ct. Il ne fonctionnera qu'avec une version d'OpenSSL assez récente.

Pour obtenir le SCT de fullchain.pem il nous faut utiliser ct-submit.

git clone https://github.com/grahamedgecombe/ct-submit --depth=1
cd ct-submit && go build
./ct-submit ct.googleapis.com/pilot <fullchain.pem>fullchain.sct
xxd fullchain.sct # Pour tester

00000000: 00a4 b909 90b4 1858 1487 bb13 a2cc 6770  .......X......gp
00000010: 0a3c 3598 04f9 1bdf b8e3 77cd 0ec8 0ddc  .<5.......w.....
00000020: 1000 0001 4bc7 e617 c800 0004 0300 4830  ....K.........H0
00000030: 4602 2100 b9fe e206 f0f5 f600 93d5 e04c  F.!............L
00000040: d2fd 75c9 e1fc a5c8 4812 a8b7 bc2c eb0c  ..u.....H....,..
00000050: ee16 1fe9 0221 008a 5974 e1b6 a0e0 281a  .....!..Yt....(.
00000060: 61e8 3447 895f 7ad4 2f70 f528 6133 a445  a.4G._z./p.(a3.E
00000070: 4fd4 ab60 ba36 db                        O..`.6.

Je vous suggère ici de créer un fichier ct_domain.tld.conf où l'on mettra les paramètres nginx pour servir le SCT.

ssl_ct on;
ssl_ct_static_scts /path/to/sct/dir;

Normalement nginx devrait servir les SCTs qui sont dans ce dossier. Si jamais ça ne marche pas, il est possible que ce soit dû à une limitation pour laquelle le serveur par défaut doit impérativement servir des SCTs (ajoutez default_server aux directives listen d'un vhost d'un domaine qui utilise CT). Pour vérifier si Certificate Transparency est opérationnel, Chrome et Qualys SSL feront l'affaire.

A ce fichier de configuration on peut aussi ajouter un header, Expect-CT. Il indique au browser que l'on attend l'utilisation de Certificate Transparency. Au jour d'aujourd'hui aucun browser ne l'utilise mais ce sera bientôt le cas. Surtout vérifiez bien que Certificate Transparency fonctionne et que les SCTs sont bien servis.

add_header Expect-CT "enforce; max-age=86400";

Dans ce cas-ci le browser devrait forcer l'utilisation de Certificate Transparency (erreur si pas possible) pendant un jour.

11. On récapitule ?

Et c'est après, je l'espère, vous avoir expliqué en quoi consistent les mécanismes utilisés, que je vais vous fournir les exemples de configuration. À chaque fois qu'il y a un domain.tld, c'est bien sûr à remplacer par votre domaine. Créons peut-être un dossier /etc/nginx/conf dans lequel on mettra des fichiers de configuration personnalisés, histoire d'éviter la redondance plus tard et de pouvoir maintenir plus facilement.

Créons un fichier tls.conf où l'on mettra les paramètres TLS :

ssl_protocols TLSv1.2;
ssl_ecdh_curve X25519:P-521:P-384;
ssl_ciphers EECDH+CHACHA20:EECDH+AESGCM;
ssl_prefer_server_ciphers on;

ssl_session_cache shared:SSL:20m;
ssl_session_timeout 15m;
ssl_session_tickets off;

On désactive les tickets TLS car leur mauvaise implémentation nuit au principe de confidentialité persistance (FS). Les autres directives modifient le cache alloué aux sessions et le timeout d'une session, elles sont à adapter selon les capacités de votre serveur et l'affluence des connexions.

Un fichier hsts.conf pour HSTS :

add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload";

Un fichier hpkp_domain.tld.conf pour HPKP d'un domaine :

add_header Public-Key-Pins 'pin-sha256="empreinte_ec_1"; pin-sha256="empreinte ec_2"; pin-sha256="empreinte_rsa"; max-age=5184000; includeSubdomains';

Un fichier ocsp.conf avec les paramètres OCSP :

ssl_stapling on;
ssl_stapling_verify on;
resolver 213.133.98.98 213.133.99.99 valid=300s;
resolver_timeout 5s;

Un fichier headers.conf qui peut être ajouté si l'application n'ajoute pas déjà ces headers :

add_header X-Frame-Options SAMEORIGIN;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";

Dans sites-enabled/, on aura notre fichier lambda.conf ainsi construit :

server {
  listen 80;
  listen [::]:80;
  server_name domain.tld;
  return 301 https://$host$request_uri;
}

server {
  listen 443 ssl http2;
  listen [::]:443 ssl http2;
  server_name domain.tld;

  ssl_certificate /path/to/fullchain.pem;
  ssl_certificate_key /path/to/privkey.pem;
  ssl_trusted_certificate /path/to/chain.pem;

  include /etc/nginx/conf/tls.conf;
  include /etc/nginx/conf/hsts.conf;
  include /etc/nginx/conf/headers.conf;
  include /etc/nginx/conf/ocsp.conf;
  include /etc/nginx/conf/hpkp_domain.tld.conf;
 #include /etc/nginx/conf/ct_domain.tld.conf;

  ...
}

N'oubliez pas de redémarrer nginx une fois les paramètres appliqués, sans oublier un nginx -t avant pour éviter les mauvaises surprises.

Les tests pour voir si vous avez tout bon :

QualysSSL

Ce n'est pas difficile, et je ne pense pas avoir menti sur la marchandise, il s'agit d'une excellente configuration HTTPS, peut-être même un peu trop extrémiste. Si la compatibilité est un trop gros problème, appliquez les conseils que j'ai disséminés le long de l'article.

Article rédigé par Wonderfall, vous pouvez le suivre sur Mastodon ou Twitter