Compilation, paramétrage, certificat SSL/TLS Let'sEncrypt sur adresse IPv6 dédiée d'un serveur web openresty (NGINX + Lua)

Introduction

Cette fiche opérationnelle décrit la mise en place d'un serveur web openresty, ou plus simplement Resty.

Le serveur openResty est une édition spéciale du serveur NGINX qui intègre un compilateur « juste à temps » LuaJIT du langage Lua. Ce serveur est réputé pour sa rapidité, ses capacités asynchrones et sa consommation mémoire maîtrisée. Son Lua intégré s'appuie lui aussi sur ces qualités, notamment avec ses coroutines. Ainsi, on peut charger une fois pour toutes les programmes Lua composant un site web, et ce dès le démarrage du serveur, sans besoin de devoir ensuite (re-)lire et (re-)compiler en permanence les pages d'un site.

Il n'existe pas de version précompilée pour l'architecture ARM des cartes Raspberry(ies), nous devrons compiler les sources pour Raspbian, une distribution Debian stretch.

Préparation du serveur

Nous partons d'un serveur Raspian minimal auquel un utilisateur non privilégié or a été ajouté. Il sera l'utilisateur par défaut du serveur web.

On charge, sous login root, les outils Lua, liste à compléter à convenance.

apt-get install git luarocks curl libssl-dev libpcre3-dev openssl
Le gestionnaire de paquets luarocks est l'analogue pour Lua des apt, pip et autres npm.

Rappel : Sous Debian, la numérotation des deux versions des PCRE est piègeuse : libpcre2 est la nouvelle librairie compatible Perl6+ tandis que libpcre3 est l'ancienne compatible Perl5. Nous avons besoin de cette dernière.

Du fait des restrictions imposées par la politique maison de sécurité des systèmes, on relaxe certains des droits portant sur tous les utilisateurs pour les fichiers et répertoires à créer, luarocks ne les gérant pas spécifiquement

umask 022 # default system policy should be retricted to 027 or even 077

Nous commençons par installer moonscript. Il s'agir d'un DSLDomain Specific Language — qui ajoute une notion d'objet à Lua ainsi qu'une syntaxe expressive et compacte exprimée par indentation — cf. Python. Cette lisibilité est vitale pour maintenir à terme les sources des programmes car, pour plagier un célèbre acronyme, il s'agit de programmation WYSIWYC : « ce qu'on voit est ce qu'on Code ». Plus tard, ou à de nouveaux yeux, cela signifiera surtout « ce qu'on voit est bien ce qui a été codé ».

Les paquets Lua nécessaires seront automatiquement téléchargés et compilés

luarocks install moonscript

Passons à lapis, un framework dans le style de Ruby On Rails ou de django.

luarocks install lapis

Sources & Compilation

Cette phase ne nécessitant aucun privilège, elle se déroulera ici sous le login dédié or, auquel nous affecterons les workers du démon openresty. Il sera toutefois possible de le modifier avec la directive user du fichier de configuration de Resty.

Visiter http://openresty.org/en/download.html puis choisir la dernière version du code source

wget https://openresty.org/download/openresty-1.13.6.2.tar.gz
tar xpf openresty-1.13.6.2.tar.gz
cd openresty-1.13.6.2/
./configure \
    --build=resty \
    --user=or \
    --group=or \
    --prefix=/opt/openresty \
    --conf-path=/etc/openresty/openresty.conf \
    --error-log-path=/var/log/openresty/error.log \
    --http-log-path=/var/log/openresty/access.log \
    --pid-path=/run/openresty.pid \
    --lock-path=/run/openresty.log \
    --http-client-body-temp-path=/run/openresty/body \
    --http-fastcgi-temp-path=/run/openresty/fastcgi \
    --http-proxy-temp-path=/run/openresty/proxy \
    --http-scgi-temp-path=/run/openresty/scgi \
    --http-uwsgi-temp-path=/run/openresty/uwsgi \
    --with-pcre-jit \
    --with-luajit \
    --with-threads \
    --with-file-aio \
    --with-http_v2_module \
    --with-openssl-opt=enable-tls1_3 \
    -j$(($(nproc)/2))
Adapter, le cas échéant, ces options et d'autres à vos propres besoins. Compiler
make -j $(nproc)

Installation & Tests

Installer — ici depuis une session en tant que root du fait que or n'a pas accès à sudo ni à d'autres privilèges analogues.

cd /home/or/openresty-1.13.6.2/ && make install
quelques tests, toujours sous root
mkdir /run/openresty
chown or:or /run/openresty
chmod 0700 /run/openresty
/opt/openresty/nginx/sbin/nginx -t
nginx: the configuration file /etc/openresty/openresty.conf syntax is ok
nginx: configuration file /etc/openresty/openresty.conf test is successful
/opt/openresty/nginx/sbin/nginx ss -tapine |grep -F nginx
LISTEN     0      128          *:80                       *:*                   users:(("nginx",pid=32767,fd=6),("nginx",pid=300,fd=6)) ino:181820 sk:7 <->
curl localhost:80
(...
     la page HTML par défaut
...)
/opt/openresty/nginx/sbin/nginx -s stop
En cas de problème vérifier access.log et error.log sous /var/log/openresty

Paramétrage & Test

  1. Conformément aux dispositions des conventions maison, nous mettons en place une adresse IPv6 supplémentaire exclusivement dédiée au site web info.scalaire.fr. Le serveur resty sera seul à l'écouter (sur le port 443/https) et n'écoutera réciproquement aucune autre adresse. Pour cela voir la fiche opérationnelle associée ;

  2. Obtenir un certificat SSL/TLS via Let'sEncrypt en suivant la fiche opérationnelle correspondante. Dès après que le robot ait installé les certificats, on pourra les intégrer directement dans la configuration suivante ;

  3. Fichier de configuration /etc/openresty/openresty.conf (très stricte).
    F=/etc/openresty/openresty.conf
    mv -f "$F" "$F".prev
    cat > "$F" <<EOD....................
    user  or;
    worker_processes  auto;
    pcre_jit    on;
    

    events { worker_connections 100; }

    http { include mime.types; default_type application/octet-stream; charset utf-8;

    reset_timedout_connection on;

    sendfile on; tcp_nopush on; aio threads; directio 1m; #XFS: directio_alignment 4k;

    keepalive_timeout 60 60;

    ssl_session_cache shared:SSL:10m; ssl_session_timeout 2h; ssl_session_tickets off; ssl_protocols TLSv1.2 TLSv1.3;

    # jmp@scalaire : # - waiting : ECDHE-RSA-AES256-GCM-SHA512 : DHE-RSA-AES256-GCM-SHA512 : EECDHE-ECDSA-AES256-GCM-SHA384 # - by end of 2019, all -RSA- ciphers should be deprecated # => ECDSA certificates and (security updated as of nov.2019) # => ssl_ciphers 'ECDHE-ECDSA-CHACHA20-POLY1305 : ECDHE-ECDSA-AES128-GCM-SHA256 : ECDHE-ECDSA-AES256-GCM-SHA384 : !ECDHE-ECDSA-AES128-CBC-SHA256 : !ECDHE-ECDSA-AES256-CBC-SHA384';

    # SECURITY UPDATE -- nov.2019 (remove _CBC_ see https://robotattack.org/) : ssl_ciphers 'ECDHE-RSA-CHACHA20-POLY1305 : ECDHE-RSA-AES128-GCM-SHA256 : ECDHE-RSA-AES256-GCM-SHA384 : !ECDHE-RSA-AES128-CBC-SHA256 : !ECDHE-RSA-AES256-CBC-SHA384';

    ssl_prefer_server_ciphers on; ssl_dhparam /etc/openresty/dhparam.pem; ssl_ecdh_curve secp384r1; # Requires nginx >= 1.1.0

    # OCSP Stapling --- ssl_stapling on; ssl_stapling_verify on;

    # resolver <IP DNS resolver>; # resolver_timeout 5s;

    # HSTS (15768000 seconds = 6 months) add_header Strict-Transport-Security "max-age=15768000; includeSubDomains; preload"; add_header X-Frame-Options DENY; # or, relaxed: SAMEORIGINE add_header X-Content-Type-Options nosniff; add_header X-XSS-Protection "1; mode=block";

    server { # si IPv4 : listen 80 default_server; # all IPv6 : listen [::]:80 default_server; listen info.scalaire.fr:80; # listen only on this one return 301 https://$host$request_uri; # redirect clear trafic } server { listen info.scalaire.fr:443 ssl http2 reuseport; server_name info.scalaire.fr;

    ssl_certificate /etc/letsencrypt/live/info.scalaire.fr/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/info.scalaire.fr/privkey.pem; ssl_trusted_certificate /etc/letsencrypt/live/info.scalaire.fr/ca.pem; location / { root /home/or/html; index index.html; } error_page 404 500 502 503 504 /index.html; } } EOD.................... chmod 0660 "$F"
  4. Créer un solide échange Diffie–Hellman
    openssl dhparam -out /etc/openresty/dhparam.pem 4096
    

  5. Créer le fichier attendu par la configuration précédente sous le login de profil standard or
    su - or -c "mkdir ~/html && echo '<html><body><h1>A CONFIGURER</h1></body></html>' > ~/html/index.html"
    

  6. Relancer resty pour vérifier l'accès en TLS sur le répertoire opérationnel du site
    /opt/openresty/nginx/sbin/nginx -t # test config
    /opt/openresty/nginx/sbin/nginx # daemonize
    ss -tapine |grep -F nginx # http/80 redirect + 4 workers on the dedicaced IPv6
    
    LISTEN     0      128       2a21:e1b:182:f041:fedd:76f0:2bfe:443:80                      :::*                   users:(("nginx",pid=5150,fd=6),("nginx",pid=5149,fd=6),("nginx",pid=5148,fd=6),("nginx",pid=5147,fd=6),("nginx",pid=5146,fd=6)) ino:197986 sk:13 v6only:1 <->
    LISTEN     0      128       2a21:e1b:182:f041:fedd:76f0:2bfe:443:443                     :::*                   users:(("nginx",pid=5150,fd=7),("nginx",pid=5149,fd=7),("nginx",pid=5148,fd=7),("nginx",pid=5147,fd=7),("nginx",pid=5146,fd=7)) ino:197987 sk:14 v6only:1 <->
    LISTEN     0      128       2a21:e1b:182:f041:fedd:76f0:2bfe:443:443                     :::*                   users:(("nginx",pid=5150,fd=8),("nginx",pid=5149,fd=8),("nginx",pid=5148,fd=8),("nginx",pid=5147,fd=8),("nginx",pid=5146,fd=8)) ino:197988 sk:15 v6only:1 <->
    LISTEN     0      128       2a21:e1b:182:f041:fedd:76f0:2bfe:443:443                     :::*                   users:(("nginx",pid=5150,fd=9),("nginx",pid=5149,fd=9),("nginx",pid=5148,fd=9),("nginx",pid=5147,fd=9),("nginx",pid=5146,fd=9)) ino:197989 sk:16 v6only:1 <->
    LISTEN     0      128       2a21:e1b:182:f041:fedd:76f0:2bfe:443:443                     :::*                   users:(("nginx",pid=5150,fd=10),("nginx",pid=5149,fd=10),("nginx",pid=5148,fd=10),("nginx",pid=5147,fd=10),("nginx",pid=5146,fd=10)) ino:197990 sk:17 v6only:1 <->
    
    curl -6 info.scalaire.fr # http->https redirect ?
    <html>
    <head><title>301 Moved Permanently</title></head>
    <body bgcolor="white">
    <center><h1>301 Moved Permanently</h1></center>
    <hr><center>openresty/1.13.6.2</center>
    </body>
    </html>
    
    curl -6 https://info.scalaire.fr # our new page ?
    <html><body><h1>A CONFIGURER</h1></body></html>
    
    Note : le domaine ne devant plus se résoudre qu'en IPv6, le -6 n'est pas nécessaire.

    Pour l'aide au diagnostic d'un éventuel problème
    openssl s_client -connect info.scalaire.fr:443
    

  7. Mise en place du lancement automatique par systemd
    F=/etc/systemd/system/openresty.service
    cat > "$F" <<EOD.........................
    # start/stop/reload Resty web server
    # (jmp@scalaire.fr -- Raspberry Pi3+B)
    #
    [Unit]
    Description=OpenResty (NGINX with Lua engine) web server
    Documentation=man:nginx(8)
    After=network.target
    

    [Service] Environment="TEMP=/run/openresty" Type=forking PIDFile=/run/openresty.pid ExecStartPre=/bin/mkdir "\$TEMP" ExecStartPre=/bin/chown or:or "\$TEMP" ExecStartPre=/opt/openresty/nginx/sbin/nginx -t -q ExecStart=/opt/openresty/nginx/sbin/nginx ExecReload=/opt/openresty/nginx/sbin/nginx -s reload ExecStop=-/opt/openresty/nginx/sbin/nginx -s stop ExecStopPost=/bin/rm -rf "\$TEMP" TimeoutStopSec=5 KillMode=mixed

    [Install] WantedBy=multi-user.target EOD......................... chmod 0640 "$F" systemctl daemon-reload systemctl start openresty systemctl status openresty
    Si tout se déroule bien comme prévu, passer en mode démarrage automatique au reboot
    systemctl enable openresty
    

  8. Dernières vérifications
    • accéder au site depuis l'internet avec un navigateur décent ;
    • vérifier le certificat ;
    • tester via https://www.ssllabs.com/ssltest/index.html. La note A+ obtenue n'est pas nécessairement souhaitable, en particulier si l'on subit la contrainte d'accepter les paléonavigateurs...

Ressources

En plus des liens précédents, une aide au choix des ciphers