Offloader le SSL d’Apache avec Nginx en reverse proxy

Dans cet article, nous allons voir comment décharger le traitement du SSL sur Apache. Les requêtes HTTPS seront traités en amont puis passées vers apache sur le port non SSL.

Le but est d’avoir un serveur Nginx en reverse Proxy qui se place devant apache, traite la partie SSL et renvoi le trafic vers le serveur web Apache (en non SSL). Cela permet de protéger Apache contre les pics de trafic, Nginx étant plus résistant sur ce point là et bénéficiant nativement de règles pour mettre en place des limites. Il est également possible d’ajouter du cache sur Nginx mais ça ne sera pas le sujet de cet article, on n’évoquera que la partie SSL / Offloading.

Déchargement SSL Nginx

Schéma de l’intrastructure pour décharger le SSL avec un reverse Proxy Nginx

Il y a quelques subtilités à connaitre pour faire cela sinon cela risque de générer des erreurs (400 Bad Request) et des boucles de redirections. Nous allons également voir pourquoi cela n’est pas totalement transparent.

Installation et configuration d’apache

La première étape est d’installer un serveur web apache puis de configurer un vhost pour le site internet qui nous intéresse. Dans mon cas, je travaille sur Centos donc l’installation se passe avec Yum.

On va également installer PHP, prendre une version assez récente depuis les dépôts de Remi. L’implémentation de PHP choisi sera PHP-FPM, le PHP sera exécuté avec l’utilisateur système qui sera utilisé pour l’hébergement du site internet.

yum install epel-release
yum install httpd yum-utils
wget https://rpms.remirepo.net/enterprise/remi-release-7.rpm
rpm -Uvh remi-release-7.rpm
yum-config-manager --enable remi-php72
yum install php-fpm php
Installation d'apache, d'une version récente de PHP et du dépôt Epel

L’hôte virtuel d’apache est assez classique, il y a juste quelques subtilités :

  • On écoute sur le port 8080 car le reverse proxy Nginx sera sur les ports 80 et 443. Il faudra bien penser à modifier cela dans le httpd.conf également.
  • Il y a un ProxyPassMatch pour php-fpm, rien d’inhabituel. La configuration PHP-FPM n’est pas l’objet de cet article mais si cela vous intéresse, c’est .
  • On créé une variable d’environnement HTTPS défini à On, s’il y a un header X-Forwaded-Proto. Ce dernier sera généré par Nginx, c’est utile pour les scripts PHP. Certains scripts PHP se base la dessus pour déterminer si l’environnement est https ou non.
<VirtualHost *:8080>
        ServerName website.test
        ServerAlias www.website.test
        DocumentRoot /home/website/www
        SetEnvIf X-Forwarded-Proto "https" HTTPS=on
        ProxyPassMatch ^/(.*\.php)$ fcgi://127.0.0.1:9000/home/website/www/$1
        <Directory /home/website/www/>
                Options Indexes FollowSymLinks MultiViews
                AllowOverride All
                Require all granted
        </Directory>
        ErrorLog /var/log/httpd/website.dev.error
        CustomLog /var/log/httpd/website.dev.log combined
</VirtualHost>

Il reste une configuration supplémentaire à faire dans Apache, pour qu’il récupère l’adresse IP du client qui est passé par Nginx dans un header X-Forwarded-For. Sans cette configuration supplémentaire, Apache verrait simplement l’adresse IP de Nginx et non celle du vrai client. Cela doit être ajouté dans le dossier conf.modules.d.

LoadModule remoteip_module modules/mod_remoteip.so
RemoteIPHeader X-Forwarded-For
RemoteIPTrustedProxy 127.0.0.1

Si la bonne adresse IP n’apparaît dans les logs, cela peut venir de trois choses :

  • RemoteIPTrustedProxy n’autorise pas l’adresse IP sur laquelle se trouve le reverse proxy Nginx
  • RemoteIPHeader ne correspond pas au hearder envoyé par Nginx, parfois c’est X-Real-Ip
  • Dans le httpd.conf il faut changer le format des logs pour remplacer le %h par %a

Installation du reverse proxy Nginx

Maintenant, il faut procéder à l’installation puis à la configuration de Nginx. La configuration sera assez simple, il faudra simplement définir les fichiers du certificats SSL, définir quelques headers supplémentaires puis faire un proxy_pass sur le port d’écoute du serveur apache en arrière plan.

Habituellement, je compile Nginx manuellement pour avoir la dernière version et y intégrer des extensions supplémentaires mais dans le cas présent, on se contentera d’une installation depuis les dépôts.

yum install nginx
Installation de Nginx

Pour la configuration, c’est assez basique, la petite subtilité est dans l’ajout de header supplémentaire :

# HTTP
server {
        listen       80;
        server_name  website.test www.website.test;
        location / {
            proxy_set_header Host $host;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_pass http://127.0.0.1:8080;
        }
}
# HTTPS
server {
        listen       443 ssl http2;
        listen       [::]:443 ssl http2;
        server_name  website.test www.website.test;
        ssl_certificate "/etc/pki/nginx/server.crt";
        ssl_certificate_key "/etc/pki/nginx/private/server.key";
        ssl_session_cache shared:SSL:1m;
        ssl_session_timeout  10m;
        ssl_ciphers HIGH:!aNULL:!MD5;
        ssl_prefer_server_ciphers on;
        location / {
            proxy_set_header Host $host;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto https;
            proxy_set_header X-Forwarded-Port 443;
            proxy_pass http://127.0.0.1:8080;
       }
}

Pourquoi ce n’est pas totalement transparent au final ?

Maintenant, voyons pourquoi cela n’est pas totalement transparent. Pour plusieurs raisons, premier exemple, le code ci-dessous va générer une boucle de redirection alors qu’il est tout à fait valide lorsque cela passe par le « circuit SSL » d’apache :

RewriteEngine on
RewriteCond %{HTTPS} !=on
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
Exemple de code générant une boucle de redirection

C’est curieux, pourtant la variable HTTPS devrait être à « On » grâce à la ligne SetEnvIf dans la définition du virtualhost ? Ce n’est pas le cas :

  • La ligne SetEnvIf dans la configuration d’apache définie une variable d’environnement qui va être passée à PHP plus tard
  • Dans le RewriteCond, la variable HTTPS est une variable interne à apache. Ce n’est pas la même chose que la variable d’environnement, même si cela porte le même nom. C’est expliquée dans la documentation de la directive RewriteCond d’apache : la variable peut être utilisée sans problème, que mod_ssl soit chargé ou non. À ma connaissance, il n’est pas possible de modifier cette variable là dynamiquement, via des configurations.

Il y a également d’autres effets de bord, certaines variables apaches sont différentes et des variables d’environnements n’existent pas forcément, c’est le cas des variables suivantes :

  • SERVER_PORT contiendra la valeur 8080, parfois PHP ou même des directives .htaccess se base la dessus (exemple : forcer le HTTPS si le port utilisé est le 80)
  • REQUEST_SCHEME contiendra http alors qu’en réalité c’est HTTPS

Pour ces raisons, les règles de redirections ou même le code de l’application doivent également prendre en compte des headers supplémentaires et standard (cf. la documentation des headers de Mozilla à ce sujet), ajoutés par Nginx :

  • X-Forwarded-Proto : qui permet de passer le bon protocole car request_scheme sera dans certains cas invalide
  • X-Forwarded-For : qui contiendra l’adresse IP du client à l’origine de la requête
  • X-Forwarded-Port : qui contiendra le port d’origine d’écoute

Pour reprendre le premier exemple de .htaccess, voici un exemple corrigé qui fonctionne à tous les coups :

RewriteEngine On
RewriteCond %{HTTP:X-Forwarded-Proto} !https
RewriteCond %{HTTPS} !on
RewriteRule ^(.*) https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
Exemple corrigé du htaccess pour forcer le HTTPS correctement

Conclusion

Plusieurs conclusions à tirer de cette expérience.

Ce n’est pas forcément possible d’Offloader totalement le SSL dans le cas ou on ne maîtrise pas les applications hébergés. Par exemple : dans un contexte d’hébergement mutualisé. Certaines règles de redirections dans les .htaccess ne penseront pas à vérifier si le site est derrière un reverse proxy. C’est la même chose dans le code des applications hébergés.

Dans le cas ou on ne maîtrise pas les applications finales hébergées, c’est mieux de ne pas décharger le SSL. Pour cela il faut faire un proxy_pass sur le port SSL d’apache. Ainsi les variables d’apache et les variables d’environnement seront correctement définies.

Si vous développez des applications, il faut penser à cela. Par exemple en vérifiant les headers X-Forwarded-* pour que l’application soit déployable plus facilement.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée.

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.