Configurer correctement SSL

Le samedi 09 janvier 2016 par Benjamin Boudoir

Avant-propos

Après le petit article sur Let's Encrypt, je me suis dit qu'un article plus général sur la configuration de SSL sur ses serveurs pourrait être une bonne chose.

J'oriente tout ce qui est conf sur les navigateurs web, mais la réflexion et les conseils sur le chiffrement sont valables partout.

J'aime l'idée d'une conf SSL identique pour tous mes domaines, j'ai donc un joli fichier ssl.conf à la racine de ma config Nginx. Du coup, la configuration d'un hôte HTTPS sous Nginx ressemble à ça :

include ssl.conf;
ssl_certificate      /etc/ssl/mydomain.crt;
ssl_certificate_key  /etc/ssl/mydomain.key;

Avec toute la chaîne de certificat dans le mydomain.crt, le ou les certificats intermédiaires devant se placer APRÈS le certificat du serveur :

If intermediate certificates should be specified in addition to a primary certificate, they should be specified in the same file in the following order: the primary certificate comes first, then the intermediate certificates. A secret key in the PEM format may be placed in the same file.

Documentation de la directive ssl_certificate (Nginx, module SSL)

La config pour Apache est là à titre indicatif, je ne l'ai pas testé sur de la prod, je me base juste sur la doc.

SSL ou TLS ?

  • On vire SSL, c'est pourri
  • TLS 1.0 = SSL v3
  • On active TLS v1.2 uniquement

Sous Nginx :

ssl_protocols TLSv1.2;

Sous Apache :

SSLProtocol TLSv1.2

PFS

Il s'agit d'éviter d'avoir des échanges de clefs faillibles (en gros). Pour celà, on accepte d'effectuer des échanges de clefs qu'en utilisant le proctole DH ou eDH (DH éphémère).

On détaillera les implications en terme de configuration dans la partie sur les ciphers.

Le problème DH

Les clefs de bases sont assez pourries sur les installations par défaut, il faut donc générer un groupe DH correct.

On a des doutes sur le fait que le 1024 est pas cassé actuellement, donc faut prendre au minimum 2048. 4096 est un bon compromis :

root@server:nginx# openssl dhparam -out dhparams.pem 4096
Generating DH parameters, 4096 bit long safe prime, generator 2
This is going to take a long time
............................................................+..........+...+....
................................................................................
.....................................+.................................+........
........................................................+.........+.............
..................................................................+.............
................................................................................
................................................................................
................................................................................
....+....................................................................+......
....+...........................................................................
................................................................................
.........................................+................................+.....
................................................................................
................................................................................
................................................................................
..................+.............................................................
..................+.............+.................+.............................
............................................................................+...
.............+..................................................................
.......................................................+........................
................................................................................
..................+.................................R...........................
....+...........................................................................
........................................................+.......................
...........................................................

Et on précise l'utilisation dans notre config sous Nginx:

ssl_dhparam dhparams.pem;

Pour Apache, il faut l'ajouter à la fin du certificat défini dans SSLCertificateFile, dixit la doc : 2.2, 2.4.

Enfin, pour le DH sur courbes elliptiques, on peut changer la courbe par défaut. Pour en avoir la liste complète :

root@server:nginx# openssl ecparam -list_curves
  secp112r1 : SECG/WTLS curve over a 112 bit prime field
[SNIP]
  prime192v1: NIST/X9.62/SECG curve over a 192 bit prime field
[SNIP]
  sect113r1 : SECG curve over a 113 bit binary field
[SNIP]
  c2pnb163v1: X9.62 curve over a 163 bit binary field
[SNIP]
  wap-wsg-idm-ecid-wtls1: WTLS curve over a 113 bit binary field
[SNIP]

La courbe par défaut chez Nginx est prime256v1. Je suis pas assez bon en math pour estimer quelle courbe est la meilleure, mais je sais que (généralement) plus c'est gros, plus c'est résistant. Donc j'ai testé les plus grosses de chaque type sous FF43, Android 4.4 et Sailfish 2.0 :

  • sect571r1 : Ne passe nulle part
  • secp521r1 : OK partout
  • c2tnb431r1 : OK partout
  • prime256v1 : OK partout

Au vu de ces résultat, j'ai donc arbitrairement choisi secp521r1.

Sous Nginx :

ssl_ecdh_curve secp521r1;

Et Apache (a noter qu'il faut OpenSSL 1.0.2 minimum) :

SSLOpenSSLConfCmd ECDHParameters secp521r1

Les ciphers

On peut avoir la liste des ciphers diponibles sur notre installation avec OpenSSL :

root@server:~# openssl ciphers
ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:SRP-DSS-AES-256-CBC-SHA:SRP-RSA-AES-256-CBC-SHA:SRP-AES-256-CBC-SHA:DHE-DSS-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA256:DHE-RSA-AES256-SHA:DHE-DSS-AES256-SHA:DHE-RSA-CAMELLIA256-SHA:DHE-DSS-CAMELLIA256-SHA:ECDH-RSA-AES256-GCM-SHA384:ECDH-ECDSA-AES256-GCM-SHA384:ECDH-RSA-AES256-SHA384:ECDH-ECDSA-AES256-SHA384:ECDH-RSA-AES256-SHA:ECDH-ECDSA-AES256-SHA:AES256-GCM-SHA384:AES256-SHA256:AES256-SHA:CAMELLIA256-SHA:PSK-AES256-CBC-SHA:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:SRP-DSS-AES-128-CBC-SHA:SRP-RSA-AES-128-CBC-SHA:SRP-AES-128-CBC-SHA:DHE-DSS-AES128-GCM-SHA256:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES128-SHA256:DHE-DSS-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA:DHE-RSA-SEED-SHA:DHE-DSS-SEED-SHA:DHE-RSA-CAMELLIA128-SHA:DHE-DSS-CAMELLIA128-SHA:ECDH-RSA-AES128-GCM-SHA256:ECDH-ECDSA-AES128-GCM-SHA256:ECDH-RSA-AES128-SHA256:ECDH-ECDSA-AES128-SHA256:ECDH-RSA-AES128-SHA:ECDH-ECDSA-AES128-SHA:AES128-GCM-SHA256:AES128-SHA256:AES128-SHA:SEED-SHA:CAMELLIA128-SHA:PSK-AES128-CBC-SHA:ECDHE-RSA-RC4-SHA:ECDHE-ECDSA-RC4-SHA:ECDH-RSA-RC4-SHA:ECDH-ECDSA-RC4-SHA:RC4-SHA:RC4-MD5:PSK-RC4-SHA:ECDHE-RSA-DES-CBC3-SHA:ECDHE-ECDSA-DES-CBC3-SHA:SRP-DSS-3DES-EDE-CBC-SHA:SRP-RSA-3DES-EDE-CBC-SHA:SRP-3DES-EDE-CBC-SHA:EDH-RSA-DES-CBC3-SHA:EDH-DSS-DES-CBC3-SHA:ECDH-RSA-DES-CBC3-SHA:ECDH-ECDSA-DES-CBC3-SHA:DES-CBC3-SHA:PSK-3DES-EDE-CBC-SHA:EDH-RSA-DES-CBC-SHA:EDH-DSS-DES-CBC-SHA:DES-CBC-SHA

La doc complète est dispo dans la doc d'OpenSSL : man 1SSL ciphers.

On va commencer par virer tout ceux qui n'utilisent pas DH fixe (non implémenté dans mon cas) ou éphémère. On passe de 80 à 54 ciphers compatibles PFS en éliminant d'un coup tous les échanges de clefs utilisant :

  • a/eNull
  • Anonymous

Ensuite, un certain nombre de ciphers dont le chiffrement est aujourd'hui non fiable :

Où dont le design est mauvais :

  • DSS, trop sensible
  • EXP, clefs trop petites (40 ou 58 bits) ou utilisant du chiffrement non fiable (EXPORT1024 utilise toujours RC4 ou DES d'après man 1SSL ciphers)
  • PSK, oui, c'est de l'échange de mot de passe

Heureusement, pour nous faciliter un peu la vie, OpenSSL dispose d'une syntaxe de règles d'inclusion/exclusions facile a mettre en place :

root@server:~# openssl ciphers 'HIGH:!aNULL:!MD5:!PSK:!DSS'
ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:SRP-RSA-AES-256-CBC-SHA:SRP-AES-256-CBC-SHA:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:DHE-RSA-CAMELLIA256-SHA:ECDH-RSA-AES256-GCM-SHA384:ECDH-ECDSA-AES256-GCM-SHA384:ECDH-RSA-AES256-SHA384:ECDH-ECDSA-AES256-SHA384:ECDH-RSA-AES256-SHA:ECDH-ECDSA-AES256-SHA:AES256-GCM-SHA384:AES256-SHA256:AES256-SHA:CAMELLIA256-SHA:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:SRP-RSA-AES-128-CBC-SHA:SRP-AES-128-CBC-SHA:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-CAMELLIA128-SHA:ECDH-RSA-AES128-GCM-SHA256:ECDH-ECDSA-AES128-GCM-SHA256:ECDH-RSA-AES128-SHA256:ECDH-ECDSA-AES128-SHA256:ECDH-RSA-AES128-SHA:ECDH-ECDSA-AES128-SHA:AES128-GCM-SHA256:AES128-SHA256:AES128-SHA:CAMELLIA128-SHA

44 ciphers. ~60% de ceux disponible sur notre plateforme. Ça fait pas mal.

On peut coller la liste telle quelle dans notre config :

ssl_ciphers 'ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:SRP-RSA-AES-256-CBC-SHA:SRP-AES-256-CBC-SHA:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:DHE-RSA-CAMELLIA256-SHA:ECDH-RSA-AES256-GCM-SHA384:ECDH-ECDSA-AES256-GCM-SHA384:ECDH-RSA-AES256-SHA384:ECDH-ECDSA-AES256-SHA384:ECDH-RSA-AES256-SHA:ECDH-ECDSA-AES256-SHA:AES256-GCM-SHA384:AES256-SHA256:AES256-SHA:CAMELLIA256-SHA:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:SRP-RSA-AES-128-CBC-SHA:SRP-AES-128-CBC-SHA:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-CAMELLIA128-SHA:ECDH-RSA-AES128-GCM-SHA256:ECDH-ECDSA-AES128-GCM-SHA256:ECDH-RSA-AES128-SHA256:ECDH-ECDSA-AES128-SHA256:ECDH-RSA-AES128-SHA:ECDH-ECDSA-AES128-SHA:AES128-GCM-SHA256:AES128-SHA256:AES128-SHA:CAMELLIA128-SHA' ;

Ou bien choisir de laisser les interdictions / obligations et laisser gérer OpenSSL :

ssl_ciphers 'HIGH:!aNULL:!MD5:!PSK:!DSS';

Pour Apache, la liste est séparée par des espaces et non des : :

SSLCipherSuite "ECDHE-RSA-AES256-GCM-SHA384 ECDHE-ECDSA-AES256-GCM-SHA384 ECDHE-RSA-AES256-SHA384 ECDHE-ECDSA-AES256-SHA384 ECDHE-RSA-AES256-SHA ECDHE-ECDSA-AES256-SHA SRP-DSS-AES-256-CBC-SHA SRP-RSA-AES-256-CBC-SHA SRP-AES-256-CBC-SHA DHE-DSS-AES256-GCM-SHA384 DHE-RSA-AES256-GCM-SHA384 DHE-RSA-AES256-SHA256 DHE-DSS-AES256-SHA256 DHE-RSA-AES256-SHA DHE-DSS-AES256-SHA DHE-RSA-CAMELLIA256-SHA DHE-DSS-CAMELLIA256-SHA ECDH-RSA-AES256-GCM-SHA384 ECDH-ECDSA-AES256-GCM-SHA384 ECDH-RSA-AES256-SHA384 ECDH-ECDSA-AES256-SHA384 ECDH-RSA-AES256-SHA ECDH-ECDSA-AES256-SHA AES256-GCM-SHA384 AES256-SHA256 AES256-SHA CAMELLIA256-SHA PSK-AES256-CBC-SHA ECDHE-RSA-AES128-GCM-SHA256 ECDHE-ECDSA-AES128-GCM-SHA256 ECDHE-RSA-AES128-SHA256 ECDHE-ECDSA-AES128-SHA256 ECDHE-RSA-AES128-SHA ECDHE-ECDSA-AES128-SHA SRP-DSS-AES-128-CBC-SHA SRP-RSA-AES-128-CBC-SHA SRP-AES-128-CBC-SHA DHE-DSS-AES128-GCM-SHA256 DHE-RSA-AES128-GCM-SHA256 DHE-RSA-AES128-SHA256 DHE-DSS-AES128-SHA256 DHE-RSA-AES128-SHA DHE-DSS-AES128-SHA DHE-RSA-SEED-SHA DHE-DSS-SEED-SHA DHE-RSA-CAMELLIA128-SHA DHE-DSS-CAMELLIA128-SHA ECDH-RSA-AES128-GCM-SHA256 ECDH-ECDSA-AES128-GCM-SHA256 ECDH-RSA-AES128-SHA256 ECDH-ECDSA-AES128-SHA256 ECDH-RSA-AES128-SHA ECDH-ECDSA-AES128-SHA AES128-GCM-SHA256 AES128-SHA256 AES128-SHA SEED-SHA CAMELLIA128-SHA PSK-AES128-CBC-SHA ECDHE-RSA-RC4-SHA ECDHE-ECDSA-RC4-SHA ECDH-RSA-RC4-SHA ECDH-ECDSA-RC4-SHA RC4-SHA RC4-MD5 PSK-RC4-SHA ECDHE-RSA-DES-CBC3-SHA ECDHE-ECDSA-DES-CBC3-SHA SRP-DSS-3DES-EDE-CBC-SHA SRP-RSA-3DES-EDE-CBC-SHA SRP-3DES-EDE-CBC-SHA EDH-RSA-DES-CBC3-SHA EDH-DSS-DES-CBC3-SHA ECDH-RSA-DES-CBC3-SHA ECDH-ECDSA-DES-CBC3-SHA DES-CBC3-SHA PSK-3DES-EDE-CBC-SHA"
SSLCipherSuite "HIGH !aNULL !MD5 !PSK !DSS"

HSTS

Il s'agit d'ajouter un header pour indiquer au navigateur qu'il doit parler uniquement en HTTPS avec nous pendant max-age secondes.

D'un point de vue utilisateur, s'il se connecte en HTTP, il sera en HTTP. S'il se connecte une fois en HTTPS, il sera toujours en HTTPS même s'il valide une adresse HTTP dans son historique. En revanche, le navigateur ignore cette directive si le header est reçu en HTTP... Perso, je trouve ça con, mais j'y ai pas beaucoup réfléchi.

Concrètement, sur Nginx :

add_header Strict-Transport-Security max-age=31536000;

Et dans Apache :

Header always set Strict-Transport-Security "max-age=31536000"

On a bien entendu besoin d'activer mod_headers pour cette option.

Finalement

Une fois qu'on a fait tout ça, on se retrouve avec cette configuration SSL :

/etc/nginx/ssl.conf

ssl on;

# HSTS
add_header Strict-Transport-Security max-age=31536000;

ssl_protocols TLSv1.2;

# Paramètres Diffie-Hellman
ssl_dhparam dhparams.pem;
ssl_ecdh_curve secp521r1;

# Liste de ciphers utilisables
ssl_ciphers 'HIGH:!aNULL:!MD5:!PSK:!DSS';

#  Indique que c'est au serveur de choisir le cipher et non le client
ssl_prefer_server_ciphers on;

Ou pour Apache :

SSLEngine on

# HSTS
Header always set Strict-Transport-Security "max-age=31536000"

SSLProtocol TLSv1.2

# Paramètres Diffie-Hellman
SSLOpenSSLConfCmd ECDHParameters secp521r1

# Liste de ciphers utilisables
SSLCipherSuite 'HIGH !aNULL !MD5 !PSK !DSS'

Avec ces quelques lignes de conf dans notre serveur web, on a déjà une sécurité convenable. Après, il faut aussi des certificats corrects, mais c'est pas le sujet.

Le besoin

L'autre chose à voir, c'est le besoin. Par exemple, avec mon téléphone sous SailfishOS, j'ai une couche d'émulation pour Android 4.1 et il ne sait pas communiquer avec du TLSv1.1, a priori (d'après SSL Server Test), il faudrait attendre Android 4.4.2 pour avoir du TLSv1.1 et TLSv1.2.

S'il y a besoin de downgrader jusqu'à TLSv1.0, man 1SSL ciphers dispose d'une liste des ciphers selon le proto, ce qui est bien pratique.

A priori, le cipher le moins pire est DHE-RSA-AES256-SHA , pour l'ajouter, il faut non pas interdit "SHA" (!SHA), mais le retirer (-SHA) et d'ajouter DHE-RSA-AES256-SHA à la fin de la liste. De la même manière, Chrome ne sait plus communiquer avec nous non plus, le meilleurs cipher à autoriser est ECDHE_ECDSA_WITH_AES_128_GCM_SHA256.

Ceci fait, on a une sécurité correcte, mais on se bloque plus que quelques OS :

  • Windows XP (trop vieux)
  • Windows phone 8 / 8.1 (8.1 update, ça marche normalement)
    • ... Sérieusement ? Après 6 ans ? Et alors que ça marche sur les versions Desktop ?!
  • Toutes les plateforme Java
    • elles n'aiment pas les paramètres DH de plus de 1024bits (GTFO), Java 8 accepte de monter à 2048 (GTFO²)
    • Java 6 ne gère pas le SNI (la RFC ayant été rédigée la même année que la release de cette version)
  • Android ≤ 2.3.7
    • ne gère pas le SNI non plus (6 ans après ? Sérieusement ?)

Au final, on se trouve avec cette configuration :

ssl on;

# HSTS
add_header Strict-Transport-Security max-age=31536000;

# Paramètres Diffie-Hellman
ssl_dhparam dhparams.pem;
ssl_ecdh_curve secp521r1;

ssl_protocols TLSv1 TLSv1.1 TLSv1.2;

# Liste de ciphers utilisable
ssl_ciphers 'HIGH:!aNULL:!MD5:!PSK:!DSS:ECDHE_ECDSA_WITH_AES_128_GCM_SHA256:DHE-RSA-AES256-SHA';

#  Indique que c'est au serveur de choisir le cipher et non le client
ssl_prefer_server_ciphers on;

Ou pour Apache :

SSLEngine on

# HSTS
Header always set Strict-Transport-Security "max-age=31536000"

SSLProtocol All -SSLv2 -SSLv3

# Paramètres Diffie-Hellman
SSLOpenSSLConfCmd ECDHParameters secp521r1

# Liste de ciphers utilisable
SSLCipherSuite 'HIGH !aNULL !MD5 !PSK !DSS ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 DHE-RSA-AES256-SHA'

Outils

On a quelques webservices pour tester une configuration SSL, par exemple :

  • CryptCheck, que je trouve très cool parce que très parlant pour avoir une vue simple de sa configuration
  • SSL Server Test, que je trouve encore plus cool parce que très détaillé et fait des tests avec différents navigateurs pour vous donner une idée de la compatibilité de votre site