La requête à laquelle vous répondez devient un enregistrement que vous détenez
Tout le reste de cette série regarde vers l'extérieur : cacher l'origine, chiffrer le disque, choisir la juridiction, empêcher quelqu'un d'autre de garder un journal sur vous. Celui-ci regarde la machine elle-même, parce qu'un serveur qui répond à des requêtes note qui les a faites — par défaut, à plusieurs endroits à la fois, avec une rétention que personne n'a choisie.
Regardez ce qu'une image standard détient encore quinze jours après le déploiement. /var/log/nginx/access.log a une ligne par requête : adresse, horodatage, chemin, référent, user-agent, avec rotation quotidienne et conservation pendant quatorze jours. /var/log/auth.log a chaque session SSH, y compris l'adresse source de chacune des vôtres. Journald a les mêmes événements, plus tout ce que vos services ont écrit sur stderr. Si Docker tourne, chaque conteneur a un fichier de journal JSON qui, par défaut, n'a absolument aucune limite de taille. Si fail2ban tourne, il a un journal de chaque adresse jamais bannie, ainsi qu'une base SQLite qui dit la même chose, et les adresses y sont complètes dans les deux.
Rien de tout cela n'est malveillant, et la plupart de ces journaux sont réellement utiles — pendant environ une heure après qu'un problème survient. Le problème, c'est la forme : fidélité totale, conservée longtemps, par accident plutôt que par décision. La question à se poser pour chacun de ces fichiers n'est pas "est-ce sensible ?" mais "qu'est-ce que je ferais réellement de la ligne que j'ai écrite il y a trois semaines ?" Pour l'immense majorité des lignes sur l'immense majorité des serveurs, la réponse est rien, et un enregistrement que vous ne lirez jamais est un passif pur — face à une compromission, face à une sauvegarde qui survit à la machine, face à quiconque finira par le réclamer.
La minimisation des données est le nom officiel de ce correctif, et c'est l'idée la moins controversée de toute la protection des données : ne collecter que ce dont la tâche a besoin, le garder tant que la tâche en a besoin, puis arrêter. Ce qui suit applique cela à une machine que vous faites réellement tourner, en partant du principe que vous avez déjà fait la première heure après déploiement et qu'il y a quelque chose sur la machine qui mérite d'être protégé.
SP·02Six journaux, et les deux qui nomment des personnes
Avant de changer quoi que ce soit, faites l'inventaire. Un petit VPS Debian ou Ubuntu qui fait tourner un service web écrit généralement six flux, et ils se recoupent plus qu'on ne s'y attend — le même événement atterrit souvent dans trois fichiers avec trois rétentions différentes.
Le journal d'accès nginx est celui qui nomme vos visiteurs. Une ligne par requête, avec l'adresse du client, le chemin exact, le référent et une chaîne user-agent assez détaillée pour constituer, à elle seule, une empreinte faible. Le journal d'erreurs nginx est celui qu'on oublie : il enregistre client: 203.0.113.9 à chaque timeout amont, chaque 403, chaque requête malformée — et contrairement au journal d'accès, son format est fixe et ne peut pas être personnalisé.
Le journal auth — /var/log/auth.log sur la famille Debian, /var/log/secure sur la famille RHEL — vous nomme vous. Chaque ligne de connexion acceptée par clé publique porte votre adresse source et l'empreinte de la clé qui a ouvert la session. Quiconque en lit un mois apprend depuis quels réseaux vous administrez, à quelles heures, et combien de clés distinctes vous détenez. Sur une machine dont la propriété est anonyme, ce fichier est souvent plus révélateur que tout ce que produisent vos visiteurs.
Journald conserve une copie de presque tout ce qui précède, plus le stdout et le stderr de chaque unité, et sur une installation par défaut, il est autorisé à croître jusqu'à 10 % du système de fichiers, plafonné à 4 Go, avant de commencer à supprimer les entrées les plus anciennes. Sur tout disque de 40 Go ou plus, ce plafond atteint les 4 Go au complet, ce qui, aux volumes que produit un petit serveur, représente plusieurs mois.
Les journaux applicatifs sont la carte joker. Un framework en mode debug journalise les URL complètes, y compris les chaînes de requête, et les chaînes de requête portent couramment des jetons de session, des liens de réinitialisation de mot de passe et des termes de recherche. PHP-FPM peut être configuré pour écrire son propre journal d'accès, qui duplique la ligne de requête que nginx a déjà écrite, dans un fichier que vos réglages nginx ne touchent jamais.
Les journaux de conteneurs sont les plus discrets. Le pilote json-file par défaut de Docker n'a aucune rotation tant que vous ne la configurez pas, donc /var/lib/docker/containers/*/*-json.log garde tout ce que le conteneur a dit depuis sa création. C'est une façon courante de découvrir qu'une politique de rétention de "14 jours" conserve en réalité onze mois de données, et c'est le même genre de surprise que les règles de pare-feu que Docker écrit dans le dos d'ufw.
Sur les six, deux portent des identifiants concernant des personnes et méritent d'être délibérément minimisés : le journal d'accès nginx (vos visiteurs) et le journal auth (vous). Les autres ont surtout besoin d'un plafond de taille et d'une horloge plus courte.
SP·03Tronquer à l'écriture, pas à la rotation
Le réflexe, c'est de continuer à journaliser normalement et de nettoyer après coup — un cron nocturne qui réécrit le fichier de la veille en supprimant les adresses. Ne faites pas ça. Un job de nettoyage signifie que les adresses brutes ont réellement existé sur le disque pendant jusqu'à un jour entier, et que durant cette journée, elles ont été capturées par votre sauvegarde, potentiellement copiées par l'instantané au niveau bloc de votre hébergeur, et laissées dans ce que le système de fichiers a fait des anciens blocs. Pire, ce job est une pièce mobile de plus : il échoue silencieusement la semaine où le disque se remplit, et rien ne vous signale que le fichier de la veille est toujours complet.
La seule réduction sur laquelle vous pouvez vraiment compter est celle qui se produit avant que la ligne ne soit écrite. Dans nginx, c'est un bloc map évalué au moment de la journalisation, qui réécrit l'adresse dans une nouvelle variable, et un log_format qui utilise cette nouvelle variable à la place de $remote_addr. L'adresse complète n'est jamais sérialisée. Il n'y a rien à nettoyer après coup, rien à planifier, et rien qui puisse mal tourner le jour où vous ne surveillez pas.
# /etc/nginx/conf.d/00-privacy-log.conf -- http context, loaded before the sites
map $remote_addr $ip_trunc {
# IPv4: keep the /24, zero the host part
~(?<v4>\d+\.\d+\.\d+)\. "${v4}.0";
# IPv6: keep the first two groups, drop the rest
~(?<v6>[^:]+:[^:]+): "${v6}::";
# anything the two patterns cannot parse -- including compressed forms
# like ::1 -- falls through here, i.e. fails closed rather than open
default "0.0.0.0";
}
log_format privacy '$ip_trunc - - [$time_local] "$request" $status '
'$body_bytes_sent "$http_referer" "$http_user_agent" '
'rid=$request_id rt=$request_time';
Trois détails décident si cela fonctionne vraiment. D'abord, ce bloc map doit vivre dans le contexte http — à l'intérieur d'un bloc server, nginx refuse de démarrer. Le déposer dans conf.d/ avec un nom qui se trie tôt alphabétiquement est le moyen le plus simple de s'en assurer.
Ensuite, si vous êtes derrière Cloudflare ou un autre edge, vérifiez ce que contient $remote_addr. Le module realip le remplace par la véritable adresse du client tôt dans le traitement de la requête, et conserve l'adresse de connexion dans $realip_remote_addr. Cet ordre joue en votre faveur : le map s'exécute au moment de la journalisation, donc il tronque le vrai visiteur plutôt que l'edge. Mais cela signifie aussi que si vous n'avez pas configuré realip, vous tronquez l'adresse de Cloudflare et n'apprenez rien, pendant que la véritable adresse se trouve dans un en-tête.
Troisièmement, vérifiez le reste de la chaîne de format. Tronquer $remote_addr ne sert précisément à rien si la ligne se termine encore par "$http_x_forwarded_for" ou porte $http_cf_connecting_ip — et un grand nombre de formats standards ou fournis par des panels de contrôle incluent l'un des deux. L'adresse complète est dans les en-têtes de la requête ; elle ne reste hors du fichier que si vous laissez chaque en-tête qui la porte hors du format.
Remarquez ce qui l'a remplacée : $request_id, une chaîne hexadécimale aléatoire de 32 caractères que nginx génère par requête. C'est le pivot de la section suivante.
Ce que la troncature casse, et les trois tâches que fait vraiment un journal
Quelqu'un objecte toujours que des journaux anonymisés ne servent à rien, et il a à moitié raison — parce que "les journaux", ce sont en réalité trois tâches sans rapport qui portent le même nom de fichier, et une seule d'entre elles a besoin de l'adresse.
Tâche une : bloquer qui est en train de vous marteler maintenant. Cela nécessite l'adresse complète, et il la faut en quelques secondes. Pas besoin de l'avoir demain. fail2ban est l'outil habituel, et il ne peut tout simplement pas fonctionner à partir d'un fichier tronqué — bannir 203.0.113.0 bannit un hôte innocent et laisse l'attaquant connecté. Mais cette tâche est satisfaite par un fichier qui existe pendant un jour, ou même par l'absence totale de fichier : les propres limit_req et limit_conn de nginx gardent leur état en mémoire partagée, agissent en microsecondes plutôt qu'à l'intervalle de scrutation de fail2ban, et n'écrivent rien sur le disque.
Tâche deux : comprendre pourquoi cette requête a renvoyé une 500. Cela demande de la corrélation, pas de l'identité. Un ID de requête qui traverse nginx jusqu'à l'application relie la ligne d'accès, l'erreur amont et la pile d'appels de l'application à un seul événement — ce que vous essayiez en réalité de faire en allant chercher l'adresse. Dans la pratique, l'ID fait mieux : il survit à un client sur un réseau mobile dont l'adresse change en cours de session, et il ne devient pas obsolète quand quatre visiteurs partagent une même adresse CGNAT.
Tâche trois : comprendre le trafic dans la durée. Volume, répartition des codes de statut, quels chemins sont chauds, si le crawler est devenu incontrôlable. Une adresse tronquée suffit largement ici, et le /24 qui survit à la troncature suffit à voir qu'un seul réseau est responsable de 40 % de vos requêtes.
La conception n'est donc pas "journaliser moins" mais séparer par horloge : un flux à fidélité totale qui vit un jour et alimente les outils de blocage, et un flux tronqué qui vit aussi longtemps que vous voulez des statistiques. Les deux sont écrits par nginx au même instant, donc il n'y a aucune étape de traitement entre les deux, et aucune fenêtre où la mauvaise donnée reste sur le disque plus longtemps que prévu.
# inside the server block, or in a snippet included by it
access_log /var/log/nginx/access.log privacy; # truncated, keep for weeks
access_log /var/log/nginx/security.log secip; # full address, keep for a day
# static assets are pure noise in a privacy log -- drop them entirely
location ~* \.(?:css|js|svg|png|jpe?g|webp|woff2?|ico)$ {
access_log off;
expires 30d;
}
Le format secip tient en une ligne à côté de l'autre — $remote_addr, l'horodatage, la requête et le statut, rien de plus. C'est le seul fichier de la machine où une adresse de visiteur complète est autorisée à rester, ce qui fait de sa rétention une décision unique, prise à un seul endroit, plutôt qu'une propriété qu'il faut raisonner à travers six fichiers.
journald, auth.log, et la trace qui remonte jusqu'à vous
La vie privée des visiteurs, c'est la partie dont on parle. La trace de l'administrateur, c'est celle qui importe, sur une machine dont tout l'intérêt est que votre nom n'y soit pas rattaché, et elle se trouve presque entièrement à deux endroits.
/var/log/auth.log enregistre une ligne Accepted publickey pour chaque session que vous ouvrez, avec votre adresse source et l'empreinte de votre clé. Sur un mois, c'est un emploi du temps de vos habitudes de travail et une liste des réseaux que vous utilisez. Si vous vous connectez toujours par le même tunnel, c'est une adresse répétée et relativement terne. Si vous vous connectez d'où que vous soyez, c'est un historique de déplacements.
Journald conserve les mêmes événements, plus tout ce que vos unités ont imprimé, et ses valeurs par défaut sont généreuses : SystemMaxUse= vaut 10 % du système de fichiers, et MaxRetentionSec= n'est pas défini, ce qui veut dire qu'il n'y a aucune limite de temps — seulement une limite de taille. Sur un serveur peu chargé, cette combinaison retient des mois de données.
Il y a ici un vrai compromis, et il mérite d'être énoncé plutôt qu'esquivé. Les journaux qui vous décrivent sont les mêmes journaux qui vous disent comment quelqu'un est entré. Réglez Storage=volatile et le journal ne vit qu'en RAM, disparaissant au redémarrage — réellement privé, et réellement inutile le matin où vous trouvez un processus que vous n'avez pas lancé, parce que le premier redémarrage de votre attaquant a effacé les preuves. Pour la plupart des gens, le juste milieu est un stockage persistant avec un plafond strict et une horloge courte : assez long pour enquêter sur un incident remarqué dans la semaine, assez court pour que le fichier ne soit pas un journal intime.
Deux détails d'implémentation qui piègent pas mal de monde. Les images Debian et Ubuntu diffèrent sur la présence ou non de rsyslog ; si /var/log/auth.log existe sur votre machine, c'est que rsyslog l'écrit, et les plafonds de journald ne gouvernent pas du tout ce fichier — c'est le travail de logrotate. Et ForwardToSyslog= est ce qui alimente rsyslog depuis le journal, donc le désactiver sur une machine qui a les deux vous évite de garder deux copies de tout sous deux politiques de rétention différentes.
Votre politique de rétention est celle que disent vos sauvegardes
C'est celle qui défait tout le travail soigneux fait plus haut, et elle est invisible tant qu'on ne va pas la chercher.
Disons que logrotate garde quatorze jours de journaux nginx, et que cela vous convient. Ajoutez maintenant la sauvegarde hors site que vous avez eu raison de mettre en place : un passage quotidien de Borg ou restic, avec une rétention de sept archives quotidiennes, quatre hebdomadaires et six mensuelles. Chacune de ces archives contient /var/log tel qu'il était le jour où elle a tourné. L'archive mensuelle la plus ancienne a six mois et contient les quatorze jours de journaux qui étaient d'actualité à ce moment-là. Votre rétention de journaux effective n'est pas de quatorze jours. Elle est de six mois, dans un dépôt chiffré que vous ne pouvez pas grep sans le restaurer, sur une seconde machine, dans un autre pays.
Il y a exactement deux résolutions honnêtes. Exclure les répertoires de journaux de la sauvegarde — c'est à peu près la seule chose sur un serveur que vous pouvez presque toujours reconstruire ou dont vous pouvez vous passer, et une restauration qui omet /var/log n'est pas une moins bonne restauration. Ou les inclure délibérément et accepter que votre rétention réelle est celle du dépôt, auquel cas dites-le dans la politique que vous publiez, parce que l'alternative est une politique affichée que votre propre infrastructure contredit.
# exclude logs from the backup, and prove it took
borg create --stats \
--exclude '/var/log' \
--exclude '/var/lib/docker/containers' \
::'{hostname}-{now:%Y-%m-%d}' /
# the check that matters: can you still find an address in the newest archive?
borg list ::"$(borg list --short --last 1)" | grep -c '^var/log/' || echo "clean"
Tant que vous êtes dans cet état d'esprit, deux problèmes voisins du même genre. Les instantanés de votre hébergeur ne vous appartiennent pas. Un instantané au niveau de l'hyperviseur capture le disque tel qu'il était, y compris des journaux que vous avez fait tourner depuis, et il vit sur le stockage du fournisseur, sous la rétention du fournisseur — une raison de plus pour laquelle les adresses n'auraient jamais dû être écrites en entier, plutôt qu'une raison de faire quoi que ce soit d'astucieux après coup.
Et ne cherchez pas à utiliser shred sur un VPS. Écraser un fichier sur un disque virtuel à provisionnement fin, par-dessus un système de fichiers en copy-on-write, lui-même posé sur un SSD qui redistribue les blocs pour l'usure, ne réécrit pas de façon fiable les cellules physiques qui le contenaient. La suppression sécurisée sur du stockage loué et virtualisé, c'est du théâtre. La seule réduction qui fonctionne est celle que vous avez faite au moment de l'écriture ; tout ce qui vient après n'est qu'un effort du mieux possible, invérifiable. Chiffrer le volume change cette équation — mais il la change avant que la donnée ne soit écrite, ce qui est encore la même leçon.
Les copies que vous ne contrôlez pas
La minimisation sur votre propre machine n'est qu'une couche parmi plusieurs, et être au clair sur les autres est ce qui évite que tout ceci devienne un faux sentiment d'exhaustivité.
Le réseau de votre hébergeur voit l'enregistrement de flux. Source, destination, ports, octets, timing — pour chaque connexion entrante et sortante de la machine, que vous journalisiez quoi que ce soit ou non. Aucune configuration côté serveur ne change cela. C'est en grande partie pourquoi la juridiction où se trouve la machine est une variable réelle, pas un argument marketing ; ce que la loi exige du réseau varie énormément d'un pays à l'autre.
Votre CDN ou votre edge journalise à la périphérie. Si Cloudflare termine le TLS pour vous, il a la ligne de requête et l'adresse du client avant même que votre serveur n'intervienne, selon son propre calendrier de rétention, soumis à ses propres procédures légales. Tronquer votre journal d'origine ne remonte pas jusque-là. Faire tourner un edge que vous possédez est la version de tout ceci que vous pouvez réellement configurer — et les règles de journalisation de ce guide s'appliquent d'abord à cette machine en périphérie, puisque c'est là qu'arrivent les adresses non tronquées.
Les trackers d'erreurs et les outils d'analytics l'expédient hors de la machine à votre place. Sentry et la plupart de ses équivalents attachent l'IP du client à chaque événement par défaut ; le réglage s'appelle en général quelque chose comme send_default_pii, et il vaut mieux le vérifier que le supposer. Tout outil d'analytics hébergé est, par construction, une copie tierce du journal d'accès que vous venez de passer un après-midi à tronquer.
Le mail est le plus fuyant de tous. Si quoi que ce soit sur la machine envoie du courrier, les en-têtes portent l'hôte et l'adresse d'envoi, et chaque relais sur le chemin garde une copie de l'enveloppe avec horodatage. Un formulaire de contact qui vous envoie un email est un journal que vous n'administrez pas.
Rien de tout cela ne rend le travail local inutile — la copie locale est celle qui est saisie avec la machine, exfiltrée lors d'une intrusion, ou remise par vous-même. C'est simplement la seule couche que vous contrôlez entièrement, et l'erreur est de la prendre pour toute l'image.
SP·08Minimiser par conception, pas supprimer sur notification
Cela vaut la peine d'être précis ici, parce que les deux se confondent facilement, et la différence entre les deux est celle qui sépare une pratique d'ingénierie standard de quelque chose que vous ne devriez pas faire.
Décider à l'avance de ce que votre service collecte et pendant combien de temps il le garde est une pratique ordinaire, documentée, encouragée. Sous le RGPD, ce sont deux des principes fondamentaux — minimisation des données et limitation de la conservation — et une rétention de journaux plus courte est un contrôle que les auditeurs réclament, pas un qu'ils contestent. Il n'existe aucune obligation générale, pour un opérateur de site web ou un client d'hébergement dans l'UE, de conserver des journaux de trafic ; la directive de rétention généralisée qui a longtemps laissé croire le contraire a été invalidée par la Cour de justice en 2014, et les lois nationales qui lui survivent lient surtout les fournisseurs de télécommunications, pas les personnes qui font tourner un serveur web. Les États-Unis n'ont pas non plus d'obligation générale de rétention pour les opérateurs de sites.
Détruire des enregistrements spécifiques après avoir été notifié à leur sujet est un acte complètement différent. Une demande de conservation, une obligation de préservation dans le cadre d'un litige, une décision de justice ou une enquête de police change ce que vous pouvez faire des données qui existent à ce moment précis, et "ma politique de rétention l'a supprimé" n'est pas une défense si vous avez accéléré la suppression à cause de la notification. Rien dans ce guide ne parle de cela. Une politique de rétention est quelque chose que vous fixez un mardi tranquille et que vous laissez ensuite tranquille ; si elle ne se raccourcit que quand quelque chose arrive, ce n'était pas une politique.
La même distinction traverse le reste de ce site : il y a une vraie ligne entre l'ingénierie de la vie privée et la posture "bulletproof" qui se vend comme une immunité. Concevoir un service qui n'accumule jamais d'historique de visiteurs se tient confortablement du bon côté de cette ligne, dans la même catégorie que chiffrer ses disques ou ne pas demander aux utilisateurs une adresse email dont vous n'avez pas besoin.
Deux conséquences pratiques. Si vous publiez une politique de confidentialité, faites en sorte que ses chiffres de rétention correspondent à ce qui est réellement sur le disque — quatorze jours affichés et six mois réels dans un dépôt de sauvegarde, c'est le genre d'écart qui transforme un opérateur de bonne foi en opérateur de mauvaise foi sur le papier. Et notez la décision quelque part pour vous-même, dans un commentaire en haut du fichier logrotate si nulle part ailleurs, parce que la personne qui devra justifier ces chiffres dans dix-huit mois, c'est vous, et elle ne se souviendra pas pourquoi le nombre était sept. Rien de ce qui précède n'est un conseil juridique ; si vous opérez quelque part avec des obligations de rétention propres à un secteur, vérifiez-les par rapport à votre propre situation avant de raccourcir quoi que ce soit.
SP·09Étape par étape
-
01
Découvrez ce que la machine garde déjà
Ne configurez rien avant d'avoir mesuré. Le but de cette passe est de trouver le fichier que vous avez oublié, qui sur la plupart des machines est soit un journal de conteneur, soit un journal applicatif que personne n'a regardé depuis le déploiement.
# biggest log files anywhere on the box, largest last sudo du -ah /var/log /var/lib/docker/containers 2>/dev/null \ | sort -h | tail -20 # how much disk the journal holds, and how far back it goes journalctl --disk-usage journalctl --output=short-iso | head -1 # how old is the oldest nginx line still on disk? zcat -f /var/log/nginx/access.log* 2>/dev/null | head -1
Regardez ensuite une ligne de chaque fichier et demandez-vous ce qu'elle identifie. La commande ci-dessous compte combien d'adresses complètes distinctes sont actuellement récupérables depuis vos journaux web — c'est en général le chiffre qui justifie tout le reste de ce guide.
zcat -f /var/log/nginx/access.log* 2>/dev/null \ | grep -oE '^([0-9]{1,3}\.){3}[0-9]{1,3}' | sort -u | wc -lNotez ce que vous trouvez. Vous relancerez ces mêmes commandes à la fin pour prouver que le changement a pris effet.
-
02
Tronquez l'adresse du client avant que nginx n'écrive la ligne
Créez le map et les deux formats dans un fichier chargé dans le contexte
http. Sur Debian et Ubuntu,/etc/nginx/conf.d/est inclus depuisnginx.confavant les configs de sites, ce qui est exactement sa place.sudo tee /etc/nginx/conf.d/00-privacy-log.conf > /dev/null <<'EOF' # The client address is reduced here, at log time, and never written in full # to the long-retention file. $realip_remote_addr still holds the connecting # address if you need it while debugging a proxy problem. map $remote_addr $ip_trunc { ~(?<v4>\d+\.\d+\.\d+)\. "${v4}.0"; ~(?<v6>[^:]+:[^:]+): "${v6}::"; # anything the two patterns cannot parse -- including compressed forms # like ::1 -- falls through here, i.e. fails closed rather than open default "0.0.0.0"; } # Long retention: no full address, no forwarded-for header, request id instead. log_format privacy '$ip_trunc - - [$time_local] "$request" $status ' '$body_bytes_sent "$http_referer" "$http_user_agent" ' 'rid=$request_id rt=$request_time'; # Short retention: the one file allowed to hold a complete address. log_format secip '$remote_addr [$time_local] "$request" $status'; EOF sudo nginx -t && sudo systemctl reload nginxFaites maintenant pointer le site vers eux. Dans votre bloc server, remplacez la ligne
access_logexistante par la paire, et coupez la journalisation pour les assets statiques tant que vous y êtes.access_log /var/log/nginx/access.log privacy; access_log /var/log/nginx/security.log secip;
Rechargez, chargez une page, et lisez le résultat. Le premier champ doit se terminer par
.0, et la ligne doit porter une valeurrid=.sudo nginx -t && sudo systemctl reload nginx curl -s -o /dev/null https://your-domain.example/ sudo tail -1 /var/log/nginx/access.log
Si l'adresse est encore complète, c'est que le bloc server écrase le format quelque part plus bas —
grep -rn access_log /etc/nginx/trouve la ligne qui l'emporte. -
03
Ne gardez des adresses complètes que là où quelque chose agit dessus
Si fail2ban tourne, il lit actuellement le fichier que vous venez de tronquer. Pointez-le plutôt vers le journal de sécurité, et donnez à ce journal une vie d'un jour pour que les adresses complètes expirent d'elles-mêmes.
sudo tee /etc/fail2ban/jail.d/nginx-privacy.local > /dev/null <<'EOF' [nginx-http-auth] enabled = true logpath = /var/log/nginx/error.log [nginx-botsearch] enabled = true logpath = /var/log/nginx/security.log maxretry = 6 findtime = 10m bantime = 1h EOF sudo fail2ban-client reload
Occupez-vous ensuite de la propre mémoire de fail2ban, que la plupart des gens ne touchent jamais : il garde son propre journal des bannissements sous le
rotate 4 weeklypar défaut, ainsi qu'une base SQLite de chaque bannissement qu'il a émis.dbpurgeageest ce qui fait expirer les lignes de cette base — réglez-le sur une valeur proche de votre plus long bannissement plutôt que de le laisser à sa valeur par défaut.sudo tee /etc/fail2ban/fail2ban.d/retention.local > /dev/null <<'EOF' [Definition] dbpurgeage = 2d loglevel = NOTICE EOF sudo systemctl restart fail2ban
Mieux encore, pour les simples inondations, faites le travail dans nginx, où rien n'est écrit nulle part.
limit_reqgarde ses compteurs en mémoire partagée, répond en microsecondes plutôt qu'à un intervalle de scrutation, et ne laisse aucune trace de qui a été freiné — voir le runbook de première heure contre une attaque DDoS pour dimensionner les zones sous charge réelle.# http context: keyed on the truncated address, so nothing complete is # held in memory either. 10m of shared state is plenty for a small site. limit_req_zone $ip_trunc zone=perip:10m rate=20r/s; # in the location you want protected limit_req zone=perip burst=40 nodelay;
Faire reposer la clé de la zone sur
$ip_truncplutôt que sur$binary_remote_addrest un compromis délibéré : la limite s'applique désormais à tout un/24à la fois, donc un bureau chargé derrière un même bloc partage un budget. Pour un petit site, c'est généralement sans conséquence, voire une amélioration ; si ce n'est pas le cas, faites reposer la clé de la zone sur l'adresse complète — l'état d'un limiteur de débit vit en mémoire partagée et n'est jamais écrit sur le disque, donc il ne fait pas partie de ce que vous minimisez ici. -
04
Plafonnez le journal et arrêtez la seconde copie
journald prend un drop-in, qui survit aux mises à jour de paquets d'une façon que modifier
journald.confne permet pas. Les valeurs ci-dessous gardent environ une semaine — assez pour enquêter lundi sur quelque chose qui a commencé vendredi — sous un plafond strict de 200 Mo.sudo mkdir -p /etc/systemd/journald.conf.d sudo tee /etc/systemd/journald.conf.d/retention.conf > /dev/null <<'EOF' [Journal] Storage=persistent SystemMaxUse=200M SystemMaxFileSize=20M MaxRetentionSec=7day MaxFileSec=1day ForwardToSyslog=no EOF sudo systemctl restart systemd-journald journalctl --disk-usage
Le redémarrage applique immédiatement les plafonds de taille ; l'horloge de rétention, elle, s'applique au fur et à mesure que de nouveaux fichiers sont créés par rotation, donc un journal déjà trop gros ne rétrécit qu'à la prochaine rotation, pas instantanément.
journalctl --vacuum-time=7dle force dès maintenant si vous voulez récupérer l'espace disque aujourd'hui.ForwardToSyslog=noimporte sur toute image qui embarque rsyslog : sans cela, chaque entrée du journal est aussi ajoutée à/var/log/syslog, sous le calendrier de logrotate plutôt que celui de journald, et vous vous retrouvez avec deux copies ayant deux dates d'expiration différentes. Vérifiez dans quelle situation vous êtes avant de le supposer :systemctl is-active rsyslog 2>/dev/null || echo "rsyslog not running" ls -la /var/log/auth.log /var/log/syslog 2>/dev/null
Si ces fichiers existent, rsyslog les possède et c'est l'étape six qui fixera leur rétention. S'ils n'existent pas, le journal est la seule copie et vous venez de le plafonner.
-
05
Empêchez l'application de rejournaliser ce que vous avez retiré
Rien de ce qui précède ne touche votre application, et une application dans le mauvais mode écrira volontiers l'adresse complète, l'URL complète et le jeton de session dans un fichier bien à elle. Trois choses à vérifier.
PHP-FPM embarque une directive
access.logdans la configuration de son pool, commentée par défaut mais activée par un grand nombre de panels de contrôle. Si elle est active, c'est une seconde copie de chaque ligne de requête, issue d'un fichier que votre travail sur nginx n'a jamais touché.grep -rn '^access.log' /etc/php/*/fpm/pool.d/ || echo "fpm access log off"
Le niveau de log de votre framework décide si les chaînes de requête et les corps de requête atterrissent sur le disque. Le mode debug de la plupart des frameworks journalise l'URL complète, et un lien de réinitialisation de mot de passe est une URL complète. Réglez le niveau production et confirmez que c'est bien celui-là qui est chargé, plutôt que celui du fichier que vous croyez être lu.
Le pilote par défaut de Docker ne fait jamais de rotation. Corrigez cela au niveau du démon pour que chaque futur conteneur hérite du plafond. Notez que cela ne s'applique qu'aux conteneurs créés après le redémarrage — les conteneurs existants gardent leur fichier actuel, sans limite, jusqu'à ce qu'ils soient recréés.
sudo tee /etc/docker/daemon.json > /dev/null <<'EOF' { "log-driver": "json-file", "log-opts": { "max-size": "10m", "max-file": "3" } } EOF sudo systemctl restart docker docker inspect --format '{{.HostConfig.LogConfig}}' $(docker ps -q) 2>/dev/nullSi un conteneur détient déjà un gros fichier, le recréer avec
docker compose up -d --force-recreateest ce qui tronque réellement l'historique — un simple redémarrage garde le même fichier de journal. -
06
Fixez la rétention délibérément, à un seul endroit par fichier
logrotate est l'endroit où vit l'horloge pour tout ce que rsyslog et nginx écrivent. Modifiez la stanza fournie plutôt que d'en ajouter une seconde : deux stanzas nommant le même chemin font échouer logrotate avec une erreur d'entrée dupliquée, et arrêtent complètement la rotation de ce fichier, ce qui est la façon la plus courante dont un changement de rétention devient silencieusement une rétention infinie.
# check for duplicates BEFORE editing, then again after sudo logrotate --debug /etc/logrotate.conf 2>&1 | grep -i 'duplicate\|error'
Pour nginx, les deux fichiers veulent des horloges différentes : celui qui est tronqué peut vivre des semaines, celui qui détient les adresses complètes ne devrait pas survivre à la journée. Ajoutez une stanza séparée pour le journal de sécurité — un chemin différent, donc pas de doublon — et raccourcissez celle fournie par défaut.
sudo tee /etc/logrotate.d/nginx-security > /dev/null <<'EOF' # The only file on this box that holds complete client addresses. # One day, uncompressed so fail2ban can read it. Do not lengthen without # a reason you would be happy to write down here. /var/log/nginx/security.log { daily rotate 1 maxage 1 missingok notifempty nocompress create 0640 www-data adm sharedscripts postrotate [ -f /var/run/nginx.pid ] && kill -USR1 `cat /var/run/nginx.pid` endscript } EOF sudo sed -i 's/^\trotate 14$/\trotate 7/' /etc/logrotate.d/nginx sudo logrotate --debug /etc/logrotate.d/nginx-securityFaites le même calcul pour
/etc/logrotate.d/rsyslogsi rsyslog est installé — ses valeurs par défaut gardent quatre semaines d'auth.log, ce qui représente quatre semaines de vos propres sessions SSH. Et relancez la passe de débogage une dernière fois : elle affiche exactement les fichiers qu'elle ferait tourner et supprimerait, ce qui est le seul moyen de confirmer que les chiffres que vous venez de taper sont bien les chiffres en vigueur. -
07
Prouvez-le, y compris à travers la sauvegarde
Relancez les mesures de l'étape un. Le nombre d'adresses complètes distinctes dans le journal à longue rétention devrait maintenant cesser de croître, et après un cycle de rotation, il devrait être nul.
# should print 0 once the pre-change files have rotated out zcat -f /var/log/nginx/access.log* 2>/dev/null \ | grep -oE '^([0-9]{1,3}\.){3}[0-9]{1,3}' \ | grep -v '\.0$' | sort -u | wc -l # and the journal should now be bounded journalctl --disk-usagePuis la vérification que presque personne ne fait : regardez dans la sauvegarde. Un dépôt qui contient encore
/var/logdétient la fidélité et la rétention que vous venez de passer un après-midi à supprimer, et il continuera de les détenir aussi longtemps que survivra votre plus ancienne archive.borg list | head -3 # borg lists oldest first borg list ::"$(borg list --short | head -1)" 2>/dev/null \ | grep -c '^var/log/' || echo "no logs in oldest archive"
Si le compte n'est pas nul, ajoutez l'exclusion mentionnée plus tôt et laissez les vieilles archives s'éteindre d'elles-mêmes, ou purgez-les délibérément. Tant que l'un des deux n'est pas fait, votre rétention réelle est celle du dépôt — ce qui est la phrase la plus utile de tout ce guide, et la plus facile à oublier.
Enfin, notez les chiffres là où la prochaine personne les trouvera : la rétention que vous avez choisie, la raison, et la date. Un commentaire en haut du fichier logrotate suffit. La configuration ci-dessus est une décision, et une décision que personne ne peut reconstituer dans un an redevient un défaut.


