<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Serveurs et bases de données - Matteo Cisilino</title>
	<atom:link href="https://www.cisilino.com/fr/category/serveurs-bases-de-donnees/feed/" rel="self" type="application/rss+xml" />
	<link>https://www.cisilino.com/fr/</link>
	<description>Guide su WordPress, server e sicurezza</description>
	<lastBuildDate>Sat, 31 Oct 2026 10:03:59 +0000</lastBuildDate>
	<language>fr-FR</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.1.4</generator>
	<item>
		<title>Rotation des identifiants de base de données après une faille ISPConfig</title>
		<link>https://www.cisilino.com/fr/2026/10/31/rotation-identifiants-base-de-donnees-faille-ispconfig/</link>
					<comments>https://www.cisilino.com/fr/2026/10/31/rotation-identifiants-base-de-donnees-faille-ispconfig/#respond</comments>
		
		<dc:creator><![CDATA[matteocisilino]]></dc:creator>
		<pubDate>Sat, 31 Oct 2026 10:03:56 +0000</pubDate>
				<category><![CDATA[Serveurs et bases de données]]></category>
		<category><![CDATA[gestion de serveurs]]></category>
		<category><![CDATA[ISPConfig]]></category>
		<category><![CDATA[MySQL]]></category>
		<category><![CDATA[réponse aux incidents]]></category>
		<category><![CDATA[rotation des identifiants]]></category>
		<category><![CDATA[script bash]]></category>
		<guid isPermaLink="false">https://www.cisilino.com/?p=1416</guid>

					<description><![CDATA[<p>Après une faille ISPConfig, je lance la rotation des identifiants de base de données avec reset_DB_password.sh : détection du rôle, sauvegardes automatiques et dry-run obligatoire.</p>
<p>The post <a href="https://www.cisilino.com/fr/2026/10/31/rotation-identifiants-base-de-donnees-faille-ispconfig/">Rotation des identifiants de base de données après une faille ISPConfig</a> first appeared on <a href="https://www.cisilino.com/fr/">Matteo Cisilino</a>.</p>]]></description>
										<content:encoded><![CDATA[<p class="wp-block-paragraph">Je lance la rotation des identifiants dès que je suspecte une faille. Pas après l&rsquo;avoir confirmée. La rotation des identifiants de base de données sur un serveur ISPConfig a l&rsquo;air d&rsquo;une maintenance de routine. Elle cesse de l&rsquo;être le jour où vous trouvez un web shell sur un site hébergé. Ou un fichier de configuration que vous ne savez pas expliquer, posé dans une sauvegarde publique. À ce moment-là, chaque mot de passe lié à cette base de données doit être nouveau. Vérifié. Documenté. Pas seulement supposé correct.</p>



<figure class="wp-block-image size-large"><img fetchpriority="high" decoding="async" src="https://www.cisilino.com/wp-content/uploads/2026/10/database-credential-rotation-ispconfig.webp" alt="rotation des identifiants de base de données : cadenas et clé au-dessus d'ISPConfig" width="1200" height="630"/></figure>



<h2 class="wp-block-heading">Quand la rotation des identifiants cesse-t-elle d&rsquo;être optionnelle ?</h2>



<p class="wp-block-paragraph">Quelques scénarios reviennent assez souvent pour que je garde un plan de rotation prêt à l&rsquo;avance. Je préfère ne pas en improviser un sous pression.</p>



<p class="wp-block-paragraph">Un plugin ou un CMS sur un site hébergé se fait compromettre. J&rsquo;ai raconté un cas réel sur ce type de serveur dans <a href="https://www.cisilino.com/fr/2026/10/29/php2shell-cve-2026-109618-mitigation-serveurs/">PHP2Shell : comment j&rsquo;ai protégé mes serveurs en attendant le correctif</a>, une vulnérabilité PHP qui permettait à un attaquant de déposer un web shell avant qu&rsquo;un correctif n&rsquo;existe. Une fois qu&rsquo;un web shell tourne sous un utilisateur d&rsquo;hébergement, il peut souvent lire le config.inc.php propre à ce site. Sur un serveur ISPConfig partagé, ce fichier se trouve juste à côté des identifiants de la base de données du panneau de contrôle lui-même. Pas seulement celui que l&rsquo;attaquant visait au départ.</p>



<p class="wp-block-paragraph">Un fichier de configuration se retrouve là où il ne devrait pas être. Une archive de sauvegarde oubliée dans un dossier public. Un dépôt poussé avec un fichier égaré dedans. Un journal plus lisible que prévu. config.inc.php et mysql_clientdb.conf n&rsquo;ont rien à faire dans aucun de ces endroits. Les erreurs arrivent malgré tout, y compris les miennes.</p>



<p class="wp-block-paragraph">Un administrateur système quitte l&rsquo;équipe. Quel que soit le ton du départ, les identifiants de base de données partagés qu&rsquo;il détenait ne m&rsquo;inspirent plus une confiance totale. La confiance ne se transmet pas automatiquement quand quelqu&rsquo;un passe la porte.</p>



<p class="wp-block-paragraph">Une intrusion suspectée, même non confirmée. C&rsquo;est le cas le plus difficile à expliquer à qui n&rsquo;a jamais géré un serveur sous suspicion d&rsquo;attaque. On n&rsquo;attend pas la preuve avant d&rsquo;agir. On présume la compromission et on lance la rotation. Une rotation inutile coûte un après-midi. Une rotation sautée, après une faille réelle, peut coûter tous les sites hébergés par le serveur.</p>



<h2 class="wp-block-heading">Pourquoi la rotation manuelle des identifiants est si sujette aux erreurs</h2>



<p class="wp-block-paragraph">La première fois que j&rsquo;ai fait la rotation des identifiants d&rsquo;ISPConfig à la main, j&rsquo;ai sous-estimé le nombre de fichiers concernés. Ils doivent tous changer ensemble, sinon quelque chose casse discrètement quelques heures plus tard.</p>



<p class="wp-block-paragraph">Il y a config.inc.php côté serveur, et une seconde copie côté interface. Les deux doivent recevoir les mêmes nouvelles valeurs. Il y a mysql_clientdb.conf, que ISPConfig lit pour gérer la base de données de chaque site hébergé, pas seulement son propre ménage interne. Dovecot, Pure-FTPd et Postfix conservent chacun leur propre copie de certains de ces identifiants. Le mail et le FTP peuvent cesser de s&rsquo;authentifier silencieusement si vous mettez à jour l&rsquo;utilisateur de la base de données et oubliez un seul de ces trois services.</p>



<p class="wp-block-paragraph">Il y a ensuite la topologie Master/Slave de certaines de mes installations. Un nœud Slave s&rsquo;authentifie auprès de la base de données de son Master via le réseau, avec un mot de passe distinct. Ce mot de passe doit être tourné sur le Master lui-même, pas seulement en local. Si l&rsquo;ordre est mauvais, un Slave peut se retrouver verrouillé hors du Master même qu&rsquo;il est censé répliquer.</p>



<p class="wp-block-paragraph">Rien de tout cela n&rsquo;est conceptuellement difficile. C&rsquo;est juste beaucoup de fichiers. Plusieurs services à redémarrer, dans un ordre sensé. Presque aucune tolérance pour une faute de frappe, surtout sous pression, juste après avoir trouvé quelque chose que vous ne vouliez pas trouver. Cette combinaison explique pourquoi la rotation des identifiants paraissait toujours plus risquée à la main que la tâche elle-même ne devrait l&rsquo;être. C&rsquo;est pour cette raison que j&rsquo;ai écrit reset_DB_password.sh, après avoir vécu ce problème moi-même plus d&rsquo;une fois.</p>



<h2 class="wp-block-heading">Ce que reset_DB_password.sh fait réellement</h2>



<p class="wp-block-paragraph">reset_DB_password.sh détecte si le nœud est Standalone, Master ou Slave. Il exécute ensuite uniquement les phases que ce rôle exige.</p>



<p class="wp-block-paragraph">La phase A tourne le mot de passe root local de MySQL. C&rsquo;est le mot de passe qu&rsquo;ISPConfig utilise lui-même pour gérer la base de données de chaque site hébergé. Le script met à jour mysql_clientdb.conf avec la nouvelle valeur.</p>



<p class="wp-block-paragraph">La phase B tourne le mot de passe de l&rsquo;utilisateur de contrôle ispconfig. Elle met à jour config.inc.php côté serveur et côté interface. Puis elle met à jour les identifiants correspondants dans Dovecot, Pure-FTPd et Postfix, pour que le mail et le FTP continuent de s&rsquo;authentifier sans passer manuellement par trois fichiers de configuration supplémentaires.</p>



<p class="wp-block-paragraph">La phase C ne tourne que sur un Slave. Elle se connecte à distance à la base de données du Master. Elle tourne là le mot de passe de l&rsquo;utilisateur Slave, puis met à jour dbmaster_password en local, pour que la réplication continue de s&rsquo;authentifier correctement avec la nouvelle valeur.</p>



<p class="wp-block-paragraph">Après la phase B, le script propose de redémarrer Dovecot, Pure-FTPd et Postfix pour vous. Je dis presque toujours oui. Une rotation d&rsquo;identifiants qui laisse un service mail tourner avec l&rsquo;ancien mot de passe n&rsquo;est pas terminée, elle est juste silencieusement cassée. Redémarrer dans la même exécution, juste après le changement des fichiers de configuration, garde tout cela dans une seule courte fenêtre de maintenance au lieu de trois fenêtres séparées découvertes plus tard.</p>



<h2 class="wp-block-heading">Des sauvegardes avant tout changement</h2>



<p class="wp-block-paragraph">Avant de toucher le moindre fichier, le script fait une copie rsync complète de /usr/local/ispconfig/ dans un dossier horodaté. Il tente aussi un mysqldump non bloquant des bases mysql et dbispconfig. Non bloquant volontairement : je préfère terminer une rotation d&rsquo;identifiants plutôt qu&rsquo;attendre un dump bloqué pour une raison qui n&rsquo;a rien à voir. Chaque fichier de configuration qu&rsquo;il modifie reçoit sa propre copie .bak avant qu&rsquo;une seule chaîne à l&rsquo;intérieur ne soit remplacée.</p>



<p class="wp-block-paragraph">Rien de tout cela ne remplace une stratégie de sauvegarde indépendante. Je prends toujours un snapshot séparé, ou une sauvegarde complète, avant de lancer ceci sur un serveur qui compte vraiment. Le propre avertissement du script dit la même chose, de façon plus directe que je ne viens de le faire.</p>



<h2 class="wp-block-heading">Toujours un dry run d&rsquo;abord</h2>



<p class="wp-block-paragraph">Chaque phase demande une confirmation avant de toucher à quoi que ce soit. &#8211;dry-run simule une exécution complète sans toucher un seul fichier. J&rsquo;utilise dry-run en premier sur chaque serveur, chaque fois. Même sur ceux dont j&rsquo;ai déjà tourné les identifiants, et que je crois bien connaître. &#8211;skip-local-root saute la phase A, pour quand seul le mot de passe de l&rsquo;utilisateur de contrôle doit changer. &#8211;role=master ou &#8211;role=slave remplace la détection automatique, pour le cas rare où elle ne lit pas correctement la topologie d&rsquo;un serveur.</p>



<p class="wp-block-paragraph">Les mots de passe qu&rsquo;il génère comptent 32 caractères, lettres et chiffres uniquement. Assez longs pour résister à une tentative de deviner. Assez simples pour que les parseurs de MySQL et de PHP ne trébuchent jamais sur un symbole égaré dans une chaîne de connexion. J&rsquo;ai testé le script sur ISPConfig 3.3.1p1. Je ne l&rsquo;ai pas lancé sur des branches plus anciennes ou plus récentes. Je ne présumerais pas un comportement identique là-bas, sans vérifier moi-même d&rsquo;abord.</p>



<p class="wp-block-paragraph">Le rôle d&rsquo;un nœud n&rsquo;est pas toujours évident au premier coup d&rsquo;œil, surtout sur un serveur dont vous avez hérité plutôt que celui que vous avez construit vous-même. C&rsquo;est pour cela que &#8211;role existe, et pourquoi la sortie du dry-run vaut la peine d&rsquo;être lue ligne par ligne la première fois, pas survolée. Je préfère passer deux minutes à confirmer qu&rsquo;un Slave est vraiment un Slave plutôt que de le découvrir à la dure en plein milieu de la rotation.</p>



<h2 class="wp-block-heading">Ce que je vous dirais avant de l&rsquo;exécuter</h2>



<p class="wp-block-paragraph"><strong>Avertissement</strong> : le script est fourni tel quel, à titre informatif. Son utilisation se fait entièrement à vos risques : je n&rsquo;assume aucune responsabilité pour une perte de données, une interruption de service, ou toute erreur causée par un usage incorrect ou par un contexte différent de celui dans lequel je l&rsquo;utilise moi-même. Gardez toujours une sauvegarde indépendante ou un snapshot de VM avant de l&rsquo;exécuter en production, quoi que la propre étape de sauvegarde du script capture déjà pour vous.</p>



<p class="wp-block-paragraph">C&rsquo;est un logiciel libre. Vous pouvez le redistribuer et le modifier, avec attribution à moi comme auteur original. Le code source de reset_DB_password.sh, et le reste de la collection ispconfig-fast-tools, est sur GitHub à l&rsquo;adresse <a href="https://github.com/matteocisilino/ispconfig-fast-tools" target="_blank" rel="noopener">github.com/matteocisilino/ispconfig-fast-tools</a>.</p>



<h2 class="wp-block-heading">La rotation des identifiants de base de données est une habitude, pas une tâche de nettoyage</h2>



<p class="wp-block-paragraph">La rotation des identifiants de base de données après une faille confirmée, c&rsquo;est simplement une réponse aux incidents compétente. La plupart des administrateurs le feraient sans qu&rsquo;on ait à le leur répéter. L&rsquo;habitude la plus difficile, c&rsquo;est de le faire après une faille simplement suspectée, avant d&rsquo;avoir la moindre preuve. C&rsquo;est la seule version du plan qui limite vraiment les dégâts, tant qu&rsquo;ils sont encore limitables. Un script qui exécute les bonnes phases selon le rôle d&rsquo;un serveur, prend ses propres sauvegardes d&rsquo;abord, et me laisse faire un dry-run complet avant de toucher à quoi que ce soit de réel. Voilà ce qui rend cette habitude praticable un mardi soir, plutôt que théorique dans un article de blog.</p>



<p class="wp-block-paragraph">ISPConfig, MySQL, PHP, Dovecot, Pure-FTPd, Postfix et les autres noms mentionnés sont des marques déposées de leurs propriétaires respectifs. Ils sont utilisés uniquement pour identifier les produits.</p><p>The post <a href="https://www.cisilino.com/fr/2026/10/31/rotation-identifiants-base-de-donnees-faille-ispconfig/">Rotation des identifiants de base de données après une faille ISPConfig</a> first appeared on <a href="https://www.cisilino.com/fr/">Matteo Cisilino</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://www.cisilino.com/fr/2026/10/31/rotation-identifiants-base-de-donnees-faille-ispconfig/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Vulnérabilité kernel Linux : un use-after-free dans le TCP IPv4</title>
		<link>https://www.cisilino.com/fr/2026/10/31/vulnerabilite-kernel-linux-tcp-cve-2026-98432/</link>
					<comments>https://www.cisilino.com/fr/2026/10/31/vulnerabilite-kernel-linux-tcp-cve-2026-98432/#respond</comments>
		
		<dc:creator><![CDATA[matteocisilino]]></dc:creator>
		<pubDate>Sat, 31 Oct 2026 09:19:23 +0000</pubDate>
				<category><![CDATA[Serveurs et bases de données]]></category>
		<category><![CDATA[CVE]]></category>
		<category><![CDATA[Debian]]></category>
		<category><![CDATA[kernel Linux]]></category>
		<category><![CDATA[sécurité des serveurs]]></category>
		<guid isPermaLink="false">https://www.cisilino.com/?p=1407</guid>

					<description><![CDATA[<p>Vulnérabilité kernel Linux dans le TCP IPv4 (CVE-2026-98432, CVSS 9,8) : Debian est déjà corrigé, Ubuntu et RHEL attendent encore un correctif.</p>
<p>The post <a href="https://www.cisilino.com/fr/2026/10/31/vulnerabilite-kernel-linux-tcp-cve-2026-98432/">Vulnérabilité kernel Linux : un use-after-free dans le TCP IPv4</a> first appeared on <a href="https://www.cisilino.com/fr/">Matteo Cisilino</a>.</p>]]></description>
										<content:encoded><![CDATA[<p class="wp-block-paragraph">Je g&egrave;re des serveurs Debian en production depuis des ann&eacute;es. Chaque fois qu&#8217;un CVE sur le kernel sort, je m&#8217;arr&ecirc;te pour en lire les d&eacute;tails. Puis je d&eacute;cide s&#8217;il faut mettre &agrave; jour tout de suite ou attendre. Cette fois, l&#8217;histoire est plus compliqu&eacute;e que d&#8217;habitude&nbsp;: il y a une <strong>vuln&eacute;rabilit&eacute; kernel Linux</strong> dans le TCP IPv4, un score CVSS contest&eacute; entre fournisseurs, et m&ecirc;me un exploit root pr&eacute;sum&eacute; construit par une intelligence artificielle. Commen&ccedil;ons par une question qui semble secondaire, mais qui ne l&#8217;est pas&nbsp;: qu&#8217;est-ce que MPTCP, et pourquoi &ccedil;a compte m&ecirc;me si vous ne l&#8217;utilisez pas.</p>



<figure class="wp-block-image size-large"><img decoding="async" src="https://www.cisilino.com/wp-content/uploads/2026/10/vulnerabilita-kernel-linux-tcp-cve-2026-98432.webp" alt="vuln&eacute;rabilit&eacute; kernel Linux dans le TCP IPv4, pingouin stylis&eacute; avec un bouclier Debian corrig&eacute;" width="1200" height="630"/></figure>



<h2 class="wp-block-heading">Qu&#8217;est-ce que MPTCP et pourquoi &ccedil;a concerne aussi ceux qui ne l&#8217;utilisent pas</h2>



<p class="wp-block-paragraph">Multipath TCP (MPTCP) est le standard RFC 8684. Il &eacute;tend le TCP classique pour faire circuler une seule connexion sur plusieurs chemins r&eacute;seau &agrave; la fois. Un t&eacute;l&eacute;phone peut utiliser le WiFi et le r&eacute;seau cellulaire au m&ecirc;me moment, pour la m&ecirc;me connexion. Un serveur avec plusieurs interfaces peut faire pareil, pour la redondance ou pour additionner la bande passante. Apple l&#8217;utilise en production depuis iOS 7, en 2013, pour Siri&nbsp;: c&#8217;est le cas d&#8217;usage r&eacute;el le plus connu. Certains op&eacute;rateurs mobiles, par exemple en Cor&eacute;e du Sud, l&#8217;exploitent pour le bonding WiFi/LTE. Plusieurs data centers l&#8217;adoptent pour le multi-homing entre diff&eacute;rents uplinks. Dans le kernel Linux mainline, il est pr&eacute;sent depuis la 5.6.</p>



<p class="wp-block-paragraph">Le point qui m&#8217;a fait lever un sourcil est ailleurs. Depuis la version 1.24, les programmes &eacute;crits en Go exigent MPTCP par d&eacute;faut pour les sockets en &eacute;coute. Le Go Security Team l&#8217;a confirm&eacute; le 29 octobre. Et il a donn&eacute; un avertissement g&ecirc;nant&nbsp;: le bug dont on parle aujourd&#8217;hui n&#8217;est pas sp&eacute;cifique &agrave; MPTCP. Un syst&egrave;me sans listener MPTCP actif peut rester quand m&ecirc;me expos&eacute; &agrave; des attaques similaires. Le code vuln&eacute;rable se trouve dans le chemin TCP IPv4 g&eacute;n&eacute;rique, pas dans un module optionnel. On peut d&eacute;sactiver MPTCP programme par programme, avec la variable <code>GODEBUG=multipathtcp=0</code> &agrave; l&#8217;ex&eacute;cution. Mais le Go Security Team qualifie cette mitigation de &laquo;&nbsp;fragile&nbsp;&raquo;&nbsp;: il suffit d&#8217;un seul listener MPTCP rest&eacute; expos&eacute; pour l&#8217;annuler. Personne n&#8217;a d&eacute;montr&eacute; que chaque exploit fonctionnel d&eacute;pend vraiment de MPTCP. Voil&agrave; pourquoi le code MPTCP reste une surface d&#8217;attaque, m&ecirc;me si vous ne l&#8217;activez jamais consciemment dans votre stack&nbsp;: il suffit qu&#8217;il soit compil&eacute; dans le kernel et atteignable.</p>



<p class="wp-block-paragraph">Le projet MPTCP lui-m&ecirc;me d&eacute;clare, sur sa propre page, que les principales distributions Linux actuelles livrent le kernel avec le support MPTCP compil&eacute; par d&eacute;faut (<code>CONFIG_MPTCP=y</code>). Il cite explicitement Debian, Ubuntu, RHEL et Fedora. C&#8217;est une d&eacute;claration du projet lui-m&ecirc;me, pas une v&eacute;rification ind&eacute;pendante distribution par distribution, mais le concept reste valable&nbsp;: le support est activ&eacute; au niveau du kernel, pas &laquo;&nbsp;opt-in&nbsp;&raquo; au niveau du syst&egrave;me d&#8217;exploitation. Seule la n&eacute;gociation par connexion individuelle reste &agrave; la charge de l&#8217;application.</p>



<h2 class="wp-block-heading">La vuln&eacute;rabilit&eacute; kernel Linux CVE-2026-98432 en bref</h2>



<p class="wp-block-paragraph">Le CVE team du kernel Linux a publi&eacute; la CVE-2026-98432 le 15 octobre 2026. Le correctif upstream avait d&eacute;j&agrave; &eacute;t&eacute; appliqu&eacute; dix jours plus t&ocirc;t, le 5 octobre. Il arrive souvent que la publication suive le commit avec un certain retard. Le score CVSS de 9,8 (critique) n&#8217;est arriv&eacute; que le 24 octobre, neuf jours apr&egrave;s la publication de la CVE.</p>



<p class="wp-block-paragraph">Le probl&egrave;me se situe dans la gestion du handshake TCP pour IPv4, en particulier dans la fonction <code>tcp_v4_syn_recv_sock()</code>. Quand une connexion est cr&eacute;&eacute;e, les options IP du paquet SYN sont transmises du request socket en attente vers le nouveau socket. Par exemple Record Route ou Timestamp, si le client les inclut dans un SYN forg&eacute;. Le probl&egrave;me est qu&#8217;elles sont transmises par r&eacute;f&eacute;rence, pas par copie, et sans le lock du listener. Selon l&#8217;analyse technique du patch, les structures concern&eacute;es s&#8217;appellent <code>inet_opt</code> et <code>ireq_opt</code>. Un deuxi&egrave;me CPU peut lib&eacute;rer ce buffer avec <code>kfree()</code>, appel&eacute; depuis <code>inet_sock_destruct()</code> ou depuis <code>tcp_v4_reqsk_destructor()</code>. Pendant ce temps, un premier CPU est encore en train de le lire dans <code>ip_options_build()</code>, pour construire le SYNACK. C&#8217;est un classique use-after-free caus&eacute; par une race condition. Le chemin IPv6 &eacute;quivalent, <code>tcp_v6_syn_recv_sock()</code>, copie les options au lieu de les transmettre par r&eacute;f&eacute;rence&nbsp;: il n&#8217;est pas vuln&eacute;rable &agrave; la race principale. Il existe aussi une seconde voie d&#8217;attaque, via la migration des request-sockets&nbsp;: <code>net.ipv4.tcp_migrate_req=1</code> ou bien un programme eBPF <code>BPF_SK_REUSEPORT_SELECT_OR_MIGRATE</code>. Les deux sont d&eacute;sactiv&eacute;es par d&eacute;faut.</p>



<p class="wp-block-paragraph">Le bug est distant et ne n&eacute;cessite ni authentification ni interaction de l&#8217;utilisateur, selon l&#8217;&eacute;valuation du kernel team. Il est pr&eacute;sent depuis la version 4.4 du kernel. Le correctif est signé par Eric Dumazet, signal&eacute; par Xinyang Ge d&#8217;Anthropic. Il d&eacute;place la lib&eacute;ration de la m&eacute;moire apr&egrave;s une grace period RCU, avec <code>kfree_rcu()</code>&nbsp;: ainsi aucun CPU ne peut plus lire un buffer d&eacute;j&agrave; lib&eacute;r&eacute;. Les versions stables corrig&eacute;es sont 5.10.272, 5.15.223, 6.1.190, 6.6.159, 6.12.113, 6.18.56, 7.2.10 et la mainline depuis la 7.3-rc7.</p>



<h2 class="wp-block-heading">Scores CVSS discordants&nbsp;: les fournisseurs ne sont pas tous d&#8217;accord</h2>



<p class="wp-block-paragraph">C&#8217;est ici que l&#8217;histoire se complique. Le kernel CVE team et Ubuntu &eacute;valuent la complexit&eacute; d&#8217;attaque comme basse, et donnent 9,8 (critique). Oracle et SUSE donnent au contraire 8,1 (important)&nbsp;: m&ecirc;me vecteur CVSS, mais complexit&eacute; d&#8217;attaque haute. Red Hat, curieusement, a deux &eacute;valuations qui ne co&iuml;ncident pas entre elles. La page CVE publique affiche &laquo;&nbsp;Moderate, 5.9&nbsp;&raquo;. Le fichier d&#8217;advisory lisible par machine, mis &agrave; jour le 29 octobre, affiche au contraire &laquo;&nbsp;Important, 8.1&nbsp;&raquo;, avec un impact &eacute;lev&eacute; sur la confidentialit&eacute; et l&#8217;int&eacute;grit&eacute;. Le m&ecirc;me jour, le bug tracker interne de Red Hat a relev&eacute; la s&eacute;v&eacute;rit&eacute; de medium &agrave; high. Amazon, de son c&ocirc;t&eacute;, attribue 7,0. Au moment o&ugrave; j&#8217;&eacute;cris, le NVD n&#8217;a pas encore publi&eacute; son propre score.</p>



<p class="wp-block-paragraph">Il faut le dire honn&ecirc;tement. Cette incoh&eacute;rence interne &agrave; Red Hat n&#8217;est pas un d&eacute;tail que j&#8217;ai d&eacute;duit moi-m&ecirc;me&nbsp;: c&#8217;est ainsi que la rapporte l&#8217;article original de The Hacker News, ma source principale pour ces donn&eacute;es. Ce n&#8217;est pas la premi&egrave;re fois qu&#8217;un fournisseur publie deux chiffres diff&eacute;rents pour le m&ecirc;me bug, sur des canaux diff&eacute;rents. Mais ici, le saut de &laquo;&nbsp;mod&eacute;r&eacute;&nbsp;&raquo; &agrave; &laquo;&nbsp;important&nbsp;&raquo; le m&ecirc;me jour en dit long. M&ecirc;me pour ceux qui &eacute;crivent le code, estimer avec certitude l&#8217;exploitabilit&eacute; r&eacute;elle d&#8217;une race condition dans le kernel est difficile.</p>



<h2 class="wp-block-heading">&Eacute;tat des correctifs&nbsp;: Debian d&eacute;j&agrave; corrig&eacute;, d&#8217;autres pas encore</h2>



<p class="wp-block-paragraph">C&#8217;est le point qui me concerne le plus directement&nbsp;: les h&eacute;bergements que je g&egrave;re tournent sur Debian stable. Et ici les faits parlent clairement, sans besoin d&#8217;ajouter d&#8217;emphase&nbsp;: <strong>Debian a d&eacute;j&agrave; corrig&eacute; cette vuln&eacute;rabilit&eacute; kernel Linux</strong>, aussi bien sur trixie que sur bookworm. Debian trixie (stable) a &eacute;t&eacute; corrig&eacute;e le 16 octobre 2026, avec le paquet <code>linux 6.12.113-1</code>, avis DSA-6568-1. Debian 12 (bookworm) a &eacute;t&eacute; corrig&eacute;e en deux &eacute;tapes&nbsp;: le 17 et le 22 octobre 2026, avec <code>linux 6.1.190-1</code> (avis DLA-4841-1) et <code>linux-6.12 6.12.113-1~deb12u1</code> (avis DLA-4850-1). Dans son propre tableau de suivi, Debian ne liste pas de score CVSS s&eacute;par&eacute; pour cette CVE. Elle se limite &agrave; indiquer l&#8217;&eacute;tat du correctif pour chaque release&nbsp;: c&#8217;est l&#8217;information qui compte vraiment, pour qui doit d&eacute;cider de red&eacute;marrer un serveur.</p>



<p class="wp-block-paragraph">Comparer cela &agrave; l&#8217;&eacute;tat d&#8217;autres distributions tr&egrave;s utilis&eacute;es aide &agrave; donner du poids &agrave; la chose. Au 30 octobre, Red Hat a mis &agrave; jour l&#8217;&eacute;tat de RHEL 8, 9, 10 et OpenShift 4. Avant, c&#8217;&eacute;tait &laquo;&nbsp;fix deferred&nbsp;&raquo; (not&eacute; le 29 octobre), maintenant c&#8217;est &laquo;&nbsp;affected&nbsp;&raquo;. Aucune date de sortie n&#8217;a &eacute;t&eacute; communiqu&eacute;e. Ubuntu, sur toutes les versions support&eacute;es (22.04, 24.04, 26.04, 26.10), reste &laquo;&nbsp;en cours de traitement&nbsp;&raquo;, sans date. Ubuntu 16.04, 18.04 et 20.04 sont vuln&eacute;rables, sans m&ecirc;me un correctif list&eacute;. SUSE Linux Enterprise Server, de la SP4 &agrave; la SP7 plus la 16.0, est dans la m&ecirc;me situation. Oracle Linux (UEK), openSUSE Leap 16.0, SUSE Linux Micro 6.2 et Amazon Linux 2023 sont au contraire d&eacute;j&agrave; corrig&eacute;s. RHEL 7 n&#8217;est pas affect&eacute;&nbsp;: le chemin de code vuln&eacute;rable n&#8217;a jamais &eacute;t&eacute; distribu&eacute; sur cette release.</p>



<p class="wp-block-paragraph">Je ne dis pas cela pour faire concurrence &agrave; Ubuntu ou RHEL. Ce sont des distributions s&eacute;rieuses, avec des processus de s&eacute;curit&eacute; robustes. Je le dis parce que sur ce bug pr&eacute;cis, dans cette fen&ecirc;tre temporelle, les faits sont les suivants&nbsp;: ceux qui s&#8217;appuient sur des h&eacute;bergements bas&eacute;s sur Debian stable ont d&eacute;j&agrave; le correctif install&eacute;. Sur d&#8217;autres distributions tr&egrave;s utilis&eacute;es, en revanche, le serveur reste expos&eacute; pendant une dur&eacute;e ind&eacute;termin&eacute;e. C&#8217;est un avantage concret, v&eacute;rifiable en regardant les dates. Ce n&#8217;est pas une opinion sur le projet Debian en g&eacute;n&eacute;ral.</p>



<h2 class="wp-block-heading">L&#8217;exploit (rapport&eacute;, non v&eacute;rifi&eacute; de fa&ccedil;on ind&eacute;pendante) d&#8217;Anthropic</h2>



<p class="wp-block-paragraph">La partie la plus inhabituelle de cette histoire concerne Anthropic, l&#8217;entreprise qui a signal&eacute; le bug en premier via Xinyang Ge. Selon les rapports du secteur, un de ses mod&egrave;les d&#8217;IA internes, non publi&eacute; publiquement, a construit seul un exploit root fonctionnel. En partant uniquement du patch et du rapport comme prompt. L&#8217;attaque en deux phases fonctionnerait ainsi. Dans la premi&egrave;re phase, la race fait envoyer &agrave; l&#8217;attaquant des octets suppl&eacute;mentaires de la r&eacute;ponse du kernel. De l&agrave;, il r&eacute;cup&egrave;re une adresse kernel, pour d&eacute;samorcer KASLR. Dans la seconde phase, le m&ecirc;me bug provoquerait un d&eacute;passement de buffer. Il &eacute;crase un pointeur de callback avec l&#8217;adresse d&#8217;une charge utile d&eacute;j&agrave; plac&eacute;e en m&eacute;moire kernel par l&#8217;attaquant. Quand le kernel appelle ce pointeur, la charge utile ex&eacute;cute une commande shell au choix en tant que root. Une race perdue ferait &eacute;chouer uniquement cette tentative de connexion, sans faire planter le syst&egrave;me&nbsp;: des tentatives r&eacute;p&eacute;t&eacute;es seraient donc possibles. Dans le test d&eacute;crit par Anthropic&nbsp;: 48 minutes pour voler l&#8217;adresse kernel, 5 minutes suppl&eacute;mentaires pour arriver &agrave; l&#8217;ex&eacute;cution de commandes.</p>



<p class="wp-block-paragraph">Red Hat affirme qu&#8217;un attaquant distant qui atteint un service TCP expos&eacute; peut en g&eacute;n&eacute;ral d&eacute;clencher la race. L&#8217;exploit root d&eacute;crit par Anthropic, cependant, aurait vis&eacute; sp&eacute;cifiquement des serveurs avec un listener multipath TCP actif. Le billet d&#8217;Anthropic ne pr&eacute;cise pas si un serveur sans MPTCP est exploitable de la m&ecirc;me mani&egrave;re. Ni le record CVE ni le correctif upstream ne mentionnent MPTCP &agrave; aucun moment. C&#8217;est la presse sp&eacute;cialis&eacute;e, et non une source primaire, qui rapporte ce lien&nbsp;: je l&#8217;&eacute;cris donc avec le scepticisme qu&#8217;il m&eacute;rite, pas comme un fait &eacute;tabli &agrave; cent pour cent. Anthropic n&#8217;a publi&eacute; ni le code de l&#8217;exploit, ni la version exacte du kernel utilis&eacute;e, ni la configuration du serveur cible. Une autre soci&eacute;t&eacute;, Innora, a publi&eacute; une analyse de la cause racine, avec un environnement de reproduction QEMU. Elle a d&eacute;clar&eacute; qu&#8217;elle ne publierait pas d&#8217;exploit arm&eacute; &agrave; court terme. Pour l&#8217;instant, il n&#8217;y a aucune preuve d&#8217;exploitation r&eacute;elle par des fournisseurs ou des CERT. Le bug ne figure pas dans le catalogue CISA KEV, selon la copie mise &agrave; jour par le CIRCL du Luxembourg au 28 octobre.</p>



<h2 class="wp-block-heading">Que faire maintenant sur vos serveurs</h2>



<p class="wp-block-paragraph">Aucun fournisseur n&#8217;a publi&eacute; de contournement officiellement sanctionn&eacute; pour cette <strong>vuln&eacute;rabilit&eacute; kernel Linux</strong>. Red Hat dit explicitement qu&#8217;aucune des mesures qui circulent jusqu&#8217;&agrave; pr&eacute;sent n&#8217;atteint son standard de facilit&eacute; de d&eacute;ploiement et de stabilit&eacute;. La mitigation principale reste d&#8217;appliquer le kernel corrig&eacute; et de red&eacute;marrer, quand le correctif est disponible pour votre distribution. Sur Debian, c&#8217;est d&eacute;j&agrave; le cas. Sur RHEL et Ubuntu, pas encore, au moment o&ugrave; j&#8217;&eacute;cris.</p>



<p class="wp-block-paragraph">Comme d&eacute;fense en profondeur, pas comme solution d&eacute;finitive, certaines choses ont du sens. V&eacute;rifier que <code>net.ipv4.tcp_migrate_req</code> est bien &agrave; 0. Contr&ocirc;ler qu&#8217;aucun programme eBPF <code>BPF_SK_REUSEPORT_SELECT_OR_MIGRATE</code> n&#8217;est charg&eacute;. &Eacute;valuer un filtrage des paquets IPv4 entrants avec des options Record Route ou Timestamp. Surveiller les &eacute;ventuels crashs qui citent <code>ip_options_build</code>, <code>ip_build_and_send_pkt</code> ou <code>skb_under_panic</code>. Faire l&#8217;inventaire des &eacute;ventuels listeners MPTCP expos&eacute;s, en &eacute;vitant de les laisser atteignables sans raison. Si vous g&eacute;rez aussi des services contre les attaques applicatives plus courantes, j&#8217;ai d&eacute;crit la m&ecirc;me approche dans <a href="https://www.cisilino.com/fr/2026/10/29/php2shell-cve-2026-109618-mitigation-serveurs/">comment j&#8217;ai prot&eacute;g&eacute; mes serveurs en attendant le correctif pour PHP2Shell</a>. Att&eacute;nuer ce qui peut l&#8217;&ecirc;tre, en attendant le correctif d&eacute;finitif, sans promettre des protections qui n&#8217;existent pas.</p>



<p class="wp-block-paragraph">Pour ceux qui me demandent s&#8217;il faut mettre &agrave; jour tout de suite&nbsp;: si vous utilisez Debian stable, le kernel corrig&eacute; est d&eacute;j&agrave; dans les d&eacute;p&ocirc;ts de s&eacute;curit&eacute;. La mise &agrave; jour est simple, et la fen&ecirc;tre d&#8217;exposition est d&eacute;j&agrave; ferm&eacute;e. Si vous utilisez d&#8217;autres distributions, il n&#8217;y a pas encore de correctif &agrave; installer aujourd&#8217;hui. Cela vaut la peine de suivre de pr&egrave;s les avis de votre fournisseur dans les prochains jours&nbsp;: la situation &eacute;volue rapidement.</p>



<p class="wp-block-paragraph">Sources&nbsp;: <a href="https://thehackernews.com/2026/10/linux-kernel-tcp-flaw-with-98-cvss.html" target="_blank" rel="nofollow noopener">The Hacker News</a>, <a href="https://security-tracker.debian.org/tracker/CVE-2026-98432" target="_blank" rel="nofollow noopener">Debian Security Tracker</a> et le <a href="https://github.com/torvalds/linux/commit/e3f33b0a1d89d738e2b913dab519226eaf76e15b" target="_blank" rel="noopener">commit du correctif</a>. Debian, Red Hat, Ubuntu et les autres noms cit&eacute;s sont des marques de leurs propri&eacute;taires respectifs.</p><p>The post <a href="https://www.cisilino.com/fr/2026/10/31/vulnerabilite-kernel-linux-tcp-cve-2026-98432/">Vulnérabilité kernel Linux : un use-after-free dans le TCP IPv4</a> first appeared on <a href="https://www.cisilino.com/fr/">Matteo Cisilino</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://www.cisilino.com/fr/2026/10/31/vulnerabilite-kernel-linux-tcp-cve-2026-98432/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Journal email WordPress : pourquoi je le contrôle</title>
		<link>https://www.cisilino.com/fr/2026/10/31/journal-email-wordpress-pourquoi-je-controle/</link>
					<comments>https://www.cisilino.com/fr/2026/10/31/journal-email-wordpress-pourquoi-je-controle/#respond</comments>
		
		<dc:creator><![CDATA[matteocisilino]]></dc:creator>
		<pubDate>Sat, 31 Oct 2026 08:00:00 +0000</pubDate>
				<category><![CDATA[Serveurs et bases de données]]></category>
		<category><![CDATA[email]]></category>
		<category><![CDATA[FluentSMTP]]></category>
		<category><![CDATA[plugin WordPress]]></category>
		<category><![CDATA[SMTP]]></category>
		<guid isPermaLink="false">https://www.cisilino.com/?p=1383</guid>

					<description><![CDATA[<p>Je gère plusieurs sites WordPress : le journal email WordPress de FluentSMTP me dit si un envoi a vraiment réussi, sans fouiller les logs du serveur.</p>
<p>The post <a href="https://www.cisilino.com/fr/2026/10/31/journal-email-wordpress-pourquoi-je-controle/">Journal email WordPress : pourquoi je le contrôle</a> first appeared on <a href="https://www.cisilino.com/fr/">Matteo Cisilino</a>.</p>]]></description>
										<content:encoded><![CDATA[<p class="wp-block-paragraph">Je gère plusieurs sites WordPress. Sur chacun partent des emails en nombre que personne n&rsquo;imagine&nbsp;: notifications de contact, réinitialisations de mot de passe, confirmations de commande, alertes aux gestionnaires. Le journal email WordPress de FluentSMTP est l&rsquo;outil que je consulte dès qu&rsquo;un client m&rsquo;écrit «&nbsp;je n&rsquo;ai rien reçu&nbsp;». Avant, je regardais les logs du serveur SMTP en ligne de commande. Je devais faire correspondre à la main des grep et des horodatages. Maintenant j&rsquo;ouvre une page dans le tableau de bord de WordPress et j&rsquo;ai tout sous les yeux.</p>



<figure class="wp-block-image size-large"><img decoding="async" src="https://www.cisilino.com/wp-content/uploads/2026/10/fluent-smtp-log-invii-email-wordpress.webp" alt="journal email WordPress vu dans le panneau de FluentSMTP" width="1200" height="630"/></figure>



<h2 class="wp-block-heading">Pourquoi je consulte le journal email WordPress avant les logs du serveur</h2>



<p class="wp-block-paragraph">Le cas typique est toujours le même. Un formulaire de contact qui «&nbsp;n&rsquo;arrive pas&nbsp;». Un client qui ne reçoit pas l&#8217;email de réinitialisation de mot de passe. Une commande qui reste sans confirmation. Sans un journal à portée de main, la première hypothèse est le dossier spam. Puis les logs du serveur SMTP. Puis peut-être le pare-feu. Avec FluentSMTP, je saute ces étapes. Je vois tout de suite si WordPress a vraiment tenté d&rsquo;envoyer cet email. À qui, quand, avec quelle réponse du serveur d&rsquo;envoi.</p>



<p class="wp-block-paragraph">FluentSMTP enregistre chaque appel à <code>wp_mail()</code>. Pas seulement les emails des plugins tiers, aussi ceux générés par le cœur de WordPress&nbsp;: notifications de nouvel utilisateur, rappels de mise à jour, alertes d&rsquo;erreur. Pour chaque envoi, le journal affiche l&rsquo;expéditeur, le destinataire, l&rsquo;objet, les en-têtes complets, le corps du message. Et la réponse du fournisseur SMTP ou de l&rsquo;API utilisée. Si un email a échoué, cette réponse du serveur est là. Je n&rsquo;ai pas à la deviner.</p>



<h2 class="wp-block-heading">Ce que je trouve vraiment dans le registre des envois</h2>



<p class="wp-block-paragraph">Le plugin prend en charge des dizaines de connecteurs&nbsp;: Amazon SES, SendGrid, Mailgun, Brevo, Postmark, SparkPost. Aussi Google Workspace et Microsoft 365 via OAuth, en plus du SMTP générique classique. Sur les sites que je gère, j&rsquo;utilise presque toujours du SMTP authentifié avec le domaine du client. Parfois une API dédiée, quand le volume d&#8217;emails augmente. Dans les deux cas, le journal email WordPress ne change pas. Chaque ligne a un statut, envoyé ou échoué. Il y a un bouton pour revoir le contenu ou renvoyer le message, sans que l&rsquo;utilisateur doive refaire l&rsquo;action qui l&rsquo;a généré.</p>



<p class="wp-block-paragraph">Le renvoi manuel est la fonction que j&rsquo;utilise le plus souvent. Un formulaire rempli deux fois parce qu&rsquo;il «&nbsp;n&rsquo;arrivait pas&nbsp;» devient un clic sur le journal. Pas un nouveau signalement au client. Pour les échecs répétés, j&rsquo;ai activé les notifications sur Telegram. Dès qu&rsquo;une connexion arrête de fonctionner, je le sais avant qu&rsquo;on me le signale. Plutôt que de le découvrir par un ticket de support.</p>



<p class="wp-block-paragraph">Le journal ne concerne pas seulement les notifications adressées à moi ou au client qui administre le site. Y finit aussi chaque email sortant généré par le site vers des tiers&nbsp;: la confirmation de commande qu&rsquo;une boutique en ligne envoie au client final, le reçu d&rsquo;un paiement, la notification d&rsquo;expédition. Si cette confirmation n&rsquo;arrive pas, je la vois dans le journal email WordPress exactement comme une notification interne, avec le même bouton pour la renvoyer au bon destinataire sans lui faire répéter l&rsquo;achat.</p>



<h2 class="wp-block-heading">Comment je configure FluentSMTP sur les sites que je gère</h2>



<p class="wp-block-paragraph">Après avoir connecté le bon connecteur, je change la conservation du journal. Par défaut elle reste active longtemps. Moi, je la porte à 30-60 jours, selon le trafic du site. Ça sert à retrouver un problème signalé en retard. Sans remplir la base de données de lignes que je ne consulterai jamais. J&rsquo;active ensuite au moins un canal de notification pour les échecs, Telegram ou Slack selon le client. Le journal seul n&rsquo;avertit personne&nbsp;: il faut l&rsquo;ouvrir pour être lu.</p>



<p class="wp-block-paragraph">Sur les sites avec formulaire de contact, j&rsquo;utilise aussi <a href="https://www.cisilino.com/fr/2026/10/29/plugin-securite-wordpress-cisilino/">Cisilino Site Defense Network</a> pour bloquer les bots. Moins de spam entrant signifie moins d&#8217;emails générés par les formulaires. Le journal reste plus lisible, avec moins de bruit à filtrer quand je cherche le bon message.</p>



<h2 class="wp-block-heading">Les identifiants SMTP ne restent pas en clair dans la base de données</h2>



<p class="wp-block-paragraph">Un détail que je vérifie toujours avant d&rsquo;enregistrer une connexion&nbsp;: où finissent les identifiants. FluentSMTP les chiffre par défaut avec les clés SALT de WordPress, celles déjà présentes dans <code>wp-config.php</code> pour la sécurité des sessions. On peut désactiver le chiffrement, mais le plugin lui-même signale que c&rsquo;est déconseillé. Sur les sites que je gère, je préfère une autre option&nbsp;: enregistrer le mot de passe API ou SMTP directement dans <code>wp-config.php</code>, hors de la base de données. Je définis les constantes avant que le plugin ne les lise. Ainsi, même un dump de la base de données ou un accès non autorisé au tableau de bord n&rsquo;expose pas la clé du fournisseur d&rsquo;envoi. C&rsquo;est le même principe que j&rsquo;applique ailleurs&nbsp;: moins de secrets dans la base de données, mieux c&rsquo;est.</p>



<h2 class="wp-block-heading">Plusieurs connexions en même temps, pas une seule à la fois</h2>



<p class="wp-block-paragraph">FluentSMTP permet de configurer plusieurs connexions en même temps, pas une seule à la fois. Sur un site, je règle celle par défaut et j&rsquo;en ajoute une seconde comme réserve&nbsp;: si le premier fournisseur ne répond pas, l&rsquo;envoi passe automatiquement à la connexion de réserve, sans que j&rsquo;aie besoin d&rsquo;intervenir. Le choix de la connexion dépend de l&rsquo;adresse de l&rsquo;expéditeur&nbsp;: chaque connexion a son «&nbsp;From&nbsp;», le reste passe par celle par défaut. La réserve n&rsquo;est prête que si la connexion par défaut échoue vraiment (une erreur du fournisseur, pas un email tombé en spam). C&rsquo;est une des raisons pour lesquelles je le préfère à un simple SMTP sans alternative. Si SendGrid ou Amazon SES ont un problème pendant quelques heures, le site continue quand même à envoyer, et je le vois tout de suite dans le journal email WordPress. Tout est inclus dans la version gratuite, sans version pro payante&nbsp;: on paie seulement le fournisseur d&rsquo;envoi choisi, pas le plugin.</p>



<h2 class="wp-block-heading">Un contrôle de santé chaque jour, pas seulement quand quelque chose casse</h2>



<p class="wp-block-paragraph">Au-delà du journal ligne par ligne, FluentSMTP fait un contrôle de santé de la connexion chaque jour, automatiquement. Si le fournisseur arrête de répondre, l&rsquo;alerte arrive avant qu&rsquo;un client ne m&rsquo;écrive «&nbsp;le site n&rsquo;envoie plus d&#8217;emails&nbsp;». Le même tableau de bord affiche un graphique avec les totaux journaliers d&rsquo;envois réussis et échoués. Utile pour comprendre en un coup d&rsquo;œil si une baisse est normale ou le symptôme de quelque chose qui se détériore, sans avoir à ouvrir le journal email WordPress ligne par ligne chaque fois. Il y a aussi un résumé périodique par email, avec les totaux de la semaine&nbsp;: ce n&rsquo;est pas une alerte d&rsquo;erreur, c&rsquo;est un bulletin que je lis en passant pour savoir si le volume d&rsquo;envois est dans la norme.</p>



<p class="wp-block-paragraph">Pour qui, comme moi, gère plusieurs sites en ligne de commande, FluentSMTP expose aussi des commandes WP-CLI&nbsp;: un test d&rsquo;envoi, un contrôle de santé de la connexion, les statistiques et le nettoyage des anciens journaux. Je les utilise dans les mêmes scripts qui tournent déjà sur les serveurs pour d&rsquo;autres tâches de maintenance, sans avoir à ouvrir le tableau de bord de chaque site un par un.</p>



<h2 class="wp-block-heading">Les limites honnêtes du journal email WordPress</h2>



<p class="wp-block-paragraph">Ce n&rsquo;est pas une solution magique. La conservation est basée sur les jours, pas sur un nombre maximal de lignes. Sur un site avec beaucoup de trafic transactionnel, la table du journal peut grossir vite. Il vaut mieux la surveiller, ou fixer une conservation plus courte. Les notifications d&rsquo;échec n&rsquo;arrivent que sur les canaux de chat configurés, pas par email. C&rsquo;est logique&nbsp;: un email qui avertit d&rsquo;un email en échec risque lui aussi d&rsquo;échouer. Mais il faut le savoir avant d&rsquo;y compter.</p>



<p class="wp-block-paragraph">Enfin, le journal enregistre ce qui s&rsquo;est passé côté WordPress et côté fournisseur. Il ne garantit pas la livraison dans la boîte du destinataire. Si un email est marqué comme envoyé mais finit en spam, c&rsquo;est un problème de réputation du domaine ou de configuration SPF/DKIM. Pas quelque chose qu&rsquo;un journal peut résoudre seul. Il reste malgré tout le premier endroit où je regarde, avant toute autre vérification.</p>



<p class="wp-block-paragraph">Le plugin est gratuit et open source. Page officielle sur <a href="https://wordpress.org/plugins/fluent-smtp/" target="_blank" rel="nofollow noopener">wordpress.org</a>, sans fonctions bloquées derrière une version pro. Pour qui gère même un seul site avec un formulaire de contact, ça vaut le coup de l&rsquo;installer juste pour le journal email WordPress. La première fois qu&rsquo;il résout un «&nbsp;je n&rsquo;ai pas reçu l&#8217;email&nbsp;» en une minute plutôt qu&rsquo;en une demi-heure, il est rentabilisé.</p><p>The post <a href="https://www.cisilino.com/fr/2026/10/31/journal-email-wordpress-pourquoi-je-controle/">Journal email WordPress : pourquoi je le contrôle</a> first appeared on <a href="https://www.cisilino.com/fr/">Matteo Cisilino</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://www.cisilino.com/fr/2026/10/31/journal-email-wordpress-pourquoi-je-controle/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>PHP2Shell : comment j&#8217;ai protégé mes serveurs en attendant le correctif</title>
		<link>https://www.cisilino.com/fr/2026/10/29/php2shell-cve-2026-109618-mitigation-serveurs/</link>
					<comments>https://www.cisilino.com/fr/2026/10/29/php2shell-cve-2026-109618-mitigation-serveurs/#respond</comments>
		
		<dc:creator><![CDATA[matteocisilino]]></dc:creator>
		<pubDate>Thu, 29 Oct 2026 18:53:44 +0000</pubDate>
				<category><![CDATA[Serveurs et bases de données]]></category>
		<category><![CDATA[atténuation]]></category>
		<category><![CDATA[CVE-2026-109618]]></category>
		<category><![CDATA[CVE-2026-109619]]></category>
		<category><![CDATA[Debian]]></category>
		<category><![CDATA[gestion de serveurs]]></category>
		<category><![CDATA[ISPConfig]]></category>
		<category><![CDATA[PHP]]></category>
		<category><![CDATA[php2shell]]></category>
		<category><![CDATA[sécurité des serveurs]]></category>
		<guid isPermaLink="false">https://www.cisilino.com/?p=850</guid>

					<description><![CDATA[<p>Deux failles graves dans le cœur de PHP (CVE-2026-109618 et CVE-2026-109619) : ce que c'est, les versions touchées, le script d'atténuation et la mise à jour de mes serveurs.</p>
<p>The post <a href="https://www.cisilino.com/fr/2026/10/29/php2shell-cve-2026-109618-mitigation-serveurs/">PHP2Shell : comment j’ai protégé mes serveurs en attendant le correctif</a> first appeared on <a href="https://www.cisilino.com/fr/">Matteo Cisilino</a>.</p>]]></description>
										<content:encoded><![CDATA[<p>Le 20 octobre 2026, deux failles graves ont été rendues publiques dans le cœur de PHP. Le nom courant est <strong>PHP2Shell</strong> et les identifiants sont CVE-2026-109618 et CVE-2026-109619. Le score CVSS est de 9,2. Elles permettent d&rsquo;exécuter du code sur le serveur sans s&rsquo;authentifier, et des attaques ont déjà été observées.</p>
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" src="https://www.cisilino.com/wp-content/uploads/2026/10/php2shell-faille-php-schema.webp" alt="Schéma PHP2Shell : cinq cercles reliés à un cadre avec le symbole du terminal" width="1200" height="630"/></figure>
<p>Dans cet article, j&rsquo;explique ce que c&rsquo;est et quelles versions de PHP sont concernées. Je raconte comment j&rsquo;ai sécurisé les serveurs que je gère pendant que le correctif n&rsquo;existait pas, et comment je suis arrivé à la mise à jour définitive. Tu trouveras aussi le script que j&rsquo;ai utilisé et les commandes pour le lancer.</p>
<h2 class="wp-block-heading">Qu&rsquo;est-ce que PHP2Shell</h2>
<p>Le problème vient de la façon dont PHP lit les requêtes de type <code>multipart/form-data</code>. C&rsquo;est le format que le navigateur utilise pour envoyer des fichiers. PHP l&rsquo;interprète avant le démarrage de n&rsquo;importe quel script.</p>
<p>Il n&rsquo;y a donc pas besoin d&rsquo;un module d&rsquo;upload. Une seule requête vers un fichier PHP quelconque du site peut suffire.</p>
<p>Les deux failles, en bref :</p>
<ul class="wp-block-list">
<li><strong>CVE-2026-109618.</strong> Si la mémoire s&rsquo;épuise pendant que PHP lit la requête, certaines données se corrompent. La corruption apparaît lors du nettoyage final de la requête et peut mener à l&rsquo;exécution de code.</li>
<li><strong>CVE-2026-109619.</strong> Une partie de l&rsquo;état d&rsquo;une requête reste en mémoire. Une requête suivante, sur le même processus, la réutilise.</li>
</ul>
<p>Ces détails techniques viennent de l&rsquo;<a href="https://github.com/NCSC-NL/php2shell/wiki" target="_blank" rel="noopener">analyse du NCSC néerlandais</a> et d&rsquo;autres sources publiques. Je n&rsquo;ai pas essayé d&rsquo;exploiter les failles sur mes serveurs.</p>
<h2 class="wp-block-heading">Quelles versions de PHP sont concernées</h2>
<p>Les versions corrigées sont sorties le 21 octobre 2026 : <strong>PHP 8.5.12, 8.4.27, 8.3.36 et 8.2.35</strong>. Toute version antérieure est touchée.</p>
<p>Les branches 8.1 et plus anciennes n&rsquo;ont pas de version corrigée officielle, parce qu&rsquo;elles ne sont plus supportées. PHP a publié seulement des backports minimaux du code source pour 7.4, 8.0 et 8.1. Pour les branches encore plus anciennes, rien n&rsquo;est indiqué.</p>
<p>Le risque concerne toutes les façons d&rsquo;exécuter PHP. La plus exposée est mod_php. PHP-FPM, php-cgi, FrankenPHP et LiteSpeed comptent aussi. Cela touche ensuite chaque application qui tourne dessus : WordPress, Joomla, Drupal, Nextcloud, phpMyAdmin, Roundcube et le panneau ISPConfig.</p>
<p>Sur Debian, il y a un détail à connaître. Debian applique les corrections sous forme de backports et ne change pas le numéro de version de PHP. Un serveur peut afficher un numéro bas et être déjà corrigé. C&rsquo;est pourquoi le script ne compare pas les versions. Il vérifie si le changelog du paquet installé cite les deux CVE.</p>
<h2 class="wp-block-heading">L&rsquo;atténuation, en attendant le patch</h2>
<p>Quand les failles sont devenues publiques, les correctifs n&rsquo;étaient pas encore disponibles pour les distributions. Je ne voulais pas laisser les serveurs à découvert, alors j&rsquo;ai réduit le risque avec un script.</p>
<p><strong>L&rsquo;atténuation n&rsquo;est pas un patch.</strong> Désactiver <code>session.upload_progress</code> ne ferme qu&rsquo;un des chemins connus de la CVE-2026-109619 et ne fait rien contre la CVE-2026-109618. Elle servait à réduire la marge d&rsquo;attaque jusqu&rsquo;à la mise à jour, pas à la remplacer.</p>
<h3 class="wp-block-heading">Ce que fait le script</h3>
<p>Le script est prévu pour Debian 12 et 13. Il fonctionne avec Apache (mod_php, FastCGI, suPHP, PHP-FPM) et avec Nginx (PHP-FPM). Il reconnaît les serveurs gérés avec ISPConfig, aussi bien Apache que Nginx.</p>
<p>Les options :</p>
<ul class="wp-block-list">
<li>Sans option, il contrôle, c&rsquo;est tout. Pour chaque paquet PHP, il vérifie que le changelog cite les deux CVE. Il signale les processus qui utilisent encore un ancien binaire, lit les réglages actifs et les ports web en écoute.</li>
<li><code>--update</code> met d&rsquo;abord à jour l&rsquo;index des paquets.</li>
<li><code>--logs</code> cherche des indices suspects : fichiers PHP récents dans les dossiers d&rsquo;upload ou de cache, motifs typiques des webshells, références à php2shell dans les logs. C&rsquo;est une heuristique, elle ne prouve pas une compromission.</li>
<li><code>--harden</code> applique la sécurisation décrite plus bas.</li>
<li><code>--dry-run</code>, avec <code>--harden</code>, montre ce qu&rsquo;il ferait sans rien modifier.</li>
<li><code>--skip-disable-functions</code>, avec <code>--harden</code>, ne touche pas à <code>disable_functions</code>.</li>
<li><code>--rollback</code> retire ce qu&rsquo;a appliqué <code>--harden</code>.</li>
</ul>
<h3 class="wp-block-heading">Ce que change &#8211;harden</h3>
<p>Pour chaque version de PHP présente dans <code>/etc/php</code>, le script crée le fichier <code>mods-available/cve-hardening.ini</code>. Il l&rsquo;active avec <code>phpenmod</code> pour les SAPI fpm, apache2 et cgi. La protection vaut ainsi pour tous les sites d&rsquo;un coup.</p>
<p>Les <code>php.ini</code> d&rsquo;origine restent intacts. La CLI n&rsquo;est pas touchée, donc composer, artisan et wp-cli continuent de fonctionner.</p>
<p>Le fichier règle trois choses :</p>
<ul class="wp-block-list">
<li><code>session.upload_progress.enabled = Off</code></li>
<li><code>expose_php = Off</code></li>
<li><code>disable_functions</code> avec exec, passthru, shell_exec, system, proc_open, popen et pcntl_exec, ajoutées à celles déjà présentes.</li>
</ul>
<p>Ensuite le script teste la configuration et recharge PHP-FPM et Apache seulement si le test passe. Avant d&rsquo;écrire, il fait une sauvegarde dans <code>/var/backups/php-cve-hardening/</code>.</p>
<h3 class="wp-block-heading">Les limites à connaître</h3>
<ul class="wp-block-list">
<li><code>disable_functions</code> peut casser certaines applications, par exemple WordPress avec certains plugins ou Magento. Si ça arrive, utilise <code>--rollback</code> ou relance avec <code>--skip-disable-functions</code>.</li>
<li>Les directives <code>php_admin_value</code> dans les pools PHP-FPM ont la priorité sur le fichier global. Le script les signale mais ne les modifie pas.</li>
<li>Avec ISPConfig, les fichiers de <code>pool.d</code> sont régénérés par le panneau. Les surcharges se corrigent depuis le panneau, dans Sites, Website, Options, Custom php.ini settings, pas à la main.</li>
<li>Les <code>php.ini</code> propres à chaque site (mode FastCGI ou suPHP d&rsquo;ISPConfig) et les versions de PHP dans <code>/opt</code> ne sont pas gérés. Il faut les contrôler à part.</li>
</ul>
<h2 class="wp-block-heading">Comment lancer le script</h2>
<p>Le script est ici : <a href="https://cisilino.com/mscripts/php20261.sh">https://cisilino.com/mscripts/php20261.sh</a></p>
<p>Avant d&rsquo;exécuter quoi que ce soit venu d&rsquo;internet, lis-le. Tu peux le télécharger et l&rsquo;ouvrir :</p>
<pre class="wp-block-code"><code>wget -qO php20261.sh https://cisilino.com/mscripts/php20261.sh
less php20261.sh</code></pre>
<p>Si tu veux le lancer directement à distance, il faut les droits root. Voici les commandes, dans l&rsquo;ordre que je conseille.</p>
<p>1. Contrôle seul, avec mise à jour de l&rsquo;index des paquets :</p>
<pre class="wp-block-code"><code>wget -qO- https://cisilino.com/mscripts/php20261.sh | sudo bash -s -- --update</code></pre>
<p>2. Simulation de la sécurisation, sans modification :</p>
<pre class="wp-block-code"><code>wget -qO- https://cisilino.com/mscripts/php20261.sh | sudo bash -s -- --harden --dry-run</code></pre>
<p>3. Application :</p>
<pre class="wp-block-code"><code>wget -qO- https://cisilino.com/mscripts/php20261.sh | sudo bash -s -- --harden</code></pre>
<p>4. Retrait, si quelque chose casse :</p>
<pre class="wp-block-code"><code>wget -qO- https://cisilino.com/mscripts/php20261.sh | sudo bash -s -- --rollback</code></pre>
<p>Après <code>--harden</code> et après <code>--rollback</code>, pas besoin de redémarrer quoi que ce soit à la main : le script recharge lui-même PHP-FPM et Apache, après avoir testé la configuration. Le redémarrage est en revanche nécessaire après la mise à jour du paquet PHP, car les processus en cours gardent l&rsquo;ancien binaire en mémoire :</p>
<pre class="wp-block-code"><code>sudo systemctl restart 'php*-fpm' apache2</code></pre>
<p>Les apostrophes autour de <code>php*-fpm</code> sont nécessaires. Sans elles, c&rsquo;est le shell qui développe l&rsquo;astérisque, pas systemctl. Si le serveur utilise seulement Nginx, retire <code>apache2</code>.</p>
<p>Le résultat du script est aussi un code de sortie : 0 si tout va bien, 1 s&rsquo;il y a des avertissements, 2 si le système est vulnérable ou si une action est nécessaire.</p>
<h2 class="wp-block-heading">Sur les serveurs avec ISPConfig : ispconfig-multipart-guard</h2>
<p><a href="https://www.ispconfig.org/blog/php2shell-critical-php-vulnerabilities-cve-2026-109618-cve-2026-109619/" target="_blank" rel="nofollow noopener"><img loading="lazy" decoding="async" src="https://www.cisilino.com/wp-content/uploads/2026/10/ispconfig-logo.png" alt="Logo d'ISPConfig" width="239" height="90"></a></p>
<p>Qui utilise ISPConfig a une arme de plus. Le projet a publié le script <strong>ispconfig-multipart-guard</strong>, décrit dans <a href="https://www.ispconfig.org/blog/php2shell-critical-php-vulnerabilities-cve-2026-109618-cve-2026-109619/" target="_blank" rel="nofollow noopener">l&rsquo;avis officiel d&rsquo;ISPConfig</a>.</p>
<p>Le principe est simple. Si le problème est dans les requêtes multipart, le serveur web les arrête avant qu&rsquo;elles n&rsquo;arrivent à PHP :</p>
<ul class="wp-block-list">
<li>il bloque les requêtes <code>Content-Type: multipart/...</code> pour tous les sites, pour certains sites choisis ou pour le panneau de contrôle ;</li>
<li>avec Nginx, il répond 415 aux requêtes PHP des sites bloqués et les fichiers statiques ne changent pas. Avec Apache, il répond 403, même pour les fichiers statiques ;</li>
<li>par défaut, il bloque seulement le panneau ISPConfig. Les sites se bloquent avec des commandes comme <code>block all</code> ou <code>block esempio.it</code> ;</li>
<li>il crée un cron qui, toutes les 5 minutes, rétablit ses hooks si ISPConfig réécrit la configuration ;</li>
<li>il a une liste d&rsquo;IP autorisées qui peuvent encore envoyer des fichiers.</li>
</ul>
<p>Il fonctionne avec Debian et Ubuntu, avec ISPConfig 3.2 ou 3.3 (et même sans ISPConfig), avec Nginx ou avec Apache 2.4 et suivants.</p>
<p>Le prix à payer : sur les sites bloqués, les uploads ne fonctionnent pas. Cela vaut aussi pour les médias des CMS, les pièces jointes des formulaires et celles du webmail, et les imports de phpMyAdmin. Les pages normales et les formulaires urlencoded continuent de fonctionner. Là aussi, c&rsquo;est une mesure provisoire et non une correction.</p>
<p>Ce script et le mien ne sont pas des alternatives. Le mien agit sur la configuration de PHP, celui d&rsquo;ISPConfig sur le serveur web. Je ne les ai pas comparés sur le terrain et je ne peux pas te dire comment ils se comportent ensemble sur chaque configuration.</p>
<h2 class="wp-block-heading">La mise à jour définitive, en deux étapes</h2>
<p>Les atténuations devaient durer peu. J&rsquo;ai mis à jour les serveurs dès que les mises à jour ont été disponibles, en deux étapes :</p>
<ol class="wp-block-list">
<li><strong>Compilation directe.</strong> En attendant que la distribution utilisée sur chaque serveur publie son paquet, j&rsquo;ai compilé PHP depuis les sources dans les versions corrigées.</li>
<li><strong>Paquet officiel.</strong> Quand le paquet officiel de la distribution est sorti pour ce serveur, j&rsquo;ai remplacé la compilation par le paquet.</li>
</ol>
<p>La deuxième étape compte. Un PHP compilé à la main ne se met pas à jour tout seul avec le reste du système. Le paquet de la distribution, si, et le script peut le vérifier en lisant le changelog.</p>
<h2 class="wp-block-heading">Que faire maintenant</h2>
<ol class="wp-block-list">
<li>Mets PHP à jour vers la 8.5.12, 8.4.27, 8.3.36 ou 8.2.35. Sur Debian, tu peux aussi utiliser le paquet de la distribution quand le changelog cite les deux CVE.</li>
<li>Redémarre PHP-FPM et Apache, puis relance le script sans option. Il ne doit plus signaler de problème.</li>
<li>Si tu utilises PHP 8.1 ou antérieur, prévois le passage à une branche supportée. Pour ces branches, il n&rsquo;y a pas de version officielle corrigée.</li>
<li>Garde <code>session.upload_progress</code> désactivé jusqu&rsquo;à la prochaine publication de correctifs, si ton application ne l&rsquo;utilise pas.</li>
<li>Contrôle les logs et les fichiers récents, avec <code>--logs</code> ou à la main, en cherchant des fichiers PHP apparus dans les dossiers d&rsquo;upload.</li>
<li>N&rsquo;oublie pas PHP dans les conteneurs, les images et les appliances de tiers. Ils se mettent à jour à part.</li>
<li>Si ton site utilise WordPress, le <a href="https://www.cisilino.com/fr/2026/10/29/plugin-securite-wordpress-cisilino/">plugin Cisilino Site Defense Network</a> ne résout pas PHP2Shell. Il protège toutefois la connexion et tient un journal des activités, utile pour comprendre ce qui se passe sur le site.</li>
</ol>
<h2 class="wp-block-heading">Sources</h2>
<ul class="wp-block-list">
<li><a href="https://www.ispconfig.org/blog/php2shell-critical-php-vulnerabilities-cve-2026-109618-cve-2026-109619/" target="_blank" rel="nofollow noopener">ISPConfig : PHP2Shell, vulnérabilités critiques de PHP (CVE-2026-109618, CVE-2026-109619)</a></li>
<li><a href="https://github.com/NCSC-NL/php2shell/wiki" target="_blank" rel="noopener">NCSC-NL : wiki sur PHP2Shell</a></li>
</ul>
<p>ISPConfig, PHP, Debian et les autres noms cités sont des marques de leurs propriétaires respectifs. Ils sont indiqués uniquement pour identifier les produits.</p><p>The post <a href="https://www.cisilino.com/fr/2026/10/29/php2shell-cve-2026-109618-mitigation-serveurs/">PHP2Shell : comment j’ai protégé mes serveurs en attendant le correctif</a> first appeared on <a href="https://www.cisilino.com/fr/">Matteo Cisilino</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://www.cisilino.com/fr/2026/10/29/php2shell-cve-2026-109618-mitigation-serveurs/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Quelle base de données occupe l&#8217;espace disque ? Un script pour MySQL et PostgreSQL</title>
		<link>https://www.cisilino.com/fr/2026/10/20/script-espace-disque-base-de-donnees-mysql/</link>
					<comments>https://www.cisilino.com/fr/2026/10/20/script-espace-disque-base-de-donnees-mysql/#respond</comments>
		
		<dc:creator><![CDATA[matteocisilino]]></dc:creator>
		<pubDate>Tue, 20 Oct 2026 07:53:00 +0000</pubDate>
				<category><![CDATA[Serveurs et bases de données]]></category>
		<category><![CDATA[espace disque]]></category>
		<category><![CDATA[MySQL]]></category>
		<category><![CDATA[PostgreSQL]]></category>
		<category><![CDATA[script bash]]></category>
		<guid isPermaLink="false">https://www.cisilino.com/?p=260</guid>

					<description><![CDATA[<p>Le disque du serveur se remplit et vous ne savez pas quelle base en est responsable ? Un script pour mesurer l'espace de MySQL et PostgreSQL et le comparer dans le temps.</p>
<p>The post <a href="https://www.cisilino.com/fr/2026/10/20/script-espace-disque-base-de-donnees-mysql/">Quelle base de données occupe l’espace disque ? Un script pour MySQL et PostgreSQL</a> first appeared on <a href="https://www.cisilino.com/fr/">Matteo Cisilino</a>.</p>]]></description>
										<content:encoded><![CDATA[<p>Cela arrive à tout le monde tôt ou tard. L&rsquo;espace disque du serveur se remplit, l&rsquo;alarme se déclenche et la question est toujours la même. <strong>Quelle base de données occupe tout cet espace ?</strong></p>
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" src="https://www.cisilino.com/wp-content/uploads/2026/10/espace-disque-bases-mysql-postgresql.webp" alt="Espace disque : graphique en anneau divisé en quatre secteurs de couleurs, avec trois barres de légende à droite" width="1200" height="630"/></figure>
<p>Je m&rsquo;y suis retrouvé confronté après une optimisation. J&rsquo;avais lancé <code>OPTIMIZE TABLE</code> en espérant récupérer de l&rsquo;espace, mais le système de fichiers ne baissait pas comme prévu. Sans un « avant » à comparer à l&rsquo;« après », impossible de savoir si l&rsquo;optimisation avait fonctionné. Impossible aussi de savoir quelle base était responsable, et si l&rsquo;espace disque manquant n&rsquo;était pas carrément ailleurs.</p>
<p>Les outils dont je disposais ne répondaient qu&rsquo;à la moitié du problème :</p>
<ul class="wp-block-list">
<li><code>du</code> montre les fichiers sur le disque, mais ne dit pas à quelle table ils appartiennent ;</li>
<li><code>information_schema</code> (ou <code>pg_class</code> sur PostgreSQL) donne les tailles logiques, mais ne voit ni les binlogs, ni <code>ibdata1</code>, ni les undo logs, ni les WAL ;</li>
<li>tous deux photographient un seul instant : la comparaison dans le temps reste un travail manuel, fait de copier-coller et d&rsquo;un tableur.</li>
</ul>
<p>Il me fallait quelque chose de simple : <strong>photographier l&rsquo;état de toutes les bases à un moment donné, refaire la photo plus tard et me dire ce qui a changé</strong>, en classant les différences de la plus grande à la plus petite. Le coupable saute ainsi aux yeux.</p>
<p>Il en est sorti un unique script Bash, sans dépendances au-delà du client de base de données et des outils standard de Linux (<code>awk</code>, <code>du</code>, <code>df</code>). Il fonctionne avec <strong>MySQL/MariaDB</strong> et avec <strong>PostgreSQL</strong>. Je le partage ici dans l&rsquo;espoir d&rsquo;épargner à quelqu&rsquo;un le même après-midi de frustration.</p>
<h2 class="wp-block-heading">Ce qu&rsquo;il fait</h2>
<p>Le script a quatre commandes :</p>
<ul class="wp-block-list">
<li><code>snapshot</code> enregistre l&rsquo;état actuel (bases, tables, fichiers du répertoire de données, espace du système de fichiers) ;</li>
<li><code>compare</code> compare deux instantanés et montre les différences, les plus grandes en tête ;</li>
<li><code>report</code> montre les bases et tables les plus grosses d&rsquo;un seul instantané ;</li>
<li><code>list</code> liste les instantanés enregistrés.</li>
</ul>
<p>La comparaison est divisée en quatre sections, pour ne laisser aucun angle mort :</p>
<ul class="wp-block-list">
<li><strong>taille de chaque base</strong> (données + index) ;</li>
<li><strong>espace libre ou récupérable</strong> (<code>data_free</code> sur MySQL, estimation des tuples morts sur PostgreSQL) ;</li>
<li><strong>taille de chaque table</strong> ;</li>
<li><strong>fichiers réels du répertoire de données</strong> : dossiers des bases, <code>ibdata1</code>, binlogs, undo, <code>pg_wal</code>.</li>
</ul>
<p>La dernière section est souvent celle qui résout le mystère. Bien des fois, l&rsquo;espace « anormal » n&rsquo;appartient à aucune base, mais aux journaux binaires ou aux fichiers partagés d&rsquo;InnoDB.</p>
<h2 class="wp-block-heading">Prérequis</h2>
<ul class="wp-block-list">
<li>Linux avec Bash 4 ou supérieur, <code>awk</code>, <code>du</code>, <code>df</code> (le tout en standard) ;</li>
<li>le client <code>mysql</code> ou <code>psql</code>, avec un utilisateur capable de lire les métadonnées de toutes les bases ;</li>
<li>les droits root (ou ceux de l&rsquo;utilisateur <code>mysql</code>/<code>postgres</code>) si vous voulez aussi mesurer les fichiers sur disque ; sans eux, cette section est ignorée avec un avertissement.</li>
</ul>
<h2 class="wp-block-heading">Installation</h2>
<p>Enregistrez le script (vous le trouverez à la fin de l&rsquo;article) dans un fichier et rendez-le exécutable :</p>
<pre class="wp-block-code"><code>nano db_space_compare.sh
chmod +x db_space_compare.sh</code></pre>
<h3 class="wp-block-heading">Identifiants</h3>
<p>Pour MySQL/MariaDB, le plus propre est un fichier <code>~/.my.cnf</code> contenant les identifiants, afin qu&rsquo;ils n&rsquo;apparaissent pas dans l&rsquo;historique des commandes :</p>
<pre class="wp-block-code"><code>&#91;client]
user=root
password=LA_TUA_PASSWORD</code></pre>
<p>Vous pouvez aussi passer les options via une variable d&rsquo;environnement :</p>
<pre class="wp-block-code"><code>export MYSQL_OPTS="-u root -pPASSWORD -h 127.0.0.1"</code></pre>
<p>Pour PostgreSQL, <code>~/.pgpass</code>, les variables <code>PG*</code> ou <code>PG_OPTS</code> fonctionnent :</p>
<pre class="wp-block-code"><code>export PG_OPTS="-U postgres -h localhost"</code></pre>
<h2 class="wp-block-heading">Comment l&rsquo;utiliser</h2>
<p>Le déroulement type est <em>avant, optimisation, après, comparaison</em> :</p>
<pre class="wp-block-code"><code># 1. fotografia prima dell'ottimizzazione
sudo ./db_space_compare.sh snapshot prima

# 2. esegui l'ottimizzazione (OPTIMIZE TABLE, VACUUM FULL, ...)

# 3. fotografia dopo
sudo ./db_space_compare.sh snapshot dopo

# 4. confronto
sudo ./db_space_compare.sh compare prima dopo</code></pre>
<p>Pour PostgreSQL, il suffit d&rsquo;ajouter l&rsquo;option <code>-e postgres</code> :</p>
<pre class="wp-block-code"><code>sudo ./db_space_compare.sh -e postgres snapshot prima
sudo ./db_space_compare.sh -e postgres snapshot dopo
sudo ./db_space_compare.sh -e postgres compare prima dopo</code></pre>
<p>Les options disponibles sont :</p>
<ul class="wp-block-list">
<li><code>-e mysql|postgres</code> choisit le moteur (par défaut : <code>mysql</code>) ;</li>
<li><code>-d DIR</code> change le dossier où sont enregistrés les instantanés (par défaut : <code>./db_snapshots</code>) ;</li>
<li><code>-n N</code> fixe le nombre de lignes affichées par section (par défaut : 20).</li>
</ul>
<p>Si vous n&rsquo;indiquez pas d&rsquo;étiquette, l&rsquo;instantané prend pour nom la date et l&rsquo;heure. C&rsquo;est pratique pour surveiller la croissance dans le temps.</p>
<h2 class="wp-block-heading">Exemple de sortie</h2>
<p>Voici une comparaison sur des données de test, construites exprès pour montrer un cas typique. La base <code>shop</code> diminue après l&rsquo;optimisation, mais la base <code>log</code> grossit et les binlogs doublent.</p>
<pre class="wp-block-code"><code>Confronto 'prima' (2026-10-11 20:00:00)  -&gt;  'dopo' (2026-10-11 22:00:00)
Filesystem /var: usato 46.6 GiB -&gt; 48.4 GiB (+1.9 GiB)

=== Database - dimensione totale ===
  NOME                                                      PRIMA         DOPO    DIFFERENZA  NOTE
  log                                                    21.4 GiB     27.5 GiB      +6.1 GiB  +28.3%
  shop                                                    5.6 GiB      3.6 GiB      -2.0 GiB  -35.0%

=== Tabelle - dimensione (dati + indici) ===
  NOME                                                      PRIMA         DOPO    DIFFERENZA  NOTE
  log.eventi                                             21.4 GiB     27.5 GiB      +6.1 GiB  +28.3%
  shop.ordini                                             5.6 GiB      3.6 GiB      -2.0 GiB  -35.0%
  log.nuova                                                   0 B      1.0 KiB      +1.0 KiB  NUOVO

=== File nella data directory (cartelle DB, ibdata1, binlog, undo, WAL) ===
  NOME                                                      PRIMA         DOPO    DIFFERENZA  NOTE
  log                                                    21.4 GiB     27.5 GiB      +6.1 GiB  +28.3%
  mysql-bin.*                                             3.7 GiB      8.4 GiB      +4.7 GiB  +125.0%
  shop                                                    5.7 GiB      3.6 GiB      -2.0 GiB  -36.1%
  ibdata1                                                11.2 GiB     11.2 GiB           0 B</code></pre>
<p>Ici le coupable est évident. La table <code>log.eventi</code> a grossi de plus de 6 GiB. Les fichiers <code>mysql-bin.*</code> sont passés de 3,7 à 8,4 GiB. L&rsquo;optimisation sur <code>shop</code> a fonctionné, mais le gain d&rsquo;espace disque a été englouti par le reste.</p>
<h2 class="wp-block-heading">Comment lire les résultats</h2>
<ul class="wp-block-list">
<li>En haut de chaque section se trouvent les éléments qui ont le plus changé. Une base ou une table qui <strong>ne diminue pas</strong> (voire grossit) après l&rsquo;optimisation est le premier suspect.</li>
<li>Si les tables diminuent mais pas le système de fichiers, l&rsquo;espace disque est ailleurs. Regardez la section sur les fichiers du répertoire de données (binlogs, <code>ibdata1</code>, undo, <code>pg_wal</code>).</li>
<li><strong>MySQL/MariaDB :</strong> <code>ibdata1</code> ne rétrécit jamais, pas même avec <code>OPTIMIZE TABLE</code>. Avec <code>innodb_file_per_table=OFF</code>, la valeur de <code>data_free</code> est partagée et peu indicative.</li>
<li><strong>PostgreSQL :</strong> un simple <code>VACUUM</code> ne rend pas d&rsquo;espace au système d&rsquo;exploitation, il faut <code>VACUUM FULL</code> (ou <code>pg_repack</code>). L&rsquo;espace « récupérable » affiché est une estimation fondée sur les tuples morts.</li>
</ul>
<p>Pour PostgreSQL, la <a href="https://www.postgresql.org/docs/current/diskusage.html" target="_blank" rel="noopener">documentation officielle sur l&rsquo;utilisation du disque</a> explique comment mesurer l&rsquo;espace occupé.</p>
<h2 class="wp-block-heading">Surveiller l&rsquo;espace disque dans le temps</h2>
<p>Si le problème n&rsquo;est pas lié à une seule optimisation mais à une croissance progressive, mieux vaut automatiser les instantanés avec cron, un par heure, puis comparer deux moments quelconques :</p>
<pre class="wp-block-code"><code># crontab -e (come root)
0 * * * * /usr/local/bin/db_space_compare.sh -d /var/lib/db_snapshots snapshot &gt;/dev/null 2&gt;&amp;1</code></pre>
<p>Sans étiquette, chaque instantané prend pour nom la date et l&rsquo;heure (par exemple <code>20261011-140000</code>). Avec <code>list</code> vous voyez ceux qui existent et avec <code>compare</code> vous en comparez deux. En quelques jours, on voit quelle base grossit de façon anormale.</p>
<pre class="wp-block-code"><code>./db_space_compare.sh -d /var/lib/db_snapshots list
./db_space_compare.sh -d /var/lib/db_snapshots compare 20261011-140000 20261012-140000</code></pre>
<h2 class="wp-block-heading">Le script complet</h2>
<p>Voici le code. Copiez-le dans un fichier nommé <code>db_space_compare.sh</code>, rendez-le exécutable et vous êtes prêt.</p>
<pre class="wp-block-code"><code>#!/usr/bin/env bash
#
# db_space_compare.sh
# Fotografa l'occupazione di spazio di tutti i database (MySQL/MariaDB o
# PostgreSQL) e confronta due fotografie (es. prima/dopo l'ottimizzazione)
# per capire quale database, tabella o file occupa spazio in modo anomalo.
#
# Uso:
#   db_space_compare.sh &#91;opzioni] snapshot &#91;etichetta]   (default: data-ora)
#   db_space_compare.sh &#91;opzioni] compare  &lt;prima&gt; &lt;dopo&gt;
#   db_space_compare.sh &#91;opzioni] report   &lt;etichetta&gt;
#   db_space_compare.sh &#91;opzioni] list
#
# Opzioni:
#   -e mysql|postgres   motore del database (default: mysql)
#   -d DIR              cartella delle fotografie (default: ./db_snapshots)
#   -n N                righe mostrate per sezione (default: 20)
#
# Credenziali:
#   MySQL/MariaDB: ~/.my.cnf oppure MYSQL_OPTS="-u root -pPASSWORD -h 127.0.0.1"
#   PostgreSQL:    ~/.pgpass, variabili PG* oppure PG_OPTS="-U postgres -h localhost"
#                  (non mettere -d in PG_OPTS: lo script si collega a ogni database)
#
# Per misurare anche i file su disco (cartelle dei DB, ibdata1, binlog, undo,
# pg_wal) va eseguito come root o come utente mysql/postgres; altrimenti
# quella parte viene saltata con un avviso.
#
# Esempio:
#   sudo ./db_space_compare.sh snapshot prima
#   ... ottimizzazione (OPTIMIZE TABLE / VACUUM FULL / ...) ...
#   sudo ./db_space_compare.sh snapshot dopo
#   sudo ./db_space_compare.sh compare prima dopo

set -euo pipefail
export LC_ALL=C

ENGINE=mysql
OUTDIR=./db_snapshots
TOPN=20
SNAP_IN_PROGRESS=""

die()  { echo "ERRORE: $*" &gt;&amp;2; exit 1; }
warn() { echo "ATTENZIONE: $*" &gt;&amp;2; }
usage() { sed -n '2,/^$/{s/^# \{0,1\}//;p}' "$0"; exit "${1:-0}"; }

cleanup() {
  if &#91;&#91; -n $SNAP_IN_PROGRESS ]]; then
    rm -rf -- "$SNAP_IN_PROGRESS"
    echo "Fotografia incompleta rimossa: $SNAP_IN_PROGRESS" &gt;&amp;2
  fi
}
trap cleanup EXIT

while getopts ":e:d:n:h" opt; do
  case $opt in
    e) ENGINE=$OPTARG ;;
    d) OUTDIR=$OPTARG ;;
    n) TOPN=$OPTARG ;;
    h) usage 0 ;;
    *) usage 1 ;;
  esac
done
shift $((OPTIND - 1))

&#91;&#91; $ENGINE == mysql || $ENGINE == postgres ]] || die "motore non valido: '$ENGINE' (usa mysql o postgres)"
&#91;&#91; $TOPN =~ ^&#91;0-9]+$ ]] || die "-n deve essere un numero"

read -r -a MYSQL_ARGS &lt;&lt;&lt; "${MYSQL_OPTS:-}"
read -r -a PG_ARGS    &lt;&lt;&lt; "${PG_OPTS:-}"

my() { mysql "${MYSQL_ARGS&#91;@]+"${MYSQL_ARGS&#91;@]}"}" --batch --skip-column-names "$@"; }
pg() { psql  "${PG_ARGS&#91;@]+"${PG_ARGS&#91;@]}"}" -X -q -A -t -F $'\t' -v ON_ERROR_STOP=1 "$@"; }

# Funzioni awk comuni: formattazione in KiB/MiB/GiB, chiave e valore di riga.
#   nk = numero di colonne che formano il nome (1 = database, 2 = db.tabella)
#   vc = colonne da sommare per il valore (es. "4,5" = dati + indici)
AWK_LIB='
function h(b,   a, i, u) {
  split("B KiB MiB GiB TiB PiB", u, " ")
  a = (b &lt; 0) ? -b : b; i = 1
  while (a &gt;= 1024 &amp;&amp; i &lt; 6) { a /= 1024; i++ }
  if (i == 1) return sprintf("%s%d B", (b &lt; 0 ? "-" : ""), a)
  return sprintf("%s%.1f %s", (b &lt; 0 ? "-" : ""), a, u&#91;i])
}
function hs(b) { return (b &gt; 0 ? "+" : "") h(b) }
function key(   k, i) { k = $1; for (i = 2; i &lt;= nk; i++) k = k "." $i; return k }
function val(   n, c, i, s) { n = split(vc, c, ","); s = 0; for (i = 1; i &lt;= n; i++) s += $(c&#91;i]); return s }
'

meta_get() { awk -F= -v k="$2" '$1 == k { sub(/^&#91;^=]*=/, ""); print; exit }' "$1/meta.txt"; }

# ---------------------------------------------------------------- raccolta dati

# Occupazione reale su disco delle voci di primo livello della data directory:
# cartelle dei database, ibdata1, binlog, undo, redo log, pg_wal, base, ...
# I file numerati (mysql-bin.000123, relay-log.000045, ...) sono sommati in una voce.
collect_disk() {
  local d=$1 out=$2
  : &gt; "$out"
  if &#91;&#91; -z $d || ! -r $d || ! -x $d ]]; then
    warn "data directory '${d:-?}' non leggibile: salto i file su disco (esegui come root per includerli)"
    return 0
  fi
  { du -sb -- "$d"/* 2&gt;/dev/null || true; } |
    awk -F'\t' -v p="$d/" '
      { n = $2
        if (index(n, p) == 1) n = substr(n, length(p) + 1)
        if (n ~ /\.&#91;0-9]&#91;0-9]&#91;0-9]&#91;0-9]&#91;0-9]&#91;0-9]$/) sub(/&#91;0-9]+$/, "*", n)
        s&#91;n] += $1 }
      END { for (n in s) printf "%s\t%.0f\n", n, s&#91;n] }' &gt; "$out"
}

write_meta() {
  local dir=$1 datadir=$2 version=$3
  {
    echo "timestamp=$(date '+%Y-%m-%d %H:%M:%S')"
    echo "engine=$ENGINE"
    echo "host=$(hostname)"
    echo "version=$version"
    echo "datadir=$datadir"
    if &#91;&#91; -n $datadir &amp;&amp; -e $datadir ]]; then
      { df -P -B1 -- "$datadir" 2&gt;/dev/null || true; } |
        awk 'NR == 2 { print "fs_mount=" $6; print "fs_used=" $3; print "fs_avail=" $4 }'
    fi
  } &gt; "$dir/meta.txt"
}

# File prodotti da ogni fotografia (separati da TAB):
#   tables.tsv     db, tabella, engine, dati, indici, libero/recuperabile, righe
#   databases.tsv  db, dati, indici, libero/recuperabile, totale
#   disk.tsv       voce della data directory, byte su disco
#   meta.txt       data, host, versione, filesystem

snapshot_mysql() {
  local dir=$1 pre="" datadir version
  command -v mysql &gt;/dev/null 2&gt;&amp;1 || die "client 'mysql' non trovato"
  my -e "SELECT 1" &gt;/dev/null || die "connessione a MySQL/MariaDB fallita (controlla ~/.my.cnf o MYSQL_OPTS)"

  # MySQL 8 tiene in cache le statistiche di information_schema fino a 24 ore:
  # senza questo, i valori "dopo" l'ottimizzazione potrebbero essere quelli vecchi.
  if &#91;&#91; -n $(my -e "SHOW VARIABLES LIKE 'information_schema_stats_expiry'") ]]; then
    pre="SET SESSION information_schema_stats_expiry = 0;"
  fi

  my -e "$pre
    SELECT table_schema, table_name, IFNULL(engine, '-'),
           IFNULL(data_length, 0), IFNULL(index_length, 0), IFNULL(data_free, 0), IFNULL(table_rows, 0)
      FROM information_schema.TABLES
     WHERE table_type IN ('BASE TABLE', 'SYSTEM VERSIONED')
       AND table_schema NOT IN ('information_schema', 'performance_schema', 'sys')" &gt; "$dir/tables.tsv"

  my -e "$pre
    SELECT s.schema_name,
           IFNULL(SUM(t.data_length), 0), IFNULL(SUM(t.index_length), 0), IFNULL(SUM(t.data_free), 0),
           IFNULL(SUM(t.data_length), 0) + IFNULL(SUM(t.index_length), 0)
      FROM information_schema.SCHEMATA s
      LEFT JOIN information_schema.TABLES t
             ON t.table_schema = s.schema_name
            AND t.table_type IN ('BASE TABLE', 'SYSTEM VERSIONED')
     WHERE s.schema_name NOT IN ('information_schema', 'performance_schema', 'sys')
     GROUP BY s.schema_name" &gt; "$dir/databases.tsv"

  datadir=$(my -e "SELECT @@datadir"); datadir=${datadir%/}
  version=$(my -e "SELECT VERSION()")
  collect_disk "$datadir" "$dir/disk.tsv"
  write_meta "$dir" "$datadir" "$version"
}

snapshot_postgres() {
  local dir=$1 maindb=${PGDATABASE:-postgres} datadir version db
  command -v psql &gt;/dev/null 2&gt;&amp;1 || die "client 'psql' non trovato"
  pg -d "$maindb" -c "SELECT 1" &gt;/dev/null || die "connessione a PostgreSQL fallita (controlla PG_OPTS, PGUSER, ~/.pgpass)"

  pg -d "$maindb" -c "SELECT datname FROM pg_database WHERE datallowconn AND NOT datistemplate ORDER BY 1" &gt; "$dir/.dblist"

  # Per ogni database: dimensione di ogni tabella (catalogo incluso, es. pg_largeobject)
  # e stima dello spazio occupato da tuple morte (bloat recuperabile con VACUUM FULL).
  : &gt; "$dir/tables.tsv"
  while IFS= read -r db; do
    &#91;&#91; -n $db ]] || continue
    pg -d "$db" -c "
      SELECT current_database(), s.schemaname || '.' || s.relname, 'table',
             pg_table_size(s.relid), pg_indexes_size(s.relid),
             CASE WHEN s.n_live_tup + s.n_dead_tup &gt; 0
                  THEN (pg_table_size(s.relid)::numeric * s.n_dead_tup / (s.n_live_tup + s.n_dead_tup))::bigint
                  ELSE 0 END,
             s.n_live_tup
        FROM pg_stat_all_tables s
       WHERE s.schemaname &lt;&gt; 'information_schema'
         AND s.schemaname NOT LIKE 'pg_toast%'" &lt; /dev/null &gt;&gt; "$dir/tables.tsv" \
      || warn "impossibile leggere le tabelle del database '$db'"
  done &lt; "$dir/.dblist"

  pg -d "$maindb" -c "SELECT datname, pg_database_size(oid) FROM pg_database WHERE datallowconn AND NOT datistemplate" &gt; "$dir/.dbsize"
  awk -F'\t' '
    FILENAME == ARGV&#91;1] { d&#91;$1] += $4; i&#91;$1] += $5; f&#91;$1] += $6; next }
    { printf "%s\t%.0f\t%.0f\t%.0f\t%s\n", $1, d&#91;$1], i&#91;$1], f&#91;$1], $2 }' \
    "$dir/tables.tsv" "$dir/.dbsize" &gt; "$dir/databases.tsv"

  datadir=$(pg -d "$maindb" -c "SHOW data_directory" 2&gt;/dev/null || true)
  version=$(pg -d "$maindb" -c "SHOW server_version")
  collect_disk "$datadir" "$dir/disk.tsv"

  # Dettaglio di base/&lt;oid&gt; con il nome del database al posto dell'oid
  if &#91;&#91; -n $datadir &amp;&amp; -r $datadir/base &amp;&amp; -x $datadir/base ]]; then
    pg -d "$maindb" -c "SELECT oid, datname FROM pg_database" &gt; "$dir/.oidmap"
    { du -sb -- "$datadir"/base/* 2&gt;/dev/null || true; } |
      awk -F'\t' '
        FILENAME == ARGV&#91;1] { name&#91;$1] = $2; next }
        { o = $2; sub(/.*\//, "", o); printf "base/%s\t%s\n", ((o in name) ? name&#91;o] : o), $1 }' \
        "$dir/.oidmap" - &gt;&gt; "$dir/disk.tsv"
  fi
  write_meta "$dir" "$datadir" "$version"
}

# --------------------------------------------------------------- presentazione

print_fs() {
  local d=$1 used avail mount
  used=$(meta_get "$d" fs_used); avail=$(meta_get "$d" fs_avail); mount=$(meta_get "$d" fs_mount)
  &#91;&#91; -n $used ]] || return 0
  awk -v u="$used" -v a="$avail" -v m="$mount" "$AWK_LIB"'
    BEGIN { printf "Filesystem %s: usato %s, libero %s\n", m, h(u), h(a) }'
}

top_section() {
  local f=$1 nk=$2 vc=$3 title=$4
  echo
  echo "=== $title ==="
  if &#91;&#91; ! -s $f ]]; then echo "  (nessun dato)"; return 0; fi
  awk -F'\t' -v nk="$nk" -v vc="$vc" "$AWK_LIB"'
    { v = val(); printf "%.0f\t%s\t%s\n", v, key(), h(v) }' "$f" |
    sort -t $'\t' -k1,1nr -k2,2 |
    awk -F'\t' -v top="$TOPN" 'NR &lt;= top &amp;&amp; $1 &gt; 0 { printf "  %-60s %12s\n", $2, $3 }'
}

compare_section() {
  local fa=$1 fb=$2 nk=$3 vc=$4 title=$5
  &#91;&#91; -f $fa ]] || fa=/dev/null
  &#91;&#91; -f $fb ]] || fb=/dev/null
  echo
  echo "=== $title ==="
  if &#91;&#91; ! -s $fa &amp;&amp; ! -s $fb ]]; then echo "  (nessun dato)"; return 0; fi
  printf '  %-50s %12s %12s %13s  %s\n' "NOME" "PRIMA" "DOPO" "DIFFERENZA" "NOTE"
  awk -F'\t' -v nk="$nk" -v vc="$vc" "$AWK_LIB"'
    FILENAME == ARGV&#91;1] { k = key(); A&#91;k] = val(); S&#91;k] = 1; next }
                        { k = key(); B&#91;k] = val(); S&#91;k] = 1 }
    END {
      for (k in S) {
        inA = (k in A); inB = (k in B)
        a = inA ? A&#91;k] : 0; b = inB ? B&#91;k] : 0; d = b - a
        if (!inA)                 note = "NUOVO"
        else if (!inB)            note = "RIMOSSO"
        else if (a &gt; 0 &amp;&amp; d != 0) note = sprintf("%+.1f%%", 100 * d / a)
        else                      note = ""
        printf "%.0f\t%s\t%s\t%s\t%s\t%s\n", (d &lt; 0 ? -d : d), k, h(a), h(b), hs(d), note
      }
    }' "$fa" "$fb" |
    sort -t $'\t' -k1,1nr -k2,2 |
    awk -F'\t' -v top="$TOPN" 'NR &lt;= top { printf "  %-50s %12s %12s %13s  %s\n", $2, $3, $4, $5, $6 }'
}

# -------------------------------------------------------------------- comandi

cmd_snapshot() {
  local label=${1:-$(date '+%Y%m%d-%H%M%S')}
  &#91;&#91; $label =~ ^&#91;A-Za-z0-9]&#91;A-Za-z0-9._-]*$ ]] || die "etichetta non valida: usa solo lettere, numeri, . _ -"
  local dir=$OUTDIR/$label
  &#91;&#91; ! -e $dir ]] || die "la fotografia '$label' esiste già ($dir)"
  mkdir -p -- "$dir"
  SNAP_IN_PROGRESS=$dir
  echo "Fotografia '$label' ($ENGINE) in corso..."
  "snapshot_$ENGINE" "$dir"
  rm -f -- "$dir/.dblist" "$dir/.dbsize" "$dir/.oidmap"
  SNAP_IN_PROGRESS=""
  echo "Salvata in $dir ($(wc -l &lt; "$dir/tables.tsv") tabelle)"
  print_fs "$dir"
  top_section "$dir/databases.tsv" 1 5 "Database più grandi"
}

cmd_report() {
  local label=${1:-}
  &#91;&#91; -n $label ]] || die "uso: report &lt;etichetta&gt;"
  local d=$OUTDIR/$label
  &#91;&#91; -f $d/meta.txt ]] || die "fotografia '$label' non trovata in $OUTDIR"
  echo "Fotografia '$label' del $(meta_get "$d" timestamp) - $(meta_get "$d" engine) $(meta_get "$d" version) su $(meta_get "$d" host)"
  print_fs "$d"
  top_section "$d/databases.tsv" 1 5   "Database più grandi (dati + indici)"
  top_section "$d/tables.tsv"    2 4,5 "Tabelle più grandi (dati + indici)"
  top_section "$d/tables.tsv"    2 6   "Tabelle con più spazio recuperabile (data_free / tuple morte stimate)"
  top_section "$d/disk.tsv"      1 2   "Voci più grandi nella data directory"
}

cmd_compare() {
  local a=${1:-} b=${2:-}
  &#91;&#91; -n $a &amp;&amp; -n $b ]] || die "uso: compare &lt;etichetta_prima&gt; &lt;etichetta_dopo&gt;"
  &#91;&#91; $a != "$b" ]] || die "le due etichette sono uguali"
  local da=$OUTDIR/$a db=$OUTDIR/$b
  &#91;&#91; -f $da/meta.txt ]] || die "fotografia '$a' non trovata in $OUTDIR"
  &#91;&#91; -f $db/meta.txt ]] || die "fotografia '$b' non trovata in $OUTDIR"
  &#91;&#91; $(meta_get "$da" engine) == "$(meta_get "$db" engine)" ]] || warn "le due fotografie sono di motori diversi"

  echo "Confronto '$a' ($(meta_get "$da" timestamp))  -&gt;  '$b' ($(meta_get "$db" timestamp))"
  local ua ub m
  ua=$(meta_get "$da" fs_used); ub=$(meta_get "$db" fs_used); m=$(meta_get "$db" fs_mount)
  if &#91;&#91; -n $ua &amp;&amp; -n $ub ]]; then
    awk -v a="$ua" -v b="$ub" -v m="$m" "$AWK_LIB"'
      BEGIN { printf "Filesystem %s: usato %s -&gt; %s (%s)\n", m, h(a), h(b), hs(b - a) }'
  fi

  compare_section "$da/databases.tsv" "$db/databases.tsv" 1 5   "Database - dimensione totale"
  compare_section "$da/databases.tsv" "$db/databases.tsv" 1 4   "Database - spazio libero/recuperabile"
  compare_section "$da/tables.tsv"    "$db/tables.tsv"    2 4,5 "Tabelle - dimensione (dati + indici)"
  compare_section "$da/disk.tsv"      "$db/disk.tsv"      1 2   "File nella data directory (cartelle DB, ibdata1, binlog, undo, WAL)"

  cat &lt;&lt;'EOF'

Come leggere il risultato:
  - In cima a ogni sezione ci sono le voci cambiate di più: un database o una
    tabella che dopo l'ottimizzazione non cala, o addirittura cresce, è il
    primo sospetto.
  - Se le tabelle calano ma il filesystem no, lo spazio sta altrove: guarda la
    sezione "File nella data directory" (binlog, ibdata1, undo, pg_wal).
  - MySQL/MariaDB: ibdata1 non si restringe mai, nemmeno con OPTIMIZE; con
    innodb_file_per_table=OFF il data_free è condiviso e poco indicativo.
  - PostgreSQL: VACUUM semplice non restituisce spazio al disco, serve
    VACUUM FULL (o pg_repack); lo spazio "recuperabile" è una stima.
EOF
}

cmd_list() {
  if &#91;&#91; ! -d $OUTDIR ]]; then echo "Nessuna fotografia in $OUTDIR"; return 0; fi
  local d found=0
  for d in "$OUTDIR"/*/; do
    &#91;&#91; -f $d/meta.txt ]] || continue
    found=1
    printf '%-24s %s  %-8s  %s\n' "$(basename "$d")" "$(meta_get "$d" timestamp)" \
      "$(meta_get "$d" engine)" "$(meta_get "$d" host)"
  done
  &#91;&#91; $found -eq 1 ]] || echo "Nessuna fotografia in $OUTDIR"
}

cmd=${1:-}
shift || true
case $cmd in
  snapshot) cmd_snapshot "$@" ;;
  compare)  cmd_compare "$@" ;;
  report)   cmd_report "$@" ;;
  list)     cmd_list ;;
  ""|help)  usage 0 ;;
  *)        die "comando sconosciuto: '$cmd' (usa snapshot, compare, report, list)" ;;
esac</code></pre>
<h2 class="wp-block-heading">Notes finales</h2>
<ul class="wp-block-list">
<li>Le script est en <strong>lecture seule</strong> sur les bases : il n&rsquo;exécute que <code>SELECT</code> sur les métadonnées. Il n&rsquo;écrit que dans le dossier des instantanés.</li>
<li>Sur MySQL 8, il définit <code>information_schema_stats_expiry = 0</code> dans la session. Sinon, les statistiques en cache (jusqu&rsquo;à 24 heures) pourraient afficher d&rsquo;« anciennes » valeurs même après l&rsquo;optimisation.</li>
<li>Si un instantané échoue à mi-parcours, il est supprimé automatiquement pour ne pas laisser de données incomplètes.</li>
<li>Avant de l&rsquo;utiliser en production, essayez-le dans un environnement de test et vérifiez les droits de l&rsquo;utilisateur de la base.</li>
</ul>
<p>Si MySQL ne démarre plus après une mise à jour, voyez <a href="https://www.cisilino.com/fr/2026/10/13/mysql-ne-demarre-plus-debian-12/">l&rsquo;article sur Debian 12</a>.</p>
<p>Si vous l&rsquo;utilisez pour surveiller l&rsquo;espace disque et trouvez un cas qu&rsquo;il ne couvre pas, écrivez-moi dans les commentaires. Vos idées pour l&rsquo;étendre sont aussi bienvenues (autres moteurs, export CSV, alertes par e-mail).</p><p>The post <a href="https://www.cisilino.com/fr/2026/10/20/script-espace-disque-base-de-donnees-mysql/">Quelle base de données occupe l’espace disque ? Un script pour MySQL et PostgreSQL</a> first appeared on <a href="https://www.cisilino.com/fr/">Matteo Cisilino</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://www.cisilino.com/fr/2026/10/20/script-espace-disque-base-de-donnees-mysql/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>MySQL 5.7 dans Docker protégé par nftables</title>
		<link>https://www.cisilino.com/fr/2026/10/17/mysql-5-7-docker-nftables-pare-feu/</link>
					<comments>https://www.cisilino.com/fr/2026/10/17/mysql-5-7-docker-nftables-pare-feu/#respond</comments>
		
		<dc:creator><![CDATA[matteocisilino]]></dc:creator>
		<pubDate>Sat, 17 Oct 2026 08:10:45 +0000</pubDate>
				<category><![CDATA[Serveurs et bases de données]]></category>
		<category><![CDATA[Docker]]></category>
		<category><![CDATA[MySQL]]></category>
		<category><![CDATA[nftables]]></category>
		<category><![CDATA[pare-feu]]></category>
		<guid isPermaLink="false">https://www.cisilino.com/?p=273</guid>

					<description><![CDATA[<p>Comment faire tourner MySQL 5.7 dans Docker sur un serveur moderne sans l'exposer à internet : docker-compose, port fermé et pare-feu nftables.</p>
<p>The post <a href="https://www.cisilino.com/fr/2026/10/17/mysql-5-7-docker-nftables-pare-feu/">MySQL 5.7 dans Docker protégé par nftables</a> first appeared on <a href="https://www.cisilino.com/fr/">Matteo Cisilino</a>.</p>]]></description>
										<content:encoded><![CDATA[<h2 class="wp-block-heading">Préambule : MySQL 5.7 dans Docker</h2>
<p>Nous avions besoin d&rsquo;une base <strong>MySQL 5.7 dans Docker</strong>. Le serveur, lui, tourne pour le reste avec une distribution récente. Certaines applications héritées ne sont pas compatibles avec MySQL 8. Les raisons : changement du plugin d&rsquo;authentification par défaut, modes SQL plus stricts, nouveaux mots réservés. Les migrer rapidement n&rsquo;était pas envisageable.</p>
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" src="https://www.cisilino.com/wp-content/uploads/2026/10/mysql-5-7-docker-nftables-pare-feu.webp" alt="MySQL 5.7 dans Docker : une base de données dans un cadre à contour orange en pointillés, avec un cercle et une barre" width="1200" height="630"/></figure>
<p>Installer une version aussi ancienne directement sur l&rsquo;hôte pose plusieurs problèmes. Les dépôts officiels des distributions actuelles ne la proposent plus. Les paquets tiers risquent d&rsquo;entrer en conflit avec le reste du système. Une mise à jour de l&rsquo;OS pourrait la casser.</p>
<p>Nous avons donc décidé de l&rsquo;<strong>isoler dans un conteneur Docker</strong>. L&rsquo;hôte reste propre et à jour. La version de la base est figée et reproductible. Données et configuration se trouvent dans un dossier facile à sauvegarder et à déplacer.</p>
<p>Cet article raconte comment nous l&rsquo;avons configurée et, surtout, les obstacles rencontrés en chemin. Il y a l&rsquo;exposition involontaire du port 3306 et une erreur iptables au redémarrage. Il y a aussi l&rsquo;écriture d&rsquo;un pare-feu avec <strong>nftables</strong> qui ne laisse passer que les IP autorisées.</p>
<blockquote class="wp-block-quote">
<p><strong>Note :</strong> MySQL 5.7 a atteint sa fin de vie (EOL) en octobre 2023. Il ne reçoit plus de correctifs de sécurité. C&rsquo;est une solution de transition : l&rsquo;objectif reste de migrer vers MySQL 8.x ou MariaDB. C&rsquo;est précisément pour cela qu&rsquo;il est encore plus important de ne pas la laisser exposée.</p>
</blockquote>
<hr class="wp-block-separator has-alpha-channel-opacity"/>
<h2 class="wp-block-heading">1. Structure du dossier</h2>
<pre class="wp-block-code"><code>/opt/mysql57/
├── docker-compose.yml
├── .env
├── conf.d/        # configurazioni personalizzate (my.cnf aggiuntivi)
├── data/          # dati del database (volume persistente)
└── logs/
</code></pre>
<p>Le fichier <code>.env</code> contient les identifiants, afin qu&rsquo;ils ne se retrouvent pas dans le compose. Voici son contenu :</p>
<pre class="wp-block-code"><code>TZ=Europe/Rome
MYSQL_ROOT_PASSWORD=una_password_molto_lunga_e_casuale
MYSQL_DATABASE=nome_db
MYSQL_USER=nome_utente
MYSQL_PASSWORD=altra_password_robusta
</code></pre>
<p>Protégez-le avec <code>chmod 600 .env</code>.</p>
<h2 class="wp-block-heading">2. Le docker-compose.yml</h2>
<pre class="wp-block-code"><code>services:
  mysql57:
    image: mysql:5.7
    container_name: mysql57
    restart: unless-stopped
    env_file:
      - .env
    ports:
      - "3306:3306"
    environment:
      TZ: "${TZ:-Europe/Rome}"
      MYSQL_ROOT_PASSWORD: "${MYSQL_ROOT_PASSWORD}"
      MYSQL_DATABASE: "${MYSQL_DATABASE}"
      MYSQL_USER: "${MYSQL_USER}"
      MYSQL_PASSWORD: "${MYSQL_PASSWORD}"
    command:
      - --default-authentication-plugin=mysql_native_password
    volumes:
      - ./data:/var/lib/mysql
      - ./conf.d:/etc/mysql/conf.d:ro
      - ./logs:/var/log/mysql
    healthcheck:
      test: &#91;"CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -uroot -p$$MYSQL_ROOT_PASSWORD --silent"]
      interval: 20s
      timeout: 5s
      retries: 10
      start_period: 45s
    ulimits:
      nofile:
        soft: 65535
        hard: 65535
</code></pre>
<p>Quelques choix à noter :</p>
<ul class="wp-block-list">
<li><strong><code>restart: unless-stopped</code></strong> : la base redémarre toute seule après un redémarrage du serveur.</li>
<li><strong>Volumes en bind mount</strong> : données, configuration et logs sont sur l&rsquo;hôte. Sauvegardes et migrations se résument donc à des copies de dossiers.</li>
<li><strong>Healthcheck</strong> : permet aux autres services d&rsquo;attendre que MySQL soit vraiment prêt. Le <code>$$</code> sert à faire développer la variable dans le conteneur et non par Compose.</li>
<li><strong><code>ulimits nofile</code></strong> : évite l&rsquo;erreur « too many open files » sous charge.</li>
</ul>
<h2 class="wp-block-heading">3. Premier problème : MySQL 5.7 dans Docker exposé</h2>
<p>La ligne <code>- "3306:3306"</code> équivaut à <code>0.0.0.0:3306:3306</code>. Docker publie le port sur <strong>toutes les interfaces</strong> de l&rsquo;hôte, y compris la publique.</p>
<p>Le plus insidieux : Docker écrit ses propres règles directement dans le pare-feu du noyau. Elles sont évaluées avant celles d&rsquo;UFW ou de firewalld. Avoir un pare-feu actif <strong>ne suffit pas</strong> : le port reste joignable.</p>
<p>Voici les solutions possibles.</p>
<ol class="wp-block-list">
<li><strong>Localhost uniquement</strong> : <code>"127.0.0.1:3306:3306"</code>, si la base ne sert qu&rsquo;à l&rsquo;hôte ou via un tunnel SSH.</li>
<li><strong>Aucun port publié</strong> : si les clients sont d&rsquo;autres conteneurs, un réseau Docker partagé suffit.</li>
<li><strong>IP précise</strong> : publier uniquement sur une adresse d&rsquo;un réseau privé/VPN.</li>
<li><strong>Port public avec filtrage par IP</strong> : notre cas. Un client externe (une instance phpMyAdmin sur un autre serveur) doit pouvoir se connecter.</li>
</ol>
<p>Pour le cas 4, il faut un pare-feu qui filtre correctement le trafic des ports publiés par Docker. La <a href="https://docs.docker.com/engine/network/packet-filtering-firewalls/" target="_blank" rel="noopener">documentation de Docker sur le filtrage des paquets</a> explique comment Docker interagit avec les règles de l&rsquo;hôte.</p>
<h2 class="wp-block-heading">4. Deuxième problème : erreur iptables au démarrage</h2>
<p>Après avoir touché au pare-feu, <code>docker compose up</code> a renvoyé cette erreur.</p>
<pre class="wp-block-code"><code>failed to create network mysql57_default: Error response from daemon:
Failed to Setup IP tables: Unable to enable ACCEPT OUTGOING rule:
(iptables failed: iptables --wait -t filter -A DOCKER-FORWARD -i br-... -j ACCEPT:
iptables: No chain/target/match by that name.
</code></pre>
<p>L&rsquo;erreur ne dépend pas du compose. La chaîne <code>DOCKER-FORWARD</code> (introduite dans les versions récentes de Docker) n&rsquo;existait plus. Elle avait été supprimée par quelque chose qui avait nettoyé les règles <strong>après</strong> le démarrage du démon. Un redémarrage de Docker (<code>systemctl restart docker</code>) la recrée. Mais le problème revient tant que le vrai coupable n&rsquo;est pas éliminé.</p>
<h3 class="wp-block-heading">Le coupable : <code>flush ruleset</code></h3>
<p>Notre <code>/etc/nftables.conf</code> commençait par cette ligne.</p>
<pre class="wp-block-code"><code>flush ruleset
</code></pre>
<p>Cette commande supprime <strong>toutes</strong> les tables nftables, y compris celles créées par Docker. De plus, le fichier contenait une copie statique des règles de Docker. Le nom du bridge (<code>br-278550235c3f</code>) et l&rsquo;IP du conteneur (<code>172.18.0.2</code>) y étaient écrits à la main. Or le nom du bridge change chaque fois que le réseau Compose est recréé. La copie devenait aussitôt obsolète. La table <code>ip nat</code> entrait en conflit avec celle gérée par Docker.</p>
<p><strong>Règle d&rsquo;or :</strong> avec Docker, dans le fichier nftables on n&rsquo;utilise pas <code>flush ruleset</code>. On ne copie pas ses règles. On ne gère que son propre pare-feu, dans des tables dédiées.</p>
<h2 class="wp-block-heading">5. Le pare-feu avec nftables</h2>
<p>Le principe est le suivant pour MySQL 5.7 dans Docker. Les ports publiés par Docker sont réécrits par DNAT, puis transférés par l&rsquo;hôte vers le conteneur. Ils traversent donc la chaîne <strong><code>forward</code></strong>, pas <code>input</code>. Une règle <code>drop</code> dans une chaîne à nous, <code>forward</code>, rejette le paquet. Sa priorité est légèrement plus basse, donc elle est évaluée avant. Les règles <code>accept</code> de Docker n&rsquo;y changent rien.</p>
<p>Pour ne pas toucher aux tables des autres, nous utilisons le motif <code>table ...</code> + <code>delete table ...</code> : il rend le fichier ré-exécutable sans erreur et sans <code>flush</code>.</p>
<p><code>/etc/nftables.conf</code>:</p>
<pre class="wp-block-code"><code>#!/usr/sbin/nft -f

# NIENTE "flush ruleset": cancellerebbe anche le tabelle di Docker

# ---------------- firewall host ----------------
table inet filter
delete table inet filter

table inet filter {

    chain input {
        type filter hook input priority filter; policy drop;
        ct state established,related accept
        iifname "lo" accept
        ip6 nexthdr icmpv6 accept
        iifname "enp7s0" accept                       # rete privata
        ip saddr 203.0.113.20 tcp dport 22 accept     # SSH da host fidato
        ip saddr 203.0.113.30 tcp dport 22 accept     # SSH da VPN
        ip saddr 203.0.113.20 icmp type echo-request accept
        tcp dport { 80, 443 } accept
    }

    chain output {
        type filter hook output priority filter; policy accept;
    }
}

# ---------------- guardia MySQL (porta pubblicata da Docker) ----------------
table ip mysql_guard
delete table ip mysql_guard

table ip mysql_guard {
    set allowed {
        type ipv4_addr
        flags interval
        elements = {
            203.0.113.10        # phpMyAdmin
        }
    }

    chain forward {
        type filter hook forward priority filter - 5; policy accept;
        iifname "enp7s0" accept
        ct status dnat tcp dport 3306 ip saddr @allowed accept
        ct status dnat tcp dport 3306 counter drop
    }
}
</code></pre>
<p>Voici comment fonctionne la garde.</p>
<ul class="wp-block-list">
<li><code>ct status dnat</code> ne sélectionne que les paquets passés par le DNAT, c&rsquo;est-à-dire ceux destinés aux ports publiés par Docker.</li>
<li>La première règle accepte ceux qui sont dans l&rsquo;ensemble <code>allowed</code>. La seconde rejette tous les autres et les compte (<code>counter</code>).</li>
<li>Le trafic local (<code>127.0.0.1</code>) et celui entre conteneurs du même réseau ne passent pas par le forward. Ils ne sont donc pas touchés.</li>
</ul>
<blockquote class="wp-block-quote">
<p><strong>Compatibilité :</strong> dans une première version, nous utilisions <code>ct original proto-dst 3306</code>. Mais certaines versions de <code>nft</code> répondent par <em>« Can&rsquo;t parse symbolic invalid expressions »</em>. Dans le hook <code>forward</code>, le DNAT a déjà eu lieu. Le port du conteneur coïncide avec le port publié (<code>3306:3306</code>). L&rsquo;expression <code>tcp dport 3306</code> combinée à <code>ct status dnat</code> est donc équivalente et fonctionne partout. Si vous changez le mapping (par exemple <code>3307:3306</code>), la règle doit être adaptée au port interne.</p>
</blockquote>
<h3 class="wp-block-heading">Appliquer les modifications sans se verrouiller dehors</h3>
<p>Gardez une session SSH de secours ouverte. Puis lancez les commandes suivantes.</p>
<pre class="wp-block-code"><code>nft -c -f /etc/nftables.conf     # solo verifica della sintassi
nft delete table ip nat          # una tantum: elimina la vecchia copia statica
nft -f /etc/nftables.conf
systemctl restart docker         # Docker ricrea le sue tabelle pulite
cd /opt/mysql57 &amp;&amp; docker compose up -d
</code></pre>
<p>Pour le chargement au démarrage, assurez-vous que le service est activé (<code>systemctl enable --now nftables</code>). Vérifiez aussi que <code>/etc/nftables.conf</code> ne contient pas <code>flush ruleset</code>.</p>
<h3 class="wp-block-heading">Gérer les IP à l&rsquo;exécution</h3>
<pre class="wp-block-code"><code>nft add element ip mysql_guard allowed { 203.0.113.50 }
nft delete element ip mysql_guard allowed { 203.0.113.50 }
</code></pre>
<p>Pour le rendre permanent, mettez aussi à jour l&rsquo;ensemble dans le fichier.</p>
<h2 class="wp-block-heading">6. Vérification</h2>
<p>Depuis le serveur autorisé, testez.</p>
<pre class="wp-block-code"><code>nc -zv IP_PUBBLICO_DB 3306        # deve rispondere "succeeded"
</code></pre>
<p>Depuis une autre IP, testez à nouveau.</p>
<pre class="wp-block-code"><code>nmap -Pn -p 3306 IP_PUBBLICO_DB   # deve risultare "filtered" / timeout
</code></pre>
<p>Sur le serveur, le compteur de la règle de rejet doit augmenter à chaque tentative non autorisée.</p>
<pre class="wp-block-code"><code>nft list chain ip mysql_guard forward
ss -tlnp | grep 3306
</code></pre>
<h2 class="wp-block-heading">7. Seconde barrière : les utilisateurs MySQL</h2>
<p>Le pare-feu ne remplace pas les droits de la base de données. Créez l&rsquo;utilisateur avec un hôte limité à l&rsquo;IP du client.</p>
<pre class="wp-block-code"><code>CREATE USER 'pma_user'@'203.0.113.10' IDENTIFIED BY 'password_robusta';
GRANT ALL ON nome_db.* TO 'pma_user'@'203.0.113.10';
FLUSH PRIVILEGES;
</code></pre>
<p>Si la connexion TCP aboutit mais que MySQL répond <code>Access denied</code>, le problème est ici. Il n&rsquo;est pas dans le pare-feu.</p>
<h2 class="wp-block-heading">8. À retenir</h2>
<ul class="wp-block-list">
<li><strong>Docker et le pare-feu de l&rsquo;hôte ne s&rsquo;ignorent pas</strong>. Comprendre quelle chaîne le trafic traverse (<code>input</code> ou <code>forward</code>) est la clé de tout.</li>
<li><strong>Jamais <code>flush ruleset</code></strong> sur un hôte avec Docker ; utilisez des tables dédiées.</li>
<li><strong>Ne copiez pas les règles générées par Docker</strong> dans vos fichiers : noms de bridge et IP changent.</li>
<li>Si vous avez déjà exposé le port, vérifiez les logs dans <code>./logs</code>. Envisagez aussi de <strong>changer les mots de passe</strong>, surtout celui de root.</li>
<li>Quand c&rsquo;est possible, préférez un <strong>VPN (WireGuard) ou un tunnel SSH</strong> à l&rsquo;exposition publique d&rsquo;une base de données.</li>
<li>Planifiez la <strong>migration</strong> de MySQL 5.7 vers une version prise en charge.</li>
</ul>
<p>Si MySQL refuse de redémarrer après une mise à jour, voyez aussi <a href="https://www.cisilino.com/fr/2026/10/13/mysql-ne-demarre-plus-debian-12/">MySQL qui ne démarre plus sous Debian 12</a>.</p>
<p>Vous avez des questions ou une solution différente pour MySQL 5.7 dans Docker ? Écrivez-le dans les commentaires.</p><p>The post <a href="https://www.cisilino.com/fr/2026/10/17/mysql-5-7-docker-nftables-pare-feu/">MySQL 5.7 dans Docker protégé par nftables</a> first appeared on <a href="https://www.cisilino.com/fr/">Matteo Cisilino</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://www.cisilino.com/fr/2026/10/17/mysql-5-7-docker-nftables-pare-feu/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>MySQL ne démarre plus après la mise à jour vers Debian 12 : cause et solution</title>
		<link>https://www.cisilino.com/fr/2026/10/13/mysql-ne-demarre-plus-debian-12/</link>
					<comments>https://www.cisilino.com/fr/2026/10/13/mysql-ne-demarre-plus-debian-12/#respond</comments>
		
		<dc:creator><![CDATA[matteocisilino]]></dc:creator>
		<pubDate>Tue, 13 Oct 2026 07:50:06 +0000</pubDate>
				<category><![CDATA[Serveurs et bases de données]]></category>
		<category><![CDATA[Debian 12]]></category>
		<category><![CDATA[MariaDB]]></category>
		<category><![CDATA[mise à niveau]]></category>
		<category><![CDATA[MySQL]]></category>
		<guid isPermaLink="false">https://www.cisilino.com/?p=259</guid>

					<description><![CDATA[<p>Après la mise à jour vers Debian 12, MySQL ne démarre plus et les logs mentionnent mariadb.conf.d. Comment identifier la cause et la corriger sans toucher aux données.</p>
<p>The post <a href="https://www.cisilino.com/fr/2026/10/13/mysql-ne-demarre-plus-debian-12/">MySQL ne démarre plus après la mise à jour vers Debian 12 : cause et solution</a> first appeared on <a href="https://www.cisilino.com/fr/">Matteo Cisilino</a>.</p>]]></description>
										<content:encoded><![CDATA[<h2 class="wp-block-heading">Le symptôme : mysql.service à l&rsquo;état failed</h2>
<p>Sous Debian 12, MySQL ne démarre plus. Le service apparaît en échec et le log signale l&rsquo;erreur <code>Can't read dir of '/etc/mysql/mariadb.conf.d/'</code>.</p>
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" src="https://www.cisilino.com/wp-content/uploads/2026/10/mysql-ne-demarre-pas-debian-12.webp" alt="MySQL ne démarre plus : un cylindre de base de données avec un cercle d'avertissement orange à côté" width="1200" height="630"/></figure>
<p>Voici la sortie complète :</p>
<pre class="wp-block-code"><code># service mysql status
× mysql.service - MySQL Community Server
     Loaded: loaded (/lib/systemd/system/mysql.service; enabled; preset: enabled)
     Active: failed (Result: exit-code)
     Status: "Server startup in progress"

mysqld: Can't read dir of '/etc/mysql/mariadb.conf.d/' (OS errno 2 - No such file or directory)
mysqld: &#91;ERROR] Stopped processing the 'includedir' directive in file /etc/mysql/my.cnf at line 29.
mysqld: &#91;ERROR] Fatal error in defaults handling. Program aborted!
systemd&#91;1]: Failed to start mysql.service - MySQL Community Server.</code></pre>
<p>En vérifiant les paquets installés, MySQL est bien présent (paquets <code>mysql-community-server</code> 8.0 du dépôt officiel Oracle) :</p>
<pre class="wp-block-code"><code>dpkg -l | grep mysql</code></pre>
<h2 class="wp-block-heading">La cause : l&rsquo;alternative my.cnf pointe vers mariadb.cnf</h2>
<p>Sous Debian, le fichier <code>/etc/mysql/my.cnf</code> n&rsquo;est pas un vrai fichier. C&rsquo;est un lien géré par <strong>update-alternatives</strong>. Plusieurs paquets peuvent enregistrer leur propre version. En mode automatique, celle qui a la priorité la plus haute l&#8217;emporte.</p>
<p>Pendant la mise à jour, le paquet <strong>mariadb-common</strong> a été installé (puis retiré). Il enregistre <code>/etc/mysql/mariadb.cnf</code> avec une <strong>priorité de 500</strong>, supérieure à celle de <code>/etc/mysql/mysql.cnf</code> (priorité 300). Le paquet a été retiré mais pas entièrement purgé (état <code>rc</code>). Sa configuration est donc restée active. À la ligne 29 de ce fichier se trouve l&rsquo;inclusion de <code>/etc/mysql/mariadb.conf.d/</code>, un répertoire qui n&rsquo;existe pas. MySQL lit la configuration, ne trouve pas le répertoire et s&rsquo;arrête.</p>
<p><strong>Bonne nouvelle :</strong> les données dans <code>/var/lib/mysql</code> n&rsquo;ont pas été touchées. Ce n&rsquo;est qu&rsquo;un problème de configuration.</p>
<h2 class="wp-block-heading">Diagnostic : vérifier vers où pointe my.cnf</h2>
<pre class="wp-block-code"><code>ls -l /etc/mysql/my.cnf /etc/alternatives/my.cnf
update-alternatives --display my.cnf
sed -n '25,32p' /etc/mysql/my.cnf
dpkg -l | grep -i mariadb</code></pre>
<p>Si le problème est bien celui-ci, la sortie ressemblera à ceci :</p>
<pre class="wp-block-code"><code>/etc/alternatives/my.cnf -&gt; /etc/mysql/mariadb.cnf
/etc/mysql/my.cnf -&gt; /etc/alternatives/my.cnf

my.cnf - modalità automatica
  la versione migliore del collegamento è /etc/mysql/mariadb.cnf
  il collegamento attualmente punta a /etc/mysql/mariadb.cnf
/etc/mysql/mariadb.cnf - priorità 500
/etc/mysql/my.cnf.fallback - priorità 100
/etc/mysql/mysql.cnf - priorità 300

!includedir /etc/mysql/conf.d/
!includedir /etc/mysql/mariadb.conf.d/

rc  mariadb-common   1:10.11.18-0+deb12u1   all   MariaDB common configuration files</code></pre>
<h2 class="wp-block-heading">Solution pas à pas</h2>
<h3 class="wp-block-heading">1. Sauvegarder la configuration</h3>
<pre class="wp-block-code"><code>cp -a /etc/mysql /root/etc-mysql-backup-$(date +%F)</code></pre>
<h3 class="wp-block-heading">2. Supprimer les résidus de mariadb-common</h3>
<p>La purge supprime <code>mariadb.cnf</code> et son entrée dans update-alternatives :</p>
<pre class="wp-block-code"><code>apt purge mariadb-common</code></pre>
<h3 class="wp-block-heading">3. Repointer my.cnf vers mysql.cnf</h3>
<pre class="wp-block-code"><code>update-alternatives --remove my.cnf /etc/mysql/mariadb.cnf 2&gt;/dev/null
update-alternatives --set my.cnf /etc/mysql/mysql.cnf</code></pre>
<h3 class="wp-block-heading">4. Vérifier la configuration</h3>
<pre class="wp-block-code"><code>update-alternatives --display my.cnf
cat /etc/mysql/my.cnf
my_print_defaults mysqld</code></pre>
<p>Désormais <code>my.cnf</code> doit pointer vers <code>/etc/mysql/mysql.cnf</code> et inclure <code>/etc/mysql/conf.d/</code> et <code>/etc/mysql/mysql.conf.d/</code>. Si <code>my_print_defaults mysqld</code> affiche les paramètres sans erreur, la configuration est correcte.</p>
<h3 class="wp-block-heading">5. Démarrer MySQL</h3>
<pre class="wp-block-code"><code>systemctl start mysql
systemctl status mysql
journalctl -u mysql -n 50 --no-pager</code></pre>
<h2 class="wp-block-heading">Si my.cnf n&rsquo;est pas un lien symbolique</h2>
<p>Si <code>/etc/mysql/my.cnf</code> est un fichier normal ou si <code>/etc/mysql/mysql.cnf</code> n&rsquo;existe pas, on peut recréer la configuration à la main :</p>
<pre class="wp-block-code"><code>mv /etc/mysql/my.cnf /etc/mysql/my.cnf.mariadb.bak
cat &gt; /etc/mysql/my.cnf &lt;&lt;'EOF'
!includedir /etc/mysql/conf.d/
!includedir /etc/mysql/mysql.conf.d/
EOF
mkdir -p /etc/mysql/conf.d
systemctl start mysql</code></pre>
<h2 class="wp-block-heading">Solution rapide quand MySQL ne démarre plus</h2>
<p>S&rsquo;il faut remettre la base en ligne tout de suite, créez le dossier manquant. Ce n&rsquo;est toutefois qu&rsquo;un pis-aller. MySQL continuerait à lire la configuration prévue pour MariaDB. Il vaut donc mieux appliquer la solution complète dès que possible.</p>
<pre class="wp-block-code"><code>mkdir /etc/mysql/mariadb.conf.d &amp;&amp; systemctl start mysql</code></pre>
<h2 class="wp-block-heading">Comment éviter que cela se reproduise</h2>
<p>Il est utile de comprendre quelle mise à jour a installé MariaDB, en consultant l&rsquo;historique d&rsquo;apt :</p>
<pre class="wp-block-code"><code>grep -i -B3 -A3 mariadb /var/log/apt/history.log*</code></pre>
<p>Pour empêcher <code>mariadb-common</code> d&rsquo;être réinstallé à l&rsquo;avenir, on peut le bloquer avec un pin APT à priorité négative :</p>
<pre class="wp-block-code"><code>cat &gt; /etc/apt/preferences.d/no-mariadb &lt;&lt;'EOF'
Package: mariadb-*
Pin: release *
Pin-Priority: -1
EOF</code></pre>
<p>Après chaque mise à jour importante, c&rsquo;est aussi une bonne habitude de vérifier l&rsquo;état de l&rsquo;alternative avec <code>update-alternatives --display my.cnf</code>.</p>
<p>Les <a href="https://www.debian.org/releases/bookworm/amd64/release-notes/" target="_blank" rel="noopener">notes de version de Debian 12</a> décrivent les changements de cette version.</p>
<p>Un autre piège de configuration à connaître : <a href="https://www.cisilino.com/fr/2026/10/17/mysql-5-7-docker-nftables-pare-feu/">MySQL 5.7 dans Docker et le port 3306 exposé</a>.</p>
<h2 class="wp-block-heading">Questions fréquentes</h2>
<h3 class="wp-block-heading">Vais-je perdre les données de mes bases ?</h3>
<p>Non. Le problème ne concerne que les fichiers de configuration dans <code>/etc/mysql</code> ; les données dans <code>/var/lib/mysql</code> restent intactes.</p>
<h3 class="wp-block-heading">Pourquoi Debian a-t-il choisi mariadb.cnf ?</h3>
<p>Parce que l&rsquo;alternative <code>my.cnf</code> était en mode automatique et que <code>mariadb.cnf</code> a une priorité de 500, supérieure aux 300 de <code>mysql.cnf</code>. Avec <code>update-alternatives --set</code>, le choix devient manuel et ne change plus tout seul.</p>
<h3 class="wp-block-heading">Cela vaut-il aussi pour Ubuntu ?</h3>
<p>Oui, le mécanisme de <code>update-alternatives</code> pour <code>my.cnf</code> est le même sur Ubuntu et sur les autres distributions dérivées de Debian.</p>
<h2 class="wp-block-heading">Conclusion</h2>
<p>L&rsquo;erreur <strong>« Can&rsquo;t read dir of /etc/mysql/mariadb.conf.d/ »</strong> n&rsquo;indique pas que MySQL est endommagé. Quand MySQL ne démarre plus, le coupable est une configuration résiduelle de MariaDB. Elle est restée active via update-alternatives. En supprimant <code>mariadb-common</code> avec <code>apt purge</code> et en remettant <code>my.cnf</code> sur <code>mysql.cnf</code>, MySQL refonctionne normalement.</p></p><p>The post <a href="https://www.cisilino.com/fr/2026/10/13/mysql-ne-demarre-plus-debian-12/">MySQL ne démarre plus après la mise à jour vers Debian 12 : cause et solution</a> first appeared on <a href="https://www.cisilino.com/fr/">Matteo Cisilino</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://www.cisilino.com/fr/2026/10/13/mysql-ne-demarre-plus-debian-12/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
