Introduction
Parfois, il est nécessaire de mettre en ligne un site internet mais de ne le rendre accessible que depuis certains emplacements. Par exemple, un mini-site évènementiel de marque pourrait n’être accessible que depuis les boutiques affiliées à cette dernière.
Dans cet article, je propose une solution simple et avancée de restriction des adresses IP des visiteurs pour le serveur web Apache. Elle ne demande que la mise en place d’un fichier .htaccess et ne dépend que du module mod_rewrite, la plupart des hébergeurs devraient donc la supporter.
On va procéder du plus facile (renvoyer une erreur 403 Forbidden) jusqu’au plus complet (permettre aux visiteurs de saisir un identifiant et un mot de passe).
Note : le code a été testé sur un environnement Apache 2.4 chez l’hébergeur o2switch. N’oubliez pas de toujours faire une sauvegarde avant de modifier les fichiers de votre site !
Liste blanche des adresses IP
Tout d’abord, voyons comment n’autoriser l’accès qu’à certaines adresses IP. Le code ci-dessous bloque l’accès aux visiteurs non-autorisés en affichant une erreur 403 Forbidden. Il est à placer aussi haut que possible dans un fichier .htaccess à la racine du site ou du dossier à protéger.
<IfModule mod_rewrite.c>
RewriteEngine On
# Liste blanche des adresses IPs
# Autoriser le localhost :
RewriteCond expr "-R '127.0.0.0/8'" [OR]
# Autoriser votre serveur (remplacer avec son adresse) :
RewriteCond expr "-R '109.234.164.136'" [OR]
# Autoriser les visiteurs :
RewriteCond expr "-R '1.2.3.4'" [OR]
RewriteCond expr "-R '192.168.1.0/24'"
# Variable d'état d'autorisation
RewriteRule .* - [E=visitor_is_whitelisted:1]
# Blocage 403 des visiteurs interdits
RewriteCond %{ENV:visitor_is_whitelisted} !1
RewriteRule .* - [L,F]
</IfModule>
On utilise les expressions ap_expr d’Apache, qui permettent soit de matcher une adresse IP exacte (ex : 1.2.3.4), soit de matcher un ensemble d’adresses IP (ex : 192.168.1.0/24). Il ne faut pas oublier d’autoriser le localhost ainsi que le serveur lui-même, notamment si des scripts cron doivent être exécutés.
Si une directive ErrorDocument 403
est présente, la page d’erreur prévue sera affichée au visiteur (voir la documentation). Dans le cas contraire, c’est la page d’erreur 403 par défaut qui sera utilisée :
Liste blanche des pages web
Si vous avez besoin de permettre l’accès à certaines ressources aux visiteurs non-autorisés, il est possible de placer leurs URLs sur une liste blanche. On pense notamment aux favicons, mentions légales et formulaires de demande de support. Il suffit de rajouter quelques lignes au fichier .htaccess précédent, comme ci-dessous.
<IfModule mod_rewrite.c>
RewriteEngine On
# Liste blanche des adresses IPs
# Autoriser le localhost :
RewriteCond expr "-R '127.0.0.0/8'" [OR]
# Autoriser votre serveur (remplacer avec son adresse) :
RewriteCond expr "-R '109.234.164.136'" [OR]
# Autoriser les visiteurs :
RewriteCond expr "-R '1.2.3.4'" [OR]
RewriteCond expr "-R '192.168.1.0/24'"
# Variable d'état d'autorisation
RewriteRule .* - [E=visitor_is_whitelisted:1]
# Liste blanche des URLs
# Autoriser ces URLs :
RewriteCond %{REQUEST_URI} ^/pageautorisee\.html$ [OR]
RewriteCond %{REQUEST_URI} ^/dossierautorise/
# Variable d'etat d'autorisation
RewriteRule .* - [E=visitor_is_whitelisted:1]
# Blocage 403 des visiteurs interdits
RewriteCond %{ENV:visitor_is_whitelisted} !1
RewriteRule .* - [L,F]
</IfModule>
Il est possible de matcher à la fois des fichiers et des dossiers, ce qui permet de donner facilement accès aux ressources publiques du site.
On note que l’on travaille avec des expressions régulières, et qu’il faut donc échapper les caractères spéciaux tels que le point.
Afficher une page accès refusé
Plutôt que d’afficher une bête erreur 403, pourquoi ne pas rediriger les visiteurs non-autorisés vers une page d’erreur personnalisée ? Quelque chose comme ça :
Il faut d’abord créer la page web qui affichera le message d’erreur. Insérer et personnalisez le code suivant dans un fichier accesrefuse.html à la racine du site.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Accès refusé</title>
<meta name="robots" content="noindex, nofollow">
<style>
body{ font-size: 16px; line-height: 1.6; font-weight: 400; font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif; text-align: center; color: #111 }
img{ display:inline-block; max-width:100%; min-height:1px; height:auto }
section{ padding: 100px 50px }
</style>
</head>
<body>
<section>
<div>
<img alt="Accès refusé" src="data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8'%3F%3E%3Csvg viewBox='0 0 1000 1000' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath transform='matrix(5.1965 0 0 5.3726 -738.86 -644.43)' d='m324.45 213.01a86.051 83.23 0 1 1 -172.1 0 86.051 83.23 0 1 1 172.1 0z' fill='none' stroke='%23f00' stroke-width='20'/%3E%3Cpath d='m166.83 166.83 666.33 666.33' fill='none' stroke='%23f00' stroke-width='100'/%3E%3C/svg%3E" width="1000" height="1000" style="width: 250px; margin-bottom: 20px" />
</div>
<h1>Accès refusé :-(</h1>
<p>Nous sommes désolés, mais l'accès au service n'est pas autorisé depuis votre adresse de connexion.<br />Veuillez vous connecter à un point d'accès autorisé et réessayer.<br /><br /><a href="/">Cliquez ici pour essayer à nouveau.</a></p>
</section>
</body>
</html>
Il faut que toutes les ressources utilisées soient en liste blanche dans le fichier .htaccess. Il est préférable d’éviter d’utiliser des ressources externes à la page ; par exemple, dans le cas des images, on peut les intégrer directement dans le code HTML grâce au format data-uri.
Ne pas oublier la balise meta robots contenant la valeur noindex ! Il serait dommage de voir les moteurs de recherche indexer la page de message d’erreur.
On peut finalement reprendre le code du fichier .htaccess, ajouter la page d’accès refusé à la liste blanche et remplacer l’erreur 403 par une redirection 302 :
<IfModule mod_rewrite.c>
RewriteEngine On
# Liste blanche des adresses IPs
# Autoriser le localhost :
RewriteCond expr "-R '127.0.0.0/8'" [OR]
# Autoriser votre serveur (remplacer avec son adresse) :
RewriteCond expr "-R '109.234.164.136'" [OR]
# Autoriser les visiteurs :
RewriteCond expr "-R '1.2.3.4'" [OR]
RewriteCond expr "-R '192.168.1.0/24'"
# Variable d'état d'autorisation
RewriteRule .* - [E=visitor_is_whitelisted:1]
# Liste blanche des URLs
# Autoriser la page acces refuse
RewriteCond %{REQUEST_URI} ^/accesrefuse\.html$ [OR]
# Autoriser ces URLs :
RewriteCond %{REQUEST_URI} ^/pageautorisee\.html$ [OR]
RewriteCond %{REQUEST_URI} ^/dossierautorise/
# Variable d'etat d'autorisation
RewriteRule .* - [E=visitor_is_whitelisted:1]
# Redirection 302 des visiteurs interdits vers la page acces refuse
RewriteCond %{ENV:visitor_is_whitelisted} !1
RewriteRule .* %{REQUEST_SCHEME}://%{HTTP_HOST}/accesrefuse.html [L,R=302,QSD]
</IfModule>
❓ Si vous vous demandez pourquoi je n’ai pas utilisé une directive ErrorDocument 403
, la réponse est que la redirection 302 permet de gérer le cas où un site dispose déjà d’un ErrorDocument mais que l’on veut afficher une page différente. Vous pouvez tout à fait utiliser un ErrorDocument de votre côté 😉
Identifiant et mot de passe
La mise en liste blanche peut ne pas s’avérer suffisante. Par exemple, prenons le cas d’un outil web d’entreprise, qui ne doit être disponible qu’au personnel de cette dernière. On peut mettre en liste blanche les adresses IP des locaux de l’entreprise, mais comment permettre au personnel en déplacement d’avoir aussi un accès complet à l’outil ?
L’identification par login et mot de passe des visiteurs externes est une solution. Ca consiste à utiliser une page de connexion qui ressemble à ceci :
Commençons d’abord par créer la page de saisie des identifiants du visiteur. Créez un fichier accesexterne.html à la racine du site, et placez y le code suivant, en le personnalisant si besoin. N’oubliez pas de limiter autant que possible l’utilisation de ressources externes, que vous devrez placer en liste blanche le cas échéant. La balise meta robots noindex est aussi indispensable.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Accès externe</title>
<meta name="robots" content="noindex, nofollow">
<style>
body{ font-size: 16px; line-height: 1.6; font-weight: 400; font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif; text-align: center; color: #111 }
img{ display:inline-block; max-width:100%; min-height:1px; height:auto }
section{ padding: 30px 30px }
@media screen and (max-width: 600px){
input[type=text], input[type=password]{ width: 100%; margin-bottom: 15px }
}
</style>
</head>
<body>
<section>
<div>
<img alt="Accès externe" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' version='1.1' viewBox='0 0 256 256'%3E%3Cg transform='translate(128 128) scale(0.72 0.72)' style=''%3E%3Cg style='stroke: none; stroke-width: 0; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: none; fill-rule: nonzero; opacity: 1;' transform='translate(-175.05 -175.05000000000004) scale(3.89 3.89)' %3E%3Cpath d='M 0 68.798 v 11.914 c 0 1.713 1.401 3.114 3.114 3.114 h 0 c 3.344 0 4.805 -2.642 4.805 -2.642 L 8.14 29.281 l 2.739 -2.827 l 72.894 -2.977 v -1.482 c 0 -2.396 -1.942 -4.338 -4.338 -4.338 H 50.236 c -1.15 0 -2.254 -0.457 -3.067 -1.27 l -8.943 -8.943 c -0.813 -0.813 -1.917 -1.27 -3.067 -1.27 H 4.338 C 1.942 6.174 0 8.116 0 10.512 v 7.146 v 2.332 V 68.798' style='stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(224,173,49); fill-rule: nonzero; opacity: 1;' transform=' matrix(1 0 0 1 0 0) ' stroke-linecap='round' /%3E%3Cpath d='M 3.114 83.826 L 3.114 83.826 c 1.713 0 3.114 -1.401 3.114 -3.114 V 27.81 c 0 -2.393 1.94 -4.333 4.333 -4.333 h 75.107 c 2.393 0 4.333 1.94 4.333 4.333 v 51.684 c 0 2.393 -1.94 4.333 -4.333 4.333 C 85.667 83.826 3.114 83.826 3.114 83.826 z' style='stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(255,200,67); fill-rule: nonzero; opacity: 1;' transform=' matrix(1 0 0 1 0 0) ' stroke-linecap='round' /%3E%3Cpath d='M 57.675 48.711 h -2.651 v -7.515 c 0 -4.424 -3.6 -8.023 -8.023 -8.023 c -4.424 0 -8.024 3.599 -8.024 8.023 v 7.515 h -2.651 c -0.829 0 -1.5 0.672 -1.5 1.5 v 21.351 c 0 0.828 0.671 1.5 1.5 1.5 h 21.35 c 0.828 0 1.5 -0.672 1.5 -1.5 V 50.211 C 59.175 49.383 58.503 48.711 57.675 48.711 z M 41.976 41.196 c 0 -2.77 2.254 -5.023 5.024 -5.023 c 2.77 0 5.023 2.253 5.023 5.023 v 7.515 H 41.976 V 41.196 z M 56.175 70.062 h -18.35 V 51.711 h 18.35 V 70.062 z' style='stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(184,53,53); fill-rule: nonzero; opacity: 1;' transform=' matrix(1 0 0 1 0 0) ' stroke-linecap='round' /%3E%3Cpath d='M 52.425 59.215 c 0 -2.99 -2.434 -5.424 -5.425 -5.424 s -5.425 2.434 -5.425 5.424 c 0 2.47 1.662 4.556 3.925 5.209 v 2.057 c 0 0.828 0.672 1.5 1.5 1.5 s 1.5 -0.672 1.5 -1.5 v -2.057 C 50.763 63.771 52.425 61.685 52.425 59.215 z M 47 61.64 c -1.337 0 -2.425 -1.088 -2.425 -2.425 s 1.088 -2.424 2.425 -2.424 s 2.425 1.087 2.425 2.424 S 48.337 61.64 47 61.64 z' style='stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(184,53,53); fill-rule: nonzero; opacity: 1;' transform=' matrix(1 0 0 1 0 0) ' stroke-linecap='round' /%3E%3C/g%3E%3C/g%3E%3C/svg%3E" width="256" height="256" style="width: 150px; margin-bottom: 20px" />
</div>
<h1>Authentification accès externe</h1>
<p>L'accès au service n'est possible que depuis un point d'accès autorisé ou aux utilisateurs externes identifiés.</p>
<p>Utilisez le formulaire ci-dessous pour confirmer votre identité.</p>
<form action="" style="margin-bottom: 20px;">
<input type="text" name="login" placeholder="Identifiant" />
<input type="password" name="password" placeholder="Mot de passe" />
<input type="submit" value="Connexion" />
</form>
<p>Vous pouvez aussi vous reconnecter depuis un point d'accès autorisé et réessayer.<br /><a href="/">Cliquez ici pour essayer à nouveau.</a></p>
</section>
<script>
// Variables personnalisables
var REDIRECT_URL = "/"; // Personnaliser la redirection apres la soumission du formulaire
var COOKIE_DURATION = 86400; // Personnaliser la durée du cookie d'authentification (secondes)
// sha1() from webtoolkit.info
function sha1(r){function o(r,o){return r<<o|r>>>32-o}function e(r){var o,e="";for(o=7;o>=0;o--)e+=(r>>>4*o&15).toString(16);return e}var t,a,h,n,C,c,f,d,A,u=new Array(80),g=1732584193,i=4023233417,s=2562383102,S=271733878,m=3285377520,p=(r=function(r){r=r.replace(/\r\n/g,"\n");for(var o="",e=0;e<r.length;e++){var t=r.charCodeAt(e);t<128?o+=String.fromCharCode(t):t>127&&t<2048?(o+=String.fromCharCode(t>>6|192),o+=String.fromCharCode(63&t|128)):(o+=String.fromCharCode(t>>12|224),o+=String.fromCharCode(t>>6&63|128),o+=String.fromCharCode(63&t|128))}return o}(r)).length,l=new Array;for(a=0;a<p-3;a+=4)h=r.charCodeAt(a)<<24|r.charCodeAt(a+1)<<16|r.charCodeAt(a+2)<<8|r.charCodeAt(a+3),l.push(h);switch(p%4){case 0:a=2147483648;break;case 1:a=r.charCodeAt(p-1)<<24|8388608;break;case 2:a=r.charCodeAt(p-2)<<24|r.charCodeAt(p-1)<<16|32768;break;case 3:a=r.charCodeAt(p-3)<<24|r.charCodeAt(p-2)<<16|r.charCodeAt(p-1)<<8|128}for(l.push(a);l.length%16!=14;)l.push(0);for(l.push(p>>>29),l.push(p<<3&4294967295),t=0;t<l.length;t+=16){for(a=0;a<16;a++)u[a]=l[t+a];for(a=16;a<=79;a++)u[a]=o(u[a-3]^u[a-8]^u[a-14]^u[a-16],1);for(n=g,C=i,c=s,f=S,d=m,a=0;a<=19;a++)A=o(n,5)+(C&c|~C&f)+d+u[a]+1518500249&4294967295,d=f,f=c,c=o(C,30),C=n,n=A;for(a=20;a<=39;a++)A=o(n,5)+(C^c^f)+d+u[a]+1859775393&4294967295,d=f,f=c,c=o(C,30),C=n,n=A;for(a=40;a<=59;a++)A=o(n,5)+(C&c|C&f|c&f)+d+u[a]+2400959708&4294967295,d=f,f=c,c=o(C,30),C=n,n=A;for(a=60;a<=79;a++)A=o(n,5)+(C^c^f)+d+u[a]+3395469782&4294967295,d=f,f=c,c=o(C,30),C=n,n=A;g=g+n&4294967295,i=i+C&4294967295,s=s+c&4294967295,S=S+f&4294967295,m=m+d&4294967295}return(A=e(g)+e(i)+e(s)+e(S)+e(m)).toLowerCase()}
// Soumission du formulaire
var form = document.querySelector("form");
form.addEventListener("submit", function(event){
event.preventDefault();
var login = form.querySelector("[name=login]").value;
var password = form.querySelector("[name=password]").value;
document.cookie = "accesexterne_login=" + encodeURIComponent(sha1(login)) + "; path=/; max-age=" + COOKIE_DURATION + "; samesite=lax";
document.cookie = "accesexterne_password=" + encodeURIComponent(sha1(password)) + "; path=/; max-age=" + COOKIE_DURATION + "; samesite=lax";
window.location = REDIRECT_URL;
});
</script>
</body>
</html>
La page enregistre 2 cookies dans le navigateur lors de la soumission du formulaire. L’un contient le hash SHA-1 de l’identifiant, l’autre le hash du mot de passe. Le visiteur est ensuite redirigé vers l’adresse configurée dans le code source, par défaut la racine du site.
La validation des cookies s’effectue au niveau du fichier .htaccess. Nous devons placer la page d’accès externe en liste blanche, et vérifier si les cookies contiennent les bons hash d’identifiant et mot de passe.
<IfModule mod_rewrite.c>
RewriteEngine On
# Liste blanche des adresses IPs
# Autoriser le localhost :
RewriteCond expr "-R '127.0.0.0/8'" [OR]
# Autoriser votre serveur (remplacer avec son adresse) :
RewriteCond expr "-R '109.234.164.136'" [OR]
# Autoriser les visiteurs :
RewriteCond expr "-R '1.2.3.4'" [OR]
RewriteCond expr "-R '192.168.1.0/24'"
# Variable d'état d'autorisation
RewriteRule .* - [E=visitor_is_whitelisted:1]
# Liste blanche des URLs
# Autoriser la page acces externe
RewriteCond %{REQUEST_URI} ^/accesexterne\.html$ [OR]
# Autoriser la page acces refuse
RewriteCond %{REQUEST_URI} ^/accesrefuse\.html$ [OR]
# Autoriser ces URLs :
RewriteCond %{REQUEST_URI} ^/pageautorisee\.html$ [OR]
RewriteCond %{REQUEST_URI} ^/dossierautorise/
# Variable d'etat d'autorisation
RewriteRule .* - [E=visitor_is_whitelisted:1]
# Autorisation par cookie
# Autoriser l'utilisateur 1
RewriteCond expr "%{HTTP_COOKIE} -strmatch '*accesexterne_login=%{sha1:Login1}*'"
RewriteCond expr "%{HTTP_COOKIE} -strmatch '*accesexterne_password=%{sha1:Password1}*'"
RewriteRule .* - [E=visitor_is_whitelisted:1]
# Autoriser l'utilisateur 2
RewriteCond expr "%{HTTP_COOKIE} -strmatch '*accesexterne_login=%{sha1:Login2}*'"
RewriteCond expr "%{HTTP_COOKIE} -strmatch '*accesexterne_password=%{sha1:Password2}*'"
RewriteRule .* - [E=visitor_is_whitelisted:1]
# Redirection 302 des visiteurs interdits vers la page acces refuse
RewriteCond %{ENV:visitor_is_whitelisted} !1
RewriteRule .* %{REQUEST_SCHEME}://%{HTTP_HOST}/accesrefuse.html [L,R=302,QSD]
</IfModule>
A vous de remplacer Login1, Password1, Login2, Password2 par des identifiants et mots de passe plus adaptés, et de rajouter des utilisateurs si nécessaire.
On utilise la fonction sha1 intégrée à Apache qui permet de calculer des hash directement depuis les fichiers htaccess.
Si vous ne souhaitez pas inscrire en clair les identifiants dans le code, vous pouvez remplacer les appels %{sha1:Login1}
par la valeur du hash lui-même.
Vous pouvez également remplacer la redirection vers la page d’accès refusé en une redirection vers la page de saisie des identifiants. Pour ma part, je préfère garder secrète l’adresse de la page de saisie.
Il ne vous reste plus qu’à transmettre aux visiteurs autorisés l’URL de la page d’accès externe, ainsi que leurs identifiants et mots de passe.
Pour aller plus loin
Avant de conclure, voyons quelques réflexions et pistes d’amélioration pour notre bloqueur d’accès.
🔒 Le protocole HTTPS est obligatoire pour l’identification par cookie. Sinon, n’importe quel intermédiaire entre le visiteur et le serveur peut dérober les cookies et accéder au site.
💥 Attention à l’impact sur la charge. Si chaque page, chaque script, chaque image du site doit passer par la validation IP + URL + cookie, il se pourrait que le serveur soit impacté.
💡 Idée évolution #1 : et si on ne filtrait que certaines URLs ? Il suffit de placer le code de filtrage dans une directive <If>. A vous de jouer, ce n’est pas très compliqué 😉
💡 Idée évolution #2 : placer les adresses IP, les URLs en liste blanche et les identifiants dans des fichiers séparés. Ca facilitera la vie du sysadmin.
💡 Idée évolution #3 : fluidifier l’arrivée des visiteurs en les redirigeant automatiquement vers la page qu’ils souhaitaient afficher après leur authentification. Ca leur évitera d’avoir à reparcourir le site depuis l’accueil.
Conclusion
Voila, nous disposons d’un mécanisme facile à déployer et à personnaliser pour bloquer l’accès d’un site internet aux visiteurs non-autorisés. Il devrait être compatible avec WordPress, Drupal, Symfony et consorts. Il devrait également se montrer assez performant, car il est exécuté directement par le serveur Apache, sans avoir à passer par PHP.
Si cet article vous a plu ou si vous avez besoin d’un coup de main pour votre projet web, n’hésitez pas à m’écrire depuis la page contact du site.
Bon code à vous, à la prochaine !
Références
Documentation mod_rewrite – Apache HTTP Server
Les expressions ap_expr – Apache HTTP Server
La directive ErrorDocument – Apache HTTP Server
Les cookies HTTP – Mozilla Developers

A propos de l'auteur
Développeur web indépendant, avec une tendance à couper les plumes en quatre pour que les sites se chargent plus rapidement.