Nous allons créer un serveur web sous apache qui héberge un site wordpress. Nous allons ensuite mettre en place un serveur de cache “Varnish” qui va permettre d’accélérer le chargement du site mais également de permettre grace à ces performances d’encaisser une montée en charge en n’utilisant quasiment aucune ressources matérielles supplémentaires.

Au préalable nous aurons mis en service un serveur sous debian (c’est ma préférence).

Apache avec php-fpm

Nous commençons par installer apache avec php-fpm (et non mod-php). L’utilisation de php-fpm permet de n’utiliser php que lorsque cela est nécessaire, contrairement à mod-php qui charge php en permanence avec apache.

apt-get update
apt-get install apache2

php-fpm consiste en un serveur php autonome avec lequel Apache communique via fastCGI. On utilisera un socket unix pour la communication.

Nous allons installer php-fpm et configurer Apache afin qu’il transmette les requêtes à php lorsqu’elles nécessitent d’être interprétées.

# installation de php-fpm et ses dépendances
apt-get install php-fpm

# apache a besoin du module proxy_fcgi pour transmettre les requêtes
a2enmod proxy_fcgi

# on précise de manière globale à apache (pour tous les vhost)
# de transmettre toutes requête destinée à php
a2enconf php7.2-fpm

# on s'assure que mod_php est désactivé
a2dismod php7.2

La configuration de la connexion se trouve dans /etc/php/7.2/fpm/pool.d/www.conf. La partie qui nous intéresse ici est celle qui définit la création du socket d’écoute :

listen = /run/php/php7.2-fpm.sock

C’est cette information qui est reprise dans le fichier de configuration pre-rempli qui nous est automatiquement fournit et que l’on active via a2enconf php7.2-fpm. On peut inspecter son contenu (et le créer s’il n’existe pas). Il se trouve à /etc/apache2/conf-available/php7.2-fpm.conf :

<IfModule !mod_php7.c>
    # Enable http authorization headers
    SetEnvIfNoCase ^Authorization$ "(.+)" HTTP_AUTHORIZATION=$1

    <FilesMatch ".+\.ph(p[3457]?|t|tml)$">
        SetHandler "proxy:fcgi://localhost:9000"
    </FilesMatch>
    <FilesMatch ".+\.phps$">
        # Deny access to raw php sources by default
        # To re-enable it's recommended to enable access to the files
        # only in specific virtual host or directory
        Require all denied
    </FilesMatch>
    # Deny access to files without filename (e.g. '.php')
    <FilesMatch "^\.ph(p[3457]?|t|tml|ps)$">
        Require all denied
    </FilesMatch>
</IfModule>

On voit bien que la directive SetHandler reprend (en ajoutant une option pour écouter via une connexion TCP au besoin) notre socket UNIX. Il est également possible de ne pas activer la configuration et d’ajouter ceci directement dans les ou les vhost concernés (cela permet de ne pas l’activer pour tous…).

Il y a un autre paramètre très important dans ce fichier de configuration, il s’agit de pm.max_children. Cette valeur défini combien de processus PHP peuvent s’exécuter en paralèlle. Si vous atteingnez le maximum, vous verrez un warning dans les logs de php et la requête sera placée en file d’attente.

La valeur est à 5 par défaut. Si vous avez du trafic, il peut être judicieux d’augmenter cette valeur. Évidemment, c’est à mettre en relation avec la charge CPU et la RAM disponible car des processus supplémentaires ont un coût.

Réglages du php.ini

Quelle que soit la méthode choisie, php est désormais fonctionnel ! Il reste néanmoins quelques paramètres à modifier dans le fichier de configuration de php, le php.ini. Vous trouverez ce fichier dans /etc/php/7.2/apache ou /etc/php/7.2/fpm.

Je vous mets ci-dessous, une liste des paramètres intéressants que l’on doit ou que l’on a souvent à changer.

;;;;;;;;;;;;;;;;;;;;
; Language Options ;
;;;;;;;;;;;;;;;;;;;;
# Mettre à Off si possible
# les shorts open tags sont les balises d'ouvertures courtes de php <?
# elles posent des problèmes de compatibilité avec XML
# puisque c'est justement la syntaxe des balises XML. 
short_open_tag = Off

# le safe_mode est déprécié dans PHP 5.3 et a été supprimé de 5.4
# par conséquent, laissez cette option à Off
safe_mode = Off

# l'open basedir permet de limiter les dossiers dans lequel php va pouvoir interpréter des données
# cela est plus sur car un attaquant ne pourra pas utiliser php pour lire des données en dehors de dossier spécifié
; http://php.net/open-basedir
open_basedir = /var/www

# désactiver des fonctions de php
# il y en a certaines de désactivées par défaut, toutefois, certaines fonctions sont souvent utilisées pour 
# exploiter une faille dans l'application et mettre en place des backdors, ainsi, si votre application ne 
# les utilise pas, il est judicieux de les désactiver.
# à celle par défaut listées ci-dessous, on peut ajouter :
# exec,passthru,shell_exec,system,proc_open,popen,curl_exec,curl_multi_exec,parse_ini_file,show_source
disable_functions = pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signasignal,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,

;;;;;;;;;;;;;;;;;
; Miscellaneous ;
;;;;;;;;;;;;;;;;;
# ajoute ou non la signature de php dans les en-tête http
# ce n'est pas primordial, mais personne n'a à savoir
# que nous utilisons php, ni la version utilisée
expose_php = Off

;;;;;;;;;;;;;;;;;;;
; Resource Limits ;
;;;;;;;;;;;;;;;;;;;
# ces options concernent les performances de php
# les réglages par défaut doivent normalement suffire pour la plupart des cas

# temps maximum d'exécution après lequel un script sera arrêté
# sauf cas particulier, vos script ne devraient jamais mettre 
# plus de quelques secondes à s'exécuter
max_execution_time = 30

# temps maximum qu'un script peut mettre pour recevoir des données input
# au delà de ce laps de temps, le traitement est stoppé
# il peut-être ici judicieux d'augmenter ce temps si vous proposer l'upload de fichier
max_input_time = 60

# taille de mémoire qu'un script peut consommer
# de même, si vous avez un formulaire d'upload de gros fichiers
# il se peut que vos scripts aient besoin de plus de mémoire
memory_limit = 512M

# affiche les erreurs php
# mettre à On pendant le dev, Off en production
display_errors = On

# affiche les erreurs qui ont cours durant la séquence de démarrage de php
# mettre à Off en production. Il peut être intéressant de les mettre 
# à On en développement pour le débogage. 
display_startup_errors = Off

# consigne les erreurs dans les fichiers de log
# je conseille de toujours laisser à On
log_errors = On

;;;;;;;;;;;;;;;;;
; Data Handling ;
;;;;;;;;;;;;;;;;;
# taille maximale que php accepte pour les données en POST
# dans le cas d'un upload de fichier par post, 
# cette valeur sera aussi à modifier si vous voulez uploader des fichiers de plus de 8M
post_max_size = 8M

;;;;;;;;;;;;;;;;
; File Uploads ;
;;;;;;;;;;;;;;;;

# peut-être désactivé si vous ne comptez pas autoriser l'upload de fichiers
file_uploads = On

# définit la taille maximale de fichiers que l'on peut uploader
# cette taille ne peut toutefois pas dépasser celle de post_max_size
upload_max_filesize = 2M

;;;;;;;;;;;;;;;;;;;
; Module Settings ;
;;;;;;;;;;;;;;;;;;;

# règle le fuseau horaire
date.timezone = Europe/Paris

Ensuite nous allons configurer notre site. Nous écoutons sur le port 8080

<VirtualHost [::1]:8080>

ServerName www.example.com

ServerAdmin webmaster@localhost
DocumentRoot /var/www/html

Include /etc/phpmyadmin/apache.conf

ErrorLog ${APACHE_LOG_DIR}/example.com/error.log
CustomLog ${APACHE_LOG_DIR}/example.com/access.log combined

</VirtualHost>

J’ai utilisé [::1] qui est l’adresse ipv6 correpondante à 127.0.0.1 en ipv4 (donc localhost) car ipv4 et en fin de vie donc il ne faut pas hésiter à basculer.

Il faut maintenant redémarrer le serveur apache pour que ces modifications soient prisent en compte.

service apache2 restart

Installation de Varnish

On commence par installer varnish :

apt-get install varnish

Configuration test d’un site sous apache avec Varnish

Avant lancer Varnish en écoute sur le port 80 : on va l’essayer sur un autre port, vérifier que tout fonctionne bien (sans toucher à l’accès standard http sur apache pour l’instant), puis on échangera les ports pour mettre Varnish « en frontal » quand on aura validé que tout marche bien comme on veut.

La configuration de Varnish se trouve dans les fichiers suivant :

/etc/varnish/default.vcl
/etc/default/varnish ou 
/etc/systemd/system/multi-user.target.wants/varnish.service

Un peu de théorie…

Lorsqu’un client envoie une requête à Varnish :

  1. La routine vcl_recv est appelée en premier : son but est de décider s’il faut « servir » la demande, ou la refuser,
    et faire éventuellement un peu de ménage dans la requête (cookies).
    Elle peut terminer par un appel à l’une de ces 5 autres routines :
    1. pass : la requête sera traitée mais le résultat ne sera pas « caché » par Varnish (pour les pages d’admin du site par exemple) ;
    2. pipe : une fois appelé, la requête sera traitée sans action de Varnish, il se contentera juste de transférer la communication ;
    3. synth : renvoi un objet construit par Varnish (mais hors cache), typiquement pour retourner « coucou-test » ou quelques cas d’erreur ;
    4. purge : demande à Varnish de vider le cache (sur demande, par exemple à la publication d’un nouvel article) ;
    5. hash : C’est ce qui va nous intéresser, en fait pour indexer les pages dans le cache Varnish utilise un « hash » de la requête, à partir de ce hash on va pouvoir faire un « lookup », c’est-à-dire chercher la page dans le cache pour la servir.
  2. lookup : Cette partie n’apparaît pas dans le fichier VCL : Varnish fait son boulot de recherche dans son cache et passe ensuite la main à une des trois sous routines suivante :
    1. pass (comme au-dessus), dans le cas où la recherche dans la cache à retourner un « hit_for_pass »
      Un « hit_for_pass » (qui est très bien expliqué ) : C’est un cas identique au « miss » ci-dessous (on ne trouve pas la page dans le cache), mais une requête précédente a marqué la page comme « ne pouvant pas être mise en cache », indiquant ainsi à Varnish de passer en mode pass sur ces requêtes systématiquement.
      1. De là, on passe la demande au serveur (backend) pour récupérer la page.
      2. et on la passe à deliver
    2. miss : la page n’a pas été trouvé dans le cache ;
      1. on passe donc la demande au serveur (backend) pour récupérer la page.
      2. et on la passe à deliver
    3. hit : la page est présente dans le cache
      1. on charge la page depuis le cache (avec vérification d’expiration du cache, si c’est le cas on lance un rechargement la page dans le cache en arrière-plan)
      2. et on la passe à deliver
  3. Deliver : C’est la dernière fonction dans Varnish avant de renvoyer le contenu au client.
    1. Typiquement on va y indiquer si la page provient du cache ou non, masquer la version de PHP d’Apache, etc.

Et c’est tout pour la partie Front End. La partie Back End fonctionne comme on pourrait s’y attendre pour un mécanisme de cache. La seule fonction qui va vraiment nous intéresser là-dedans, c’est vcl_backend_response qui permet de modifier la réponse renvoyée par le serveur « backend ».

En pratique …

le premier truc à regarder c’est le fichier /etc/default/varnish pour contrôler sur quel port Varnish écoute. Pour ma config de test, j’ai laissé le choix deux proposé par la config par défaut :

## Alternative 2, Configuration with VCL # 
# Listen on port 6081, administration on localhost:6082, and forward to 
# one content server selected by the vcl file, based on the request. 
# Use a 256MB memory based cache. 
# firewall if 
DAEMON_OPTS="-a [::1]:7080 \
             -T [::1]:6082 \ 
             -f /etc/varnish/default.vcl \ 
             -S /etc/varnish/secret \ 
             -s malloc,256m"

L’option -T signifie que l’administration de varnish est accessible via localhost sur le port 6082.

Le fichier vcl

Ensuite on va définir nos « backends », pour Varnish : c’est le serveur de contenu qu’il doit cacher.

# Default backend definition. Set this to point to your content server. 
backend default {
     .host = "::1";
     .port = "8080";
 } 

Une ACL

Ensuite une micro étape où on définit une ACL (par IP) local dont on se servira juste après :

acl purge {
     "localhost";
     "::1";
 }

vcl_recv

La première fonction appelé c’est vcl_recv (pour « receive ») à la réception d’une requête, et c’est donc sur cette requête HTTP qu’on va agir dans le VCL.

# This function is used when a request is send by a HTTP client (Browser) 
sub vcl_recv {

Première étape, on va supprimer le port TCP du champ host (si présent), dans mon cas ça va permettre d’utiliser le même cache si mon site est accessible depuis un port ou un autre (genre le 80, et le 6081 pour les tests)

# Normalize the header, remove the port (in case you're testing this on various TCP ports) 
set req.http.Host = regsub(req.http.Host, ":[0-9]+", "");

Ensuite, diriger vers le bon « backend » en fonction du champ host dans la recherche.

if (req.http.host ~ "www.man-narbonne.fr") {
     set req.backend_hint = default;
} 

Ensuite, on va faire plein de traitement pour optimiser et sécuriser le mécanisme de cache. Déjà, on va autoriser uniquement local host à demander des requêtes PURGE, à l’aide de l’ACL définie juste AVANT :

# Allow purging from ACL 
if (req.method == "PURGE") {
     # If not allowed then a error 405 is returned
     if (!client.ip ~ purge) {
         return(synth(405, "This IP is not allowed to send PURGE requests."));
     }
     # If allowed, do a cache_lookup -> vlc_hit() or vlc_miss()
     return (purge);
}

Puis s’assurer qu’on ne cachera pas les pages incluant des requêtes de type POST (parce que ce serait mal) :

# Post requests will not be cached 
if (req.http.Authorization || req.method == "POST") {
      return (pass);
 }

On arrive à un bloc d’optimisation spécifique à WordPress :

# --- WordPress specific configuration
# Did not cache the admin and login pages
if (req.url ~ "wp-(login|admin)" || req.url ~ "preview=true") {
    return (pass);
}

# Did not cache the RSS feed
if (req.url ~ "/feed") {
    return (pass);
}

# Remove has_js and CloudFlare/Google Analytics __* cookies.
set req.http.Cookie = regsuball(req.http.Cookie, "(^|;\s*)(_[_a-z]+|has_js)=[^;]*", "");

# Remove a ";" prefix, if present.
set req.http.Cookie = regsub(req.http.Cookie, "^;\s*", "");

# Remove the "has_js" cookie
set req.http.Cookie = regsuball(req.http.Cookie, "has_js=[^;]+(; )?", "");

# Remove any Google Analytics based cookies
set req.http.Cookie = regsuball(req.http.Cookie, "__utm.=[^;]+(; )?", "");

# Remove the Quant Capital cookies (added by some plugin, all __qca)
set req.http.Cookie = regsuball(req.http.Cookie, "__qc.=[^;]+(; )?", "");

# Remove the wp-settings-1 cookie
set req.http.Cookie = regsuball(req.http.Cookie, "wp-settings-1=[^;]+(; )?", "");

# Remove the wp-settings-time-1 cookie
set req.http.Cookie = regsuball(req.http.Cookie, "wp-settings-time-1=[^;]+(; )?", "");

# Remove the wp test cookie
set req.http.Cookie = regsuball(req.http.Cookie, "wordpress_test_cookie=[^;]+(; )?", "");

# Are there cookies left with only spaces or that are empty?
if (req.http.cookie ~ "^ *$") {
    unset req.http.cookie;
}

# Cache the following files extensions
if (req.url ~ "\.(css|js|png|gif|jp(e)?g|swf|ico)") {
    unset req.http.cookie;
}

# Normalize Accept-Encoding header and compression
# https://www.varnish-cache.org/docs/3.0/tutorial/vary.html
if (req.http.Accept-Encoding) {
    # Do no compress compressed files...
    if (req.url ~ "\.(jpg|png|gif|gz|tgz|bz2|tbz|mp3|ogg)$") {
        unset req.http.Accept-Encoding;
    } elsif (req.http.Accept-Encoding ~ "gzip") {
        set req.http.Accept-Encoding = "gzip";
    } elsif (req.http.Accept-Encoding ~ "deflate") {
        set req.http.Accept-Encoding = "deflate";
    } else {
        unset req.http.Accept-Encoding;
    }
}

# Check the cookies for wordpress-specific items
if (req.http.Cookie ~ "wordpress_" || req.http.Cookie ~ "comment_") {
    return (pass);
}
if (!req.http.cookie) {
    unset req.http.cookie;
}
# --- End of WordPress specific configuration

Vous noterez que depuis le départ, on ne fait que des « return (pass) » ou retirer des éléments de notre requête. Du coup arrivé ici, la requête est « propre » : on peut la passer à hash (expliquée dans la partie algo plus haut).

# Cache all others requests return (hash);

Et c’est tout pour vcl_recv()

pipe et pass

Du coup, on va définir pipe et pass (qui ne font rien en particulier chez moi, on pourrait ne pas le mettre) :

sub vcl_pipe {
         return (pipe); 
} 
sub vcl_pass {
         return (fetch); 
}

vcl_hash

Et la plus importante : hash, dans laquelle on va construire l’élément (le condensat ou hash issu de notre requête) sur lequel Varnish va indexer la page demandée dans le cache. Ici c’est le comportement par défaut (très bien expliqué ici), à chaque appel on va construire un hash à partir de l’URL demandée et, selon la disponibilité, du host HTTP ou de l’adresse IP du serveur interrogé :

# The data on which the hashing will take place 
sub vcl_hash {
     hash_data(req.url);
     if (req.http.host) {
         hash_data(req.http.host);
     } else {
         hash_data(server.ip);
     }
    # If the client supports compression, keep that in a different cache 
    if (req.http.Accept-Encoding) {
         hash_data(req.http.Accept-Encoding);
    }
    return (lookup); 
}

vcl_backend_response

Et comme expliqué plus haut, hash passe à lookup pour rechercher un élément correspondant à notre condensat dans le cache, ou demander au backend de servir la page si elle n’y est pas présente. Dans le cas où on descend jusqu’au Back End, on va passer dans vcl_backend_response avant de renvoyer la réponse à deliver.

vcl_backend_response permet d’agir sur la réponse du serveur (et son header HTTP donc) avant que celle-ci ne soit mise en cache par Varnish. Typiquement on va y refaire des traitements comme pour recv() mais indiquant cette fois de ne pas cacher la page. Je vous la donne en un bloc :

# This function is used when a request is sent by our backend (Nginx server)
sub vcl_backend_response {
        # Remove some headers we never want to see
        unset beresp.http.Server;
        unset beresp.http.X-Powered-By;

        # For static content strip all backend cookies
        if (bereq.url ~ "\.(css|js|png|gif|jp(e?)g)|swf|ico") {
                unset beresp.http.cookie;
        }
        # Don't store backend
        if (bereq.url ~ "wp-(login|admin)" || bereq.url ~ "preview=true") {
                set beresp.uncacheable = true;
                set beresp.ttl = 30s;
                return (deliver);
        }

        # Only allow cookies to be set if we're in admin area
                if (!(bereq.url ~ "(wp-login|wp-admin|preview=true)")) {
                unset beresp.http.set-cookie;
        }

        # don't cache response to posted requests or those with basic auth
        if ( bereq.method == "POST" || bereq.http.Authorization ) {
                set beresp.uncacheable = true;
                set beresp.ttl = 120s;
                return (deliver);
        }

        # don't cache search results
        if ( bereq.url ~ "\?s=" ){
                set beresp.uncacheable = true;
                set beresp.ttl = 120s;
                return (deliver);
        }

        # only cache status ok
        if ( beresp.status != 200 ) {
                set beresp.uncacheable = true;
                set beresp.ttl = 120s;
                return (deliver);
        }

        # A TTL of 2h
        set beresp.ttl = 2h;
        # Define the default grace period to serve cached content
        set beresp.grace = 30s;

        return (deliver);
}

vcl_deliver

On arrive au bout, notre réponse va être renvoyée au client, mais on veut encore faire quelques traitements sur celle-ci, par exemple de sécurité pour éviter de pousser tous les numéros de versions des technologies utilisées par le site :

sub vcl_deliver {
        if (obj.hits > 0) {
                set resp.http.X-Cache = "cached";
        } else {
                set resp.http.x-Cache = "uncached";
        }

        # Remove some headers: PHP version
        unset resp.http.X-Powered-By;

        # Remove some headers: Apache version & OS
        unset resp.http.Server;

        # Remove some heanders: Varnish
        unset resp.http.Via;
        unset resp.http.X-Varnish;

        return (deliver);
}

init et fini

Et c’est tout ! On va juste rajouter les fonctions init et fini s’exécutant 1 fois au chargement et déchargement du fichier VCL respectivement (pour charger des modules typiquement, mais pas utilisée dans mon cas)

sub vcl_init {
         return (ok); 
} sub vcl_fini {
         return (ok); 
}

Voilà, il ne vous reste plus qu’à comprendre, concaténer et adapter les lignes de code ci-dessus et vous devriez obtenir deux fichiers de configuration pour Varnish 4 tout beau pour un WordPress basique.

Mise en route

Pour démarrer Varnish et lui faire charger ses 2 fichiers de conf :

service varnish restart

Passage en https avec nginx

Nous allons maintenant installer et configurer nginx en reverse proxy afin de transformer notre site en https.

Nous avons d’abord besoin d’un certificat ssl que l’on va obtenir grâce à let’s encrypt.

Let’s Encrypt

Commençons par installer git

apt-get install git

Puis nginx

apt-get install nginx

Maintenant nous allons cloner Let’s Encrypt:

git clone https://github.com/letsencrypt/letsencrypt /opt/letsencrypt --depth=1

J’ai rajouté l’option depth=1 afin d’éviter de récupérer l’entièreté de l’historique git. Nous pourrons continuer à utiliser ce dépôt pour mettre à jour le client, mais nous n’avons pas besoin de revenir à des versions antérieures. Si plus tard on souhaite mettre à jour le client, il nous suffira de faire un pull :

cd /opt/letsencrypt 
git pull

Mise en place du certificat

Let’s Encrypt dispose d’un mode automatique qui va installer les dépendances nécessaires à l’outil et mettre en place les certificats en fonction de votre configuration serveur. Cette installation automatique fonctionne dans le cadre d’un serveur Web apache mais reste expérimentale pour nginx. Nous allons donc utiliser let’s encrypt pour la génération de certificat seulement.

Afin de vérifier que vous êtes bien le possesseur du nom de domaine pour lequel vous souhaitez obtenir un certificat, Let’s Encrypt va générer un fichier sur votre serveur et va ensuite essayer d’y accéder depuis leur serveur. Pour distribuer ce fichier, il est possible d’utiliser un serveur Web interne à Let’s Encrypt, mais on peut aussi choisir d’utiliser notre propre serveur Web (ici nginx). C’est cette seconde méthode que l’on va choisir ici car nous ne souhaitons pas interrompre nginx pendant la phase de génération et d’obtention du certificat. Nous allons modifier la configuration de notre virtual host nginx :

server { 
    # .well-known doit resté accessible
    location ~ /\.well-known/acme-challenge {
        allow all;
    }
    # On interdit habituellement l'accès au dotfiles
    location ~ /\. { deny all; access_log off; log_not_found off; }
}

Une fois nginx configuré pour renvoyer les fichiers contenus dans ce dossier on peut alors utiliser le module webroot pour générer le certificat :

/opt/letsencrypt/letsencrypt-auto certonly --rsa-key-size 4096 --webroot --webroot-path /var/www/mondomaine.fr -d mondomaine.fr

Les certificats générés, ainsi que les clés privées sont stockés dans le dossier /etc/letsencrypt/live/. Il va ensuite falloir modifier notre configuration nginx pour prendre en compte ces certificats. 

# Redirection http vers https
server {
    listen 80;
    listen [::]:80; 
    server_name mondomaine.fr;
    location ~ /\.well-known/acme-challenge {
        allow all;
    }
    location / {
        return 301 https://$host$request_uri; 
    }
}

# Notre bloc serveur
server {

    # spdy pour Nginx < 1.9.5
    listen 443 ssl spdy;
    listen [::]:443 ssl spdy;
    spdy_headers_comp 9;

    # http2 pour Nginx >= 1.9.5
    #listen 443 ssl http2;
    #listen [::]:443 ssl http2;

    server_name mondomaine.fr;
    root /var/www/mondomaine.fr;
    index index.html index.htm;
    error_log /var/log/nginx/mondomaine.fr.log notice;
    access_log off;

    ####    Locations
    # On cache les fichiers statiques si l'on ne se sert pas de varnish
    # location ~* \.(html|css|js|png|jpg|jpeg|gif|ico|svg|eot|woff|ttf)$ { expires max; }
    
    #### SSL
    ssl on;
    ssl_certificate /etc/letsencrypt/live/mondomaine.fr/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/mondomaine.fr/privkey.pem;

    ssl_stapling on;
    ssl_stapling_verify on;
    ssl_trusted_certificate /etc/letsencrypt/live/mondomaine.fr/fullchain.pem;
    # Google DNS, Open DNS, Dyn DNS
    resolver 8.8.8.8 8.8.4.4 208.67.222.222 208.67.220.220 216.146.35.35 216.146.36.36 valid=300s;
    resolver_timeout 3s;



    ####    Session Tickets
    # Session Cache doit avoir la même valeur sur tous les blocs "server".
    ssl_session_cache shared:SSL:100m;
    ssl_session_timeout 24h;
    ssl_session_tickets on;
    # [ATTENTION] il faudra générer le ticket de session.
    ssl_session_ticket_key /etc/nginx/ssl/ticket.key;

    # [ATTENTION] Les paramètres Diffie-Helman doivent être générés
    ssl_dhparam /etc/nginx/ssl/dhparam4.pem;



    ####    ECDH Curve
    ssl_ecdh_curve secp384r1;
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    ssl_prefer_server_ciphers on;
    ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!3DES:!MD5:!PSK';

}

Enfin pour générer les clefs utilisées pour les sessions et le Diffie-Helman (soyez patient :))

mkdir -p /etc/nginx/ssl &&
openssl rand 48 -out /etc/nginx/ssl/ticket.key &&
openssl dhparam -out /etc/nginx/ssl/dhparam4.pem 4096

Le renouvellement

Les certificats proposés par Let’s Encrypt sont valables pour une durée de 90 jours. Il faudra donc penser à les renouveler avant la fin de cette période. Pour cela on peut utiliser la commande :

/opt/letsencrypt/letsencrypt-auto renew

Cette commande renouvelle les certificats sans interaction de la part de l’utilisateur, vous pouvez donc la rajouter dans les tâches récurrentes de votre système afin de renouveler le certificat au bout d’une certaine période de temps. Cette commande vérifie la date d’expiration avant de lancer la procédure alors on peut la programmer de manière hebdomadaire

sudo crontab -e
30 3 * * 0 /opt/letsencrypt/letsencrypt-auto renew >> /var/log/letsencrypt/renewal.log

Le Reverse Proxy

Pour la configuration de nginx je vous donne les fichiers tels que je les ai configurés et vous laisse le soin de regler à votre convenance en vous aidant de la doc.

/etc/nginx/nginx.conf

user www-data;
worker_processes 5;
pid /run/nginx.pid;

include /etc/nginx/modules-enabled/*.conf;

events {
        worker_connections 1024;
}

http {

        # don't send the nginx version number in error pages and Server header
        server_tokens off;

        # config to don't allow the browser to render the page inside an frame or iframe
        # and avoid clickjacking http://en.wikipedia.org/wiki/Clickjacking
        # if you need to allow [i]frames, you can use SAMEORIGIN or even set an uri with ALLOW-FROM uri
        # https://developer.mozilla.org/en-US/docs/HTTP/X-Frame-Options
        add_header X-Frame-Options SAMEORIGIN;

        # when serving user-supplied content, include a X-Content-Type-Options: nosniff header along with the Content-Type: header,
        # to disable content-type sniffing on some browsers.
        # https://www.owasp.org/index.php/List_of_useful_HTTP_headers
        # currently suppoorted in IE > 8 http://blogs.msdn.com/b/ie/archive/2008/09/02/ie8-security-part-vi-beta-2-update.aspx
        # http://msdn.microsoft.com/en-us/library/ie/gg622941(v=vs.85).aspx
        # 'soon' on Firefox https://bugzilla.mozilla.org/show_bug.cgi?id=471020
        add_header X-Content-Type-Options nosniff;

        # This header enables the Cross-site scripting (XSS) filter built into most recent web browsers.
        # It's usually enabled by default anyway, so the role of this header is to re-enable the filter for
        # this particular website if it was disabled by the user.
        # https://www.owasp.org/index.php/List_of_useful_HTTP_headers
        add_header X-XSS-Protection "1; mode=block";

        # with Content Security Policy (CSP) enabled(and a browser that supports it(http://caniuse.com/#feat=contentsecuritypolicy),
        # you can tell the browser that it can only download content from the domains you explicitly allow
        # http://www.html5rocks.com/en/tutorials/security/content-security-policy/
        # https://www.owasp.org/index.php/Content_Security_Policy
        # I need to change our application code so we can increase security by disabling 'unsafe-inline' 'unsafe-eval'
        # directives for css and js(if you have inline css or js, you will need to keep it too).
        # more: http://www.html5rocks.com/en/tutorials/security/content-security-policy/#inline-code-considered-harmful
        add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://ssl.google-analytics.com https://assets.zendesk.com https://connect.facebook.net; img-src 'self' https://ssl.google-analytics.com https://s-static.ak.facebook.com https://assets.zendesk.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://assets.zendesk.com; font-src 'self' https://themes.googleusercontent.com; frame-src https://assets.zendesk.com https://www.facebook.com https://s-static.ak.facebook.com https://tautt.zendesk.com; object-src 'none'";

        ##
        # Basic Settings
        ##

        sendfile on;
#       tcp_nopush on;
#       tcp_nodelay on;
        keepalive_timeout 3;
        types_hash_max_size 2048;

        # server_names_hash_bucket_size 64;
        # server_name_in_redirect off;

        include /etc/nginx/mime.types;
        default_type application/octet-stream;

        ##
        # SSL Settings
        ##

        ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # Dropping SSLv3, ref: POODLE
        ssl_prefer_server_ciphers on;

        ##
        # Logging Settings
        ##

        access_log /var/log/nginx/access.log;
        error_log /var/log/nginx/error.log;

        ##
        # Gzip Settings
        ##

        # global gzip on
        gzip on;
        gzip_min_length 1024;
        gzip_disable "MSIE [1-6]\.";
        gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;

        ##
        # Virtual Host Configs
        ##

        include /etc/nginx/conf.d/*.conf;
        include /etc/nginx/sites-enabled/*;
}

/etc/nginx/conf.d/proxy_params.conf

Ici nous avons créer un fichier pour la configuration de notre proxy.

proxy_buffering on;
proxy_redirect off;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
client_max_body_size    10m;
client_body_buffer_size 128k;
client_header_buffer_size 64k;
proxy_connect_timeout   90;
proxy_send_timeout      90;
proxy_read_timeout      90;
proxy_buffer_size   16k;
proxy_buffers       32   16k;
proxy_busy_buffers_size 64k;

/etc/nginx/conf.d/backend.conf

Ce fichier configure la redirection vers varnish (port 7080), ou vers apache directement (port 8080). Comme cela nous pourrons facilement basculer de l’un à l’autre au besoin (si par exemple vous préférez utiliser le cache de nginx).

upstream backend_varnish{
        server [::1]:7080;
}
upstream backend_apache{
        server [::1]:8080;
}

/etc/nginx/sites-available/monsite.conf

La première partie serveur écoute sur le port 80 afin de rediriger les connexion http vers notre https, et de permettre l’accès à .well-known sur ce même port pour le renouvellement du certificat Let’s Encrypt.
Nous retrouvons la configuration ssl. Et en fin de fichier l’appel vers notre serveur web mis en cache par varnish.

#
server {
    listen 80;
    listen [::]:80;
    server_name www.monsite.fr;
    root /var/www/html;
    location ~ /\.well-known/acme-challenge {
        allow all;
    }
    location / {
        return 301 https://$host$request_uri;
    }
}


server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name www.monsite.fr;

    ssl_certificate /etc/letsencrypt/live/www.monsite.fr/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/www.monsite.fr/privkey.pem;

    ssl_stapling on;
    ssl_stapling_verify on;
    ssl_trusted_certificate /etc/letsencrypt/live/www.monsite.fr/fullchain.pem;
    resolver 8.8.8.8 8.8.4.4 208.67.222.222 208.67.220.220 216.146.35.35 216.146.36.36 valid=300s;
    resolver_timeout 3s;

    ####    Session Tickets
    # Session Cache doit avoir la même valeur sur tous les blocs "server".
    ssl_session_cache shared:SSL:50m;
    ssl_session_timeout 24h;
    ssl_session_tickets off;
    # [ATTENTION] il faudra générer le ticket de session.
    #ssl_session_ticket_key /etc/nginx/ssl/ticket.key;

    # [ATTENTION] Les paramètres Diffie-Helman doivent être générés
    ssl_dhparam /etc/nginx/ssl/dhparam4.pem;

    ####    ECDH Curve
    ssl_ecdh_curve secp384r1;
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    ssl_prefer_server_ciphers on;
    ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!3DES:!MD5:!PSK';

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

    location ~ /\.well-known/acme-challenge {
            allow all;
    }

    access_log   /var/log/nginx/www.access.log;
    error_log    /var/log/nginx/www.error.log;

    location / {
        # include proxy_params;
        proxy_pass http://backend_varnish;
    }

}

Démarrage de nginx

Maintenant il nous faut activer la configuration de notre serveur

ln -s /etc/nginx/sites-available/monsite.conf /etc/nginx/sites-enabled/monsite.conf

Puis tester la configuration

nginx -t

Et enfin démarrer nginx

systemctl start nginx

Laisser un commentaire