Un cache de proxy ne se juge pas à sa vitesse mais à ce qu'il refuse de partager : une réponse publique servie deux fois sans solliciter l'origine, et une réponse privée qui ne quitte jamais son propriétaire. Cette leçon s'adresse à une personne qui publie déjà une application derrière Nginx et veut en réduire la charge sans exposer les données d'un utilisateur à un autre. Elle enseigne proxy_cache, la clé de cache, les en-têtes Cache-Control et Vary, les directives qui écartent une réponse du cache, la compression, puis la preuve d'isolation qui conclut le travail.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »La leçon va de la déclaration du cache à la preuve qu'il ne fuit pas.
- Déclarer un cache avec
proxy_cache_pathetproxy_cache, et lire son statut réel. - Laisser l'origine décider de l'éligibilité avec
Cache-Control, au lieu de la contredire. - Composer une clé de cache et séparer les variantes d'une même URL avec
Vary. - Exclure du cache les réponses authentifiées avec
proxy_no_cacheetproxy_cache_bypass. - Prouver qu'aucune réponse privée ne passe d'un compte à l'autre.
- Compresser les réponses texte et mesurer le gain avant et après.
Prérequis
Section intitulée « Prérequis »La leçon suppose que vous savez publier une application et lire les en-têtes d'une réponse.
- Publier une application avec
proxy_pass: Publier une application avec Nginx. - Lire une requête, une réponse, leurs en-têtes et la négociation de contenu : Lire HTTP avant de configurer un proxy.
- Valider et recharger une configuration : Installer Nginx.
- Une machine Debian 13 avec
sudo, dont Nginx est en version 1.31.8, installé depuis le dépôt de l'éditeur.
Qu'est-ce qu'un cache de proxy, et qu'a-t-il à prouver ?
Section intitulée « Qu'est-ce qu'un cache de proxy, et qu'a-t-il à prouver ? »Un cache de proxy range la réponse d'une origine pour la rendre à la requête suivante sans solliciter l'origine : il gagne en latence, et il prend le risque de rendre à quelqu'un ce qui était destiné à un autre. L'origine est le serveur d'application derrière Nginx. Un MISS est une requête que le cache ne sait pas servir et qui part donc à l'origine ; un HIT est une réponse rendue depuis le cache, sans que l'origine ait produit quoi que ce soit.
Le gain est réel, mais il ne justifie rien tout seul. Un cache qui accélère la page d'accueil et sert à un visiteur la fiche d'un autre compte est une panne de sécurité, pas une optimisation. La leçon pose donc deux exigences qui se vérifient ensemble. D'abord, les réponses publiques et éligibles sont réellement servies depuis le cache. Ensuite, aucune réponse privée ne passe d'un compte à l'autre, et les variantes d'une même ressource (la langue, par exemple) ne se mélangent pas.
Retirer le cache satisfait la seconde exigence et trahit la première : c'est une façon de supprimer le problème, pas de le résoudre. Toute la méthode qui suit repose sur cette idée, qu'une preuve d'isolation doit toujours être accompagnée de sa contre-épreuve : la page publique reste servie depuis le cache.
Comment déclarer un cache avec proxy_cache_path et proxy_cache ?
Section intitulée « Comment déclarer un cache avec proxy_cache_path et proxy_cache ? »Un cache se déclare en deux temps : proxy_cache_path réserve l'espace disque et la zone mémoire, puis proxy_cache l'active sur un location. La première directive se pose dans le contexte http, hors de tout server. Elle donne un répertoire, une zone de clés (keys_zone, un index en mémoire partagé entre les workers) et des bornes de taille et de durée.
# Contexte httpproxy_cache_path /var/cache/nginx/boutique levels=1:2 keys_zone=boutique:10m max_size=200m inactive=30m;
server { listen 80; server_name boutique.lab.test;
location / { proxy_cache boutique; add_header X-Cache-Status $upstream_cache_status always; proxy_pass http://127.0.0.1:9100; proxy_set_header Host $host; }}Chaque paramètre a un rôle. levels=1:2 répartit les fichiers dans des sous-répertoires pour éviter un répertoire géant. keys_zone=boutique:10m nomme la zone que proxy_cache boutique; référence et lui accorde 10 Mo. max_size borne le disque, et inactive supprime une entrée non demandée pendant cette durée, quelle que soit sa fraîcheur. Prévoyez le répertoire et ses droits pour l'utilisateur des workers, nginx avec le paquet de l'éditeur : c'est lui qui doit pouvoir écrire dans /var/cache/nginx/boutique, sans quoi Nginx ne peut pas remplir son propre cache.
L'en-tête X-Cache-Status rend le cache observable. Sa valeur vient de la variable $upstream_cache_status, que Nginx remplit lui-même avec MISS, HIT, EXPIRED, STALE, UPDATING, REVALIDATED ou BYPASS selon ce qui s'est passé. EXPIRED désigne une entrée périmée rechargée depuis l'origine, STALE une entrée périmée servie faute de mieux quand l'origine ne répond pas, UPDATING une entrée périmée servie pendant qu'une autre requête la rafraîchit, REVALIDATED une entrée périmée que l'origine a confirmée, et BYPASS une réponse prise à l'origine parce que le cache a été volontairement contourné. Le paramètre always fait afficher l'en-tête aussi sur les réponses d'erreur.
Qui décide de ce qui entre dans le cache, Nginx ou l'origine ?
Section intitulée « Qui décide de ce qui entre dans le cache, Nginx ou l'origine ? »L'origine connaît ses réponses et le dit avec Cache-Control ; le rôle du cache est d'appliquer ce contrat, pas de le remplacer par une durée uniforme. Par défaut, Nginx respecte les en-têtes de l'origine : un Cache-Control: max-age=600 fixe la durée de fraîcheur, un private, no-cache ou no-store empêche de ranger la réponse, et une réponse porteuse de Set-Cookie n'est pas mise en cache. La directive proxy_cache_valid n'intervient qu'en repli, pour les réponses qui ne portent aucune durée. Une entrée dont la durée est écoulée est dite périmée : la revalidation consiste alors à demander à l'origine si la réponse a changé, au lieu de lui réclamer le corps entier.
location / { proxy_cache boutique;
# Repli : seulement pour les réponses qui ne disent rien proxy_cache_valid 200 1m;
# Une entrée périmée se revalide avec l'ETag plutôt que de se régénérer proxy_cache_revalidate on;
proxy_pass http://127.0.0.1:9100;}C'est ici que se joue le défaut le plus grave de la leçon : proxy_ignore_headers Cache-Control Expires Set-Cookie;. Cette directive demande à Nginx de ne pas lire ces en-têtes. Le contrat que l'origine annonce sur chaque réponse est alors jeté, et deux choses se produisent à la fois. Les réponses privées deviennent éligibles, puisque leur private, no-store n'est plus lu. Et une durée uniforme, proxy_cache_valid 200 10m; par exemple, s'applique à tout, y compris à un état de stock qui bouge toutes les secondes.
Une seconde porte mène au même résultat, et elle est moins connue : un
en-tête X-Accel-Expires valide dans la réponse de l'origine
court-circuite le test de Cache-Control. Nginx le lit en premier, et une
valeur positive fixe la durée de conservation sans que private ou no-store
soit même consulté. Une application qui émet cet en-tête pour piloter le cache
du proxy annule donc, sans le savoir, la protection que son propre
Cache-Control annonce. Les deux conditions se vérifient ensemble : aucun
proxy_ignore_headers sur Cache-Control, et aucun X-Accel-Expires émis par
une application qui sert aussi des réponses privées.
Une ressource qui change souvent reste pourtant éligible : l'origine annonce Cache-Control: public, max-age=5 et Nginx la garde cinq secondes, pas dix minutes. Avec proxy_cache_revalidate on;, une entrée périmée est revalidée par une requête conditionnelle (If-None-Match avec l'ETag) ; si l'origine répond 304 Not Modified, elle ne régénère pas le corps. Les deux politiques, durée courte et revalidation, tiennent la fraîcheur sans renoncer au cache. Ce que Nginx ne doit jamais faire, c'est décider à la place de l'origine pour gagner du cache.
Comment la clé de cache sépare-t-elle les variantes ?
Section intitulée « Comment la clé de cache sépare-t-elle les variantes ? »La clé de cache est la chaîne qui désigne une entrée : deux requêtes qui produisent la même clé reçoivent la même réponse rangée, et la première arrivée gagne. Par défaut, Nginx la construit avec $scheme$proxy_host$request_uri, c'est-à-dire le schéma, l'hôte de l'origine et l'URI complète avec sa chaîne de requête. proxy_cache_key permet de la redéfinir, et c'est un moyen sûr de se tromper : une clé trop pauvre mélange des réponses, une clé trop riche disperse le cache.
Une clé réduite à $uri ignore trois choses : la chaîne de requête, donc /catalogue?page=1 et /catalogue?page=2 se confondent ; la langue demandée ; et la session. Deux utilisateurs qui demandent la même URI reçoivent alors la première réponse rangée, quel que soit son destinataire.
Pour séparer les représentations d'une même URL (une fiche produit en français et en anglais), le mécanisme prévu est l'en-tête Vary. L'origine annonce Vary: Accept-Language pour dire que sa réponse dépend de la langue demandée, et Nginx, qui honore Vary depuis la version 1.7.7, range alors chaque variante séparément sous la même clé, sans qu'on touche à celle-ci.
Ajouter $http_accept_language à la clé donnerait un résultat voisin, avec un défaut : chaque valeur distincte de l'en-tête (fr, fr-FR,fr;q=0.9, en-US,en;q=0.8…) créerait une entrée, et le cache se disperserait en variantes qui ne diffèrent que par leur formulation. Vary laisse l'origine, qui sait de quoi sa réponse dépend, décider. Il faut cependant qu'elle l'annonce : un Vary oublié reste un défaut de l'application.
Le même mécanisme gouverne la compression : une origine qui compresse selon Accept-Encoding doit annoncer Vary: Accept-Encoding, sans quoi une réponse compressée peut être servie à un client qui ne sait pas la lire.
Comment mesurer un HIT sans croire l'en-tête ?
Section intitulée « Comment mesurer un HIT sans croire l'en-tête ? »Un HIT se mesure à l'origine, par ce qu'elle a produit, et jamais par ce que Nginx déclare. X-Cache-Status est une déclaration du proxy, pas une mesure, et c'est le proxy qu'on vérifie. Un add_header X-Cache-Status "HIT" always; écrit à la main afficherait HIT sur toutes les réponses, y compris celles qui viennent de traverser l'origine, et un contrôle qui croirait cet en-tête conclurait au vert sur un cache inexistant. L'en-tête n'a de valeur que s'il reprend $upstream_cache_status, et il ne prouve rien.
La mesure fiable consiste à faire compter à l'origine ce qu'elle produit. Une application d'essai incrémente un compteur à chaque corps de réponse qu'elle génère, et l'expose sur la boucle locale de la machine, là où Nginx n'a aucune prise. Deux requêtes de suite sur la même ressource publique donnent alors un résultat sans ambiguïté : le compteur monte d'une unité à la première, et ne bouge pas à la seconde. Un 304 ne compte pas comme génération, pour qu'une politique de revalidation passe la mesure au même titre qu'une durée courte.
Il reste un piège : une mesure hérite du cache de la précédente. Chaque mesure doit donc partir d'une entrée froide, c'est-à-dire d'une ressource absente du cache. La parade consiste à ajouter au chemin un jeton unique par mesure (/catalogue/accueil/3f9a1c0d2b7e), que l'origine ignore et qui désigne la même ressource. Chaque jeton produit une clé neuve, quelle que soit la clé que vous avez choisie, et la mesure conclut sur un état qu'elle a elle-même posé.
Comment empêcher une réponse privée de passer d'un compte à l'autre ?
Section intitulée « Comment empêcher une réponse privée de passer d'un compte à l'autre ? »Trois garde-fous se cumulent : ne jamais ignorer le Cache-Control de l'origine, écarter explicitement les requêtes authentifiées du cache, et prouver le résultat avec des marqueurs que l'on choisit. Le premier est déjà acquis si vous n'avez pas écrit proxy_ignore_headers Cache-Control. Les deux autres sont des filets de sécurité que vous posez vous-même, parce qu'une origine qui oublie un jour son private ne doit pas suffire à exposer des données.
Regardez le schéma de haut en bas : le compte alpha reçoit sa réponse, qui porte un marqueur propre. Dans le premier cas, le cache la range sous une clé qui ne voit pas la session, et le compte beta qui demande la même URL reçoit le marqueur d'alpha. Dans le second cas, rien n'est rangé et beta reçoit le sien.
Les directives qui écartent une requête du cache sont au nombre de deux, et elles ne font pas la même chose. proxy_no_cache décide de ne pas ranger la réponse, proxy_cache_bypass de ne pas servir depuis le cache. Les deux se déclarent, car une seule laisse un trou : sans proxy_cache_bypass, une entrée déjà rangée est servie ; sans proxy_no_cache, la réponse d'un compte est rangée pour les suivants.
location /compte/ { proxy_cache boutique;
# Pas de lecture dans le cache si la requête porte une session ou une autorisation proxy_cache_bypass $cookie_session $http_authorization;
# Pas d'écriture dans le cache pour ces mêmes requêtes proxy_no_cache $cookie_session $http_authorization;
proxy_pass http://127.0.0.1:9100; proxy_set_header Host $host;}Ces directives acceptent des variables : si l'une d'elles est non vide et différente de 0, la condition est vraie. Le location /compte/ ne sert que des réponses privées : on y écarte du cache toute requête qui porte une session ou une autorisation, et on garde le cache ailleurs. Remarquez la variable $cookie_session : elle désigne le cookie nommé session et non « un cookie quelconque ». Écarter du cache toute requête qui porte un cookie, y compris celui d'un outil de mesure d'audience, ferait disparaître le cache de la page publique, ce qui est la contre-épreuve échouée. Les cookies, Vary et Authorization ne se résument donc pas à une règle générique : on nomme ce qui identifie réellement un utilisateur.
Comment prouver l'isolation et la fraîcheur ?
Section intitulée « Comment prouver l'isolation et la fraîcheur ? »Une preuve d'isolation tire au hasard un marqueur par compte, lit ce marqueur à l'origine, puis vérifie qu'il n'apparaît que chez son propriétaire. Un corps fixe ne prouverait rien, puisqu'il serait le même des deux côtés ; un marqueur aléatoire rend une fuite indiscutable. La séquence comporte quatre constats, dans cet ordre.
- Le compte alpha reçoit sa réponse, avec son marqueur : la route privée fonctionne pour son titulaire.
- Le compte beta ne reçoit pas le marqueur d'alpha, et reçoit le sien.
- Une requête anonyme sur la même route est refusée (
401dans le lab) et ne reçoit le marqueur d'aucun compte. - La contre-épreuve : la page publique reste servie depuis le cache, ce qui se constate comme au HIT, deux requêtes de suite et un compteur de l'origine qui ne bouge pas à la seconde. Sans elle, « désactiver le cache partout » passerait les trois premiers constats.
La fraîcheur se prouve de la même façon. Pour une ressource volatile, on change l'état à l'origine, on attend plus longtemps que la durée annoncée (sept secondes pour un max-age=5), puis on exige que la réponse porte la nouvelle valeur. Un test symétrique exige que deux requêtes qui se suivent ne fassent pas produire deux corps : une durée de zéro tiendrait la fraîcheur en renonçant au cache, une durée de dix minutes tiendrait le cache en servant un état mort. Les deux côtés comptent.
Peut-on purger le cache avec Nginx open source ?
Section intitulée « Peut-on purger le cache avec Nginx open source ? »Non : la directive proxy_cache_purge appartient à NGINX Plus, la version commerciale ; Nginx open source n'a pas de purge intégrée. Les moyens d'invalider une entrée avec le paquet du lab sont des contournements : un proxy_cache_bypass déclenché par un en-tête de requête qui force un rechargement depuis l'origine, une durée de fraîcheur assez courte pour que la question ne se pose pas, ou la suppression des fichiers du répertoire de cache.
Le choix de la durée est donc le vrai levier. Une ressource qui doit changer vite reçoit un max-age court de l'origine, avec proxy_cache_revalidate on;, plutôt qu'une durée longue accompagnée d'une purge qu'on ne sait pas faire proprement. Une ressource stable peut garder une durée longue, puisque la changer passe de toute façon par un nouveau déploiement.
Comment compresser les réponses et mesurer le gain ?
Section intitulée « Comment compresser les réponses et mesurer le gain ? »gzip on; compresse les réponses texte à la volée quand le client annonce Accept-Encoding: gzip, et le gain se mesure en comparant les octets reçus avec et sans compression. La négociation est simple : le client déclare les encodages qu'il sait lire, le serveur en choisit un et le dit dans Content-Encoding. Par défaut, Nginx ne compresse que text/html ; les autres types se déclarent avec gzip_types.
http { gzip on; gzip_types text/plain text/css application/json application/javascript; gzip_min_length 1024; gzip_vary on;}gzip_min_length évite de compresser une réponse si petite que la compression coûterait plus qu'elle ne rapporte ; sa valeur par défaut est de 20 octets. Nginx ne juge ce seuil que sur l'en-tête Content-Length : une réponse qui arrive en segments, sans longueur annoncée, n'est donc pas filtrée par ce seuil et se compresse même si elle est minuscule. gzip_vary on; ajoute Vary: Accept-Encoding, qui dit aux caches intermédiaires qu'il existe deux représentations de la même URL. Ne compressez pas les formats déjà compressés (images JPEG, archives) : le gain est nul et le processeur travaille pour rien.
Pour mesurer, demandez la même ressource avec et sans l'encodage, et comparez la taille transférée.
curl -s -o /dev/null -w '%{size_download}\n' http://boutique.lab.test/catalogue/liste.jsoncurl -s -o /dev/null -w '%{size_download}\n' -H 'Accept-Encoding: gzip' http://boutique.lab.test/catalogue/liste.jsonLes deux nombres sont des octets réellement reçus : le second doit être sensiblement plus petit pour un contenu texte, et égal au premier pour une ressource dont le type n'est pas dans gzip_types. Le chemin de l'exemple est fictif ; mesurez sur une ressource qui existe chez vous. Ne retenez pas un « gain moyen » trouvé en ligne, il dépend entièrement du contenu.
Une réserve de sécurité demande de ne pas compresser à l'aveugle. Quand une même réponse contient à la fois un secret (un jeton, un identifiant de session) et du texte que l'attaquant peut faire refléter par la page, la taille de la réponse compressée peut renseigner l'attaquant sur le secret, même si la connexion est chiffrée. C'est une classe de risque établie, décrite dans la RFC 7457, section 2.6 : elle explique qu'on évite de compresser les pages qui mêlent secrets et entrée contrôlée par l'utilisateur. La leçon ne détaille pas le mécanisme ; retenez le principe, et ne compressez que les réponses où ce mélange n'existe pas. La compression côté Traefik suit la même logique, décrite dans Middlewares Traefik.
Mettre en pratique
Section intitulée « Mettre en pratique »Le lab pose un catalogue derrière un cache déjà en place, et mal réglé : l'origine y annonce son contrat sur chaque réponse, et la configuration le jette. Vous appliquez ce contrat. La page d'accueil publique doit être réellement servie depuis le cache, les variantes de langue ne doivent pas se mélanger sans que la variante française cesse d'être mise en cache, un compte ne doit jamais recevoir le marqueur d'un autre, et un état de stock modifié à l'origine doit apparaître en moins de sept secondes.
La validation ne lit jamais X-Cache-Status : elle compte les générations de corps à l'origine, sur la boucle locale de la VM, et part d'une entrée froide à chaque mesure. Le contrat de contenu qui liste les quatre ressources et leurs règles est déposé sur la machine, et c'est lui qui décide, pas votre préférence.
À retenir
Section intitulée « À retenir »- Un cache a deux exigences qui se prouvent ensemble : les réponses publiques éligibles sont réellement servies depuis le cache, et aucune réponse privée ne passe d'un compte à l'autre.
proxy_cache_pathréserve l'espace,proxy_cachel'active.$upstream_cache_statusdonne le statut que Nginx observe, mais ce n'est pas une mesure.- Un HIT se mesure à l'origine, en comptant ce qu'elle produit, avec un jeton unique par mesure pour partir d'une entrée froide.
proxy_ignore_headers Cache-Controljette le contrat de l'origine : il rend le privé éligible et fige le volatil. Laissez l'origine décider, et réservezproxy_cache_validaux réponses muettes.- Une seconde porte mène à la même fuite : un
X-Accel-Expiresvalide émis par l'origine court-circuite le test deCache-Control, si bien queprivateetno-storene sont jamais consultés. Les deux conditions se vérifient ensemble. - La clé désigne l'entrée : une clé réduite à
$uriignore langue, session et chaîne de requête.Varysépare les représentations sans toucher à la clé. proxy_no_cacheempêche de ranger,proxy_cache_bypassempêche de servir : on déclare les deux, sur ce qui identifie réellement un utilisateur (le cookie de session nommé,Authorization), pas sur n'importe quel cookie.- Une preuve d'isolation exige un marqueur aléatoire par compte, une requête anonyme, et la contre-épreuve : la page publique reste en cache.
- La purge est une fonction de NGINX Plus. Avec Nginx open source, c'est la durée annoncée par l'origine qui règle la fraîcheur.
gzip on;etgzip_typescompressent le texte ; on mesure aveccurl -w '%{size_download}', et on ne compresse pas une réponse qui mêle un secret et une entrée que l'utilisateur contrôle.
Contrôle de connaissances
Section intitulée « Contrôle de connaissances »Vérifiez que l'essentiel de ce guide est acquis. Les questions portent uniquement sur ce qui vient d'être expliqué ici.
Contrôle de connaissances
Validez vos connaissances avec ce quiz interactif
Informations
- Le chronomètre démarre au clic sur Démarrer
- Questions à choix multiples, vrai/faux et réponses courtes
- Vous pouvez naviguer entre les questions
- Les résultats détaillés sont affichés à la fin
Lance le quiz et démarre le chronomètre
Vérification
(0/0)Score par domaine
Un domaine regroupe plusieurs guides et reçoit plusieurs questions : son score est plus fiable qu'un score par thème, qui porte souvent sur une seule question.
Bilan
Points forts
Aucun sujet réussi à 80 % sur au moins deux questions pour l’instant.
Points faibles
Aucun sujet sous le seuil de réussite.
À faire ensuite
Des indices pour retenter votre chance ?
Continuer le cours : Observer Nginx et relier proxy et upstream
Nouveau quiz complet avec des questions aléatoires
Retravailler uniquement les questions ratées
Retour à la liste des certifications