<?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>Quelques pensées</title>
	<atom:link href="https://blog.neurozone.fr/feed/" rel="self" type="application/rss+xml" />
	<link>https://blog.neurozone.fr/</link>
	<description>Terres dévastées</description>
	<lastBuildDate>Fri, 06 Nov 2026 19:05:23 +0000</lastBuildDate>
	<language>fr-FR</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	

<image>
	<url>https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2024/03/cropped-2.png?fit=32%2C32&#038;ssl=1</url>
	<title>Quelques pensées</title>
	<link>https://blog.neurozone.fr/</link>
	<width>32</width>
	<height>32</height>
</image> 
<site xmlns="com-wordpress:feed-additions:1">154219366</site>	<item>
		<title>Pas à pas : mettre à jour Pi-hole et vérifier qu&#8217;il bloque encore</title>
		<link>https://blog.neurozone.fr/2026/11/13/pas-a-pas-mettre-a-jour-pi-hole-tester-blocage/</link>
					<comments>https://blog.neurozone.fr/2026/11/13/pas-a-pas-mettre-a-jour-pi-hole-tester-blocage/#respond</comments>
		
		<dc:creator><![CDATA[Hécate Raven]]></dc:creator>
		<pubDate>Fri, 13 Nov 2026 09:00:00 +0000</pubDate>
				<category><![CDATA[Informatique]]></category>
		<category><![CDATA[Debian]]></category>
		<category><![CDATA[Français]]></category>
		<category><![CDATA[pas à pas]]></category>
		<category><![CDATA[Pi-hole]]></category>
		<category><![CDATA[Raspberry Pi]]></category>
		<guid isPermaLink="false">https://blog.neurozone.fr/?p=4951</guid>

					<description><![CDATA[<p>Mettre à jour Pi-hole en six étapes : sauvegarde Teleporter, pihole -up, puis test du blocage. Les commandes que j'utilise sur mon réseau domestique.</p>
<p>L’article <a href="https://blog.neurozone.fr/2026/11/13/pas-a-pas-mettre-a-jour-pi-hole-tester-blocage/">Pas à pas : mettre à jour Pi-hole et vérifier qu&rsquo;il bloque encore</a> est apparu en premier sur <a href="https://blog.neurozone.fr">Quelques pensées</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Pour mettre à jour Pi-hole sur mon réseau domestique, je lance une seule commande, mais je ne lui fais jamais confiance seule. Dans ce pas à pas, je décris comment mettre à jour Pi-hole en six étapes. L&rsquo;étape qui compte le plus vérifie que le blocage fonctionne encore, et pas seulement que les noms se résolvent.</p>



<div class="quick-answer" style="background:#eef3fc;border-left:5px solid #4c7bd9;border-radius:4px;padding:16px 20px;margin:24px 0;color:#1f2937">
<p style="margin:0 0 8px"><strong>Réponse rapide</strong> — mettre à jour Pi-hole et prouver qu&rsquo;il bloque encore.</p>
<pre style="margin:0 0 8px;overflow:auto"><code>pihole -v                                   # versions avant
pihole-FTL --teleporter                     # sauvegarde de la configuration
PIHOLE_SKIP_OS_CHECK=true pihole -up        # mise à jour
dig +short doubleclick.net @192.0.2.19      # doit afficher 0.0.0.0 (bloqué)</code></pre>
<p style="margin:0;font-size:0.9em">Les détails, les explications et les vérifications suivent plus bas.</p>
</div>



<p class="wp-block-paragraph">Mon Pi-hole tourne sur un Raspberry Pi avec Debian 12 et sert de seul serveur DNS de la maison. Par conséquent, chaque redémarrage du service DNS provoque une petite coupure. Mon rôle Ansible ne gère pas les versions de Pi-hole, donc je fais la mise à jour à la main. J&rsquo;envoie les commandes avec une commande Ansible ad hoc depuis ma machine de contrôle, mais je montre ici les commandes elles-mêmes.</p>



<figure class="wp-block-image size-full"><a href="https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/mettre-a-jour-pi-hole-cinq-etapes_fr.png?ssl=1"><img data-recalc-dims="1" fetchpriority="high" decoding="async" width="629" height="227" src="https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/mettre-a-jour-pi-hole-cinq-etapes_fr.png?resize=629%2C227&#038;ssl=1" alt="Mettre à jour Pi-hole en cinq étapes : versions, sauvegarde Teleporter, mise à jour, test du blocage, comparaison" class="wp-image-4950" srcset="https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/mettre-a-jour-pi-hole-cinq-etapes_fr.png?w=1720&amp;ssl=1 1720w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/mettre-a-jour-pi-hole-cinq-etapes_fr.png?resize=300%2C108&amp;ssl=1 300w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/mettre-a-jour-pi-hole-cinq-etapes_fr.png?resize=1024%2C369&amp;ssl=1 1024w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/mettre-a-jour-pi-hole-cinq-etapes_fr.png?resize=768%2C277&amp;ssl=1 768w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/mettre-a-jour-pi-hole-cinq-etapes_fr.png?resize=1536%2C554&amp;ssl=1 1536w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/mettre-a-jour-pi-hole-cinq-etapes_fr.png?resize=900%2C324&amp;ssl=1 900w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/mettre-a-jour-pi-hole-cinq-etapes_fr.png?resize=1280%2C461&amp;ssl=1 1280w" sizes="(max-width: 629px) 100vw, 629px" /></a><figcaption class="wp-element-caption">Les cinq contrôles autour d&rsquo;une commande. L&rsquo;étape 4 est celle qui a détecté un vrai problème.</figcaption></figure>



<h2 class="wp-block-heading">Avant de commencer</h2>



<ul class="wp-block-list"><li>Un shell sur la machine Pi-hole avec les droits root.</li><li>L&rsquo;adresse de votre Pi-hole et celle de votre routeur. Dans cet article, ce sont 192.0.2.19 et 192.0.2.1.</li><li>Quelques minutes de libres. Dans mes notes, le DNS du réseau local redémarre pendant quelques secondes, et le blocage des publicités peut s&rsquo;arrêter pendant environ cinq minutes (voir l&rsquo;étape 4).</li><li>Les notes de version de Pi-hole. La page officielle de mise à jour demande de les lire pour chaque composant avant de mettre à jour, surtout pour une version majeure.</li></ul>



<h2 class="wp-block-heading">Comment mettre à jour Pi-hole, étape par étape</h2>



<h3 class="wp-block-heading">Étape 1 : vérifier les versions</h3>



<p class="wp-block-paragraph">D&rsquo;abord, je liste ce que j&rsquo;utilise maintenant. La sortie donne les versions de Core, de Web et de FTL.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">pihole -v</pre>



<h3 class="wp-block-heading">Étape 2 : sauvegarder la configuration</h3>



<p class="wp-block-paragraph">Ensuite, je sauvegarde deux fois : avec Teleporter, l&rsquo;export propre à Pi-hole, et avec une simple copie du fichier TOML. Je travaille dans un dossier daté, pour que les deux restent ensemble.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">mkdir -p /root/pre-update-$(date +%Y%m%d) &amp;&amp; cd $_   # un dossier daté, puis on y entre
pihole-FTL --teleporter                                # l'archive Teleporter
cp -a /etc/pihole/pihole.toml pihole.toml.bak          # une copie du fichier de configuration
ls -la                                                 # vérifier que les deux sont là</pre>



<p class="wp-block-paragraph">La copie du fichier TOML sert encore à l&rsquo;étape 5.</p>



<h3 class="wp-block-heading">Étape 3 : lancer la mise à jour</h3>



<p class="wp-block-paragraph">Vient maintenant la mise à jour elle-même. Je garde les 20 dernières lignes de sa sortie, j&rsquo;affiche à nouveau les versions, et je demande à systemd si le service DNS tourne.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">PIHOLE_SKIP_OS_CHECK=true pihole -up 2&gt;&amp;1 | tail -20   # mise à jour, on garde les 20 dernières lignes
pihole -v                                              # versions après la mise à jour
systemctl is-active pihole-FTL                         # doit afficher : active</pre>



<p class="wp-block-paragraph">Mes notes utilisent PIHOLE_SKIP_OS_CHECK=true, qui saute le contrôle du système d&rsquo;exploitation par l&rsquo;installeur. La page de mise à jour de Pi-hole ne le mentionne pas, donc testez-le sur votre système avant de le copier.</p>



<h3 class="wp-block-heading">Étape 4 : tester le blocage, pas seulement la résolution</h3>



<p class="wp-block-paragraph">Cette étape est obligatoire pour moi. Le 29 octobre 2026, FTL, le moteur DNS de Pi-hole, a pris de vitesse le remplacement de la base gravity. Le journal affichait <code>no such table: main.gravity</code> dans <code>/var/log/pihole/FTL.log</code>, et Pi-hole n&rsquo;a rien bloqué pendant environ cinq minutes. Cela s&rsquo;est arrêté seulement quand FTL a écrit son propre message : <code>Gravity database has been replaced, reloading now</code>.</p>



<p class="wp-block-paragraph">Pendant ces minutes, les noms se résolvaient toujours. Donc un simple test de résolution m&rsquo;aurait dit que tout allait bien.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">dig +short doubleclick.net @192.0.2.19   # doit afficher 0.0.0.0 (bloqué)
dig +short example.com @192.0.2.19       # doit afficher une vraie adresse
dig +short doubleclick.net @192.0.2.1    # via le routeur : doit aussi afficher 0.0.0.0</pre>



<p class="wp-block-paragraph">La troisième requête passe par le routeur. Sur mon réseau, le routeur transmet ses requêtes à Pi-hole, donc une réponse bloquée à cet endroit prouve tout le chemin.</p>



<p class="wp-block-paragraph">Si le blocage ne fonctionne pas après cinq minutes, je recharge la configuration DNS, puis je lis le journal de FTL.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">pihole reloaddns</pre>



<h3 class="wp-block-heading">Étape 5 : vérifier que la configuration n&rsquo;a pas changé</h3>



<p class="wp-block-paragraph">Une mise à jour peut toucher la configuration. Je compare donc ma sauvegarde avec le fichier en place, en écartant les commentaires et les lignes qui contiennent des empreintes de mot de passe.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">diff &lt;(grep -vE '^\s*#|pwhash|password|totp' /root/pre-update-*/pihole.toml.bak) \
     &lt;(grep -vE '^\s*#|pwhash|password|totp' /etc/pihole/pihole.toml)</pre>



<p class="wp-block-paragraph">Aucune sortie signifie aucune différence.</p>



<h3 class="wp-block-heading">Étape 6 : synchroniser le mot de passe d&rsquo;administration s&rsquo;il a changé</h3>



<p class="wp-block-paragraph">Si le mot de passe d&rsquo;administration a changé pour une raison quelconque, je mets à jour mon gestionnaire de secrets le jour même. En août 2026, je l&rsquo;ai oublié une fois, et chaque déploiement de mon rôle Ansible a échoué jusqu&rsquo;à la correction.</p>



<h2 class="wp-block-heading">Vérifier que tout a fonctionné</h2>



<ul class="wp-block-list"><li><code>pihole -v</code> affiche les nouvelles versions.</li><li><code>systemctl is-active pihole-FTL</code> affiche <code>active</code>.</li><li>La première et la troisième requête <code>dig</code> affichent <code>0.0.0.0</code>, et la deuxième affiche une vraie adresse.</li><li>Le <code>diff</code> n&rsquo;affiche rien.</li></ul>



<h2 class="wp-block-heading">Si quelque chose se passe mal</h2>



<ul class="wp-block-list"><li>Le blocage ne fonctionne pas après cinq minutes : lancez <code>pihole reloaddns</code>, puis lisez <code>/var/log/pihole/FTL.log</code>.</li><li>Vous voulez revenir en arrière : <code>pihole -up</code> ne peut pas réinstaller une version précédente. À la place, restaurez l&rsquo;archive Teleporter sur une installation neuve avec <code>pihole-FTL --teleporter &lt;zip&gt;</code>.</li></ul>



<h2 class="wp-block-heading">Ce que j&rsquo;en retiens</h2>



<ul class="wp-block-list"><li>Je sauvegarde deux fois avant de mettre à jour Pi-hole, parce qu&rsquo;il n&rsquo;existe pas de retour en arrière.</li><li>Je teste le blocage, parce que la résolution seule a caché un trou de cinq minutes.</li><li>Je compare la configuration, parce que je veux savoir ce que la mise à jour a changé.</li></ul>



<h2 class="wp-block-heading">Sources</h2>



<ul class="wp-block-list"><li><a href="https://docs.pi-hole.net/main/update/">Documentation Pi-hole : mise à jour (en anglais)</a></li><li><a href="https://docs.pi-hole.net/ftldns/configfile/">Documentation Pi-hole : configuration de FTL, pihole-FTL &#8211;config (en anglais)</a></li><li>Mon propre manuel de procédure, dernier passage le 29 octobre 2026.</li></ul>



<h2 class="wp-block-heading">Articles liés</h2>



<ul class="wp-block-list"><li><a href="https://blog.neurozone.fr/?p=2043">Pas à pas : installer et configurer WireGuard entre un serveur et la maison</a></li><li><a href="https://blog.neurozone.fr/?p=1419">CrowdSec 1.8 sur Debian : réinstaller depuis le dépôt officiel, piège du postinst et vérifications</a></li><li><a href="https://blog.neurozone.fr/?p=2044">Pas à pas : les réglages MariaDB qui comptent vraiment, et comment les vérifier</a></li></ul>


<p>L’article <a href="https://blog.neurozone.fr/2026/11/13/pas-a-pas-mettre-a-jour-pi-hole-tester-blocage/">Pas à pas : mettre à jour Pi-hole et vérifier qu&rsquo;il bloque encore</a> est apparu en premier sur <a href="https://blog.neurozone.fr">Quelques pensées</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.neurozone.fr/2026/11/13/pas-a-pas-mettre-a-jour-pi-hole-tester-blocage/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">4951</post-id>	</item>
		<item>
		<title>Dans les fichiers d&#8217;une base de données (4/3+1) : SQLite</title>
		<link>https://blog.neurozone.fr/2026/11/12/dans-les-fichiers-d-une-base-de-donnees-sqlite/</link>
					<comments>https://blog.neurozone.fr/2026/11/12/dans-les-fichiers-d-une-base-de-donnees-sqlite/#respond</comments>
		
		<dc:creator><![CDATA[Hécate Raven]]></dc:creator>
		<pubDate>Thu, 12 Nov 2026 09:00:00 +0000</pubDate>
				<category><![CDATA[Informatique]]></category>
		<category><![CDATA[carnet d'admin]]></category>
		<category><![CDATA[Français]]></category>
		<category><![CDATA[série : Dans les fichiers d'une base de données]]></category>
		<category><![CDATA[SQLite]]></category>
		<category><![CDATA[stockage]]></category>
		<guid isPermaLink="false">https://blog.neurozone.fr/?p=5455</guid>

					<description><![CDATA[<p>Les fichiers SQLite lus octet par octet sur une vraie base : en-tête, pages d'arbre B, enregistrements, WAL et VACUUM, mesurés sur Rocky Linux 10.2.</p>
<p>L’article <a href="https://blog.neurozone.fr/2026/11/12/dans-les-fichiers-d-une-base-de-donnees-sqlite/">Dans les fichiers d&rsquo;une base de données (4/3+1) : SQLite</a> est apparu en premier sur <a href="https://blog.neurozone.fr">Quelques pensées</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">SQLite est l&rsquo;intrus de cette série : pas de serveur, pas de répertoire de données, pas de processus d&rsquo;arrière-plan. Les fichiers SQLite tiennent dans un seul fichier ordinaire que l&rsquo;on peut lire avec un vidage hexadécimal. Cet article prolonge les trois premiers, <a href="https://blog.neurozone.fr/?p=1787">InnoDB</a>, <a href="https://blog.neurozone.fr/?p=1788">PostgreSQL</a> et <a href="https://blog.neurozone.fr/?p=1789">Oracle avec les trois côte à côte</a>. Le tableau final ne remplace rien : il ajoute une quatrième colonne à la comparaison.</p>



<p class="wp-block-paragraph">J&rsquo;ai rencontré SQLite dans d&rsquo;autres outils, par exemple ProxySQL, qui garde sa configuration dans une base SQLite. Alors j&rsquo;ai voulu voir les octets. Tout ce qui suit vient d&rsquo;une base de laboratoire : <em>testé sur Rocky Linux 10.2, SQLite 3.46.1, WSL2</em>. Je prévois aussi une série plus détaillée en sept parties, &#171;&#160;SQLite from the inside&#160;&#187;, pour 2027. Cet article reste court et suit le même plan que les trois autres.</p>



<h2 class="wp-block-heading">Un fichier, jusqu&rsquo;à trois voisins</h2>



<figure class="wp-block-image size-full"><a href="https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/fichiers-sqlite-journal-wal-voisins_fr.png?ssl=1"><img data-recalc-dims="1" decoding="async" width="629" height="328" src="https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/fichiers-sqlite-journal-wal-voisins_fr.png?resize=629%2C328&#038;ssl=1" alt="Les fichiers SQLite : le fichier principal et ses voisins journal, WAL et mémoire partagée" class="wp-image-5450" srcset="https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/fichiers-sqlite-journal-wal-voisins_fr.png?w=1800&amp;ssl=1 1800w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/fichiers-sqlite-journal-wal-voisins_fr.png?resize=300%2C157&amp;ssl=1 300w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/fichiers-sqlite-journal-wal-voisins_fr.png?resize=1024%2C535&amp;ssl=1 1024w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/fichiers-sqlite-journal-wal-voisins_fr.png?resize=768%2C401&amp;ssl=1 768w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/fichiers-sqlite-journal-wal-voisins_fr.png?resize=1536%2C802&amp;ssl=1 1536w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/fichiers-sqlite-journal-wal-voisins_fr.png?resize=900%2C470&amp;ssl=1 900w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/fichiers-sqlite-journal-wal-voisins_fr.png?resize=1280%2C668&amp;ssl=1 1280w" sizes="(max-width: 629px) 100vw, 629px" /></a><figcaption class="wp-element-caption">Une base, c&rsquo;est un fichier. Le journal, le WAL et le shm n&rsquo;existent que lorsque SQLite en a besoin.</figcaption></figure>



<p class="wp-block-paragraph">Une base SQLite est un seul fichier, qu&rsquo;un programme ouvre directement. Dans le mode par défaut, un deuxième fichier n&rsquo;apparaît que pendant une transaction d&rsquo;écriture. C&rsquo;est le journal de retour arrière, et il garde la version d&rsquo;origine de chaque page que la transaction modifie.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">-rw-r--r--. 1 root root 8192 Nov  2 21:21 demo.db
-rw-r--r--. 1 root root 4616 Nov  2 21:21 demo.db-journal</pre>



<p class="wp-block-paragraph">Le journal fait 4 616 octets. Son en-tête l&rsquo;explique : un secteur de 512 octets, puis un enregistrement de page de 4 + 4 096 + 4 octets (numéro de page, page, somme de contrôle). Au COMMIT, le journal a disparu. Pendant la transaction, ses huit premiers octets, là où se trouve la chaîne magique, valaient encore zéro. Je n&rsquo;ai pas testé pourquoi, donc je rapporte seulement ce que j&rsquo;ai vu.</p>



<p class="wp-block-paragraph">En mode WAL, deux autres fichiers apparaissent : le fichier <code>-wal</code>, où s&rsquo;ajoutent les nouvelles versions de pages, et le fichier <code>-shm</code>, un index de 32 Kio en mémoire partagée. Contrairement aux autres modes de journal, le mode WAL est enregistré dans la base : il survit à un redémarrage. Les deux fichiers disparaissent à la fermeture propre de la dernière connexion, comme le dit la documentation.</p>



<h2 class="wp-block-heading">L&rsquo;en-tête de 100 octets</h2>



<figure class="wp-block-image size-full"><a href="https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/sqlite-en-tete-100-octets_fr.png?ssl=1"><img data-recalc-dims="1" decoding="async" width="629" height="361" src="https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/sqlite-en-tete-100-octets_fr.png?resize=629%2C361&#038;ssl=1" alt="L'en-tête de 100 octets d'un fichier de base SQLite avec les valeurs lues dans le laboratoire" class="wp-image-5451" srcset="https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/sqlite-en-tete-100-octets_fr.png?w=1800&amp;ssl=1 1800w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/sqlite-en-tete-100-octets_fr.png?resize=300%2C172&amp;ssl=1 300w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/sqlite-en-tete-100-octets_fr.png?resize=1024%2C587&amp;ssl=1 1024w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/sqlite-en-tete-100-octets_fr.png?resize=768%2C440&amp;ssl=1 768w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/sqlite-en-tete-100-octets_fr.png?resize=1536%2C881&amp;ssl=1 1536w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/sqlite-en-tete-100-octets_fr.png?resize=900%2C516&amp;ssl=1 900w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/sqlite-en-tete-100-octets_fr.png?resize=1280%2C734&amp;ssl=1 1280w" sizes="(max-width: 629px) 100vw, 629px" /></a><figcaption class="wp-element-caption">L&rsquo;en-tête, champ par champ, avec les valeurs lues dans la base du laboratoire.</figcaption></figure>



<p class="wp-block-paragraph">Chaque fichier de base SQLite commence par un en-tête de 100 octets. Les 16 premiers octets sont le texte <code>SQLite format 3</code> et un octet nul. Viennent ensuite la taille de page, le compteur de changements, la taille en pages, la liste libre, le cookie de schéma, l&rsquo;encodage du texte et la version de la bibliothèque qui a écrit le fichier. Tous les nombres sont en gros-boutiste.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">$ xxd -l 32 demo.db
00000000: 5351 4c69 7465 2066 6f72 6d61 7420 3300  SQLite format 3.
00000010: 1000 0101 0040 2020 0000 0002 0000 0002  .....@  ........</pre>



<p class="wp-block-paragraph">J&rsquo;ai décodé les champs avec quelques lignes de Python, et le client en ligne de commande est d&rsquo;accord.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">$ sqlite3 demo.db ".dbinfo"
database page size:  4096
file change counter: 2
database page count: 2
freelist page count: 0
schema cookie:       1
text encoding:       1 (utf8)
[...]
software version:    3046001</pre>



<p class="wp-block-paragraph">Deux champs sont à ta disposition. <code>PRAGMA user_version</code> et <code>PRAGMA application_id</code> écrivent directement dans les octets 60 à 71 : après avoir réglé 42 et 1234567890, les octets valent <code>0000 002a</code> et <code>4996 02d2</code>. Beaucoup d&rsquo;outils s&rsquo;en servent pour marquer le format d&rsquo;un fichier.</p>



<h2 class="wp-block-heading">Pages et arbres B dans les fichiers SQLite</h2>



<p class="wp-block-paragraph">Après l&rsquo;en-tête, le fichier est une suite de pages de même taille, ici 4 096 octets. La page 1 contient l&rsquo;en-tête et la table de schéma, <code>sqlite_schema</code>. Chaque table est un arbre B classé par son rowid, et chaque index est un autre arbre B. Chaque arbre a une page racine, dont le numéro est dans <code>sqlite_schema</code>. Ce numéro n&rsquo;a jamais changé pendant mes tests, même quand l&rsquo;arbre grandissait. La table virtuelle <code>dbstat</code>, compilée dans le paquet Rocky, compte les pages de chaque arbre.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">name|pagetype|pages|cells|payload|unused
customers|internal|1|21|0|3917
customers|leaf|22|2000|77781|2282
idx_email|internal|1|13|316|3677
idx_email|leaf|14|1987|48449|2822
sqlite_schema|leaf|1|2|186|3794</pre>



<p class="wp-block-paragraph">Avec 2 000 lignes, la table a besoin d&rsquo;une page interne et de 22 feuilles. L&rsquo;index est un arbre à part, avec une page interne et 14 feuilles. Dans un index, les cellules internes portent aussi des entrées : 13 + 1 987 donnent les 2 000 clés. Une page interne de table ne porte que des clés et des pointeurs vers les enfants.</p>



<figure class="wp-block-image size-full"><a href="https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/sqlite-arbres-b-et-debordement_fr.png?ssl=1"><img data-recalc-dims="1" loading="lazy" decoding="async" width="629" height="297" src="https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/sqlite-arbres-b-et-debordement_fr.png?resize=629%2C297&#038;ssl=1" alt="Deux arbres B SQLite, une table et un index, et une chaîne de débordement" class="wp-image-5453" srcset="https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/sqlite-arbres-b-et-debordement_fr.png?w=1800&amp;ssl=1 1800w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/sqlite-arbres-b-et-debordement_fr.png?resize=300%2C142&amp;ssl=1 300w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/sqlite-arbres-b-et-debordement_fr.png?resize=1024%2C484&amp;ssl=1 1024w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/sqlite-arbres-b-et-debordement_fr.png?resize=768%2C363&amp;ssl=1 768w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/sqlite-arbres-b-et-debordement_fr.png?resize=1536%2C725&amp;ssl=1 1536w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/sqlite-arbres-b-et-debordement_fr.png?resize=900%2C425&amp;ssl=1 900w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/sqlite-arbres-b-et-debordement_fr.png?resize=1280%2C604&amp;ssl=1 1280w" sizes="auto, (max-width: 629px) 100vw, 629px" /></a><figcaption class="wp-element-caption">La forme des arbres mesurée avec dbstat, et la règle de débordement vérifiée avec la documentation.</figcaption></figure>



<p class="wp-block-paragraph">Une table déclarée <code>WITHOUT ROWID</code> est stockée dans un seul arbre, classé par sa clé primaire. Une table normale avec une clé texte a besoin de deux arbres, la table et un index automatique.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">type|name|tbl_name|rootpage
table|kv|kv|45                      -- WITHOUT ROWID: one tree
table|kv2|kv2|46                    -- rowid table with a TEXT primary key: the table...
index|sqlite_autoindex_kv2_1|kv2|47 -- ...and a second tree for the key</pre>



<p class="wp-block-paragraph">Les grandes valeurs quittent la page. Avec une valeur de 20 000 caractères, la feuille a gardé 3 637 octets et quatre pages de débordement ont porté le reste, 4 092 octets chacune. La documentation donne la règle, avec U = 4 096 et une charge P = 20 005 : X = U &#8211; 35 = 4 061 et M = 489. Comme P dépasse X, la partie locale vaut K = M + ((P &#8211; M) % (U &#8211; 4)) = 3 637. C&rsquo;est exactement ce que rapporte <code>dbstat</code>.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">name|pageno|pagetype|ncell|payload|unused|mx_payload
notes|40|leaf|1|3637|441|20005
notes|41|overflow|0|4092|0|0
notes|42|overflow|0|4092|0|0
notes|43|overflow|0|4092|0|0
notes|44|overflow|0|4092|0|0</pre>



<h2 class="wp-block-heading">Anatomie d&rsquo;une page</h2>



<figure class="wp-block-image size-full"><a href="https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/sqlite-anatomie-page-feuille_fr.png?ssl=1"><img data-recalc-dims="1" loading="lazy" decoding="async" width="629" height="349" src="https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/sqlite-anatomie-page-feuille_fr.png?resize=629%2C349&#038;ssl=1" alt="Anatomie d'une page feuille de table SQLite : en-tête, pointeurs, espace libre et cellules" class="wp-image-5452" srcset="https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/sqlite-anatomie-page-feuille_fr.png?w=1800&amp;ssl=1 1800w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/sqlite-anatomie-page-feuille_fr.png?resize=300%2C167&amp;ssl=1 300w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/sqlite-anatomie-page-feuille_fr.png?resize=1024%2C569&amp;ssl=1 1024w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/sqlite-anatomie-page-feuille_fr.png?resize=768%2C427&amp;ssl=1 768w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/sqlite-anatomie-page-feuille_fr.png?resize=1536%2C853&amp;ssl=1 1536w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/sqlite-anatomie-page-feuille_fr.png?resize=900%2C500&amp;ssl=1 900w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/sqlite-anatomie-page-feuille_fr.png?resize=1280%2C711&amp;ssl=1 1280w" sizes="auto, (max-width: 629px) 100vw, 629px" /></a><figcaption class="wp-element-caption">La page 2 de la base du laboratoire. Les pointeurs croissent depuis le début, les cellules depuis la fin.</figcaption></figure>



<p class="wp-block-paragraph">Une page d&rsquo;arbre B a un en-tête de 8 octets sur une feuille (12 sur une page interne), puis un tableau de pointeurs de 2 octets vers les cellules. Les cellules elles-mêmes sont écrites depuis la fin de la page, donc l&rsquo;espace libre se trouve au milieu. Sur la page 2 du laboratoire, le premier octet est <code>0d</code> (une feuille de table), il y a 2 cellules, et le contenu commence au décalage 4 038.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">$ xxd -s 4096 -l 16 demo.db
00001000: 0d00 0000 020f c600 0fe1 0fc6 0000 0000  ................
$ xxd -s 8128 -l 64 demo.db        # the end of page 2, where the cells live
00001fc0: 0000 0000 0000 1902 0500 132b 1142 6f62  ...........+.Bob
00001fd0: 626f 6240 6578 616d 706c 652e 6e65 7444  bob@example.netD
00001fe0: 451d 0105 0017 2f11 416c 6963 6561 6c69  E...../.Aliceali
00001ff0: 6365 4065 7861 6d70 6c65 2e6e 6574 4652  ce@example.netFR</pre>



<p class="wp-block-paragraph">L&rsquo;espace libéré dans la zone de contenu devient un bloc libre, une petite liste chaînée dans la page. Les trous de 1 à 3 octets sont seulement comptés comme octets fragmentés dans l&rsquo;en-tête.</p>



<h2 class="wp-block-heading">Un enregistrement, octet par octet</h2>



<p class="wp-block-paragraph">Une cellule contient une taille de données, un rowid et un enregistrement. Les tailles et les rowids sont des entiers variables : 1 à 9 octets, 7 bits à la fois, et le bit de poids fort signifie qu&rsquo;un octet suit. L&rsquo;enregistrement commence par la taille de son en-tête, puis un type par colonne. Un type de 13 ou plus et impair désigne du texte, et la longueur vaut (type &#8211; 13) / 2. Le type 0 désigne NULL. Ici la colonne <code>id</code> est le rowid : SQLite stocke NULL et garde la valeur dans la clé de la cellule.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">1d 01 05 00 17 2f 11 41 6c 69 63 65 61 6c 69 63 65 40 65 78 61 6d 70 6c 65 2e 6e 65 74 46 52
|  |  |  |  |  |  '----- 'Alice' 'alice@example.net' 'FR' (5 + 17 + 2 bytes)
|  |  |  |  |  '-- serial type 17: text of 2 bytes
|  |  |  |  '----- serial type 47: text of 17 bytes
|  |  |  '-------- serial type 23: text of 5 bytes
|  |  '----------- serial type 0: NULL (id is the rowid)
|  '-------------- record header: 5 bytes
'----------------- rowid 1, payload of 29 bytes (1d is the payload size, 01 the rowid)</pre>



<p class="wp-block-paragraph"><code>SELECT rowid, id, name, email, country FROM customers</code> renvoie <code>1|1|Alice|alice@example.net|FR</code> : le décodage est juste.</p>



<h2 class="wp-block-heading">UPDATE et DELETE sur le disque</h2>



<p class="wp-block-paragraph">Rien dans les fichiers SQLite ne fonctionne comme un stock d&rsquo;annulation. SQLite modifie la page elle-même, et il garde l&rsquo;ancienne version dans le journal ou le WAL. Chaque changement validé incrémente le compteur de changements dans l&rsquo;en-tête. Un UPDATE de même taille a réécrit la valeur sur place, avec le même nombre de cellules et le même espace libre, mais le compteur est passé de 7 à 8.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">-- page 3 is the first leaf of customers
before          change counter 7   page 3: ncell 102, payload 3655, unused 25    page_count 39
same-size UPDATE        counter 8   page 3: ncell 102, payload 3655, unused 25
larger UPDATE           counter 9   page 3: ncell  89, payload 3242, unused 490   page_count 40</pre>



<p class="wp-block-paragraph">L&rsquo;UPDATE plus grand a fait davantage. La page était presque pleine, donc elle a dû se diviser : la page 3 est passée de 102 à 89 cellules, le fichier a gagné une page et la racine un pointeur. En bref, une ligne qui grossit peut coûter une page.</p>



<p class="wp-block-paragraph">Un DELETE libère la cellule et transforme son espace en bloc libre. Le paquet Rocky est compilé avec <code>SECURE_DELETE</code>, donc <code>PRAGMA secure_delete</code> renvoie 1 et les octets libérés sont mis à zéro. Avec le réglage désactivé, deux occurrences de l&rsquo;adresse e-mail sont restées dans le fichier.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">$ sqlite3 del.db "PRAGMA secure_delete;"
1
-- page 3 header after DELETE FROM customers WHERE id=5:
cells 101 | first freeblock offset 3921 | fragmented bytes 0
freeblock at 3921: next 0, size 36 bytes
its bytes: 00 00 00 24 00 00 00 00 00 00 00 00 [...] 00 00 00 00     &lt;- zeroed
$ grep -a -c "user5@example.net" del.db       # secure_delete on
0
$ sqlite3 del2.db "PRAGMA secure_delete=OFF; DELETE FROM customers WHERE id=5;"
$ grep -a -c "user5@example.net" del2.db      # secure_delete off
2</pre>



<h2 class="wp-block-heading">Pourquoi les fichiers SQLite ne rétrécissent pas</h2>



<p class="wp-block-paragraph">Quand une page entière se vide, elle rejoint la liste libre, que décrivent les octets 32 à 39 de l&rsquo;en-tête. Le fichier garde sa taille. Après un DELETE de 1 800 lignes, 31 pages sur 39 étaient libres, et le fichier faisait toujours 159 744 octets. <code>VACUUM</code> l&rsquo;a reconstruit en 32 768 octets.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="sql">before DELETE         page_count 39   freelist_count 0    size 159744
DELETE ... id &gt; 200   page_count 39   freelist_count 31   size 159744   (header bytes 32-39: 0000 0006 0000 001f)
after VACUUM          page_count 8    freelist_count 0    size 32768</pre>



<p class="wp-block-paragraph">Avec <code>auto_vacuum</code> en mode incrémental, on peut rendre des pages par étapes. L&rsquo;octet 52 de l&rsquo;en-tête contient alors la plus grande page racine, et l&rsquo;octet 64 passe à 1. La page 2 devient une table de pointeurs, faite d&rsquo;entrées de 5 octets : un type et une page parente. <code>dbstat</code> saute cette page.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">PRAGMA auto_vacuum=INCREMENTAL; VACUUM;    -&gt; header byte 52 = 4, byte 64 = 1
DELETE ... id &gt; 200                         page_count 40  freelist_count 31  size 163840
PRAGMA incremental_vacuum(5);               page_count 35  freelist_count 26  size 143360
PRAGMA incremental_vacuum;                  page_count  9  freelist_count  0  size  36864
page 2 (pointer map) starts: 01 00000000 | 01 00000000 | 05 00000003 | 05 00000003 | ...</pre>



<p class="wp-block-paragraph">Un type de page est documenté mais non testé ici : la page d&rsquo;octet verrou, qui n&rsquo;existe que si le fichier dépasse 1 Gio.</p>



<h2 class="wp-block-heading">Le WAL</h2>



<figure class="wp-block-image size-full"><a href="https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/sqlite-wal-ajout-et-point-de-controle_fr.png?ssl=1"><img data-recalc-dims="1" loading="lazy" decoding="async" width="629" height="241" src="https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/sqlite-wal-ajout-et-point-de-controle_fr.png?resize=629%2C241&#038;ssl=1" alt="Comment le WAL de SQLite ajoute des trames et comment un point de contrôle les copie" class="wp-image-5454" srcset="https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/sqlite-wal-ajout-et-point-de-controle_fr.png?w=1800&amp;ssl=1 1800w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/sqlite-wal-ajout-et-point-de-controle_fr.png?resize=300%2C115&amp;ssl=1 300w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/sqlite-wal-ajout-et-point-de-controle_fr.png?resize=1024%2C393&amp;ssl=1 1024w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/sqlite-wal-ajout-et-point-de-controle_fr.png?resize=768%2C294&amp;ssl=1 768w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/sqlite-wal-ajout-et-point-de-controle_fr.png?resize=1536%2C589&amp;ssl=1 1536w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/sqlite-wal-ajout-et-point-de-controle_fr.png?resize=900%2C345&amp;ssl=1 900w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/sqlite-wal-ajout-et-point-de-controle_fr.png?resize=1280%2C491&amp;ssl=1 1280w" sizes="auto, (max-width: 629px) 100vw, 629px" /></a><figcaption class="wp-element-caption">Trois trames mesurées dans le WAL, puis des points de contrôle PASSIVE et TRUNCATE.</figcaption></figure>



<p class="wp-block-paragraph">En mode WAL, les fichiers SQLite changent dans un autre ordre : un écrivain ajoute d&rsquo;abord des trames au fichier <code>-wal</code>. Chaque trame est un en-tête de 24 octets suivi d&rsquo;une page. Une trame dont le champ de taille n&rsquo;est pas nul est un commit. Les lecteurs utilisent le dernier commit et l&rsquo;index shm pour trouver la bonne version de chaque page.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">-rw-r--r--. 1 root root 159744 Nov  2 21:23 wal.db
-rw-r--r--. 1 root root  32768 Nov  2 21:23 wal.db-shm
-rw-r--r--. 1 root root  12392 Nov  2 21:23 wal.db-wal</pre>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">WAL magic       : 0x377f0682
format version  : 3007000
page size       : 4096
checkpoint seq  : 0
file size       : 12392 bytes = 32 + 3 frames x (24 + 4096)
  frame 1: page 24, commit-size-after 0 (not a commit frame)
  frame 2: page 39, commit-size-after 39
  frame 3: page 3, commit-size-after 39</pre>



<p class="wp-block-paragraph">Un point de contrôle copie les trames dans le fichier principal. Un point de contrôle PASSIVE l&rsquo;a fait sans toucher au fichier WAL, alors que TRUNCATE l&rsquo;a vidé. J&rsquo;ai désactivé le point de contrôle automatique pour garder les trames visibles ; par défaut, il se déclenche à 1 000 pages.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">PRAGMA wal_checkpoint(PASSIVE);     -&gt; 0|3|3     (busy, frames in the WAL, frames copied)  -wal still 12392 bytes
PRAGMA wal_checkpoint(TRUNCATE);    -&gt; 0|0|0                                              -wal now 0 bytes
last connection closes              -&gt; demo.db-wal and demo.db-shm are gone</pre>



<h2 class="wp-block-heading">Les quatre moteurs côte à côte</h2>



<p class="wp-block-paragraph">Voici comment les fichiers SQLite se comparent aux trois premières parties, avec les valeurs par défaut.</p>



<figure class="wp-block-table"><table><thead><tr><th></th><th>InnoDB</th><th>PostgreSQL</th><th>Oracle</th><th>SQLite</th></tr></thead><tbody><tr><td>Où vivent les données</td><td>fichiers de tablespace, un par table par défaut</td><td>un fichier par table ou index, en segments</td><td>datafiles dans des tablespaces</td><td>un seul fichier pour tout</td></tr><tr><td>Unité d&rsquo;E/S</td><td>page de 16 Ko</td><td>page de 8 Ko</td><td>bloc, 8 Ko par défaut</td><td>page, 4 096 octets ici</td></tr><tr><td>Format d&rsquo;une ligne</td><td>index clusterisé : la table est l&rsquo;arbre de la clé primaire</td><td>tas, les index y pointent</td><td>bloc avec un répertoire de lignes et un ROWID</td><td>arbre B classé par rowid, ou par clé pour WITHOUT ROWID</td></tr><tr><td>Anciennes versions</td><td>journaux d&rsquo;annulation, à part</td><td>dans la table, nettoyées par VACUUM</td><td>tablespace d&rsquo;annulation</td><td>aucune : le journal ou le WAL les garde jusqu&rsquo;au commit</td></tr><tr><td>Sécurité en cas de panne</td><td>journal de reprise et doublewrite buffer</td><td>WAL et écriture de pages complètes</td><td>journaux de reprise</td><td>journal de retour arrière ou WAL</td></tr><tr><td>Récupérer de la place</td><td>reconstruire la table</td><td>VACUUM, VACUUM FULL</td><td>shrink, move</td><td>VACUUM, incremental_vacuum</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Le plus proche cousin de SQLite ici est PostgreSQL pour le WAL, et InnoDB pour l&rsquo;arbre. La grande différence est qu&rsquo;aucun processus serveur ne se tient entre ton programme et le fichier : le format du fichier et les verrous de fichier du système portent les garanties.</p>



<h2 class="wp-block-heading">Sources</h2>



<ul class="wp-block-list"><li><a href="https://www.sqlite.org/fileformat2.html">SQLite documentation: database file format</a></li><li><a href="https://www.sqlite.org/wal.html">SQLite documentation: write-ahead logging</a></li><li><a href="https://www.sqlite.org/pragma.html">SQLite documentation: PRAGMA statements</a></li><li><a href="https://www.sqlite.org/dbstat.html">SQLite documentation: the dbstat virtual table</a></li><li><a href="https://www.sqlite.org/lang_vacuum.html">SQLite documentation: VACUUM</a></li><li>Les sorties enregistrées viennent de mon propre laboratoire : Rocky Linux 10.2, SQLite 3.46.1, WSL2.</li></ul>



<p class="wp-block-paragraph"><em>Cet article fait partie d&rsquo;une série de notes d&rsquo;admin système (étiquette &#171;&#160;carnet d&rsquo;admin&#160;&#187;).</em><br>Série &#171;&#160;Dans les fichiers d&rsquo;une base de données&#160;&#187;, partie 4/3+1. · <a href="https://blog.neurozone.fr/?p=1789">← Article précédent</a></p>



<h2 class="wp-block-heading">Articles liés</h2>



<ul class="wp-block-list"><li><a href="https://blog.neurozone.fr/?p=1787">Dans les fichiers d&rsquo;une base de données (1/3) : InnoDB, de MySQL à MariaDB</a></li><li><a href="https://blog.neurozone.fr/?p=1788">Dans les fichiers d&rsquo;une base de données (2/3) : PostgreSQL</a></li><li><a href="https://blog.neurozone.fr/?p=1789">Dans les fichiers d&rsquo;une base de données (3/3) : Oracle, et les trois côte à côte</a></li></ul>


<p>L’article <a href="https://blog.neurozone.fr/2026/11/12/dans-les-fichiers-d-une-base-de-donnees-sqlite/">Dans les fichiers d&rsquo;une base de données (4/3+1) : SQLite</a> est apparu en premier sur <a href="https://blog.neurozone.fr">Quelques pensées</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.neurozone.fr/2026/11/12/dans-les-fichiers-d-une-base-de-donnees-sqlite/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">5455</post-id>	</item>
		<item>
		<title>Pas à pas : fail2ban bien configuré, avec bannissements progressifs</title>
		<link>https://blog.neurozone.fr/2026/11/11/pas-a-pas-fail2ban-bien-configure-avec-bannissements-progressifs/</link>
					<comments>https://blog.neurozone.fr/2026/11/11/pas-a-pas-fail2ban-bien-configure-avec-bannissements-progressifs/#respond</comments>
		
		<dc:creator><![CDATA[Hécate Raven]]></dc:creator>
		<pubDate>Wed, 11 Nov 2026 09:00:00 +0000</pubDate>
				<category><![CDATA[Informatique]]></category>
		<category><![CDATA[fail2ban]]></category>
		<category><![CDATA[firewalld]]></category>
		<category><![CDATA[Français]]></category>
		<category><![CDATA[pas à pas]]></category>
		<category><![CDATA[Rocky Linux]]></category>
		<category><![CDATA[sécurité]]></category>
		<category><![CDATA[SSH]]></category>
		<guid isPermaLink="false">https://blog.neurozone.fr/?p=2065</guid>

					<description><![CDATA[<p>Configurer fail2ban sur Rocky Linux : jail.local ligne par ligne, seuils, bannissements progressifs, jail SSH, récidivistes, filtre maison.</p>
<p>L’article <a href="https://blog.neurozone.fr/2026/11/11/pas-a-pas-fail2ban-bien-configure-avec-bannissements-progressifs/">Pas à pas : fail2ban bien configuré, avec bannissements progressifs</a> est apparu en premier sur <a href="https://blog.neurozone.fr">Quelques pensées</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">fail2ban est l&rsquo;un des outils les plus anciens et les plus répandus de la boîte à outils de l&rsquo;administrateur : il lit les journaux, compte les échecs d&rsquo;authentification et bannit au pare-feu les adresses trop insistantes. Simple sur le papier, il est pourtant souvent mal réglé : bannissements de dix minutes qui n&rsquo;arrêtent personne, réglages écrits dans un fichier écrasé à la mise à jour, jail SSH qui ne voit aucune ligne parce qu&rsquo;elle lit le mauvais journal, ou administrateur banni de son propre serveur.</p>



<p class="wp-block-paragraph">Ce tutoriel part de la configuration réelle de mes serveurs Rocky Linux : bannissements progressifs, quatre niveaux de récidive, une jail pour une application maison, et les vérifications qui permettent de savoir que tout cela fonctionne vraiment.</p>



<figure class="wp-block-image size-full"><a href="https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/pas-a-pas-fail2ban-bien-configure-avec-bannissements-progres_schema_fr.png?ssl=1"><img data-recalc-dims="1" loading="lazy" decoding="async" width="629" height="285" src="https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/pas-a-pas-fail2ban-bien-configure-avec-bannissements-progres_schema_fr.png?resize=629%2C285&#038;ssl=1" alt="Échelle des bannissements fail2ban de 1 h à 30 jours, avec la prison recidive et les réglages importants" class="wp-image-4577" srcset="https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/pas-a-pas-fail2ban-bien-configure-avec-bannissements-progres_schema_fr.png?w=1720&amp;ssl=1 1720w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/pas-a-pas-fail2ban-bien-configure-avec-bannissements-progres_schema_fr.png?resize=300%2C136&amp;ssl=1 300w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/pas-a-pas-fail2ban-bien-configure-avec-bannissements-progres_schema_fr.png?resize=1024%2C464&amp;ssl=1 1024w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/pas-a-pas-fail2ban-bien-configure-avec-bannissements-progres_schema_fr.png?resize=768%2C348&amp;ssl=1 768w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/pas-a-pas-fail2ban-bien-configure-avec-bannissements-progres_schema_fr.png?resize=1536%2C697&amp;ssl=1 1536w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/pas-a-pas-fail2ban-bien-configure-avec-bannissements-progres_schema_fr.png?resize=900%2C408&amp;ssl=1 900w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/pas-a-pas-fail2ban-bien-configure-avec-bannissements-progres_schema_fr.png?resize=1280%2C580&amp;ssl=1 1280w" sizes="auto, (max-width: 629px) 100vw, 629px" /></a><figcaption class="wp-element-caption">Un bannissement qui grandit à chaque récidive, plafonné à 30 jours.</figcaption></figure>



<h2 class="wp-block-heading">Trois notions</h2>



<figure class="wp-block-table"><table><thead><tr><th>Notion</th><th>Ce que c&rsquo;est</th><th>Où</th></tr></thead><tbody><tr><td>filtre</td><td>les expressions régulières qui reconnaissent un échec dans un journal</td><td><code>filter.d/*.conf</code></td></tr><tr><td>action</td><td>ce qui est fait pour bannir et débannir (règle firewalld, nftables, courriel…)</td><td><code>action.d/*.conf</code></td></tr><tr><td>jail</td><td>l&rsquo;association d&rsquo;un filtre, d&rsquo;une source de journaux, d&rsquo;une action et de seuils</td><td><code>jail.local</code>, <code>jail.d/*.local</code></td></tr></tbody></table></figure>



<p class="wp-block-paragraph">fail2ban fournit des dizaines de filtres et d&rsquo;actions prêts à l&#8217;emploi. Le travail consiste surtout à écrire les jails : quoi surveiller, avec quels seuils, et comment bannir.</p>



<h2 class="wp-block-heading">Étape 1 : installer, et ne jamais toucher jail.conf</h2>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">dnf install epel-release
dnf install fail2ban fail2ban-firewalld
systemctl enable --now fail2ban</pre>



<p class="wp-block-paragraph">Le paquet <code>fail2ban-firewalld</code> installe un fichier qui fait de firewalld l&rsquo;action par défaut. Les fichiers <code>jail.conf</code> et <code>fail2ban.conf</code> appartiennent au paquet et seront remplacés à la prochaine mise à jour : tous vos réglages vont dans les fichiers <code>.local</code>, qui les surchargent clé par clé. Un <code>jail.local</code> peut donc ne contenir que les quelques lignes qui diffèrent.</p>



<h2 class="wp-block-heading">Étape 2 : le service lui-même</h2>



<pre class="EnlighterJSRAW" data-enlighter-language="ini"># /etc/fail2ban/fail2ban.local
[DEFINITION]
logtarget  = /var/log/fail2ban/fail2ban.log
dbfile     = /var/lib/fail2ban/fail2ban.sqlite3
dbpurgeage = 35d</pre>



<ul class="wp-block-list"><li><strong>logtarget</strong> : un fichier dédié plutôt que le journal système. Il servira de source à la jail de récidive, et il est plus facile à lire ;</li><li><strong>dbfile</strong> : la base SQLite où fail2ban garde les bannissements en cours et leur historique. C&rsquo;est elle qui permet de rétablir les bannissements après un redémarrage, et de savoir combien de fois une adresse a déjà été bannie ;</li><li><strong>dbpurgeage</strong> : la durée de conservation de l&rsquo;historique. Par défaut, un jour seulement : trop court pour que les bannissements progressifs et la récidive à six mois aient une mémoire. 35 jours est un compromis ; la récidive longue s&rsquo;appuie de toute façon sur le fichier journal.</li></ul>



<p class="wp-block-paragraph">Créez le répertoire et le fichier avant de redémarrer : <code>install -d /var/log/fail2ban && touch /var/log/fail2ban/fail2ban.log</code>. Pensez aussi à adapter la configuration logrotate du paquet à ce nouveau chemin.</p>



<h2 class="wp-block-heading">Étape 3 : jail.local, ligne par ligne</h2>



<pre class="EnlighterJSRAW" data-enlighter-language="ini"># /etc/fail2ban/jail.local
[DEFAULT]
ignoreip  = 127.0.0.1/8 ::1 203.0.113.25 203.0.113.40 198.51.100.10
bantime   = 1h
findtime  = 10m
maxretry  = 5
bantime.increment = true
bantime.formula   = ban.Time * (1&lt;&lt;(ban.Count if ban.Count&lt;20 else 20)) * banFactor
bantime.maxtime   = 720h
banaction          = firewallcmd-rich-rules
banaction_allports = firewallcmd-rich-rules

[sshd]
enabled  = true
backend  = systemd
maxretry = 5
bantime  = 2h</pre>



<h3 class="wp-block-heading">Ne pas se bannir soi-même</h3>



<p class="wp-block-paragraph"><strong>ignoreip</strong> liste les adresses jamais bannies : la boucle locale, l&rsquo;adresse publique de chacun de vos serveurs, et celle de votre accès d&rsquo;administration. Sans elle, quelques tests de connexion ratés suffisent à vous enfermer dehors, et un serveur qui interroge un autre serveur avec un mauvais mot de passe le fera bannir. Mettez la même liste partout.</p>



<h3 class="wp-block-heading">Les seuils</h3>



<p class="wp-block-paragraph"><strong>findtime</strong> et <strong>maxretry</strong> : 5 échecs en 10 minutes déclenchent un bannissement. Plus bas, on bannit des utilisateurs qui se trompent de mot de passe ; plus haut, on laisse aux robots plus d&rsquo;essais. Pour SSH, si seule l&rsquo;authentification par clé est autorisée, un utilisateur légitime n&rsquo;échoue presque jamais et on peut descendre à 3.</p>



<h3 class="wp-block-heading">Les bannissements progressifs</h3>



<p class="wp-block-paragraph"><strong>bantime.increment</strong> active l&rsquo;allongement automatique. La formule multiplie la durée de base par 2 à la puissance du nombre de bannissements précédents de cette adresse (<code>1&lt;&lt;n</code> vaut 2<sup>n</sup>), plafonné à 2<sup>20</sup> pour éviter les dépassements, puis par <code>banFactor</code> (1 par défaut). <strong>bantime.maxtime</strong> limite le résultat à 30 jours :</p>



<figure class="wp-block-table"><table><thead><tr><th>Bannissement n°</th><th>Durée (bantime 1 h)</th><th>Durée (jail sshd, 2 h)</th></tr></thead><tbody><tr><td>1</td><td>1 h</td><td>2 h</td></tr><tr><td>2</td><td>2 h</td><td>4 h</td></tr><tr><td>3</td><td>4 h</td><td>8 h</td></tr><tr><td>5</td><td>16 h</td><td>32 h</td></tr><tr><td>8</td><td>128 h (5 j)</td><td>256 h (10 j)</td></tr><tr><td>10 et plus</td><td>plafond : 30 j</td><td>plafond : 30 j</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">L&rsquo;effet est exactement celui qu&rsquo;on veut : une erreur ponctuelle coûte une heure, un robot qui revient tous les jours se retrouve banni pour des semaines au bout d&rsquo;une dizaine de passages.</p>



<h3 class="wp-block-heading">L&rsquo;action</h3>



<p class="wp-block-paragraph"><strong>banaction = firewallcmd-rich-rules</strong> : chaque bannissement devient une règle riche firewalld qui rejette l&rsquo;adresse sur le port de la jail. <strong>banaction_allports</strong> est l&rsquo;action utilisée par les jails qui bannissent sur tous les ports (la récidive). Sur un serveur sans firewalld, utilisez <code>nftables-multiport</code> et <code>nftables-allports</code>.</p>



<h3 class="wp-block-heading">La jail SSH</h3>



<p class="wp-block-paragraph"><strong>backend = systemd</strong> est indispensable sur Rocky Linux : sshd écrit dans le journal systemd, et une jail qui lit un fichier ne verrait rien, sans aucune erreur. La jail lit les entrées de l&rsquo;unité <code>sshd.service</code>. Une seconde subtilité : les versions récentes d&rsquo;OpenSSH écrivent une partie de leurs messages sous le nom <code>sshd-session</code>. Si <code>fail2ban-client status sshd</code> reste à zéro alors que des échecs apparaissent dans <code>journalctl</code>, vérifiez que le filtre de votre version de fail2ban connaît ce nom (<code>journalmatch</code>).</p>



<h2 class="wp-block-heading">Étape 4 : punir les récidivistes</h2>



<p class="wp-block-paragraph">La jail <code>recidive</code> lit le propre journal de fail2ban et compte les lignes « Ban ». Une adresse bannie plusieurs fois, quelle que soit la jail, est alors bannie <strong>sur tous les ports</strong>, pour plus longtemps. Quatre paliers complètent bien les bannissements progressifs :</p>



<pre class="EnlighterJSRAW" data-enlighter-language="ini">[recidive1]
enabled   = true
filter    = recidive
logpath   = /var/log/fail2ban/fail2ban.log
banaction = %(banaction_allports)s
findtime  = 1d
maxretry  = 4
bantime   = 3h

[recidive2]
enabled   = true
filter    = recidive
logpath   = /var/log/fail2ban/fail2ban.log
banaction = %(banaction_allports)s
findtime  = 1w
maxretry  = 7
bantime   = 1d

[recidive3]
enabled   = true
filter    = recidive
logpath   = /var/log/fail2ban/fail2ban.log
banaction = %(banaction_allports)s
findtime  = 30d
maxretry  = 10
bantime   = 1w

[recidive4]
enabled   = true
filter    = recidive
logpath   = /var/log/fail2ban/fail2ban.log
banaction = %(banaction_allports)s
findtime  = 180d
maxretry  = 20
bantime   = 30d</pre>



<p class="wp-block-paragraph">Chaque palier a sa propre fenêtre : une adresse bannie 4 fois en une journée l&rsquo;est pour 3 heures sur tous les ports ; 20 fois en six mois, pour un mois. Les fenêtres longues imposent de garder le journal assez longtemps : réglez la rotation pour conserver au moins six mois, compressés. La jail refuse de démarrer si le fichier journal n&rsquo;existe pas encore, d&rsquo;où sa création à l&rsquo;étape 2.</p>



<h2 class="wp-block-heading">Étape 5 : une jail pour sa propre application</h2>



<p class="wp-block-paragraph">Pour protéger une application qui n&rsquo;a pas de filtre fourni, écrivez-en un. Prenons une application qui journalise en JSON :</p>



<pre class="EnlighterJSRAW" data-enlighter-language="ini"># /etc/fail2ban/filter.d/monappli.conf
[Definition]
failregex = ^.*&quot;msg&quot;:&quot;login failed&quot;.*&quot;remote_ip&quot;:&quot;&lt;HOST&gt;&quot;.*$
ignoreregex =
datepattern = &quot;time&quot;:&quot;%%Y-%%m-%%dT%%H:%%M:%%S</pre>



<ul class="wp-block-list"><li><strong>&lt;HOST&gt;</strong> : la partie qui capture l&rsquo;adresse à bannir ; fail2ban la remplace par une expression qui reconnaît IPv4 et IPv6 ;</li><li><strong>datepattern</strong> : comment lire la date de la ligne. fail2ban ignore les lignes plus vieilles que <code>findtime</code>, il doit donc comprendre la date. Les <code>%%</code> sont des <code>%</code> échappés pour le format du fichier ;</li><li><strong>ignoreregex</strong> : des lignes à ne jamais compter, même si elles correspondent.</li></ul>



<pre class="EnlighterJSRAW" data-enlighter-language="ini"># /etc/fail2ban/jail.d/monappli.local
[monappli]
enabled  = true
filter   = monappli
logpath  = /var/log/monappli/app.log
port     = 2022,8443
maxretry = 5</pre>



<p class="wp-block-paragraph"><strong>port</strong> limite le bannissement aux ports de l&rsquo;application : l&rsquo;adresse fautive garde l&rsquo;accès au reste, à moins de récidiver.</p>



<h2 class="wp-block-heading">Étape 6 : tester un filtre avant de l&rsquo;activer</h2>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">fail2ban-regex /var/log/monappli/app.log /etc/fail2ban/filter.d/monappli.conf
#   Failregex: 12 total
#   Lines: 4821 lines, 0 ignored, 12 matched, 4809 missed
fail2ban-regex --print-all-matched /var/log/monappli/app.log /etc/fail2ban/filter.d/monappli.conf</pre>



<p class="wp-block-paragraph">Un filtre qui ne trouve rien (« 0 matched ») sur un journal contenant des échecs connus est faux ; un filtre qui trouve trop (des connexions réussies) bannira vos utilisateurs. Vérifiez aussi la ligne « Date template hits » : si la date n&rsquo;est pas reconnue, aucune ligne ne sera jamais comptée en production. Pour SSH avec le backend systemd : <code>fail2ban-regex systemd-journal sshd</code>.</p>



<h2 class="wp-block-heading">Étape 7 : vérifier et exploiter</h2>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">fail2ban-client -t                         # la configuration est-elle valide ?
systemctl restart fail2ban
fail2ban-client status                     # les jails actives
fail2ban-client status sshd                # échecs comptés, adresses bannies
fail2ban-client get sshd banip --with-time # avec la fin de chaque bannissement
fail2ban-client set sshd unbanip 192.0.2.66
fail2ban-client unban --all                # en cas d&#x27;urgence
firewall-cmd --list-rich-rules | grep -c reject</pre>



<p class="wp-block-paragraph">Faites le test pour de vrai, depuis une adresse qui n&rsquo;est pas dans <code>ignoreip</code> (une connexion mobile, par exemple) : cinq mots de passe faux, puis vérifiez que la sixième tentative est refusée et que l&rsquo;adresse apparaît dans le statut de la jail.</p>


<!-- nz-enrich:fail2ban-real -->


<h3 class="wp-block-heading">À quoi ça ressemble après quelques mois</h3>



<p class="wp-block-paragraph">Mon serveur web le 2 novembre 2026 (fail2ban 1.1.0). La jail SSH lit le journal systemd, et quatre jails de récidive s&#8217;empilent derrière elle :</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell"># fail2ban-client status
|- Number of jail:	5
`- Jail list:	recidive1, recidive2, recidive3, recidive4, sshd

# fail2ban-client status sshd
|- Filter
|  |- Currently failed:	0
|  |- Total failed:	6
|  `- Journal matches:	_SYSTEMD_UNIT=sshd.service + _COMM=sshd + _COMM=sshd-session
`- Actions
   |- Currently banned:	2
   |- Total banned:	2

# for j in recidive1 recidive2 recidive3 recidive4; do ...; done
recidive1: bantime=10800   findtime=86400    maxretry=4
recidive2: bantime=86400   findtime=604800   maxretry=7
recidive3: bantime=604800  findtime=2592000  maxretry=10
recidive4: bantime=2592000 findtime=15552000 maxretry=20</pre>



<ul class="wp-block-list"><li><strong>Total failed: 6</strong> depuis le redémarrage du matin. Les connexions par mot de passe étant désactivées, presque personne n&rsquo;arrive même jusqu&rsquo;à un échec : la plupart des robots abandonnent dès l&rsquo;échange de clés. Les compteurs repartent de zéro avec le service ; l&rsquo;historique est dans la base.</li><li><strong>Journal matches</strong> cite à la fois <code>sshd</code> et <code>sshd-session</code> : depuis OpenSSH 9.8, les messages d&rsquo;authentification viennent du processus <code>sshd-session</code>. Un filtre qui ne regarde que <code>_COMM=sshd</code> ne compte plus rien, sans le dire, sur un système récent.</li><li>Les quatre jails de <strong>récidive</strong> lisent le journal de fail2ban lui-même : 4 bannissements en un jour donnent 3 heures, 7 en une semaine une journée, 10 en un mois une semaine, 20 en six mois un mois.</li></ul>



<p class="wp-block-paragraph">Les bannissements progressifs se lisent dans la base SQLite de fail2ban, qui garde chaque adresse avec son nombre de bannissements :</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell"># sqlite3 -readonly /var/lib/fail2ban/fail2ban.sqlite3 \
    &quot;select jail, bancount, count(*) from bips group by jail, bancount&quot;
recidive3|1|1
sshd|1|21
sshd|3|1
sshd|7|1
sshd|10|1

# sqlite3 -readonly /var/lib/fail2ban/fail2ban.sqlite3 &quot;select max(bantime) from bips where jail=&#x27;sshd&#x27;&quot;
2592000</pre>



<p class="wp-block-paragraph">Vingt et une adresses ont été bannies une fois et ne sont jamais revenues : c&rsquo;est l&rsquo;essentiel du trafic, des robots opportunistes. Trois sont revenues, dont une dix fois. Pour celle-là, chaque bannissement a doublé la durée du précédent jusqu&rsquo;à atteindre <code>bantime.maxtime = 720h</code> : 2 592 000 secondes, trente jours, le plafond. C&rsquo;est tout l&rsquo;intérêt de la progression : la première faute coûte peu, les obstinés finissent exclus un mois, sans liste manuelle à entretenir.</p>



<h2 class="wp-block-heading">Les pannes fréquentes</h2>



<h3 class="wp-block-heading">La jail ne compte rien</h3>



<p class="wp-block-paragraph">Mauvais backend (fichier au lieu de systemd), mauvais chemin de journal, ou date non reconnue. <code>fail2ban-client status &lt;jail&gt;</code> montre la source lue ; <code>fail2ban-regex</code> dit si les lignes correspondent.</p>



<h3 class="wp-block-heading">Les adresses sont bannies mais continuent de passer</h3>



<p class="wp-block-paragraph">L&rsquo;action ne correspond pas au pare-feu réellement utilisé (une action iptables sur un système nftables pur, par exemple), ou le trafic arrive par un mandataire inverse et c&rsquo;est l&rsquo;adresse du mandataire qui est bannie. Derrière un mandataire, l&rsquo;application doit journaliser l&rsquo;adresse réelle du client (<code>X-Forwarded-For</code>), et le bannissement doit avoir lieu sur la machine qui reçoit la connexion.</p>



<h3 class="wp-block-heading">Une adresse bloquée alors que fail2ban ne la bannit pas</h3>



<p class="wp-block-paragraph">Si CrowdSec tourne aussi, il a peut-être pris sa propre décision : <code>cscli decisions list --ip …</code>. Les deux se complètent bien (fail2ban réagit à vos propres journaux, CrowdSec ajoute des scénarios plus riches et une liste communautaire), mais il faut penser à regarder des deux côtés.</p>



<h3 class="wp-block-heading">fail2ban met longtemps à démarrer</h3>



<p class="wp-block-paragraph">À chaque démarrage, il rétablit les bannissements en cours depuis sa base et relit la fin des journaux. Avec des milliers d&rsquo;entrées et des règles riches firewalld, cela peut prendre une minute. Une action nftables par ensemble (<code>nftables-allports</code>) est bien plus rapide quand le nombre de bannissements grossit.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p class="wp-block-paragraph"><em>Ce billet fait partie d&rsquo;une série de tutoriels d&rsquo;administration système (tag « pas à pas »).</em><br>Série « Pas à pas », tutoriel 11/77. · <a href="https://blog.neurozone.fr/?p=2047">← Tutoriel précédent</a> · <span class="link-upcoming">Tutoriel suivant → (à paraître le 16/11)</span> · <a href="https://hecate.neurozone.fr/?p=620">English version</a></p>



<h2 class="wp-block-heading">Articles liés</h2>



<ul class="wp-block-list"><li><a href="https://blog.neurozone.fr/?p=2046">Installer CrowdSec sans jamais se bannir</a></li><li><a href="https://blog.neurozone.fr/?p=2047">Suricata en IPS avec NFQUEUE</a></li><li><a href="https://blog.neurozone.fr/?p=1419">CrowdSec 1.8 sur Debian : réinstaller proprement</a></li></ul>
<p>L’article <a href="https://blog.neurozone.fr/2026/11/11/pas-a-pas-fail2ban-bien-configure-avec-bannissements-progressifs/">Pas à pas : fail2ban bien configuré, avec bannissements progressifs</a> est apparu en premier sur <a href="https://blog.neurozone.fr">Quelques pensées</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.neurozone.fr/2026/11/11/pas-a-pas-fail2ban-bien-configure-avec-bannissements-progressifs/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2065</post-id>	</item>
		<item>
		<title>Séquences et colonnes identity dans PostgreSQL : trous, rollbacks et crashs, testés</title>
		<link>https://blog.neurozone.fr/2026/11/10/sequences-postgresql-colonnes-identity-trous-rollbacks-crashs-testes/</link>
					<comments>https://blog.neurozone.fr/2026/11/10/sequences-postgresql-colonnes-identity-trous-rollbacks-crashs-testes/#respond</comments>
		
		<dc:creator><![CDATA[Hécate Raven]]></dc:creator>
		<pubDate>Tue, 10 Nov 2026 09:00:00 +0000</pubDate>
				<category><![CDATA[Informatique]]></category>
		<category><![CDATA[carnet d'admin]]></category>
		<category><![CDATA[Français]]></category>
		<category><![CDATA[InnoDB]]></category>
		<category><![CDATA[MariaDB]]></category>
		<category><![CDATA[MySQL]]></category>
		<category><![CDATA[Oracle]]></category>
		<category><![CDATA[PostgreSQL]]></category>
		<category><![CDATA[série : Dans les fichiers d'une base de données]]></category>
		<category><![CDATA[stockage]]></category>
		<guid isPermaLink="false">https://blog.neurozone.fr/?p=5533</guid>

					<description><![CDATA[<p>Séquences PostgreSQL et colonnes identity en 18.6 : serial ou identity, ALWAYS ou BY DEFAULT, trous après rollback, CACHE et trou après kill -9.</p>
<p>L’article <a href="https://blog.neurozone.fr/2026/11/10/sequences-postgresql-colonnes-identity-trous-rollbacks-crashs-testes/">Séquences et colonnes identity dans PostgreSQL : trous, rollbacks et crashs, testés</a> est apparu en premier sur <a href="https://blog.neurozone.fr">Quelques pensées</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph"><strong>En bref :</strong> j&rsquo;ai fait un <code>kill -9</code> sur les processus postgres : une colonne identity PostgreSQL est passée de l&rsquo;id 9 à 37, parce que les séquences PostgreSQL journalisent 32 valeurs d&rsquo;avance dans le WAL. Rollbacks et insertions en échec brûlent aussi des valeurs, et une colonne <code>BY DEFAULT</code> ignore les id explicites. Préférer <code>GENERATED ... AS IDENTITY</code> à <code>serial</code>, et ne jamais compter sur des id continus.</p>



<p class="wp-block-paragraph"><em>PostgreSQL 18.6, cluster jetable sous Rocky Linux 10.2. Partie 1 sur 5 (PostgreSQL, MariaDB, Oracle, SQL Server, résumé). Toutes les sorties viennent de mes propres tests.</em></p>



<figure class="wp-block-image size-full"><a href="https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/sequences-postgresql-colonnes-identity-trous-rollbacks-crashs-testes.png?ssl=1"><img data-recalc-dims="1" loading="lazy" decoding="async" width="629" height="164" src="https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/sequences-postgresql-colonnes-identity-trous-rollbacks-crashs-testes.png?resize=629%2C164&#038;ssl=1" alt="Séquences PostgreSQL après kill -9 : id 1 à 9, puis un trou de 27 valeurs, puis 37" class="wp-image-5532" srcset="https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/sequences-postgresql-colonnes-identity-trous-rollbacks-crashs-testes.png?w=1800&amp;ssl=1 1800w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/sequences-postgresql-colonnes-identity-trous-rollbacks-crashs-testes.png?resize=300%2C78&amp;ssl=1 300w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/sequences-postgresql-colonnes-identity-trous-rollbacks-crashs-testes.png?resize=1024%2C267&amp;ssl=1 1024w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/sequences-postgresql-colonnes-identity-trous-rollbacks-crashs-testes.png?resize=768%2C201&amp;ssl=1 768w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/sequences-postgresql-colonnes-identity-trous-rollbacks-crashs-testes.png?resize=1536%2C401&amp;ssl=1 1536w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/sequences-postgresql-colonnes-identity-trous-rollbacks-crashs-testes.png?resize=900%2C235&amp;ssl=1 900w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/sequences-postgresql-colonnes-identity-trous-rollbacks-crashs-testes.png?resize=1280%2C334&amp;ssl=1 1280w" sizes="auto, (max-width: 629px) 100vw, 629px" /></a><figcaption class="wp-element-caption">Un kill -9 sur PostgreSQL 18.6 : 27 valeurs réservées et jamais distribuées.</figcaption></figure>



<h2 class="wp-block-heading">Trois façons d&rsquo;obtenir une valeur générée</h2>



<pre class="EnlighterJSRAW" data-enlighter-language="sql">CREATE TABLE t_ident  (id int GENERATED ALWAYS AS IDENTITY PRIMARY KEY, v text);
CREATE TABLE t_serial (id serial PRIMARY KEY, v text);
CREATE SEQUENCE s1;
CREATE TABLE t_seq    (id int PRIMARY KEY DEFAULT nextval('s1'), v text);</pre>



<p class="wp-block-paragraph">Chaque <code>INSERT ... RETURNING id</code> renvoie 1 sur une table neuve. Le catalogue montre la différence :</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell"> column_name |            column_default            | is_identity | identity_generation
-------------+--------------------------------------+-------------+---------------------
 id          |                                      | YES         | ALWAYS
 id          | nextval('s1'::regclass)              | NO          |
 id          | nextval('t_serial_id_seq'::regclass) | NO          |</pre>



<p class="wp-block-paragraph">Une colonne identity n&rsquo;a pas d&rsquo;expression par défaut ; la séquence est attachée à la colonne (<code>pg_get_serial_sequence('t_ident','id')</code> renvoie <code>public.t_ident_id_seq</code>). <code>serial</code> est un raccourci : colonne entière plus défaut appelant <code>nextval</code>.</p>



<h2 class="wp-block-heading">ALWAYS ou BY DEFAULT</h2>



<p class="wp-block-paragraph">Avec <code>GENERATED BY DEFAULT</code>, une valeur explicite est acceptée mais ne fait pas avancer le compteur :</p>



<pre class="EnlighterJSRAW" data-enlighter-language="sql">INSERT INTO t_by(id, v) VALUES (2, 'explicit 2');         -- ok
INSERT INTO t_by(v) VALUES ('default 1') RETURNING id;    -- 1
INSERT INTO t_by(v) VALUES ('default 2') RETURNING id;    -- ERROR: duplicate key ... (id)=(2) already exists
INSERT INTO t_by(v) VALUES ('default 3') RETURNING id;    -- 3</pre>



<p class="wp-block-paragraph">L&rsquo;insertion en échec a brûlé la valeur 2 du compteur. Avec <code>GENERATED ALWAYS</code>, une valeur explicite est refusée sauf demande expresse :</p>



<pre class="EnlighterJSRAW" data-enlighter-language="yaml">ERROR:  cannot insert a non-DEFAULT value into column "id"
DETAIL:  Column "id" is an identity column defined as GENERATED ALWAYS.
HINT:  Use OVERRIDING SYSTEM VALUE to override.</pre>



<p class="wp-block-paragraph">Après <code>OVERRIDING SYSTEM VALUE VALUES (50, ...)</code>, l&rsquo;insertion par défaut suivante a renvoyé 1 : le compteur est indépendant des données.</p>



<h2 class="wp-block-heading">Rollback et échecs consomment des valeurs</h2>



<pre class="EnlighterJSRAW" data-enlighter-language="sql">BEGIN; INSERT INTO t_ident(v) VALUES ('rolled back') RETURNING id;   -- 2
ROLLBACK;
INSERT INTO t_ident(v) VALUES ('after rollback') RETURNING id;       -- 3</pre>



<p class="wp-block-paragraph">Idem avec une violation d&rsquo;unicité : id 1, erreur sur le doublon, puis 3. Un compteur sans trou demande une autre conception (table de compteur verrouillée), au prix de la sérialisation des écritures.</p>



<h2 class="wp-block-heading">Concurrence : l&rsquo;ordre des id n&rsquo;est pas l&rsquo;ordre des commits</h2>



<p class="wp-block-paragraph">La session A insère en premier et valide 3 secondes après B ; la session B insère en second et valide aussitôt :</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">A got id 1
B got id 2
B: committed 08:18:48.652
A: committed 08:18:51.154</pre>



<p class="wp-block-paragraph">La ligne de B est visible avant celle de A, bien que son id soit plus grand. Ne pas utiliser « max(id) déjà vu » comme curseur de réplication ou de polling.</p>



<h2 class="wp-block-heading">CACHE est par session</h2>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">CREATE SEQUENCE s_cache CACHE 10;
same session twice : 1|2
new session        : 11
another new session: 21</pre>



<p class="wp-block-paragraph">Avec les séquences PostgreSQL, chaque session réserve un bloc de 10. Les valeurs de sessions différentes s&rsquo;entrelacent et des trous apparaissent dès qu&rsquo;une session se termine.</p>



<h2 class="wp-block-heading">Limites et CYCLE</h2>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">ERROR:  nextval: reached maximum value of sequence "s_small" (3)</pre>



<p class="wp-block-paragraph">Après <code>ALTER SEQUENCE s_small CYCLE</code>, la valeur suivante est 1. Une identity sur <code>smallint</code> s&rsquo;arrête à 32767 avec la même erreur sur <code>t_small_id_seq</code>. Les identity <code>integer</code> s&rsquo;arrêtent à 2147483647 : prendre <code>bigint</code> pour les tables à longue vie.</p>



<h2 class="wp-block-heading">Redémarrage et crash</h2>



<p class="wp-block-paragraph">Trois lignes insérées, la dernière supprimée, redémarrage propre :</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">sequence before the restart: 3|30|t     (last_value | log_cnt | is_called)
sequence after the restart:  3|30|t
new id = 4</pre>



<p class="wp-block-paragraph">Puis cinq lignes de plus (dernier id 9), <code>kill -9</code> de tous les processus postgres, redémarrage :</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">sequence before the crash: 9|27|t
sequence after the crash:  36|0|t
new id = 37
ids now: 1,2,4,5,6,7,8,9,37</pre>



<p class="wp-block-paragraph"><code>log_cnt</code> est le nombre de valeurs encore couvertes par le dernier enregistrement WAL. PostgreSQL écrit un enregistrement WAL pour 32 valeurs ; après la récupération sur crash, il repart de la fin de la plage réservée (9 + 27 = 36), donc l&rsquo;id suivant est 37. Un arrêt propre écrit l&rsquo;état exact, sans trou.</p>



<h2 class="wp-block-heading">Où lire l&rsquo;état des séquences PostgreSQL</h2>



<pre class="EnlighterJSRAW" data-enlighter-language="sql">SELECT * FROM pg_sequences;                -- départ, incrément, max, cache_size, cycle, last_value
SELECT last_value, log_cnt, is_called FROM t_ident_id_seq;
SELECT attrelid::regclass, attname, attidentity FROM pg_attribute WHERE attidentity &lt;&gt; '';  -- 'a' ALWAYS, 'd' BY DEFAULT</pre>



<h2 class="wp-block-heading">Documentation officielle</h2>



<p class="wp-block-paragraph"><a href="https://www.postgresql.org/" rel="noopener">PostgreSQL</a>, site du projet</p>



<p class="wp-block-paragraph"><a href="https://www.postgresql.org/docs/18/sql-createsequence.html" rel="noopener">CREATE SEQUENCE</a>, documentation PostgreSQL 18</p>



<p class="wp-block-paragraph"><a href="https://www.postgresql.org/docs/18/functions-sequence.html" rel="noopener">Sequence manipulation functions</a>, <code>nextval</code>, <code>currval</code>, <code>setval</code></p>



<p class="wp-block-paragraph"><a href="https://www.postgresql.org/docs/18/sql-createtable.html" rel="noopener">CREATE TABLE</a>, <code>GENERATED ... AS IDENTITY</code> et <code>serial</code></p>



<p class="wp-block-paragraph"><a href="https://www.postgresql.org/docs/18/view-pg-sequences.html" rel="noopener">pg_sequences</a>, la vue système utilisée plus haut</p>



<p class="wp-block-paragraph">*Non testé ici : réplication logique des séquences, <code>pg_upgrade</code>, tables partitionnées, <code>ALTER SEQUENCE ... RESTART</code> en cours d&rsquo;usage.*</p>



<h2 class="wp-block-heading">Toutes les parties de la série</h2>



<p class="wp-block-paragraph"><strong>1/5 PostgreSQL</strong></p>



<p class="wp-block-paragraph"><span class="link-upcoming">2/5 MariaDB (à paraître le 17/11)</span></p>



<p class="wp-block-paragraph"><span class="link-upcoming">3/5 Oracle (à paraître le 24/11)</span></p>



<p class="wp-block-paragraph"><span class="link-upcoming">4/5 SQL Server (à paraître le 1/12)</span></p>



<p class="wp-block-paragraph"><span class="link-upcoming">5/5 TL;DR : les quatre moteurs comparés (à paraître le 8/12)</span></p>



<h2 class="wp-block-heading">Articles liés</h2>



<p class="wp-block-paragraph"><a href="https://blog.neurozone.fr/?p=1787">Dans les fichiers d&rsquo;une base de données (1/3) : InnoDB, de MySQL à MariaDB</a></p>



<p class="wp-block-paragraph"><a href="https://blog.neurozone.fr/?p=1788">Dans les fichiers d&rsquo;une base de données (2/3) : PostgreSQL</a></p>



<p class="wp-block-paragraph"><a href="https://blog.neurozone.fr/?p=1789">Dans les fichiers d&rsquo;une base de données (3/3) : Oracle, et les trois côte à côte</a></p>


<p>L’article <a href="https://blog.neurozone.fr/2026/11/10/sequences-postgresql-colonnes-identity-trous-rollbacks-crashs-testes/">Séquences et colonnes identity dans PostgreSQL : trous, rollbacks et crashs, testés</a> est apparu en premier sur <a href="https://blog.neurozone.fr">Quelques pensées</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.neurozone.fr/2026/11/10/sequences-postgresql-colonnes-identity-trous-rollbacks-crashs-testes/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">5533</post-id>	</item>
		<item>
		<title>Pas à pas : Suricata en IPS avec NFQUEUE, sans casser son serveur</title>
		<link>https://blog.neurozone.fr/2026/11/09/pas-a-pas-suricata-en-ips-sur-rocky-linux-sans-casser-son-serveur/</link>
					<comments>https://blog.neurozone.fr/2026/11/09/pas-a-pas-suricata-en-ips-sur-rocky-linux-sans-casser-son-serveur/#respond</comments>
		
		<dc:creator><![CDATA[Hécate Raven]]></dc:creator>
		<pubDate>Mon, 09 Nov 2026 09:00:00 +0000</pubDate>
				<category><![CDATA[Informatique]]></category>
		<category><![CDATA[firewalld]]></category>
		<category><![CDATA[Français]]></category>
		<category><![CDATA[IPS]]></category>
		<category><![CDATA[pas à pas]]></category>
		<category><![CDATA[Rocky Linux]]></category>
		<category><![CDATA[sécurité]]></category>
		<category><![CDATA[Suricata]]></category>
		<guid isPermaLink="false">https://blog.neurozone.fr/?p=2047</guid>

					<description><![CDATA[<p>Installer Suricata en IPS sur Rocky Linux avec NFQUEUE : règles, trafic redirigé, boucle locale exclue, cartes réseau, faux positifs, alertes vers le SIEM.</p>
<p>L’article <a href="https://blog.neurozone.fr/2026/11/09/pas-a-pas-suricata-en-ips-sur-rocky-linux-sans-casser-son-serveur/">Pas à pas : Suricata en IPS avec NFQUEUE, sans casser son serveur</a> est apparu en premier sur <a href="https://blog.neurozone.fr">Quelques pensées</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Suricata est un moteur de détection d&rsquo;intrusion réseau : il examine les paquets qui entrent et sortent d&rsquo;une machine, reconstitue les connexions, comprend les protocoles (HTTP, TLS, DNS, SSH…) et compare le tout à des dizaines de milliers de règles décrivant des attaques connues. En mode IPS, il peut aussi bloquer : un paquet qui correspond à une règle de blocage n&rsquo;arrive jamais à destination.</p>



<p class="wp-block-paragraph">Ce tutoriel installe Suricata en IPS sur un serveur Rocky Linux exposé à Internet, avec NFQUEUE. C&rsquo;est le mode le plus utile sur un serveur seul, et aussi le plus délicat : Suricata se retrouve sur le chemin de chaque paquet. Je décris donc aussi, en détail, les pannes que j&rsquo;ai rencontrées, dont une qui bloquait des connexions locales en silence pendant des semaines.</p>



<figure class="wp-block-image size-full"><a href="https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/pas-a-pas-suricata-en-ips-sur-rocky-linux-sans-casser-son-se_schema_fr.png?ssl=1"><img data-recalc-dims="1" loading="lazy" decoding="async" width="629" height="307" src="https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/pas-a-pas-suricata-en-ips-sur-rocky-linux-sans-casser-son-se_schema_fr.png?resize=629%2C307&#038;ssl=1" alt="Schéma du chemin d'un paquet via firewalld, NFQUEUE et Suricata, avec les quatre réglages clés et la leçon du loopback" class="wp-image-4575" srcset="https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/pas-a-pas-suricata-en-ips-sur-rocky-linux-sans-casser-son-se_schema_fr.png?w=1720&amp;ssl=1 1720w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/pas-a-pas-suricata-en-ips-sur-rocky-linux-sans-casser-son-se_schema_fr.png?resize=300%2C147&amp;ssl=1 300w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/pas-a-pas-suricata-en-ips-sur-rocky-linux-sans-casser-son-se_schema_fr.png?resize=1024%2C500&amp;ssl=1 1024w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/pas-a-pas-suricata-en-ips-sur-rocky-linux-sans-casser-son-se_schema_fr.png?resize=768%2C375&amp;ssl=1 768w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/pas-a-pas-suricata-en-ips-sur-rocky-linux-sans-casser-son-se_schema_fr.png?resize=1536%2C750&amp;ssl=1 1536w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/pas-a-pas-suricata-en-ips-sur-rocky-linux-sans-casser-son-se_schema_fr.png?resize=900%2C440&amp;ssl=1 900w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/pas-a-pas-suricata-en-ips-sur-rocky-linux-sans-casser-son-se_schema_fr.png?resize=1280%2C625&amp;ssl=1 1280w" sizes="auto, (max-width: 629px) 100vw, 629px" /></a><figcaption class="wp-element-caption">Le chemin d&rsquo;un paquet et les réglages qui évitent les mauvaises surprises.</figcaption></figure>



<h2 class="wp-block-heading">IDS ou IPS</h2>



<figure class="wp-block-table"><table><thead><tr><th>Mode</th><th>Fonctionnement</th><th>Risque</th></tr></thead><tbody><tr><td>IDS (af-packet)</td><td>Suricata reçoit une <em>copie</em> des paquets et alerte</td><td>aucun pour le trafic : au pire, des alertes manquées</td></tr><tr><td>IPS (NFQUEUE)</td><td>le noyau <em>confie</em> chaque paquet à Suricata, qui l&rsquo;accepte ou le jette</td><td>Suricata est sur le chemin : une erreur bloque le trafic</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Commencez en IDS si vous découvrez l&rsquo;outil. Passez en IPS quand vous savez lire ses alertes et que vous avez éliminé les faux positifs les plus bruyants.</p>



<h2 class="wp-block-heading">Étape 1 : installer</h2>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">dnf install epel-release
dnf install suricata
suricata --build-info | grep -E &#x27;NFQueue|AF_PACKET&#x27;   # les deux doivent être « yes »</pre>



<p class="wp-block-paragraph">Deux modules noyau sont nécessaires pour NFQUEUE : <code>nfnetlink_queue</code> (la file elle-même) et <code>xt_NFQUEUE</code> (la cible utilisée par les règles de pare-feu). Le second est souvent oublié : la règle échoue alors avec un message peu clair sur un module manquant. Chargez-les et rendez-les permanents :</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">modprobe nfnetlink_queue
modprobe xt_NFQUEUE
printf &#x27;nfnetlink_queue\nxt_NFQUEUE\n&#x27; &gt; /etc/modules-load.d/nfqueue.conf</pre>



<h2 class="wp-block-heading">Étape 2 : suricata.yaml, l&rsquo;essentiel</h2>



<pre class="EnlighterJSRAW" data-enlighter-language="yaml"># /etc/suricata/suricata.yaml (extraits)
vars:
  address-groups:
    HOME_NET: &quot;[203.0.113.25/32,2001:db8::25/128]&quot;
    EXTERNAL_NET: &quot;!$HOME_NET&quot;

nfq:
  mode: accept
  fail-open: yes

flow-timeouts:
  tcp:
    established: 7200

stream:
  memcap: 256mb
  midstream: true
  reassembly:
    memcap: 512mb
    depth: 1mb

outputs:
  - eve-log:
      enabled: yes
      filename: eve.json
      types: [alert, drop, anomaly, stats]</pre>



<ul class="wp-block-list"><li><strong>HOME_NET</strong> : les adresses que vous protégez. Beaucoup de règles en dépendent (« trafic venant de l&rsquo;extérieur vers HOME_NET ») ; une valeur fausse donne à la fois des faux positifs et des attaques manquées ;</li><li><strong>nfq.mode: accept</strong> : Suricata rend son verdict (accepter ou jeter) et le paquet poursuit ensuite son chemin normal dans le pare-feu ;</li><li><strong>fail-open: yes</strong> : si la file déborde parce que Suricata ne suit pas, les paquets passent au lieu d&rsquo;être jetés. Sur un serveur seul, mieux vaut laisser passer quelques paquets non inspectés que couper le service ;</li><li><strong>flow-timeouts.tcp.established: 7200</strong> : le point qui m&rsquo;a coûté le plus cher, détaillé plus bas. Par défaut, Suricata oublie une connexion TCP inactive au bout d&rsquo;une heure ; une connexion qui reste ouverte plus longtemps sans trafic voit ses paquets suivants traités comme des paquets « hors connexion », et parfois jetés ;</li><li><strong>midstream: true</strong> : Suricata accepte de prendre en compte une connexion dont il n&rsquo;a pas vu le début, par exemple après son propre redémarrage. Sans cela, toutes les connexions établies avant le démarrage sont considérées comme invalides ;</li><li><strong>memcap</strong> : les plafonds de mémoire pour le suivi des flux et la reconstitution des données. Trop bas, Suricata abandonne l&rsquo;inspection des connexions sous charge ; surveillez les compteurs <code>memcap</code> dans les statistiques ;</li><li><strong>eve.json</strong> : le journal structuré, une ligne JSON par événement. C&rsquo;est ce fichier qu&rsquo;on envoie à un SIEM.</li></ul>



<h2 class="wp-block-heading">Étape 3 : les règles</h2>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">suricata-update                      # télécharge le jeu ET Open (gratuit)
suricata-update list-sources
suricata-update enable-source et/open
ls -l /var/lib/suricata/rules/suricata.rules</pre>



<p class="wp-block-paragraph"><code>suricata-update</code> assemble toutes les règles dans un seul fichier, en appliquant vos fichiers de modification. Deux d&rsquo;entre eux sont essentiels en IPS :</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell"># /etc/suricata/drop.conf : ces familles de règles bloquent au lieu d&#x27;alerter
re:classtype:trojan-activity
re:classtype:command-and-control
re:classtype:domain-c2
re:classtype:exploit-kit
re:classtype:coin-mining

# /etc/suricata/disable.conf : protocoles industriels absents ici
re:dnp3
re:modbus</pre>



<p class="wp-block-paragraph">Par défaut, toutes les règles <strong>alertent</strong> seulement. <code>drop.conf</code> transforme en blocages celles dont la catégorie ne laisse pas de doute : communication avec un serveur de commande, cheval de Troie, minage de cryptomonnaie. Évitez de tout passer en blocage : une règle générique de « trafic suspect » déclenchée par une application légitime coupe cette application sans prévenir. <code>disable.conf</code> retire les règles inutiles, ce qui économise mémoire et processeur.</p>



<p class="wp-block-paragraph">Planifiez une mise à jour quotidienne par un timer systemd qui lance <code>suricata-update</code>, puis demande à Suricata de recharger ses règles sans redémarrer (<code>suricatasc -c reload-rules</code>).</p>



<h2 class="wp-block-heading">Étape 4 : envoyer le trafic à Suricata</h2>



<p class="wp-block-paragraph">Le service lit la file 0 :</p>



<pre class="EnlighterJSRAW" data-enlighter-language="ini"># /etc/systemd/system/suricata.service.d/ips.conf
[Service]
ExecStart=
ExecStart=/usr/sbin/suricata -c /etc/suricata/suricata.yaml -q 0 --pidfile /var/run/suricata.pid</pre>



<p class="wp-block-paragraph">Puis les règles de pare-feu, avec firewalld en mode « direct » :</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell"># 1. la boucle locale ne passe jamais par Suricata (priorité -1 : avant tout)
firewall-cmd --permanent --direct --add-rule ipv4 filter INPUT  -1 -i lo -j ACCEPT
firewall-cmd --permanent --direct --add-rule ipv4 filter OUTPUT -1 -o lo -j ACCEPT
# 2. tout le reste est confié à la file 0 ; --queue-bypass : si Suricata est arrêté, le trafic passe
firewall-cmd --permanent --direct --add-rule ipv4 filter INPUT   0 -j NFQUEUE --queue-num 0 --queue-bypass
firewall-cmd --permanent --direct --add-rule ipv4 filter OUTPUT  0 -j NFQUEUE --queue-num 0 --queue-bypass
firewall-cmd --permanent --direct --add-rule ipv4 filter FORWARD 0 -j NFQUEUE --queue-num 0 --queue-bypass
# mêmes règles en ipv6
firewall-cmd --reload</pre>



<ul class="wp-block-list"><li><strong>&#8211;queue-bypass</strong> : sans lui, si Suricata est arrêté ou plante, plus aucun paquet ne passe, SSH compris. Avec lui, le noyau laisse passer le trafic quand personne n&rsquo;écoute la file. C&rsquo;est la ligne qui évite d&rsquo;avoir à appeler la console de secours de l&rsquo;hébergeur ;</li><li><strong>la boucle locale exclue</strong> : la leçon la plus coûteuse de ce tutoriel, détaillée ci-dessous ;</li><li><strong>l&rsquo;ordre</strong> : les règles directes de priorité -1 passent avant celles de priorité 0.</li></ul>



<h2 class="wp-block-heading">La leçon de la boucle locale</h2>



<p class="wp-block-paragraph">Pendant six semaines, un service de mon serveur (un gestionnaire de tâches qui parle à Redis sur <code>127.0.0.1</code>) s&rsquo;est figé à intervalles irréguliers, toujours exactement 31 minutes et 40 secondes, avant de repartir. Les journaux de l&rsquo;application ne montraient rien, ceux de Redis non plus. La cause : Suricata inspectait aussi le trafic de la boucle locale. Une longue connexion inactive dépassait le délai de suivi des flux ; au réveil, ses paquets n&rsquo;appartenaient plus à aucune connexion connue de Suricata, qui les jetait ; il fallait attendre que TCP abandonne et se reconnecte.</p>



<p class="wp-block-paragraph">Le diagnostic est venu d&rsquo;<code>eve.json</code> : chaque paquet jeté y apparaît avec sa raison.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">jq -c &#x27;select(.event_type==&quot;drop&quot;) | {timestamp, src_ip, dest_ip, dest_port, reason: .drop.reason}&#x27; /var/log/suricata/eve.json | tail</pre>



<p class="wp-block-paragraph">Trois corrections, toutes nécessaires : exclure la boucle locale (le trafic entre processus d&rsquo;une même machine n&rsquo;a rien à faire dans un IPS), allonger le délai TCP à deux heures, et activer <code>midstream</code>. Retenez le symptôme : une connexion longue qui se fige un temps fixe et reproductible, sans erreur nulle part, c&rsquo;est presque toujours un équipement intermédiaire (pare-feu, IPS, traduction d&rsquo;adresses) qui a oublié la connexion.</p>



<h2 class="wp-block-heading">Le piège des cartes réseau</h2>



<p class="wp-block-paragraph">Les cartes et pilotes modernes regroupent plusieurs paquets en un seul « super-paquet » avant de les remettre au noyau (GRO, LRO). Suricata voit alors des paquets plus gros que la taille réelle du réseau, et sa reconstitution des connexions s&rsquo;en trouve faussée. Désactivez ces optimisations sur l&rsquo;interface surveillée, de façon permanente, par NetworkManager plutôt que par un simple <code>ethtool -K</code> qui serait annulé à la reconnexion suivante :</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">nmcli connection modify eth0 ethtool.feature-gro off ethtool.feature-lro off \
    ethtool.feature-gso off ethtool.feature-tso off
nmcli connection up eth0
ethtool -k eth0 | grep -E &#x27;generic-receive|large-receive&#x27;</pre>



<h2 class="wp-block-heading">Étape 5 : démarrer et vérifier</h2>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">suricata -T -c /etc/suricata/suricata.yaml        # test de configuration et des règles
systemctl enable --now suricata
tail -f /var/log/suricata/suricata.log            # « engine started »
curl -s http://testmynids.org/uid/index.html      # doit déclencher l&#x27;alerte 2100498
jq -c &#x27;select(.event_type==&quot;alert&quot;) | .alert.signature&#x27; /var/log/suricata/eve.json | tail -3</pre>



<p class="wp-block-paragraph">Vérifiez aussi que le trafic normal passe toujours : connexion SSH neuve, site web, envoi de courrier, résolution DNS. Puis surveillez les statistiques pendant quelques jours :</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">jq -c &#x27;select(.event_type==&quot;stats&quot;) | .stats | {drop: .ips.blocked, memcap: .tcp.ssn_memcap_drop, kernel: .capture}&#x27; /var/log/suricata/eve.json | tail -1</pre>


<!-- nz-enrich:suricata-real -->


<h2 class="wp-block-heading">Les mêmes compteurs sur un serveur en production</h2>



<p class="wp-block-paragraph">Suricata 8.0.8 en mode IPS sur mon serveur web, lu par le socket de contrôle le 2 novembre 2026 après 75 minutes de fonctionnement. <code>suricatasc</code> donne les compteurs en direct, sans attendre la prochaine ligne de statistiques dans eve.json :</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell"># suricatasc -c iface-list
{&quot;message&quot;: {&quot;count&quot;: 1, &quot;ifaces&quot;: [&quot;NFQ#0&quot;]}, &quot;return&quot;: &quot;OK&quot;}

# suricatasc -c ruleset-stats
[{&quot;id&quot;: 0, &quot;rules_loaded&quot;: 53325, &quot;rules_failed&quot;: 0, &quot;rules_skipped&quot;: 0}]

# suricatasc -c dump-counters | jq &#x27;.message | {uptime, ips, alerts: .detect.alert}&#x27;
uptime        4544 s
ips.accepted  2 076 277
ips.blocked   771
  drop_reason.stream_error    769
  drop_reason.applayer_error  2
  drop_reason.rules           0
alerts        23</pre>



<ul class="wp-block-list"><li><strong>NFQ#0</strong> : Suricata est branché sur la file netfilter 0, il voit donc les paquets au passage et peut les jeter. En mode IDS, cette liste montrerait une interface réseau.</li><li><strong>53 325 règles chargées, 0 en échec</strong> : un <em>rules_failed</em> non nul signifie le plus souvent qu&rsquo;une règle utilise un mot-clé que votre version ne connaît pas, après une mise à jour des règles ou du moteur.</li><li><strong>771 paquets bloqués, 769 pour stream_error, 0 par les règles</strong> : c&rsquo;est le tableau normal d&rsquo;un serveur web. Les rejets par règle sont rares ; la plupart des paquets jetés n&rsquo;ont aucun sens pour le flux TCP auquel ils prétendent appartenir (retransmissions après la fermeture, numéros de séquence faux, sommes de contrôle invalides). Si ce nombre grimpe d&rsquo;un coup, regardez les événements <code>drop</code> dans eve.json : la leçon de la boucle locale, plus haut, a commencé par exactement ce genre de pic.</li><li><strong>23 alertes</strong> en 75 minutes, aucune bloquante : des anomalies de protocole (trop de SYN pour une même poignée de main, protocoles applicatifs incohérents, horodatages invalides) journalisées pour le SIEM. Pourtant, 23 665 des règles chargées sont réglées pour bloquer, par classe (chevaux de Troie, serveurs de commande, kits d&rsquo;exploitation, minage…) grâce à <code>drop.conf</code> : pendant cette heure, aucune ne s&rsquo;est déclenchée. Une règle de blocage qui ne se déclenche jamais, c&rsquo;est l&rsquo;état attendu, pas le signe d&rsquo;une panne.</li></ul>



<h2 class="wp-block-heading">Faire taire un faux positif</h2>



<p class="wp-block-paragraph">Une règle qui se déclenche sur un trafic légitime se neutralise, du plus précis au plus large : seuil ou suppression pour une adresse dans <code>threshold.config</code>, puis désactivation de la règle par son numéro dans <code>disable.conf</code>. Ne modifiez jamais le fichier de règles directement : la prochaine mise à jour l&rsquo;écraserait.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell"># /etc/suricata/threshold.config
suppress gen_id 1, sig_id 2210045, track by_src, ip 203.0.113.40</pre>



<h2 class="wp-block-heading">Envoyer les alertes au SIEM</h2>



<p class="wp-block-paragraph"><code>eve.json</code> est lisible par Wazuh, Elastic, Graylog et la plupart des SIEM. Avec un agent Wazuh, quelques lignes suffisent (format <code>json</code>, chemin du fichier) et les alertes de Suricata apparaissent à côté des journaux système, ce qui permet de rapprocher une alerte réseau d&rsquo;une connexion SSH ou d&rsquo;un changement de fichier. Pensez à la rotation : <code>eve.json</code> grossit vite, et logrotate doit envoyer à Suricata le signal de rouvrir le fichier.</p>



<h2 class="wp-block-heading">Mettre à jour Suricata</h2>



<p class="wp-block-paragraph">Après chaque mise à jour du paquet, relancez <code>suricata -T</code> avant le redémarrage : une option renommée ou une règle incompatible empêche le démarrage, et grâce à <code>--queue-bypass</code> le trafic passera alors <em>sans inspection</em>, sans que rien ne casse visiblement. C&rsquo;est le revers de la sécurité de fonctionnement : surveillez l&rsquo;état du service, pas seulement le trafic.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p class="wp-block-paragraph"><em>Ce billet fait partie d&rsquo;une série de tutoriels d&rsquo;administration système (tag « pas à pas »).</em><br>Série « Pas à pas », tutoriel 10/77. · <a href="https://blog.neurozone.fr/?p=2046">← Tutoriel précédent</a> · <a href="https://blog.neurozone.fr/?p=2065">Tutoriel suivant →</a> · <a href="https://hecate.neurozone.fr/?p=599">English version</a></p>



<h2 class="wp-block-heading">Articles liés</h2>



<ul class="wp-block-list"><li><a href="https://blog.neurozone.fr/?p=2046">Installer CrowdSec sans jamais se bannir</a></li><li><a href="https://blog.neurozone.fr/?p=1419">CrowdSec 1.8 sur Debian : réinstaller proprement</a></li><li><a href="https://blog.neurozone.fr/?p=2975">Sous le capot des failles d&rsquo;octobre 2026</a></li></ul>

<p>L’article <a href="https://blog.neurozone.fr/2026/11/09/pas-a-pas-suricata-en-ips-sur-rocky-linux-sans-casser-son-serveur/">Pas à pas : Suricata en IPS avec NFQUEUE, sans casser son serveur</a> est apparu en premier sur <a href="https://blog.neurozone.fr">Quelques pensées</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.neurozone.fr/2026/11/09/pas-a-pas-suricata-en-ips-sur-rocky-linux-sans-casser-son-serveur/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2047</post-id>	</item>
		<item>
		<title>Table LanceDB sur disque : 265 versions, 13,4 Gio, un seul fragment vivant</title>
		<link>https://blog.neurozone.fr/2026/11/08/table-lancedb-sur-disque-versions-manifestes-fragments/</link>
					<comments>https://blog.neurozone.fr/2026/11/08/table-lancedb-sur-disque-versions-manifestes-fragments/#respond</comments>
		
		<dc:creator><![CDATA[Hécate Raven]]></dc:creator>
		<pubDate>Sun, 08 Nov 2026 09:00:00 +0000</pubDate>
				<category><![CDATA[Informatique]]></category>
		<category><![CDATA[carnet d'admin]]></category>
		<category><![CDATA[Français]]></category>
		<category><![CDATA[InnoDB]]></category>
		<category><![CDATA[MariaDB]]></category>
		<category><![CDATA[MySQL]]></category>
		<category><![CDATA[Oracle]]></category>
		<category><![CDATA[PostgreSQL]]></category>
		<category><![CDATA[série : Dans les fichiers d'une base de données]]></category>
		<category><![CDATA[stockage]]></category>
		<guid isPermaLink="false">https://blog.neurozone.fr/?p=5572</guid>

					<description><![CDATA[<p>Ce que contient une table LanceDB sur disque : 265 versions, 13,4 Gio, un fragment vivant. Manifestes et transactions décodés sur une vraie table.</p>
<p>L’article <a href="https://blog.neurozone.fr/2026/11/08/table-lancedb-sur-disque-versions-manifestes-fragments/">Table LanceDB sur disque : 265 versions, 13,4 Gio, un seul fragment vivant</a> est apparu en premier sur <a href="https://blog.neurozone.fr">Quelques pensées</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph"><strong>J&rsquo;ai lancé un script en lecture seule sur ma propre machine pour regarder à l&rsquo;intérieur d&rsquo;une table LanceDB, et un assistant IA (Claude, d&rsquo;Anthropic) m&rsquo;a aidée à écrire ce script et à rédiger cet article.</strong> Les chiffres ci-dessous sont la sortie réelle de ce lancement, avec LanceDB 0.41.0 et pyarrow 26.0.1. J&rsquo;ai remplacé le nom de la table et les chemins. C&rsquo;est le premier article d&rsquo;une série sur LanceDB vu par une DBA.</p>



<h2 class="wp-block-heading">Réponse rapide</h2>



<p class="wp-block-paragraph">Une table LanceDB est un répertoire, et rien d&rsquo;autre : des fichiers de données dans <code>data/</code>, un manifeste par version dans <code>_versions/</code>, un fichier de transaction par validation dans <code>_transactions/</code>, et un répertoire <code>_indices/</code> dès qu&rsquo;on construit un index. Ma table contient 1 130 595 lignes de vecteurs à 1024 dimensions et pèse 13,4 Gio sur disque, mais sa version courante, la 265, ne référence qu&rsquo;un seul fichier de données de 5,8 Gio. Les 7,6 Gio restants environ sont répartis dans 259 anciens fichiers de données dont aucune version courante n&rsquo;a besoin, et ils restent sur disque tant que personne ne nettoie les anciennes versions. Chaque ajout crée un nouveau fichier de données et une nouvelle version, c&rsquo;est ainsi qu&rsquo;une table atteint 265 versions et 260 fichiers de données, et la dernière version a fusionné 226 fragments en un sans supprimer les anciens fichiers.</p>



<figure class="wp-block-image size-full"><a href="https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/table-lancedb-sur-disque-versions-manifestes-fragments.png?ssl=1"><img data-recalc-dims="1" loading="lazy" decoding="async" width="629" height="224" src="https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/table-lancedb-sur-disque-versions-manifestes-fragments.png?resize=629%2C224&#038;ssl=1" alt="Table LanceDB sur disque : la version 265 réécrit 226 fragments en un, 5,8 Gio lus sur 13,4 Gio dans data/" class="wp-image-5571" srcset="https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/table-lancedb-sur-disque-versions-manifestes-fragments.png?w=1800&amp;ssl=1 1800w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/table-lancedb-sur-disque-versions-manifestes-fragments.png?resize=300%2C107&amp;ssl=1 300w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/table-lancedb-sur-disque-versions-manifestes-fragments.png?resize=1024%2C364&amp;ssl=1 1024w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/table-lancedb-sur-disque-versions-manifestes-fragments.png?resize=768%2C273&amp;ssl=1 768w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/table-lancedb-sur-disque-versions-manifestes-fragments.png?resize=1536%2C546&amp;ssl=1 1536w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/table-lancedb-sur-disque-versions-manifestes-fragments.png?resize=900%2C320&amp;ssl=1 900w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/table-lancedb-sur-disque-versions-manifestes-fragments.png?resize=1280%2C455&amp;ssl=1 1280w" sizes="auto, (max-width: 629px) 100vw, 629px" /></a><figcaption class="wp-element-caption">Versions 263 à 265 et part de data/ que lit la version courante.</figcaption></figure>



<h2 class="wp-block-heading">Ce que contient le répertoire d&rsquo;une table LanceDB</h2>



<p class="wp-block-paragraph">La première partie de mon script parcourt le répertoire de la table LanceDB, <code>docs_index.lance</code>, et n&rsquo;a pas besoin de LanceDB. L&rsquo;arborescence contient trois sous-répertoires. La <a href="https://lance.org/format/table/layout/" rel="noopener">documentation de l&rsquo;agencement des fichiers</a> en liste d&rsquo;autres, dont deux, <code>_indices/</code> et <code>_deletions/</code>, sont absents de ma table.</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Répertoire</th><th>Ce que dit la documentation</th><th>Ce que j&rsquo;ai trouvé</th></tr></thead><tbody><tr><td><code>data/</code></td><td>fichiers de données (<code>*.lance</code>)</td><td>260 fichiers, 13,4 Gio, médiane 21,6 Mio, le plus gros 5,8 Gio</td></tr><tr><td><code>_versions/</code></td><td>manifestes (<code>*.manifest</code>) et un <code>latest_version_hint.json</code> optionnel</td><td>265 manifestes, 2,4 Mio au total, plus un fichier d&rsquo;indice de 15 octets</td></tr><tr><td><code>_transactions/</code></td><td>fichiers de transaction (<code>*.txn</code>)</td><td>265 fichiers, 56,7 Kio au total, médiane 132 octets</td></tr><tr><td><code>_indices/</code></td><td>un dossier par index</td><td>absent</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">La table compte 791 fichiers en tout. Les statistiques donnent <code>num_indices: 0</code> et <code>num_deleted_rows: 0</code>, ce qui correspond à l&rsquo;absence de ces deux répertoires : je n&rsquo;ai jamais construit d&rsquo;index et jamais supprimé de ligne.</p>



<h2 class="wp-block-heading">Une version, c&rsquo;est un manifeste et une transaction</h2>



<p class="wp-block-paragraph">Le manifeste est le cœur du format. La <a href="https://lance.org/format/table/" rel="noopener">documentation</a> le décrit comme la description immuable d&rsquo;une version unique, avec le schéma, la liste des fragments, le numéro de version, un horodatage et une référence à la transaction qui l&rsquo;a créée. Ma table a 265 manifestes pour 265 versions, donc exactement un manifeste par version, et 265 fichiers de transaction, donc chaque validation en a laissé un.</p>



<p class="wp-block-paragraph">Les noms portent de l&rsquo;information. Un fichier de transaction s&rsquo;appelle <code>{read_version}-{uuid}.txn</code>, où le nombre est la version à partir de laquelle la transaction a été construite, et le plus gros de ma table est <code>263-45522501-…txn</code>, de 20,1 Kio. Les manifestes déroutent au début, mais la logique est simple : le fichier de la version 265 est <code>18446744073709551350.manifest</code>. Ce nombre est 18446744073709551615, le plus grand entier non signé sur 64 bits, moins 265, et la version 1 est <code>18446744073709551614.manifest</code>. Les noms décroissent donc quand les versions augmentent. Les pages de documentation que j&rsquo;ai lues n&rsquo;expliquent pas ce schéma, donc ma lecture n&rsquo;est qu&rsquo;une hypothèse : la version la plus récente arrive en premier dans un listing de répertoire. Le fichier <code>latest_version_hint.json</code> fait 15 octets. La documentation parle d&rsquo;une « simple optimisation », destinée à accélérer la recherche de la dernière version sur les stockages où lister un répertoire coûte cher.</p>



<h2 class="wp-block-heading">Chaque ajout crée un fragment</h2>



<p class="wp-block-paragraph">Les cinq dernières versions montrent comment la table a grossi. <code>list_versions</code> a renvoyé ces lignes, que j&rsquo;ai réduites aux champs intéressants et alignées en tableau :</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">version  timestamp                    rows       data files
261      2026-11-03 22:55:19.615921   1,123,032  224
262      2026-11-03 22:56:55.260015   1,127,160  225
263      2026-11-03 22:58:15.392524   1,130,595  226
264      2026-11-03 22:59:22.144490   1,130,595  226
265      2026-11-03 22:59:22.177593   1,130,595  1</pre>



<p class="wp-block-paragraph">De la 261 à la 263, chaque version ajoute un fichier de données et quelques milliers de lignes : 4 128 lignes pour le premier ajout et 3 435 pour le second. C&rsquo;est l&rsquo;<a href="https://lance.org/format/table/transaction/" rel="noopener">opération Append</a> de la documentation, qui ajoute de nouveaux fragments sans modifier les données existantes. De petits ajouts produisent donc de petits fichiers de données, et une table alimentée par petits lots accumule les fragments : la mienne en avait 226, et le fichier de données médian fait 21,6 Mio.</p>



<p class="wp-block-paragraph">Les versions 264 et 265 sont séparées de 33 millisecondes, et le nombre de lignes ne change pas. J&rsquo;ai décodé leurs transactions avec les définitions protobuf publiées dans le dépôt Lance. La version 264 est une transaction <code>reserve_fragments</code> de 46 octets : elle réserve un identifiant de fragment, et le <code>max_fragment_id</code> du manifeste passe de 258 à 259 alors que les 226 fragments restent les mêmes. La version 265 est une transaction <code>rewrite</code>, le compactage : elle remplace les 226 anciens fragments par un nouveau fragment, d&rsquo;identifiant 259, qui contient les 1 130 595 lignes. La documentation décrit une Rewrite comme une opération qui « réorganise les données sans modification sémantique », et c&rsquo;est ce que montre le fichier. Les versions 21 et 33 de la même table contiennent des réécritures plus anciennes, de 21 fragments en un et de 6 en un, et la première version est un <code>overwrite</code> de 329 octets.</p>



<h2 class="wp-block-heading">La version courante d&rsquo;une table LanceDB est plus petite que la table</h2>



<p class="wp-block-paragraph">Les statistiques de la version 265 donnent la taille de ce dont la table a réellement besoin :</p>



<pre class="EnlighterJSRAW" data-enlighter-language="yaml">total_bytes: 6,203,819,954   num_rows: 1,130,595   num_deleted_rows: 0
num_indices: 0               num_fragments: 1      num_small_fragments: 0</pre>



<p class="wp-block-paragraph">Cela fait 5,8 Gio, soit environ 5 487 octets par ligne. La colonne de vecteurs contient 1024 flottants de 4 octets, donc 4 096 octets par ligne, et les 1 391 octets restants par ligne contiennent le texte, le titre, le chemin et le numéro de segment. Le répertoire <code>data/</code> pèse 13,4 Gio, ce qui laisse environ 7,6 Gio dans 259 fichiers de données que la version 265 ne référence pas. Ce sont les fragments des versions précédentes, toujours là parce que rien ne les a supprimés. Dans cette table, la fusion des fragments n&rsquo;a pas réduit le répertoire : le nouveau fichier a été ajouté et les anciens sont restés en place, toujours référencés par les anciennes versions.</p>



<h2 class="wp-block-heading">Pourquoi le manifeste d&rsquo;un seul fragment pèse 20,5 Kio</h2>



<p class="wp-block-paragraph">Les manifestes grossissent avec le nombre de fragments : 628 et 736 octets pour les premiers, 20,4 Kio à 226 fragments, soit environ 90 octets par fragment. La version 265 n&rsquo;a qu&rsquo;un fragment et son fichier fait pourtant 21 031 octets, et je l&rsquo;ai décodé pour comprendre pourquoi. Le fichier commence par une longueur sur 4 octets et une copie de la transaction, puis une seconde longueur et le manifeste proprement dit, puis un pied de 16 octets qui se termine par la marque <code>LANC</code>. Le manifeste proprement dit fait 391 octets. Les 20 620 autres octets sont la transaction intégrée, la même réécriture que celle stockée dans <code>263-45522501-…txn</code>, qui liste les 226 fragments qu&rsquo;elle remplace, à 88,6 octets chacun en moyenne. À titre de comparaison, les manifestes des versions 263 et 264 font chacun 20,8 Kio de liste de fragments, plus une petite transaction intégrée de 136 et 50 octets.</p>



<p class="wp-block-paragraph">Les 20 Kio du dernier manifeste ne décrivent donc pas la version courante. Ils enregistrent ce que le compactage a retiré de la table, et ils sont présents deux fois sur disque, dans le manifeste et dans le fichier de transaction. Ce champ s&rsquo;appelle <code>transaction_section</code> dans le protobuf. Je n&rsquo;ai pas trouvé cela décrit dans les pages du format que j&rsquo;ai lues.</p>



<h2 class="wp-block-heading">Ce que je n&rsquo;ai pas testé</h2>



<p class="wp-block-paragraph">Je n&rsquo;ai lancé aucune opération d&rsquo;écriture sur la table d&rsquo;origine, parce que je voulais la laisser intacte : ni compactage, ni nettoyage des anciennes versions, ni index. Le script ne fait que lister des fichiers et appeler <code>count_rows</code>, <code>schema</code>, <code>list_versions</code>, <code>list_indices</code> et <code>stats</code>. J&rsquo;ai décodé les manifestes et les fichiers de transaction uniquement pour les versions 1, 21, 33 et 263 à 265, et uniquement sur une copie de la table. Je n&rsquo;ai pas non plus mesuré de temps de requête, qui dépend de l&rsquo;index et viendra dans un prochain article.</p>



<h2 class="wp-block-heading">Documentation officielle</h2>



<p class="wp-block-paragraph">Les pages <a href="https://lance.org/format/table/" rel="noopener">format de table Lance</a>, <a href="https://lance.org/format/table/layout/" rel="noopener">agencement des fichiers</a>, <a href="https://lance.org/format/table/versioning/" rel="noopener">versions</a> et <a href="https://lance.org/format/table/transaction/" rel="noopener">transactions</a> décrivent ce que j&rsquo;ai mesuré, et les définitions protobuf <a href="https://github.com/lance-format/lance/blob/main/protos/table.proto" rel="noopener">table.proto</a> et <a href="https://github.com/lance-format/lance/blob/main/protos/transaction.proto" rel="noopener">transaction.proto</a> donnent les noms de champs. Le <a href="https://lancedb.com/" rel="noopener">projet LanceDB</a> documente l&rsquo;API Python derrière le script.</p>


<p>L’article <a href="https://blog.neurozone.fr/2026/11/08/table-lancedb-sur-disque-versions-manifestes-fragments/">Table LanceDB sur disque : 265 versions, 13,4 Gio, un seul fragment vivant</a> est apparu en premier sur <a href="https://blog.neurozone.fr">Quelques pensées</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.neurozone.fr/2026/11/08/table-lancedb-sur-disque-versions-manifestes-fragments/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">5572</post-id>	</item>
		<item>
		<title>Durcir systemd, directive par directive : mesurer avec systemd-analyze security</title>
		<link>https://blog.neurozone.fr/2026/11/07/systemd-analyze-security-mesurer-avant-de-durcir/</link>
					<comments>https://blog.neurozone.fr/2026/11/07/systemd-analyze-security-mesurer-avant-de-durcir/#respond</comments>
		
		<dc:creator><![CDATA[Hécate Raven]]></dc:creator>
		<pubDate>Sat, 07 Nov 2026 09:00:00 +0000</pubDate>
				<category><![CDATA[Informatique]]></category>
		<category><![CDATA[carnet d'admin]]></category>
		<category><![CDATA[durcissement]]></category>
		<category><![CDATA[Français]]></category>
		<category><![CDATA[hardening]]></category>
		<category><![CDATA[Rocky Linux]]></category>
		<category><![CDATA[sécurité]]></category>
		<category><![CDATA[systemd]]></category>
		<guid isPermaLink="false">https://blog.neurozone.fr/?p=4897</guid>

					<description><![CDATA[<p>Partie 1 d'une série sur le durcissement de systemd : lire systemd-analyze security, écrire un drop-in, tester une directive à la fois et l'annuler, avec un lab sur un service inoffensif.</p>
<p>L’article <a href="https://blog.neurozone.fr/2026/11/07/systemd-analyze-security-mesurer-avant-de-durcir/">Durcir systemd, directive par directive : mesurer avec systemd-analyze security</a> est apparu en premier sur <a href="https://blog.neurozone.fr">Quelques pensées</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph"><code>systemd-analyze security</code> est la commande que je lance avant de modifier quoi que ce soit dans une unité systemd. Elle attribue à chaque service une note d&rsquo;exposition, liste les options de sandboxing que l&rsquo;unité n&rsquo;utilise pas encore et indique par où commencer. Cet article est la partie 1 d&rsquo;une série de huit sur le durcissement de systemd, directive par directive. Je couvre ici la méthode : comment fonctionnent les drop-ins, comment lire la sortie de systemd-analyze security, comment tester une modification sans risque et quels services traiter en premier.</p>



<p class="wp-block-paragraph">Les parties suivantes passent en revue les directives, une famille à la fois. Pour chaque directive, j&rsquo;explique ce qu&rsquo;elle fait, quels services de ma stack elle touche et ce qu&rsquo;elle peut casser. Cette première partie reste sur la méthode, parce que toutes les suivantes s&rsquo;appuient dessus. Aucune expérience de systemd n&rsquo;est nécessaire : chaque étape vient avec la commande complète, un commentaire sur chaque ligne et un petit lab sur un service de démonstration inoffensif.</p>



<p class="wp-block-paragraph"><strong>Banc de test.</strong> Toutes les sorties de cet article viennent d&rsquo;exécutions réelles sur <strong>Rocky Linux 10.2 avec systemd 257</strong>, dans une machine virtuelle WSL2 (noyau 6.18.54.1-microsoft-standard-WSL2, session root). SELinux y est désactivé et certaines fonctions du noyau peuvent différer d&rsquo;un serveur classique. Je le signale à chaque endroit où cela compte. Quand je coupe des lignes d&rsquo;une sortie, je les remplace par <code>[...]</code>, et je marque <em>non testées</em> les commandes que je n&rsquo;ai pas lancées.</p>



<h2 class="wp-block-heading">Avant de commencer</h2>



<p class="wp-block-paragraph">Lisez d&rsquo;abord cette courte liste. Elle vous évite l&rsquo;erreur classique : durcir un service de production du premier coup.</p>



<ul class="wp-block-list"><li><strong>Une machine de test.</strong> Utilisez une machine virtuelle ou un serveur de rechange sous systemd. J&rsquo;ai tout exécuté sur Rocky Linux 10.2 uniquement ; ces commandes appartiennent à systemd lui-même et devraient se comporter de la même façon sur d&rsquo;autres distributions, mais je ne l&rsquo;ai pas testé.</li><li><strong>Un shell avec les droits <code>sudo</code>.</strong> Chaque commande qui modifie le système commence par <code>sudo</code>.</li><li><strong>Un service inoffensif pour vos premiers essais.</strong> Le lab ci-dessous crée un service de démonstration nommé <code>demo-hardening-sandbox.service</code> : vous ne touchez donc à aucun vrai service.</li><li><strong>Un moyen de tout annuler.</strong> Chaque changement vit dans un seul fichier drop-in, et supprimer ce fichier annule le changement. Le lab se termine par une étape de nettoyage qui supprime tout ce qu&rsquo;il a créé.</li><li><strong>Un systemd récent.</strong> Plusieurs options de systemd-analyze security sont apparues dans la version 250 : vérifiez donc la vôtre avec <code>systemctl --version</code>.</li></ul>



<h2 class="wp-block-heading">Un court glossaire</h2>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Terme</th><th>Sens dans cet article</th></tr></thead><tbody><tr><td>Unité</td><td>Un fichier qui décrit ce que systemd gère. Une unité de type <em>service</em> se termine par <code>.service</code>.</td></tr><tr><td>Service</td><td>Une unité qui lance un programme : un démon qui reste actif, ou une commande qui s&rsquo;exécute une fois puis se termine.</td></tr><tr><td>Drop-in</td><td>Un petit fichier <code>.conf</code> placé dans un répertoire <code>.d</code>, qui modifie une unité sans toucher au fichier d&rsquo;origine.</td></tr><tr><td>Directive</td><td>Un réglage d&rsquo;un fichier d&rsquo;unité, par exemple <code>ProtectSystem=strict</code>.</td></tr><tr><td>Sandboxing</td><td>Les directives qui limitent ce qu&rsquo;un service peut voir et faire.</td></tr><tr><td>Namespace</td><td>Une fonction du noyau qui donne à un processus sa propre vue du système, par exemple ses propres points de montage. Plusieurs directives s&rsquo;appuient dessus.</td></tr><tr><td>Capability</td><td>Une part des pouvoirs de root, par exemple ouvrir un port bas, que le noyau peut accorder séparément.</td></tr><tr><td>Appel système</td><td>Une requête d&rsquo;un programme au noyau, par exemple ouvrir un fichier.</td></tr><tr><td>Niveau d&rsquo;exposition</td><td>La note que <code>systemd-analyze security</code> calcule pour une unité.</td></tr><tr><td>Unité transitoire</td><td>Une unité temporaire que <code>systemd-run</code> crée à la volée ; elle disparaît quand elle s&rsquo;arrête.</td></tr></tbody></table></figure>



<h2 class="wp-block-heading">En un coup d&rsquo;œil</h2>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Étape</th><th>Commande</th><th>Ce qu&rsquo;elle vous apprend</th></tr></thead><tbody><tr><td>Noter tous les services actifs</td><td><code>systemd-analyze security</code></td><td>Une ligne par service chargé, avec un niveau d&rsquo;exposition</td></tr><tr><td>Détailler un service</td><td><code>systemd-analyze security UNITÉ</code></td><td>Chaque réglage, appliqué (✓) ou non (✗), avec son exposition</td></tr><tr><td>Contrôler un fichier avant de le déployer</td><td><code>systemd-analyze verify FICHIER</code> puis <code>security --offline=true FICHIER</code></td><td>Les erreurs de syntaxe, puis la revue hors ligne</td></tr><tr><td>Changer une directive</td><td><code>systemctl edit UNITÉ</code></td><td>Un fichier drop-in, rechargé à l&rsquo;enregistrement</td></tr><tr><td>Expérimenter sans modifier de fichier</td><td><code>systemd-run -p DIRECTIVE=VALEUR ...</code></td><td>Une unité temporaire avec les propriétés de votre choix</td></tr><tr><td>Lire un échec</td><td><code>systemctl status UNITÉ</code> et <code>journalctl -u UNITÉ</code></td><td>Le code de sortie, puis les messages du service lui-même</td></tr><tr><td>Revenir en arrière</td><td><code>systemctl revert UNITÉ</code></td><td>Supprime les drop-ins de l&rsquo;unité</td></tr></tbody></table></figure>



<h2 class="wp-block-heading">Pourquoi durcir les services avec systemd</h2>



<p class="wp-block-paragraph">Un service qui n&rsquo;a aucune directive de sandboxing tourne sans aucune de ces protections. Le manuel systemd.exec qualifie les options de sandboxing de moyen efficace de limiter l&rsquo;exposition du système, et recommande d&rsquo;en activer autant que possible sans empêcher le processus de fonctionner. Mon service de démonstration montre le point de départ : sans aucun sandboxing, systemd-analyze security lui attribue <strong>9,6 et l&rsquo;étiquette UNSAFE</strong>, avec 75 réglages non appliqués et 4 appliqués (vous le mesurerez vous-même dans le lab).</p>



<p class="wp-block-paragraph">Le sandboxing ne remplace pas les autres couches de défense. En revanche, il en ajoute une qui ne coûte presque rien : quelques lignes dans un drop-in. Deux limites du manuel valent d&rsquo;être retenues. D&rsquo;abord, beaucoup d&rsquo;options sont désactivées sans bruit sur les systèmes où le mécanisme sous-jacent n&rsquo;existe pas. Ensuite, certaines fonctions de sandboxing ne sont en général pas disponibles dans les services utilisateur.</p>



<h2 class="wp-block-heading">Comment une unité et ses drop-ins s&rsquo;assemblent</h2>



<p class="wp-block-paragraph">Un drop-in est la façon la plus sûre de modifier un service. Au lieu d&rsquo;éditer le fichier installé par un paquet, vous ajoutez un petit fichier à côté, et systemd fusionne les deux. Le schéma montre quel fichier l&#8217;emporte.</p>



<figure class="wp-block-image size-full"><a href="https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/systemd-analyze-security-mesurer-avant-de-durcir-1.png?ssl=1"><img data-recalc-dims="1" loading="lazy" decoding="async" width="629" height="285" src="https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/systemd-analyze-security-mesurer-avant-de-durcir-1.png?resize=629%2C285&#038;ssl=1" alt="Superposition des drop-ins d&#x27;une unité systemd : fichier du paquet, drop-ins d&#x27;exécution et votre drop-in fusionnés en unité effective" class="wp-image-4894" srcset="https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/systemd-analyze-security-mesurer-avant-de-durcir-1.png?w=1720&amp;ssl=1 1720w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/systemd-analyze-security-mesurer-avant-de-durcir-1.png?resize=300%2C136&amp;ssl=1 300w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/systemd-analyze-security-mesurer-avant-de-durcir-1.png?resize=1024%2C464&amp;ssl=1 1024w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/systemd-analyze-security-mesurer-avant-de-durcir-1.png?resize=768%2C348&amp;ssl=1 768w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/systemd-analyze-security-mesurer-avant-de-durcir-1.png?resize=1536%2C697&amp;ssl=1 1536w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/systemd-analyze-security-mesurer-avant-de-durcir-1.png?resize=900%2C408&amp;ssl=1 900w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/systemd-analyze-security-mesurer-avant-de-durcir-1.png?resize=1280%2C580&amp;ssl=1 1280w" sizes="auto, (max-width: 629px) 100vw, 629px" /></a><figcaption class="wp-element-caption">Quel fichier l&#x27;emporte quand une unité systemd et ses drop-ins sont fusionnés.</figcaption></figure>



<h3 class="wp-block-heading">Où vivent les drop-ins et lequel gagne</h3>



<p class="wp-block-paragraph">Pour une unité nommée <code>foo.service</code>, systemd cherche un répertoire <code>foo.service.d/</code>. Il ne lit que les fichiers qui se terminent par <code>.conf</code> et les fusionne par ordre alphanumérique après avoir lu le fichier principal de l&rsquo;unité. Chaque drop-in doit contenir son propre en-tête de section, par exemple <code>[Service]</code>.</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Emplacement</th><th>Rôle</th><th>Priorité</th></tr></thead><tbody><tr><td><code>/usr/lib/systemd/system/</code></td><td>Unités et drop-ins fournis par les paquets</td><td>La plus basse</td></tr><tr><td><code>/run/systemd/system/</code></td><td>Changements d&rsquo;exécution, perdus au redémarrage</td><td>Intermédiaire</td></tr><tr><td><code>/etc/systemd/system/</code></td><td>Vos propres unités et drop-ins</td><td>La plus haute</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Le manuel ajoute trois règles. Les drop-ins de <code>/etc/</code> l&#8217;emportent sur ceux de <code>/run/</code>, qui l&#8217;emportent sur ceux de <code>/usr/lib/</code>. Les drop-ins l&#8217;emportent sur les fichiers d&rsquo;unité, où qu&rsquo;ils se trouvent. À niveau égal, les fichiers de noms différents s&rsquo;appliquent dans l&rsquo;ordre lexicographique. Cette dernière règle explique pourquoi je donne à mes fichiers un nom qui indique leur rôle, comme <code>hardening.conf</code>.</p>



<h3 class="wp-block-heading">Créer, vérifier et annuler un drop-in</h3>



<p class="wp-block-paragraph">Le lab ci-dessous lance les vraies commandes dans l&rsquo;ordre, avec leur sortie : <code>systemctl edit</code> pour créer un drop-in, <code>systemctl cat</code> pour voir ce que systemd lit, <code>systemctl show</code> pour voir ce qu&rsquo;il a chargé, et <code>systemctl revert</code> pour l&rsquo;annuler. Un point mérite l&rsquo;attention. <code>systemctl edit</code> recharge la configuration à votre place, et un rechargement conserve l&rsquo;état d&rsquo;exécution. Un service déjà lancé garde donc son ancien sandbox jusqu&rsquo;à son redémarrage. L&rsquo;erreur 4 du lab le démontre.</p>



<h2 class="wp-block-heading">Lire la sortie de systemd-analyze security</h2>



<p class="wp-block-paragraph">L&rsquo;outil systemd-analyze security analyse les réglages de sécurité et de sandboxing des unités de service. Il ne démarre rien. Il lit les réglages, puis les compare aux protections que systemd sait appliquer.</p>



<figure class="wp-block-image size-full"><a href="https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/systemd-analyze-security-mesurer-avant-de-durcir-3.png?ssl=1"><img data-recalc-dims="1" loading="lazy" decoding="async" width="629" height="293" src="https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/systemd-analyze-security-mesurer-avant-de-durcir-3.png?resize=629%2C293&#038;ssl=1" alt="Lire le tableau de systemd-analyze security : lignes non appliquées et appliquées, note d&#x27;exposition de 0,0 à 10,0 et ce que la note ignore" class="wp-image-4896" srcset="https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/systemd-analyze-security-mesurer-avant-de-durcir-3.png?w=1720&amp;ssl=1 1720w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/systemd-analyze-security-mesurer-avant-de-durcir-3.png?resize=300%2C140&amp;ssl=1 300w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/systemd-analyze-security-mesurer-avant-de-durcir-3.png?resize=1024%2C476&amp;ssl=1 1024w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/systemd-analyze-security-mesurer-avant-de-durcir-3.png?resize=768%2C357&amp;ssl=1 768w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/systemd-analyze-security-mesurer-avant-de-durcir-3.png?resize=1536%2C714&amp;ssl=1 1536w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/systemd-analyze-security-mesurer-avant-de-durcir-3.png?resize=900%2C419&amp;ssl=1 900w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/systemd-analyze-security-mesurer-avant-de-durcir-3.png?resize=1280%2C595&amp;ssl=1 1280w" sizes="auto, (max-width: 629px) 100vw, 629px" /></a><figcaption class="wp-element-caption">Lire la sortie de systemd-analyze security, ligne par ligne.</figcaption></figure>



<h3 class="wp-block-heading">Un service ou tout le système</h3>



<p class="wp-block-paragraph">Sans nom d&rsquo;unité, systemd-analyze security inspecte tous les services chargés qui tournent durablement et affiche un tableau concis, une ligne par unité. Avec un ou plusieurs noms d&rsquo;unité, il affiche une analyse détaillée de chacune. Voici le tableau de tout le système, coupé après quatre lignes :</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">systemd-analyze security --no-pager</pre>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">UNIT                                 EXPOSURE PREDICATE HAPPY
NetworkManager.service                    7.8 EXPOSED   🙁
auditd.service                            9.4 UNSAFE    😨
cockpit-wsinstance-http.service           5.4 MEDIUM    😐
cockpit.service                           6.2 MEDIUM    😐
[...]</pre>



<p class="wp-block-paragraph">Les colonnes sont l&rsquo;unité, son niveau d&rsquo;exposition, une étiquette (le « predicate ») et un visage. Sur le banc de test, j&rsquo;ai vu les étiquettes MEDIUM, EXPOSED et UNSAFE ; le manuel ne les définit pas. Fiez-vous au nombre, et voyez l&rsquo;étiquette comme un repère visuel rapide.</p>



<h3 class="wp-block-heading">Le tableau, ligne par ligne</h3>



<p class="wp-block-paragraph">Pour un service, la sortie a trois colonnes : <code>NAME</code>, <code>DESCRIPTION</code> et <code>EXPOSURE</code>. Chaque ligne commence par une marque : ✗ signifie que le réglage n&rsquo;est pas appliqué, ✓ qu&rsquo;il l&rsquo;est. La dernière ligne donne le résultat global. Voici le début du tableau pour le service de démonstration du lab, avant tout sandboxing :</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">systemd-analyze security --no-pager demo-hardening-sandbox.service | head -n 6</pre>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">  NAME                                                        DESCRIPTION                                                             EXPOSURE
✗ RootDirectory=/RootImage=                                   Service runs within the host's root directory                                0.1
  SupplementaryGroups=                                        Service runs as root, option does not matter
  RemoveIPC=                                                  Service runs as root, option does not apply
✗ User=/DynamicUser=                                          Service runs as root user                                                    0.4
✗ CapabilityBoundingSet=~CAP_SYS_TIME                         Service processes may change the system clock                                0.2</pre>



<p class="wp-block-paragraph">Lisez-le comme une liste de tâches. Chaque ligne ✗ est une protection que vous pouvez encore ajouter, et le nombre à droite indique l&rsquo;exposition qu&rsquo;elle retirerait. Les lignes sans marque et sans nombre ne s&rsquo;appliquent pas à cette unité : l&rsquo;outil vous dit par exemple que <code>RemoveIPC=</code> n&rsquo;a pas d&rsquo;importance quand le service tourne en root. Pour ne garder que le travail restant, filtrez la sortie :</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">systemd-analyze security --no-pager demo-hardening-sandbox.service | grep '✗' | head -n 4</pre>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">✗ RootDirectory=/RootImage=                                   Service runs within the host's root directory                                0.1
✗ User=/DynamicUser=                                          Service runs as root user                                                    0.4
✗ CapabilityBoundingSet=~CAP_SYS_TIME                         Service processes may change the system clock                                0.2
✗ NoNewPrivileges=                                            Service processes may acquire new privileges                                 0.2</pre>



<h3 class="wp-block-heading">Ce que la note signifie, et ce qu&rsquo;elle ne signifie pas</h3>



<p class="wp-block-paragraph">Le manuel décrit le niveau d&rsquo;exposition global comme une estimation de 0,0 à 10,0. Une note élevée signale peu de sandboxing, une note basse des restrictions serrées. Soyez prudent avec ce nombre, car le manuel est explicite sur ses limites :</p>



<ul class="wp-block-list"><li>Il ne couvre que les fonctions de sécurité par service que systemd implémente lui-même. Il ignore ce que le programme fait pour se protéger, et il ignore SELinux.</li><li>Une note élevée ne veut pas dire que le service est vulnérable, et une note basse ne prouve pas qu&rsquo;il est sûr. Elle indique seulement que le service pourrait profiter de réglages supplémentaires.</li><li>Beaucoup de réglages peuvent être annulés individuellement par d&rsquo;autres réglages. L&rsquo;outil tient compte de certaines combinaisons, pas de toutes.</li><li>Les réglages s&rsquo;appliquent aux opérations propres du service. Les requêtes envoyées par IPC, comme D-Bus, peuvent atteindre des services qui ne sont pas restreints de la même façon : vérifiez donc séparément la politique d&rsquo;accès IPC.</li></ul>



<p class="wp-block-paragraph">En pratique, traitez la note de systemd-analyze security comme une boussole, pas comme un objectif. En effet, elle aide à repérer les services sans aucune protection et confirme qu&rsquo;un changement a produit l&rsquo;effet voulu.</p>



<h3 class="wp-block-heading">Les options à connaître</h3>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Option</th><th>Ce qu&rsquo;elle fait</th><th>Testée ici</th></tr></thead><tbody><tr><td><code>--offline=BOOL</code></td><td>Passe en revue des fichiers d&rsquo;unité sans interroger le gestionnaire en cours d&rsquo;exécution (ajoutée en version 250)</td><td>Oui : fonctionne avec un chemin de fichier et avec un nom d&rsquo;unité</td></tr><tr><td><code>--threshold=NOMBRE</code></td><td>Renvoie une erreur quand le niveau global dépasse la valeur (ajoutée en 250)</td><td>Oui : code de sortie 1 au-dessus du seuil, 0 en dessous</td></tr><tr><td><code>--json=MODE</code></td><td>Affiche le tableau en JSON : <code>off</code>, <code>pretty</code> ou <code>short</code></td><td>Oui : <code>short</code></td></tr><tr><td><code>--security-policy=CHEMIN</code></td><td>Applique vos propres exigences en JSON ; un poids de 0 signifie « ne pas contrôler » (ajoutée en 250)</td><td>Non testée</td></tr><tr><td><code>--profile=CHEMIN</code></td><td>Avec <code>--offline</code>, prend en compte un profil portable (ajoutée en 250)</td><td>Non testée</td></tr><tr><td><code>--no-pager</code></td><td>Affiche directement, sans pagineur</td><td>Oui</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Voici les résultats réels pour le service de démonstration une fois le drop-in du lab en place (sa note est 8,9, comme le montre l&rsquo;étape 3 du lab). J&rsquo;ai lancé deux fois <code>--offline</code>, sur le chemin du fichier puis sur le nom de l&rsquo;unité :</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">systemd-analyze security --offline=true --no-pager /etc/systemd/system/demo-hardening-sandbox.service | tail -n 1
systemd-analyze security --offline=true --no-pager demo-hardening-sandbox.service | tail -n 1</pre>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">→ Overall exposure level for demo-hardening-sandbox.service: 8.9 EXPOSED 🙁
→ Overall exposure level for demo-hardening-sandbox.service: 8.9 EXPOSED 🙁</pre>



<p class="wp-block-paragraph">J&rsquo;ai ensuite lancé <code>--threshold</code> deux fois pour vérifier le code de sortie :</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">systemd-analyze security --threshold=3 --no-pager demo-hardening-sandbox.service | tail -n 1; echo "exit=${PIPESTATUS[0]}"
systemd-analyze security --threshold=100 --no-pager demo-hardening-sandbox.service | tail -n 1; echo "exit=${PIPESTATUS[0]}"</pre>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">→ Overall exposure level for demo-hardening-sandbox.service: 8.9 EXPOSED 🙁
exit=1
→ Overall exposure level for demo-hardening-sandbox.service: 8.9 EXPOSED 🙁
exit=0</pre>



<p class="wp-block-paragraph">Le mode JSON donne un objet par ligne, avec les champs <code>set</code>, <code>name</code>, <code>json_field</code>, <code>description</code> et <code>exposure</code>. Voici les 330 premiers caractères :</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">systemd-analyze security --json=short --no-pager demo-hardening-sandbox.service | head -c 330</pre>



<pre class="EnlighterJSRAW" data-enlighter-language="json">[{"set":false,"name":"RootDirectory=/RootImage=","json_field":"RootDirectoryOrRootImage","description":"Service runs within the host's root directory","exposure":"0.1"},{"set":null,"name":"SupplementaryGroups=","json_field":"SupplementaryGroups","description":"Service runs as root, option does not matter","exposure":null},{"set"
[...]</pre>



<p class="wp-block-paragraph">Un détail mérite un avertissement. Le manuel dit que le niveau global va de 0,0 à 10,0, mais il documente un seuil par défaut de 100, et précise que <code>--offline</code> renvoie une erreur au-dessus de ce seuil. Le seuil par défaut ne se déclenche donc jamais. Si vous utilisez <code>--threshold</code> dans un script, passez une valeur explicite et testez les deux côtés du seuil, comme je l&rsquo;ai fait ci-dessus.</p>



<h2 class="wp-block-heading">Contrôler une unité avant de la déployer</h2>



<p class="wp-block-paragraph">Deux commandes vous aident avant qu&rsquo;un fichier n&rsquo;entre dans <code>/etc/systemd/system/</code>. <code>systemd-analyze verify</code> détecte les sections et directives inconnues, les dépendances manquantes, les pages de manuel citées dans <code>Documentation=</code> qui n&rsquo;existent pas, et les commandes de <code>ExecStart=</code> introuvables ou non exécutables. Ensuite, le mode hors ligne de systemd-analyze security (<code>--offline=true</code>) passe le fichier en revue sans interroger le gestionnaire en cours d&rsquo;exécution. Voici une exécution propre sur l&rsquo;unité du lab :</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">systemd-analyze verify /etc/systemd/system/demo-hardening-sandbox.service; echo "exit=$?"</pre>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">exit=0</pre>



<p class="wp-block-paragraph">L&rsquo;absence de message et le code 0 signifient que <code>verify</code> n&rsquo;a rien à signaler. Cependant, le manuel ne donne aucun exemple de revue hors ligne d&rsquo;un chemin de fichier. Cela fonctionne sur mon banc de test, mais essayez-le sur une copie avant de vous y fier.</p>



<h2 class="wp-block-heading">Une boucle de durcissement sûre</h2>



<p class="wp-block-paragraph">Le durcissement tourne mal quand vous changez cinq choses à la fois et ne savez plus laquelle a cassé le service. La boucle ci-dessous l&rsquo;évite. Elle commence par systemd-analyze security, puis ajoute une directive à la fois, et elle se termine par un test du vrai travail du service.</p>



<figure class="wp-block-image size-full"><a href="https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/systemd-analyze-security-mesurer-avant-de-durcir-2.png?ssl=1"><img data-recalc-dims="1" loading="lazy" decoding="async" width="629" height="278" src="https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/systemd-analyze-security-mesurer-avant-de-durcir-2.png?resize=629%2C278&#038;ssl=1" alt="Boucle de durcissement d&#x27;un service systemd : mesurer, changer une directive, redémarrer, lire le résultat, tester le vrai travail, garder ou annuler" class="wp-image-4895" srcset="https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/systemd-analyze-security-mesurer-avant-de-durcir-2.png?w=1720&amp;ssl=1 1720w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/systemd-analyze-security-mesurer-avant-de-durcir-2.png?resize=300%2C133&amp;ssl=1 300w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/systemd-analyze-security-mesurer-avant-de-durcir-2.png?resize=1024%2C452&amp;ssl=1 1024w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/systemd-analyze-security-mesurer-avant-de-durcir-2.png?resize=768%2C339&amp;ssl=1 768w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/systemd-analyze-security-mesurer-avant-de-durcir-2.png?resize=1536%2C679&amp;ssl=1 1536w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/systemd-analyze-security-mesurer-avant-de-durcir-2.png?resize=900%2C398&amp;ssl=1 900w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/systemd-analyze-security-mesurer-avant-de-durcir-2.png?resize=1280%2C566&amp;ssl=1 1280w" sizes="auto, (max-width: 629px) 100vw, 629px" /></a><figcaption class="wp-element-caption">La boucle de durcissement : une directive à la fois, puis la garder ou l&#x27;annuler.</figcaption></figure>



<ol class="wp-block-list"><li><strong>Mesurer.</strong> Lancez <code>systemd-analyze security UNITÉ</code> et notez les lignes ✗ que vous comptez corriger.</li><li><strong>Changer une directive</strong> dans un drop-in avec <code>sudo systemctl edit UNITÉ</code>.</li><li><strong>Redémarrer</strong> avec <code>sudo systemctl restart UNITÉ</code>. Un simple rechargement ne donne pas son nouveau sandbox à un service déjà lancé.</li><li><strong>Lire le résultat</strong> avec <code>systemctl status UNITÉ</code> et <code>journalctl -u UNITÉ</code>.</li><li><strong>Tester le vrai travail :</strong> se connecter, envoyer un fichier, lancer la sauvegarde, envoyer un courrier. Un service peut démarrer et échouer plus tard.</li><li><strong>Garder ou annuler.</strong> Gardez le drop-in dans votre gestion de configuration, ou supprimez-le, lancez <code>sudo systemctl daemon-reload</code> et redémarrez.</li></ol>



<p class="wp-block-paragraph">Les deux dernières étapes comptent le plus. Un sandbox peut laisser un service démarrer alors que l&rsquo;une de ses tâches échoue plus tard, par exemple parce qu&rsquo;elle a besoin d&rsquo;un chemin désormais en lecture seule. C&rsquo;est pourquoi la boucle se termine par un test du vrai travail, et non par un démarrage réussi.</p>



<h3 class="wp-block-heading">Lire un échec</h3>



<p class="wp-block-paragraph">Quand un sandbox casse un service, le code de sortie et le message du service vous indiquent la directive à assouplir. Voici une unité de démonstration qui tourne avec <code>ProtectSystem=strict</code> et tente de créer un fichier dans <code>/etc</code>. La sortie de status est coupée après le résumé, car les lignes du journal qui suivent portent le nom de ma machine de test :</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">sudo systemctl start demo-hardening-fail.service
systemctl status --no-pager -n 5 demo-hardening-fail.service</pre>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">× demo-hardening-fail.service - Demo unit that the sandbox breaks (harmless)
     Loaded: loaded (/etc/systemd/system/demo-hardening-fail.service; static)
     Active: failed (Result: exit-code) since Mon 2026-11-02 14:54:40 CET; 35ms ago
 Invocation: 55376c422cb741a69786c3968cc455a2
    Process: 2008 ExecStart=/bin/touch /etc/demo-hardening-fail (code=exited, status=1/FAILURE)
   Main PID: 2008 (code=exited, status=1/FAILURE)
   Mem peak: 1.2M
        CPU: 5ms
[...]</pre>



<p class="wp-block-paragraph">Le journal montre le message du programme lui-même. <code>-u</code> sélectionne l&rsquo;unité, <code>-o cat</code> n&rsquo;affiche que le message et <code>-n 6</code> limite le nombre de lignes :</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">journalctl -u demo-hardening-fail.service -o cat --no-pager -n 6</pre>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">Starting demo-hardening-fail.service - Demo unit that the sandbox breaks (harmless)...
/bin/touch: cannot touch '/etc/demo-hardening-fail': Read-only file system
demo-hardening-fail.service: Main process exited, code=exited, status=1/FAILURE
demo-hardening-fail.service: Failed with result 'exit-code'.
Failed to start demo-hardening-fail.service - Demo unit that the sandbox breaks (harmless).</pre>



<p class="wp-block-paragraph">Le message dit <em>Read-only file system</em> : la directive responsable est donc <code>ProtectSystem=</code>. La solution est d&rsquo;autoriser le chemin dont le programme a vraiment besoin avec <code>ReadWritePaths=</code>, que la partie 2 de la série détaille. Les messages de systemd restent en anglais sur le banc de test.</p>



<h2 class="wp-block-heading">Lab pratique : voir trois directives à l&rsquo;œuvre</h2>



<p class="wp-block-paragraph">Ce lab prend environ quinze minutes. Il utilise un service de démonstration qui se contente de lister, tester et créer des fichiers, et il supprime tout à la fin. Chaque sortie ci-dessous est une sortie réelle du banc de test. Les lignes qui commencent par <code>---</code> dans les sorties sont des libellés que mon script de test a affichés entre les commandes, en anglais.</p>



<h3 class="wp-block-heading">Étape 1 : créer le service de démonstration et le lancer sans sandbox</h3>



<p class="wp-block-paragraph">Créez le fichier <code>/etc/systemd/system/demo-hardening-sandbox.service</code> avec votre éditeur, par exemple <code>sudo nano /etc/systemd/system/demo-hardening-sandbox.service</code>. Les lignes qui commencent par <code>#</code> sont des commentaires. Mettez toujours un commentaire sur sa propre ligne, jamais après un réglage.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="ini"># Démonstration inoffensive : compte les entrées de /home, teste une écriture dans /etc et crée un fichier dans /tmp.
[Unit]
Description=Service de démonstration du sandboxing (inoffensif)

[Service]
# oneshot : systemd attend la fin de la commande ; l'unité ne reste jamais « active ».
Type=oneshot
# systemd ne lance pas de shell tout seul : la commande démarre donc /bin/sh explicitement.
ExecStart=/bin/sh -c 'echo "entrées dans /home :"; ls -A /home | wc -l; touch /etc/demo-hardening-test &amp;&amp; rm /etc/demo-hardening-test &amp;&amp; echo "etc : écriture ok" || echo "etc : écriture refusée"; touch /tmp/demo-hardening-marker; echo "terminé"'</pre>



<p class="wp-block-paragraph">Demandez maintenant à systemd de lire le nouveau fichier, affichez-le, lancez le service et lisez sa sortie. Un service <code>oneshot</code> s&rsquo;exécute une fois, et <code>systemctl start</code> attend sa fin :</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">sudo systemctl daemon-reload
systemctl cat demo-hardening-sandbox.service
sudo systemctl start demo-hardening-sandbox.service
journalctl -u demo-hardening-sandbox.service -o cat --no-pager
ls -l /tmp/demo-hardening-marker | awk '{print $1, $3, $NF}'</pre>



<pre class="EnlighterJSRAW" data-enlighter-language="ini">[...]
Starting demo-hardening-sandbox.service - Service de démonstration du sandboxing (inoffensif)...
entrées dans /home :
1
etc : écriture ok
terminé
demo-hardening-sandbox.service: Deactivated successfully.
Finished demo-hardening-sandbox.service - Service de démonstration du sandboxing (inoffensif).
-rw-r--r-- root /tmp/demo-hardening-marker</pre>



<p class="wp-block-paragraph"><code>systemctl cat</code> affiche le fichier précédé d&rsquo;une ligne de commentaire avec son chemin. Le journal garde les exécutions précédentes du service sur le banc de test (celles de la variante anglaise de ce lab) : j&rsquo;ai coupé ces anciennes lignes avec <code>[...]</code>. Lisez le résultat. Le service a vu une entrée dans <code>/home</code>, il a pu écrire dans <code>/etc</code> et il a créé son fichier témoin dans le vrai <code>/tmp</code>. Rien n&rsquo;est encore protégé. Mesurez le point de départ :</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">systemd-analyze security --no-pager demo-hardening-sandbox.service | tail -n 1
systemd-analyze security --no-pager demo-hardening-sandbox.service | grep -c '✗'
systemd-analyze security --no-pager demo-hardening-sandbox.service | grep -c '✓'</pre>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">→ Overall exposure level for demo-hardening-sandbox.service: 9.6 UNSAFE 😨
75
4</pre>



<p class="wp-block-paragraph">Regardez enfin les trois lignes que l&rsquo;étape suivante va corriger :</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">systemd-analyze security --no-pager demo-hardening-sandbox.service | grep -E 'ProtectSystem|ProtectHome|PrivateTmp'</pre>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">✗ ProtectSystem=                                              Service has full access to the OS file hierarchy                             0.2
✗ ProtectHome=                                                Service has full access to home directories                                  0.2
✗ PrivateTmp=                                                 Service has access to other software's temporary files                       0.2</pre>



<h3 class="wp-block-heading">Étape 2 : ajouter le drop-in</h3>



<p class="wp-block-paragraph">La méthode habituelle est l&rsquo;éditeur interactif : <code>sudo systemctl edit demo-hardening-sandbox.service</code> ouvre un nouveau fichier nommé <code>override.conf</code> (non testé ici, car il demande un terminal). Pour que le lab reste reproductible, utilisez la forme non interactive. <code>--stdin</code> lit le contenu sur l&rsquo;entrée standard et <code>--drop-in=hardening</code> choisit le nom du fichier :</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">sudo systemctl edit --stdin --drop-in=hardening demo-hardening-sandbox.service &lt;&lt;'EOF'
[Service]
# Tout le système de fichiers devient en lecture seule pour ce service (sauf /dev, /proc et /sys).
ProtectSystem=strict
# /home, /root et /run/user deviennent inaccessibles et vides.
ProtectHome=true
# /tmp et /var/tmp deviennent privés à ce service et sont supprimés à son arrêt.
PrivateTmp=true
EOF</pre>



<p class="wp-block-paragraph">Chaque ligne de ce fichier fait une chose. <code>[Service]</code> est l&rsquo;en-tête de section obligatoire. <code>ProtectSystem=strict</code> monte toute la hiérarchie du système de fichiers en lecture seule, sauf <code>/dev</code>, <code>/proc</code> et <code>/sys</code>. <code>ProtectHome=true</code> rend <code>/home</code>, <code>/root</code> et <code>/run/user</code> inaccessibles et vides. <code>PrivateTmp=true</code> donne au service son propre <code>/tmp</code> et <code>/var/tmp</code>, supprimés à son arrêt. Systemd répond avec le chemin du nouveau fichier :</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">Successfully installed edited file '/etc/systemd/system/demo-hardening-sandbox.service.d/hardening.conf'.</pre>



<p class="wp-block-paragraph">Vérifiez maintenant ce que systemd lit et ce qu&rsquo;il a chargé. La sortie de <code>systemctl cat</code> commence par le fichier principal de l&rsquo;unité ; je coupe cette partie :</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">systemctl cat demo-hardening-sandbox.service
systemctl show --property=ProtectSystem --property=ProtectHome --property=PrivateTmp demo-hardening-sandbox.service</pre>



<pre class="EnlighterJSRAW" data-enlighter-language="ini">[...]
# /etc/systemd/system/demo-hardening-sandbox.service.d/hardening.conf
[Service]
# Tout le système de fichiers devient en lecture seule pour ce service (sauf /dev, /proc et /sys).
ProtectSystem=strict
# /home, /root et /run/user deviennent inaccessibles et vides.
ProtectHome=true
# /tmp et /var/tmp deviennent privés à ce service et sont supprimés à son arrêt.
PrivateTmp=true
PrivateTmp=yes
ProtectHome=yes
ProtectSystem=strict</pre>



<p class="wp-block-paragraph"><code>systemctl show</code> affiche <code>yes</code> là où le fichier dit <code>true</code> : les deux désignent le même booléen. Les trois dernières lignes sont les valeurs que systemd a chargées, et celles d&rsquo;avant sont le fichier. Relancez le service et comparez avec l&rsquo;étape 1 :</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">rm -f /tmp/demo-hardening-marker
sudo systemctl start demo-hardening-sandbox.service
journalctl -u demo-hardening-sandbox.service -n 7 -o cat --no-pager
ls -l /tmp/demo-hardening-marker</pre>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">entrées dans /home :
0
touch: cannot touch '/etc/demo-hardening-test': Read-only file system
etc : écriture refusée
terminé
demo-hardening-sandbox.service: Deactivated successfully.
Finished demo-hardening-sandbox.service - Service de démonstration du sandboxing (inoffensif).
ls: cannot access '/tmp/demo-hardening-marker': No such file or directory</pre>



<p class="wp-block-paragraph">Trois choses ont changé, et elles correspondent au manuel. Le service voit maintenant <strong>0</strong> entrée dans <code>/home</code>. Son écriture dans <code>/etc</code> échoue avec <em>Read-only file system</em>. Et le fichier témoin qu&rsquo;il a créé n&rsquo;est pas visible depuis l&rsquo;hôte, car le service a utilisé un <code>/tmp</code> privé.</p>



<h3 class="wp-block-heading">Étape 3 : mesurer à nouveau</h3>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">systemd-analyze security --no-pager demo-hardening-sandbox.service | tail -n 1
systemd-analyze security --no-pager demo-hardening-sandbox.service | grep -E 'ProtectSystem|ProtectHome|PrivateTmp'</pre>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">→ Overall exposure level for demo-hardening-sandbox.service: 8.9 EXPOSED 🙁
✓ ProtectSystem=                                              Service has strict read-only access to the OS file hierarchy
✓ PrivateTmp=                                                 Service has no access to other software's temporary files
✓ ProtectHome=                                                Service has no access to home directories                               </pre>



<p class="wp-block-paragraph">La note de systemd-analyze security est passée de 9,6 UNSAFE à 8,9 EXPOSED, les trois lignes sont passées de ✗ à ✓, et les compteurs sont passés de 75 non appliqués et 4 appliqués à 71 et 8. Trois directives donnent un gain modeste, ce qui est normal : un service root garde beaucoup d&rsquo;autres expositions, comme l&rsquo;utilisateur sous lequel il tourne et ses capabilities. Les parties 3 et 4 de la série s&rsquo;en occupent.</p>



<h3 class="wp-block-heading">Étape 4 : essayer une directive à la fois avec systemd-run</h3>



<p class="wp-block-paragraph"><code>systemd-run</code> lance une commande comme unité transitoire, et <code>-p</code> fixe une propriété dans le même format que <code>systemctl set-property</code>. Les options ci-dessous sont celles du manuel : <code>--quiet</code> masque le message de démarrage, <code>--pipe</code> relie la commande à l&rsquo;entrée et à la sortie de votre terminal, <code>--wait</code> attend la fin et <code>--collect</code> décharge l&rsquo;unité ensuite, même après un échec. (Le manuel documente aussi <code>-t</code> pour un shell interactif ; je ne l&rsquo;ai pas testé.) Le manuel ne donne aucun exemple de sandboxing pour <code>systemd-run</code> : les résultats ci-dessous sont donc mes propres tests. Les lignes <code>echo</code> affichent les libellés que vous voyez dans la sortie, et chaque <code>ls</code> contrôle ensuite le vrai système de fichiers :</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">RUN="sudo systemd-run --quiet --pipe --wait --collect"
echo "--- no sandbox: write to /etc"; $RUN /bin/touch /etc/demo-hardening-t1; echo "exit=$?"; ls /etc/demo-hardening-t1; rm -f /etc/demo-hardening-t1
echo "--- ProtectSystem=strict: write to /etc"; $RUN -p ProtectSystem=strict /bin/touch /etc/demo-hardening-t2; echo "exit=$?"; ls /etc/demo-hardening-t2 2&gt;&amp;1
echo "--- ProtectSystem=true: write to /etc, then to /usr"
$RUN -p ProtectSystem=true /bin/touch /etc/demo-hardening-t3; echo "etc exit=$?"; ls /etc/demo-hardening-t3; rm -f /etc/demo-hardening-t3
$RUN -p ProtectSystem=true /bin/touch /usr/demo-hardening-t4; echo "usr exit=$?"; ls /usr/demo-hardening-t4 2&gt;&amp;1
echo "--- ProtectSystem=full: write to /etc"; $RUN -p ProtectSystem=full /bin/touch /etc/demo-hardening-t5; echo "exit=$?"; ls /etc/demo-hardening-t5 2&gt;&amp;1
echo "--- /home entries seen by the service, without and with ProtectHome=true"
$RUN /bin/sh -c 'ls -A /home | wc -l'
$RUN -p ProtectHome=true /bin/sh -c 'ls -A /home | wc -l'
echo "--- ProtectHome=true: ls -ld /home inside the service"; $RUN -p ProtectHome=true /bin/ls -ld /home | awk '{print $1, $3, $4}'
echo "--- PrivateTmp=true: file created in /tmp by the service, seen from the host?"
rm -f /tmp/demo-hardening-t6
$RUN -p PrivateTmp=true /bin/touch /tmp/demo-hardening-t6; echo "service exit=$?"; ls /tmp/demo-hardening-t6 2&gt;&amp;1
echo "--- without PrivateTmp, same test"
$RUN /bin/touch /tmp/demo-hardening-t7; echo "service exit=$?"; ls /tmp/demo-hardening-t7 2&gt;&amp;1; rm -f /tmp/demo-hardening-t7</pre>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">--- no sandbox: write to /etc
exit=0
/etc/demo-hardening-t1
--- ProtectSystem=strict: write to /etc
/bin/touch: cannot touch '/etc/demo-hardening-t2': Read-only file system
exit=1
ls: cannot access '/etc/demo-hardening-t2': No such file or directory
--- ProtectSystem=true: write to /etc, then to /usr
etc exit=0
/etc/demo-hardening-t3
/bin/touch: cannot touch '/usr/demo-hardening-t4': Read-only file system
usr exit=1
ls: cannot access '/usr/demo-hardening-t4': No such file or directory
--- ProtectSystem=full: write to /etc
/bin/touch: cannot touch '/etc/demo-hardening-t5': Read-only file system
exit=1
ls: cannot access '/etc/demo-hardening-t5': No such file or directory
--- /home entries seen by the service, without and with ProtectHome=true
1
0
--- ProtectHome=true: ls -ld /home inside the service
d--------- root root
--- PrivateTmp=true: file created in /tmp by the service, seen from the host?
service exit=0
ls: cannot access '/tmp/demo-hardening-t6': No such file or directory
--- without PrivateTmp, same test
service exit=0
/tmp/demo-hardening-t7</pre>



<p class="wp-block-paragraph">Ce tableau lit les résultats comme le manuel les décrit :</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Directive</th><th>Ce que mon test a montré</th><th>Manuel</th></tr></thead><tbody><tr><td><code>ProtectSystem=true</code></td><td>L&rsquo;écriture dans <code>/etc</code> fonctionne, l&rsquo;écriture dans <code>/usr</code> est refusée</td><td><code>/usr</code> et les répertoires du chargeur de démarrage en lecture seule</td></tr><tr><td><code>ProtectSystem=full</code></td><td>L&rsquo;écriture dans <code>/etc</code> est refusée</td><td><code>/etc</code> en lecture seule également</td></tr><tr><td><code>ProtectSystem=strict</code></td><td>L&rsquo;écriture dans <code>/etc</code> est refusée</td><td>Toute la hiérarchie en lecture seule, sauf <code>/dev</code>, <code>/proc</code>, <code>/sys</code></td></tr><tr><td><code>ProtectHome=true</code></td><td>0 entrée vue, et <code>/home</code> affiche le mode <code>d---------</code></td><td>Répertoires inaccessibles et vides</td></tr><tr><td><code>PrivateTmp=true</code></td><td>Le fichier créé par le service est absent sur l&rsquo;hôte</td><td><code>/tmp</code> et <code>/var/tmp</code> non partagés avec les processus extérieurs</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Ces résultats confirment le manuel sur mon banc de test. Ils viennent d&rsquo;un noyau WSL2 : confirmez-les sur votre propre serveur avant de vous appuyer sur une directive en production.</p>



<h3 class="wp-block-heading">Étape 5 : annuler et nettoyer</h3>



<p class="wp-block-paragraph"><code>systemctl revert</code> supprime les drop-ins d&rsquo;une unité. Il laisse en place un fichier d&rsquo;unité qui n&rsquo;a pas de version fournie par un paquet, comme notre fichier de démonstration. Supprimez ensuite les fichiers de démonstration et prévenez systemd :</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">sudo systemctl revert demo-hardening-sandbox.service
sudo rm -f /etc/systemd/system/demo-hardening-sandbox.service
sudo rm -f /tmp/demo-hardening-marker
sudo systemctl daemon-reload</pre>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">Removed '/etc/systemd/system/demo-hardening-sandbox.service.d/hardening.conf'.
Removed '/etc/systemd/system/demo-hardening-sandbox.service.d'.</pre>



<p class="wp-block-paragraph">La première commande a affiché les deux lignes ci-dessus. Après le revert, le fichier d&rsquo;unité était toujours là et le répertoire <code>.d</code> avait disparu. Après le nettoyage, j&rsquo;ai vérifié qu&rsquo;il ne restait aucune unité de démonstration, aucun fichier dans <code>/tmp</code>, <code>/etc</code> ou <code>/usr</code>, et aucune unité de démonstration chargée (tous les compteurs à 0).</p>



<h2 class="wp-block-heading">Erreurs courantes</h2>



<p class="wp-block-paragraph">Chaque erreur ci-dessous s&rsquo;est produite sur le banc de test, avec le message réel. Je les ai reproduites sur une unité jetable nommée <code>demo-hardening-mistakes.service</code> (un service <code>oneshot</code> qui lance <code>/bin/true</code>) et sur un service <code>sleep</code>, puis j&rsquo;ai supprimé les deux.</p>



<h3 class="wp-block-heading">Erreur 1 : un drop-in sans en-tête de section</h3>



<p class="wp-block-paragraph">Le drop-in <code>a-no-header.conf</code> ne contenait que <code>ProtectSystem=strict</code>. Systemd a ignoré la ligne, et la valeur chargée est restée <code>no</code> :</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">systemctl show --property=ProtectSystem demo-hardening-mistakes.service
journalctl -b -o cat --no-pager --since "-1min" | grep -i a-no-header | sort -u</pre>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">ProtectSystem=no
/etc/systemd/system/demo-hardening-mistakes.service.d/a-no-header.conf:1: Assignment outside of section. Ignoring.</pre>



<h3 class="wp-block-heading">Erreur 2 : un drop-in sans le suffixe .conf</h3>



<p class="wp-block-paragraph">Un fichier nommé <code>b-wrong-suffix.txt</code> n&rsquo;est jamais lu. <code>systemctl cat</code> ne liste que le fichier principal, et la valeur reste <code>no</code> :</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">systemctl cat demo-hardening-mistakes.service | grep '^#'
systemctl show --property=ProtectSystem demo-hardening-mistakes.service</pre>



<pre class="EnlighterJSRAW" data-enlighter-language="shell"># /etc/systemd/system/demo-hardening-mistakes.service
ProtectSystem=no</pre>



<h3 class="wp-block-heading">Erreur 3 : un commentaire sur la même ligne qu&rsquo;un réglage</h3>



<p class="wp-block-paragraph">J&rsquo;ai écrit <code>ProtectSystem=strict # make everything read-only</code> sur une seule ligne. Systemd a lu tout le texte comme la valeur, l&rsquo;a rejeté et a gardé la valeur par défaut :</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">systemctl show --property=ProtectSystem demo-hardening-mistakes.service
journalctl -b -o cat --no-pager --since "-1min" | grep -i c-inline-comment | sort -u</pre>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">ProtectSystem=no
/etc/systemd/system/demo-hardening-mistakes.service.d/c-inline-comment.conf:2: Failed to parse ProtectSystem=strict # make everything read-only, ignoring: Invalid argument</pre>



<h3 class="wp-block-heading">Erreur 4 : un rechargement sans redémarrage</h3>



<p class="wp-block-paragraph">Un service <code>sleep</code> tournait déjà. J&rsquo;ai ajouté <code>ProtectSystem=strict</code> puis lancé <code>daemon-reload</code>. <code>systemctl show</code> a répondu <code>strict</code>, mais le processus a gardé le même PID et le même namespace de montage que le PID 1. Seul le redémarrage lui a donné un nouveau PID et son propre namespace :</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">PID=$(systemctl show --property=MainPID --value demo-hardening-sleep.service)
readlink /proc/1/ns/mnt
readlink /proc/$PID/ns/mnt</pre>



<p class="wp-block-paragraph">J&rsquo;ai lancé ces trois lignes avant le changement, après le rechargement et après le redémarrage. Les résultats, avec leurs libellés, sont :</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">--- mount namespace of PID 1 and of the service
pid1:    mnt:[4026532218]
service: mnt:[4026532218]
--- after drop-in + daemon-reload (no restart)
show ProtectSystem: strict
same PID: 1886 (was 1886)
service: mnt:[4026532218]
--- after systemctl restart
new PID: 1948
pid1:    mnt:[4026532218]
service: mnt:[4026532287]</pre>



<h3 class="wp-block-heading">Autres erreurs à éviter</h3>



<ul class="wp-block-list"><li><strong>Modifier le fichier du paquet dans <code>/usr/lib/systemd/system/</code>.</strong> Utilisez plutôt un drop-in dans <code>/etc/systemd/system/</code> ; le manuel donne la priorité à <code>/etc/</code> sur <code>/usr/lib/</code>.</li><li><strong>Juger un changement sur « le service démarre ».</strong> Testez le vrai travail, comme dans la boucle ci-dessus.</li><li><strong>Changer plusieurs directives à la fois.</strong> Vous ne saurez pas laquelle a cassé le service.</li><li><strong>Viser une note de 0.</strong> Le manuel précise qu&rsquo;une note élevée ne signifie pas qu&rsquo;un service est vulnérable. Servez-vous de la note de systemd-analyze security pour prioriser.</li><li><strong>Utiliser des directives de namespace de montage dans un service utilisateur.</strong> Le manuel avertit que certaines fonctions de sandboxing ne sont en général pas disponibles dans les services utilisateur.</li></ul>



<h2 class="wp-block-heading">Une checklist à suivre</h2>



<ol class="wp-block-list"><li>Travaillez d&rsquo;abord sur une machine de test ou sur un service inoffensif.</li><li>Lancez <code>systemd-analyze security UNITÉ</code> et notez la note et les lignes ✗.</li><li>Écrivez une directive dans un drop-in sous <code>/etc/systemd/system/UNITÉ.d/</code>, dans un fichier <code>.conf</code> avec un en-tête <code>[Service]</code> et des commentaires sur leur propre ligne.</li><li>Lancez <code>systemctl cat UNITÉ</code> pour vérifier que systemd lit votre fichier.</li><li>Lancez <code>sudo systemctl restart UNITÉ</code> ; un simple rechargement ne suffit pas pour un service déjà lancé.</li><li>Lisez <code>systemctl status UNITÉ</code> et <code>journalctl -u UNITÉ</code>.</li><li>Testez le vrai travail du service, pas seulement son démarrage.</li><li>Mesurez à nouveau avec <code>systemd-analyze security UNITÉ</code>.</li><li>Gardez le drop-in dans votre gestion de configuration, ou supprimez-le avec <code>systemctl revert</code> puis redémarrez.</li><li>Recommencez avec la directive suivante.</li></ol>



<h2 class="wp-block-heading">Les familles de directives des prochaines parties</h2>



<p class="wp-block-paragraph">Les prochaines parties prennent chaque famille à tour de rôle. Chaque nom de directive ci-dessous est documenté dans la page de manuel <code>systemd.exec</code> ; <code>IPAddressAllow=</code> et <code>IPAddressDeny=</code> le sont dans <code>systemd.resource-control</code>.</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Partie</th><th>Famille</th><th>Directives couvertes</th><th>Question à laquelle elle répond</th></tr></thead><tbody><tr><td>2</td><td>Système de fichiers</td><td><code>ProtectSystem</code>, <code>ProtectHome</code>, <code>PrivateTmp</code>, <code>ReadWritePaths</code>, <code>ReadOnlyPaths</code>, <code>InaccessiblePaths</code>, <code>TemporaryFileSystem</code>, <code>BindPaths</code></td><td>Que peut lire et écrire le service ?</td></tr><tr><td>3</td><td>Privilèges</td><td><code>NoNewPrivileges</code>, <code>User</code>, <code>DynamicUser</code>, <code>CapabilityBoundingSet</code>, <code>AmbientCapabilities</code>, <code>SecureBits</code>, <code>RestrictSUIDSGID</code></td><td>Que peut devenir le processus ?</td></tr><tr><td>4</td><td>Noyau et périphériques</td><td><code>ProtectKernelTunables</code>, <code>ProtectKernelModules</code>, <code>ProtectKernelLogs</code>, <code>ProtectClock</code>, <code>ProtectControlGroups</code>, <code>PrivateDevices</code>, <code>ProtectProc</code>, <code>ProcSubset</code></td><td>Que peut-il modifier dans le noyau ?</td></tr><tr><td>5</td><td>Namespaces et réseau</td><td><code>PrivateNetwork</code>, <code>PrivateUsers</code>, <code>RestrictNamespaces</code>, <code>RestrictAddressFamilies</code>, <code>IPAddressAllow</code>, <code>IPAddressDeny</code></td><td>Avec qui peut-il communiquer ?</td></tr><tr><td>6</td><td>Appels système</td><td><code>SystemCallFilter</code>, <code>SystemCallArchitectures</code>, <code>SystemCallErrorNumber</code></td><td>Quelles requêtes peut-il envoyer au noyau ?</td></tr><tr><td>7</td><td>Mémoire et divers</td><td><code>MemoryDenyWriteExecute</code>, <code>LockPersonality</code>, <code>RestrictRealtime</code>, <code>UMask</code>, <code>ProtectHostname</code>, <code>RemoveIPC</code>, <code>KeyringMode</code></td><td>Quels tours de passe-passe lui restent possibles ?</td></tr><tr><td>8</td><td>Toute la stack</td><td>Une matrice de chaque directive face à chaque service</td><td>Que dois-je appliquer où ?</td></tr></tbody></table></figure>



<h2 class="wp-block-heading">Ma stack, et par où commencer</h2>



<p class="wp-block-paragraph">Le durcissement demande un modèle de menace, même simple. Je pars du principe qu&rsquo;un attaquant a déjà pris le contrôle du processus d&rsquo;un service, et je me demande ce que ce processus pourrait faire ensuite : lire des secrets, écrire un mécanisme de persistance, joindre d&rsquo;autres services ou obtenir plus de privilèges. Chaque directive retire l&rsquo;une de ces options.</p>



<p class="wp-block-paragraph">Mes serveurs font tourner une stack auto-hébergée typique, que les parties suivantes couvriront service par service :</p>



<ul class="wp-block-list"><li>un reverse proxy devant chaque application publique, avec son bouncer CrowdSec ;</li><li>des applications PHP-FPM, ainsi que des applications Node.js, Python, Go et .NET (un serveur multimédia et un serveur de bibliothèque) ;</li><li>MariaDB, PostgreSQL et Valkey, avec pgBackRest pour les sauvegardes PostgreSQL ;</li><li>un serveur de messagerie, un serveur SFTP et WebDAV, et une messagerie d&rsquo;équipe ;</li><li>des agents de sécurité et de supervision : un agent SIEM, une protection contre la force brute, un IDS et des exporters de métriques ;</li><li>un VPN et une console d&rsquo;administration web.</li></ul>



<p class="wp-block-paragraph">Je commence par les services qui réunissent trois critères : ils sont joignables depuis Internet, directement ou derrière le reverse proxy ; des tiers les ont écrits ; et ils n&rsquo;ont aucune politique SELinux propre. Ce sont les applications web. Les bases de données et les agents viennent après, car le réseau ne peut pas les joindre directement.</p>



<h2 class="wp-block-heading">Ce que je retiens</h2>



<ul class="wp-block-list"><li><strong>Mesurez avant de changer.</strong> systemd-analyze security donne une note et une liste de tâches, sans rien modifier du service.</li><li><strong>Utilisez des drop-ins.</strong> Ils résistent mieux aux mises à jour de paquets qu&rsquo;une modification du fichier du paquet, et en supprimer un suffit pour revenir en arrière.</li><li><strong>Redémarrez après un changement.</strong> Un simple rechargement laisse un processus déjà lancé dans son ancien sandbox.</li><li><strong>Changez une directive à la fois</strong> et testez le vrai travail du service.</li><li><strong>Voyez la note comme une boussole.</strong> Elle ne compte que ce que systemd restreint lui-même.</li><li><strong>Testez les affirmations.</strong> Chaque résultat de cet article vient d&rsquo;une exécution réelle ; lancez le lab vous-même et comparez.</li></ul>



<h2 class="wp-block-heading">Sources</h2>



<ul class="wp-block-list"><li>systemd-analyze(1) : <a href="https://www.freedesktop.org/software/systemd/man/latest/systemd-analyze.html">page de manuel freedesktop.org</a></li><li>systemd.unit(5) : <a href="https://www.freedesktop.org/software/systemd/man/latest/systemd.unit.html">page de manuel freedesktop.org</a></li><li>systemd.exec(5) : <a href="https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html">page de manuel freedesktop.org</a></li><li>systemd.service(5) : <a href="https://www.freedesktop.org/software/systemd/man/latest/systemd.service.html">page de manuel freedesktop.org</a></li><li>systemd.resource-control(5) : <a href="https://www.freedesktop.org/software/systemd/man/latest/systemd.resource-control.html">page de manuel freedesktop.org</a></li><li>systemctl(1) : <a href="https://www.freedesktop.org/software/systemd/man/latest/systemctl.html">page de manuel freedesktop.org</a></li><li>systemd-run(1) : <a href="https://www.freedesktop.org/software/systemd/man/latest/systemd-run.html">page de manuel freedesktop.org</a></li><li>journalctl(1) : <a href="https://www.freedesktop.org/software/systemd/man/latest/journalctl.html">page de manuel freedesktop.org</a></li></ul>



<h2 class="wp-block-heading">Articles liés</h2>



<ul class="wp-block-list"><li><a href="https://blog.neurozone.fr/?p=2975">Sous le capot des failles d’octobre 2026 : un octet de trop, un pointeur oublié, une page partagée</a></li><li><a href="https://blog.neurozone.fr/?p=2930">ZDI-CAN-33367 : quand une copie évitée écrit dans un fichier en lecture seule</a></li><li><a href="https://blog.neurozone.fr/?p=1419">CrowdSec 1.8 sur Debian (1/3) : quitter le paquet de la distribution</a></li></ul>

<p>L’article <a href="https://blog.neurozone.fr/2026/11/07/systemd-analyze-security-mesurer-avant-de-durcir/">Durcir systemd, directive par directive : mesurer avec systemd-analyze security</a> est apparu en premier sur <a href="https://blog.neurozone.fr">Quelques pensées</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.neurozone.fr/2026/11/07/systemd-analyze-security-mesurer-avant-de-durcir/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">4897</post-id>	</item>
		<item>
		<title>Pas à pas : installer CrowdSec, bloquer au pare-feu et ne jamais se bannir</title>
		<link>https://blog.neurozone.fr/2026/11/06/crowdsec-installer-bloquer-pare-feu-bouncer/</link>
					<comments>https://blog.neurozone.fr/2026/11/06/crowdsec-installer-bloquer-pare-feu-bouncer/#respond</comments>
		
		<dc:creator><![CDATA[Hécate Raven]]></dc:creator>
		<pubDate>Fri, 06 Nov 2026 09:00:00 +0000</pubDate>
				<category><![CDATA[Informatique]]></category>
		<category><![CDATA[CrowdSec]]></category>
		<category><![CDATA[Français]]></category>
		<category><![CDATA[nftables]]></category>
		<category><![CDATA[pas à pas]]></category>
		<category><![CDATA[Rocky Linux]]></category>
		<category><![CDATA[sécurité]]></category>
		<guid isPermaLink="false">https://blog.neurozone.fr/?p=2046</guid>

					<description><![CDATA[<p>Installer CrowdSec 1.8 sur Rocky Linux : collections, journaux, liste d'autorisation pour ne jamais se bannir, bouncer pare-feu, liste communautaire.</p>
<p>L’article <a href="https://blog.neurozone.fr/2026/11/06/crowdsec-installer-bloquer-pare-feu-bouncer/">Pas à pas : installer CrowdSec, bloquer au pare-feu et ne jamais se bannir</a> est apparu en premier sur <a href="https://blog.neurozone.fr">Quelques pensées</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">CrowdSec est souvent présenté comme « un fail2ban moderne ». C&rsquo;est vrai en partie : lui aussi lit les journaux et bannit les adresses agressives. Mais il ajoute deux choses que fail2ban n&rsquo;a pas. D&rsquo;abord des scénarios plus riches, qui reconnaissent un balayage de chemins web, une attaque par dictionnaire répartie ou une exploitation de faille connue, au lieu de simplement compter des lignes. Ensuite une intelligence collective : chaque installation signale les attaques qu&rsquo;elle voit, et reçoit en échange une liste d&rsquo;adresses déjà repérées ailleurs, bloquées avant même qu&rsquo;elles ne vous atteignent.</p>



<p class="wp-block-paragraph">Ce tutoriel installe CrowdSec 1.8 sur Rocky Linux, le branche sur SSH et nginx, ajoute le blocage au pare-feu, et surtout explique comment ne pas se bannir soi-même, ce qui arrive plus vite qu&rsquo;on ne le croit.</p>



<figure class="wp-block-image size-full"><a href="https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/pas-a-pas-installer-et-configurer-crowdsec-avec-son-bouncer-_schema_fr.png?ssl=1"><img data-recalc-dims="1" loading="lazy" decoding="async" width="629" height="300" src="https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/pas-a-pas-installer-et-configurer-crowdsec-avec-son-bouncer-_schema_fr.png?resize=629%2C300&#038;ssl=1" alt="Schéma du parcours CrowdSec : journaux, agent, API locale, bouncer pare-feu, liste communautaire et liste blanche" class="wp-image-4573" srcset="https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/pas-a-pas-installer-et-configurer-crowdsec-avec-son-bouncer-_schema_fr.png?w=1720&amp;ssl=1 1720w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/pas-a-pas-installer-et-configurer-crowdsec-avec-son-bouncer-_schema_fr.png?resize=300%2C143&amp;ssl=1 300w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/pas-a-pas-installer-et-configurer-crowdsec-avec-son-bouncer-_schema_fr.png?resize=1024%2C488&amp;ssl=1 1024w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/pas-a-pas-installer-et-configurer-crowdsec-avec-son-bouncer-_schema_fr.png?resize=768%2C366&amp;ssl=1 768w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/pas-a-pas-installer-et-configurer-crowdsec-avec-son-bouncer-_schema_fr.png?resize=1536%2C732&amp;ssl=1 1536w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/pas-a-pas-installer-et-configurer-crowdsec-avec-son-bouncer-_schema_fr.png?resize=900%2C429&amp;ssl=1 900w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/pas-a-pas-installer-et-configurer-crowdsec-avec-son-bouncer-_schema_fr.png?resize=1280%2C610&amp;ssl=1 1280w" sizes="auto, (max-width: 629px) 100vw, 629px" /></a><figcaption class="wp-element-caption">De la ligne de journal au blocage, et comment ne jamais se bannir soi-même.</figcaption></figure>



<h2 class="wp-block-heading">Les pièces du puzzle</h2>



<figure class="wp-block-table"><table><thead><tr><th>Élément</th><th>Rôle</th></tr></thead><tbody><tr><td>l&rsquo;agent (<code>crowdsec</code>)</td><td>lit les journaux, les découpe (parseurs), les compare à des scénarios, émet des alertes</td></tr><tr><td>l&rsquo;API locale (LAPI)</td><td>stocke les alertes et transforme celles qui le méritent en <em>décisions</em> (bannir telle adresse pendant tant de temps)</td></tr><tr><td>les bouncers</td><td>appliquent les décisions : pare-feu (nftables), serveur web, application</td></tr><tr><td>l&rsquo;API centrale (CAPI)</td><td>reçoit vos signalements et vous renvoie une liste de blocage communautaire</td></tr><tr><td>le Hub</td><td>le catalogue de collections, parseurs, scénarios, installés par <code>cscli</code></td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Point important : <strong>l&rsquo;agent ne bloque rien</strong>. Sans bouncer, CrowdSec détecte et prend des décisions, mais personne ne les applique. C&rsquo;est l&rsquo;erreur la plus fréquente lors d&rsquo;une première installation.</p>



<h2 class="wp-block-heading">Étape 1 : installer</h2>



<p class="wp-block-paragraph">CrowdSec publie ses paquets dans un dépôt packagecloud. Déclarez-le dans <code>/etc/yum.repos.d/</code> avec sa clé GPG, puis :</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">dnf install crowdsec crowdsec-firewall-bouncer-nftables
systemctl enable --now crowdsec
cscli version</pre>



<p class="wp-block-paragraph">À l&rsquo;installation, le paquet détecte les services présents (sshd, nginx…) et installe les collections correspondantes. Vérifiez ce qu&rsquo;il a choisi plutôt que de lui faire confiance : <code>cscli collections list</code>.</p>



<p class="wp-block-paragraph">Une précaution sur Debian : si vous installez CrowdSec et le bouncer dans la même commande sur une machine neuve, le script post-installation du bouncer peut s&rsquo;exécuter avant que l&rsquo;API locale soit prête et échouer à s&rsquo;enregistrer. Installez l&rsquo;agent, attendez qu&rsquo;il démarre, puis le bouncer.</p>



<h2 class="wp-block-heading">Étape 2 : les collections</h2>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">cscli collections install crowdsecurity/linux crowdsecurity/sshd crowdsecurity/nginx
cscli collections list
systemctl reload crowdsec</pre>



<p class="wp-block-paragraph">Une collection regroupe des parseurs (qui savent lire un format de journal) et des scénarios (qui reconnaissent un comportement). <code>crowdsecurity/nginx</code> apporte ainsi le parseur des journaux nginx et une dizaine de scénarios : balayage de chemins, sondes de fichiers sensibles, recherche de pages d&rsquo;administration, robots agressifs. Les collections se mettent à jour avec <code>cscli hub update && cscli hub upgrade</code>, à planifier chaque jour.</p>



<h2 class="wp-block-heading">Étape 3 : dire à CrowdSec quoi lire</h2>



<p class="wp-block-paragraph">La configuration d&rsquo;acquisition liste les sources et leur type, qui détermine le parseur utilisé :</p>



<pre class="EnlighterJSRAW" data-enlighter-language="yaml"># /etc/crowdsec/acquis.yaml
filenames:
  - /var/log/secure
labels:
  type: syslog
---
filenames:
  - /var/log/messages
labels:
  type: syslog
---
# /etc/crowdsec/acquis.d/nginx.yaml
filenames:
  - /var/log/nginx/*.log
labels:
  type: nginx</pre>



<ul class="wp-block-list"><li><strong>labels.type</strong> : <code>syslog</code> pour les journaux système (sshd y écrit sur Rocky Linux), <code>nginx</code> pour les journaux d&rsquo;accès et d&rsquo;erreur ;</li><li><strong>une source, une seule fois</strong> : le piège classique est de lister les journaux nginx à la fois dans <code>acquis.yaml</code> et dans <code>acquis.d/nginx.yaml</code>. Chaque ligne est alors lue deux fois, chaque compteur double, et les bannissements tombent deux fois plus vite. Même chose avec une source <code>journalctl</code> ajoutée par-dessus un fichier qui contient déjà les mêmes lignes ;</li><li><strong>les jokers</strong> (<code>*.log</code>) suivent automatiquement les nouveaux sites.</li></ul>



<p class="wp-block-paragraph">Pour vérifier que les lignes sont bien lues et comprises :</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">cscli metrics show acquisition parsers
# « Lines read » augmente, « Lines parsed » aussi ; beaucoup de « unparsed » = mauvais type</pre>



<h2 class="wp-block-heading">Étape 4 : ne jamais se bannir soi-même</h2>



<p class="wp-block-paragraph">C&rsquo;est la partie à ne pas sauter. CrowdSec n&rsquo;a aucune raison de faire confiance à vos propres adresses : un script de supervision qui reçoit une série de 403, des tests de connexion SSH ratés, un serveur qui interroge ses propres noms publics… et c&rsquo;est votre propre serveur que le pare-feu rejette. J&rsquo;ai vécu les deux : un serveur qui s&rsquo;est banni lui-même pour « sondage HTTP », et un autre qui a banni l&rsquo;adresse de mon poste d&rsquo;administration pendant que je vérifiais des clés SSH.</p>



<p class="wp-block-paragraph">La parade est une liste blanche locale, en parseur d&rsquo;enrichissement :</p>



<pre class="EnlighterJSRAW" data-enlighter-language="yaml"># /etc/crowdsec/parsers/s02-enrich/local-whitelist.yaml
name: local/whitelists
description: &quot;Adresses de confiance, jamais bannies&quot;
whitelist:
  reason: &quot;mes serveurs et mon adresse d&#x27;administration&quot;
  ip:
    - &quot;203.0.113.25&quot;
    - &quot;203.0.113.40&quot;
    - &quot;2001:db8::25&quot;
    - &quot;198.51.100.10&quot;</pre>



<p class="wp-block-paragraph">Mettez-y les adresses publiques de chacun de vos serveurs (IPv4 et IPv6), celle de votre accès d&rsquo;administration, et déployez la même liste <strong>sur toutes vos machines</strong> : une infrastructure personnelle où chaque serveur peut bannir les autres finit toujours par le faire. Une liste blanche s&rsquo;applique avant les scénarios, donc aucune alerte n&rsquo;est créée. Elle ne supprime pas une décision déjà prise : <code>cscli decisions delete --ip …</code> s&rsquo;en charge.</p>



<h2 class="wp-block-heading">Étape 5 : appliquer les décisions au pare-feu</h2>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">cscli bouncers add firewall-bouncer     # affiche une clé à copier dans la configuration du bouncer</pre>



<pre class="EnlighterJSRAW" data-enlighter-language="yaml"># /etc/crowdsec/bouncers/crowdsec-firewall-bouncer.yaml (extrait)
mode: nftables
update_frequency: 10s
api_url: http://127.0.0.1:8081/
api_key: &lt;clé générée par cscli bouncers add&gt;
disable_ipv6: false
deny_action: DROP
supported_decisions_types:
  - ban
nftables:
  ipv4:
    enabled: true
    table: crowdsec
  ipv6:
    enabled: true
    table: crowdsec6</pre>



<ul class="wp-block-list"><li><strong>mode: nftables</strong> : le bouncer crée ses propres tables et ensembles nftables, indépendants de firewalld. Les adresses bannies sont dans un ensemble, ce qui reste rapide même avec des dizaines de milliers d&rsquo;entrées ;</li><li><strong>update_frequency</strong> : toutes les 10 secondes, le bouncer récupère les nouvelles décisions ;</li><li><strong>deny_action: DROP</strong> : le paquet est jeté sans réponse, l&rsquo;attaquant n&rsquo;apprend rien ;</li><li><strong>IPv6</strong> : à activer explicitement ; un attaquant qui dispose des deux familles d&rsquo;adresses passera sinon par celle qui n&rsquo;est pas filtrée.</li></ul>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">systemctl enable --now crowdsec-firewall-bouncer
cscli bouncers list                    # « Last API pull » récent
nft list set ip crowdsec crowdsec-blacklists-cscli | head</pre>



<h2 class="wp-block-heading">Étape 6 : la liste communautaire</h2>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">cscli capi register
cscli console enroll &lt;clé de la console&gt;   # facultatif : tableau de bord web
cscli capi status
systemctl restart crowdsec</pre>



<p class="wp-block-paragraph">Une fois inscrit, l&rsquo;agent envoie les alertes de ses scénarios (adresse et scénario, jamais le contenu des journaux) et reçoit une liste de blocage d&rsquo;environ quinze mille adresses, renouvelée régulièrement. C&rsquo;est elle qui fait la plus grande différence au quotidien : la majorité des robots qui balaient Internet sont bloqués dès leur premier paquet. <code>cscli decisions list --origin CAPI | wc -l</code> donne la taille de la liste reçue.</p>



<h2 class="wp-block-heading">Simuler avant de bannir</h2>



<p class="wp-block-paragraph">Un nouveau scénario, ou un nouveau serveur, peut être placé en simulation : les alertes et décisions sont enregistrées mais marquées « simulées », et aucun bouncer ne les applique. C&rsquo;est le moyen de voir ce que CrowdSec <em>aurait</em> bloqué pendant une semaine, avant de lui confier la porte.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">cscli simulation enable crowdsecurity/http-crawl-non_statics
cscli simulation status
cscli alerts list --since 24h</pre>



<h2 class="wp-block-heading">Exploiter au quotidien</h2>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">cscli alerts list --since 24h           # ce qui a été détecté
cscli decisions list                    # qui est banni, pour combien de temps
cscli decisions delete --ip 192.0.2.66   # lever un bannissement
cscli decisions add --ip 192.0.2.99 --duration 72h --reason &#x27;manuel&#x27;
cscli metrics                           # sources, parseurs, scénarios, bouncers</pre>



<p class="wp-block-paragraph">La durée de bannissement par défaut est de quatre heures ; un profil (<code>profiles.yaml</code>) permet de l&rsquo;allonger pour les récidivistes ou de bannir une plage entière plutôt qu&rsquo;une adresse.</p>


<!-- nz-enrich:crowdsec-real -->


<h2 class="wp-block-heading">Une heure d&rsquo;un vrai serveur</h2>



<p class="wp-block-paragraph">Voici les chiffres de mon serveur web le 2 novembre 2026, 75 minutes après un redémarrage (CrowdSec 1.8.2, bouncer nftables 0.0.36). D&rsquo;abord, ce que l&rsquo;agent a lu :</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell"># cscli metrics show acquisition          (extrait)
| Source                                      | Lines read | Lines parsed | Lines unparsed | Poured to bucket | Whitelisted |
| appsec:appsec                               | 136        | 136          | -              | 136              | -           |
| file:/var/log/messages                      | 1.31k      | -            | 1.31k          | -                | -           |
| file:/var/log/nginx/access.blog...log       | 716        | 716          | -              | 42               | 528         |
| file:/var/log/nginx/access.log              | 8.03k      | 8.03k        | -              | 4.29k            | 1.76k       |</pre>



<ul class="wp-block-list"><li><strong>Lines parsed</strong> égal à <strong>Lines read</strong> sur les journaux web : l&rsquo;analyseur nginx comprend chaque ligne. Si un journal affiche tout en <em>unparsed</em>, la collection manque ou le format du journal a été personnalisé.</li><li><strong>/var/log/messages : 1,31k non analysées</strong> : c&rsquo;est attendu. La plupart des lignes du journal système ne sont pas des événements de sécurité et aucun analyseur ne les revendique. Les événements SSH et d&rsquo;authentification sont dans /var/log/secure, lu par sa propre entrée : 3 020 lignes lues, 39 analysées, et c&rsquo;est bien le but, puisque seules les connexions échouées et événements semblables intéressent CrowdSec.</li><li><strong>Whitelisted</strong> : les requêtes venant de mes propres adresses et des sondes de supervision, ignorées volontairement (étape 4).</li><li><strong>Poured to bucket</strong> : les lignes qui ont alimenté un scénario. La plupart ne deviennent jamais une alerte ; un compteur ne déborde que si le comportement se répète.</li></ul>



<p class="wp-block-paragraph">Puis le côté application des décisions, la seule preuve que la protection est réelle :</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell"># cscli bouncers list
 crowdsec-firewall-bouncer  127.0.0.1  ✔️  2026-11-02T10:21:35+01:00  v0.0.36
 openresty-appsec           127.0.0.1  ✔️  2026-11-02T10:21:37+01:00  v1.2.3

# nft list set ip crowdsec crowdsec-blacklists-CAPI      → 14 752 adresses IPv4
# nft list set ip6 crowdsec6 crowdsec6-blacklists-CAPI   → 475 adresses IPv6
# nft list set ip crowdsec crowdsec-blacklists-crowdsec  → 8 décisions locales

# nft list counters table ip crowdsec
counter processed                     packets 1286809
counter crowdsec-blacklists-CAPI      packets 387   → jetés
counter crowdsec-blacklists-crowdsec  packets 60    → jetés</pre>



<p class="wp-block-paragraph">En 75 minutes, 447 paquets ont été jetés avant d&rsquo;atteindre le moindre service : 387 venaient d&rsquo;adresses de la liste communautaire, qui n&rsquo;avaient jamais touché ce serveur, et 60 des 8 adresses bannies par mon propre serveur. La colonne <em>Last API pull</em>, vieille de quelques secondes pour les deux bouncers, est la première chose à regarder quand les bannissements semblent sans effet : un bouncer qui ne tire plus continue d&rsquo;appliquer une liste figée.</p>



<h2 class="wp-block-heading">Allonger les bannissements : profiles.yaml</h2>



<p class="wp-block-paragraph">Le profil décide de ce qu&rsquo;une alerte devient. Celui par défaut bannit l&rsquo;adresse quatre heures. On peut le rendre progressif, comme avec fail2ban, en s&rsquo;appuyant sur le nombre de décisions déjà prises contre cette adresse :</p>



<pre class="EnlighterJSRAW" data-enlighter-language="yaml"># /etc/crowdsec/profiles.yaml
name: default_ip_remediation
filters:
  - Alert.Remediation == true &amp;&amp; Alert.GetScope() == &quot;Ip&quot;
decisions:
  - type: ban
    duration: 4h
duration_expr: Sprintf(&#x27;%dh&#x27;, (GetDecisionsCount(Alert.GetValue()) + 1) * 4)
on_success: break</pre>



<ul class="wp-block-list"><li><strong>filters</strong> : le profil ne s&rsquo;applique qu&rsquo;aux alertes qui demandent une sanction et visent une adresse IP ;</li><li><strong>duration_expr</strong> : 4 heures la première fois, 8 la deuxième, 12 la troisième… Le calcul utilise l&rsquo;historique de l&rsquo;API locale, il survit donc aux redémarrages ;</li><li><strong>on_success: break</strong> : les profils suivants ne sont pas évalués. L&rsquo;ordre des profils dans le fichier compte.</li></ul>



<h2 class="wp-block-heading">Les pannes fréquentes</h2>



<h3 class="wp-block-heading">Rien n&rsquo;est jamais banni</h3>



<p class="wp-block-paragraph">Vérifiez dans l&rsquo;ordre : <code>cscli metrics show acquisition</code> (les lignes sont-elles lues ?), <code>cscli metrics show parsers</code> (sont-elles comprises ?), <code>cscli alerts list</code> (des scénarios se déclenchent-ils ?), <code>cscli bouncers list</code> (le bouncer récupère-t-il les décisions ?). La panne se trouve presque toujours à la première étape qui reste à zéro.</p>



<h3 class="wp-block-heading">Le bouncer ne démarre pas, « unauthorized »</h3>



<p class="wp-block-paragraph">La clé du bouncer ne correspond à aucune clé de l&rsquo;API locale, typiquement après une réinstallation qui a vidé la base. Supprimez l&rsquo;entrée (<code>cscli bouncers delete</code>), recréez-la et recopiez la clé.</p>



<h3 class="wp-block-heading">L&rsquo;API locale écoute sur un port déjà pris</h3>



<p class="wp-block-paragraph">Par défaut, l&rsquo;API locale écoute sur 8080, port très demandé. Changez-le dans <code>config.yaml</code> (<code>api.server.listen_uri</code>) <strong>et</strong> dans <code>local_api_credentials.yaml</code>, puis dans la configuration de chaque bouncer. Oublier l&rsquo;un des trois produit un agent qui démarre mais dont les outils ne parlent plus à personne.</p>



<h3 class="wp-block-heading">Les parseurs ne comprennent pas les journaux SSH</h3>



<p class="wp-block-paragraph">Les versions récentes d&rsquo;OpenSSH journalisent sous le nom <code>sshd-session</code> au lieu de <code>sshd</code>. Si les parseurs ne suivent pas, les échecs d&rsquo;authentification ne sont plus reconnus du tout. Un <code>cscli hub upgrade</code> règle généralement la question ; vérifiez avec <code>cscli explain --file /var/log/secure --type syslog</code>, qui montre, ligne par ligne, quel parseur l&rsquo;a acceptée ou rejetée.</p>



<h2 class="wp-block-heading">Mettre à jour sans surprise</h2>



<ul class="wp-block-list"><li>mettez à jour l&rsquo;agent, puis vérifiez <code>cscli metrics</code> et la liste des bouncers avant de toucher aux bouncers ;</li><li>lisez les notes de version : la version 1.8 a par exemple changé la façon dont certains champs sont exposés aux scénarios ;</li><li>après une mise à jour du hub, un <code>cscli hub upgrade</code> peut remplacer un scénario que vous aviez modifié à la main. Les modifications locales vont dans des fichiers à vous, jamais dans ceux du hub.</li></ul>



<h2 class="wp-block-heading">Et fail2ban ?</h2>



<p class="wp-block-paragraph">Les deux cohabitent sans problème, et c&rsquo;est ce que je fais : fail2ban réagit vite à mes propres journaux avec des bannissements progressifs, CrowdSec ajoute les scénarios web et la liste communautaire. Seul inconvénient : quand une adresse reste bloquée, il faut regarder des deux côtés (<code>fail2ban-client status</code> et <code>cscli decisions list</code>).</p>



<h2 class="wp-block-heading">Pour aller plus loin</h2>



<p class="wp-block-paragraph">CrowdSec sait aussi inspecter le contenu des requêtes HTTP grâce à son moteur AppSec, et bloquer les attaques directement dans le serveur web. C&rsquo;est l&rsquo;objet d&rsquo;un prochain tutoriel de cette série, avec OpenResty.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p class="wp-block-paragraph"><em>Ce billet fait partie d&rsquo;une série de tutoriels d&rsquo;administration système (tag « pas à pas »).</em><br>Série « Pas à pas », tutoriel 9/77. · <a href="https://blog.neurozone.fr/?p=2045">← Tutoriel précédent</a> · <a href="https://blog.neurozone.fr/?p=2047">Tutoriel suivant →</a> · <a href="https://hecate.neurozone.fr/?p=598">English version</a></p>



<h2 class="wp-block-heading">Articles liés</h2>



<ul class="wp-block-list"><li><a href="https://blog.neurozone.fr/?p=1419">CrowdSec 1.8 sur Debian : réinstaller proprement</a></li><li><a href="https://blog.neurozone.fr/?p=2886">La faille Brotli CVE-2026-111497</a></li><li><a href="https://blog.neurozone.fr/?p=2975">Sous le capot des failles d&rsquo;octobre 2026</a></li></ul>

<p>L’article <a href="https://blog.neurozone.fr/2026/11/06/crowdsec-installer-bloquer-pare-feu-bouncer/">Pas à pas : installer CrowdSec, bloquer au pare-feu et ne jamais se bannir</a> est apparu en premier sur <a href="https://blog.neurozone.fr">Quelques pensées</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.neurozone.fr/2026/11/06/crowdsec-installer-bloquer-pare-feu-bouncer/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2046</post-id>	</item>
		<item>
		<title>Mon blog technique</title>
		<link>https://blog.neurozone.fr/2026/11/05/mon-blog-technique/</link>
					<comments>https://blog.neurozone.fr/2026/11/05/mon-blog-technique/#respond</comments>
		
		<dc:creator><![CDATA[Hécate Raven]]></dc:creator>
		<pubDate>Thu, 05 Nov 2026 09:22:46 +0000</pubDate>
				<category><![CDATA[Informatique]]></category>
		<category><![CDATA[carnet d'admin]]></category>
		<guid isPermaLink="false">https://blog.neurozone.fr/?p=5548</guid>

					<description><![CDATA[<p>Est situé ici, en anglais : https://hecate.neurozone.fr</p>
<p>L’article <a href="https://blog.neurozone.fr/2026/11/05/mon-blog-technique/">Mon blog technique</a> est apparu en premier sur <a href="https://blog.neurozone.fr">Quelques pensées</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Est situé ici, en anglais : <a href="https://hecate.neurozone.fr ">https://hecate.neurozone.fr </a></p>
<p>L’article <a href="https://blog.neurozone.fr/2026/11/05/mon-blog-technique/">Mon blog technique</a> est apparu en premier sur <a href="https://blog.neurozone.fr">Quelques pensées</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.neurozone.fr/2026/11/05/mon-blog-technique/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">5548</post-id>	</item>
		<item>
		<title>Le retard de PostgreSQL 19 : l&#8217;IA est un facteur parmi d&#8217;autres, selon deux committers</title>
		<link>https://blog.neurozone.fr/2026/11/05/retard-postgresql-19-ia-un-facteur-deux-committers/</link>
					<comments>https://blog.neurozone.fr/2026/11/05/retard-postgresql-19-ia-un-facteur-deux-committers/#respond</comments>
		
		<dc:creator><![CDATA[Hécate Raven]]></dc:creator>
		<pubDate>Thu, 05 Nov 2026 09:00:00 +0000</pubDate>
				<category><![CDATA[Informatique]]></category>
		<category><![CDATA[bases de données]]></category>
		<category><![CDATA[carnet d'admin]]></category>
		<category><![CDATA[Français]]></category>
		<category><![CDATA[PostgreSQL]]></category>
		<category><![CDATA[Rocky Linux]]></category>
		<guid isPermaLink="false">https://blog.neurozone.fr/?p=5397</guid>

					<description><![CDATA[<p>Le retard de PostgreSQL 19 expliqué : ce que disent Haas et Eisentraut des bogues trouvés par l'IA, mes comptes de commits et des conseils aux DBA.</p>
<p>L’article <a href="https://blog.neurozone.fr/2026/11/05/retard-postgresql-19-ia-un-facteur-deux-committers/">Le retard de PostgreSQL 19 : l&rsquo;IA est un facteur parmi d&rsquo;autres, selon deux committers</a> est apparu en premier sur <a href="https://blog.neurozone.fr">Quelques pensées</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">Le retard de PostgreSQL 19 a une cause avancée dans deux billets de committers PostgreSQL : des bogues trouvés avec l&rsquo;aide de l&rsquo;IA. Robert Haas a publié son billet sur <a href="https://neon.com/blog/postgres-19-is-late-and-its-ais-fault">le blog de Neon le 21 octobre 2026</a> puis sur <a href="https://rhaas.blogspot.com/2026/10/postgres-19-is-late-and-its-ais-fault.html">son propre blog le 30 octobre 2026</a>. Peter Eisentraut a publié <a href="https://peter.eisentraut.org/blog/2026/10/09/the-reverts-will-continue-until-morale-improves">le sien le 9 octobre 2026</a>. Leurs accents diffèrent, donc cet article garde leurs voix séparées.</p>



<p class="wp-block-paragraph">J&rsquo;indique pour chaque affirmation sa source : Haas, Eisentraut, les pages du projet, un site d&rsquo;actualité français, <a href="https://www.numerama.com/tech/2365629-comment-allons-nous-rattraper-notre-retard-lia-trouve-tant-de-bugs-que-postgresql-sinquiete-de-sa-capacite-a-innover.html">Numerama</a> (une source secondaire), ou mon propre comptage. Les chiffres d&rsquo;un billet restent « selon le billet ». J&rsquo;ai lu les pages du projet début novembre 2026, et les dates postérieures au 5 novembre sont des prévisions, pas des faits.</p>



<h2 class="wp-block-heading">Ce que dit Haas sur le retard de PostgreSQL 19</h2>



<p class="wp-block-paragraph">Haas écrit que les versions majeures de la dernière décennie sont sorties entre la mi-septembre et la mi-octobre. Cette année, dit-il, PostgreSQL 19 sortira très probablement en novembre, et un nouveau report n&rsquo;est pas exclu. Il note aussi que trois grandes fonctionnalités ont quitté la version entre le 27 août et le 15 septembre : <code>UPDATE/DELETE FOR PORTION OF</code>, les requêtes de graphes de propriétés SQL (SQL/PGQ) et <code>ALTER TABLE … MERGE/SPLIT PARTITION</code>. Le gel des fonctionnalités, ajoute-t-il, tombe chaque année le 8 avril.</p>



<p class="wp-block-paragraph">Son explication, c&rsquo;est le volume. Selon le billet, les LLM ont rendu les vieux bogues bien plus faciles à trouver, alors que les corriger n&rsquo;est devenu plus facile que dans une moindre mesure. Beaucoup de gens signalent maintenant des bogues trouvés avec des LLM, et les committers sont très occupés à les corriger. Il n&rsquo;accuse pas davantage d&rsquo;erreurs des committers, car il juge peu probable que ce soit la cause principale. Il pense plutôt que les committers sont restés aussi prudents qu&rsquo;avant et que les LLM ont repéré vite une plus grande part de leurs erreurs.</p>



<p class="wp-block-paragraph">Il donne ensuite deux raisons aux retraits tardifs. D&rsquo;abord, les LLM peuvent rédiger des correctifs, mais ceux-ci ne sont pas toujours corrects, donc des humains relisent et valident encore. Ensuite, le temps qui aurait servi à relire la version est parti dans les problèmes de sécurité et les bogues des anciennes branches. À ses yeux, le retard n&rsquo;est pas particulièrement inquiétant. En revanche, il juge le long terme moins clair : comment le projet rattrapera-t-il des décennies de bogues tout en continuant d&rsquo;innover ? Il écrit qu&rsquo;il ne sait pas encore.</p>



<figure class="wp-block-image size-full"><a href="https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/retard-postgresql-19-ia-un-facteur-deux-committers-1.png?ssl=1"><img data-recalc-dims="1" loading="lazy" decoding="async" width="629" height="263" src="https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/retard-postgresql-19-ia-un-facteur-deux-committers-1.png?resize=629%2C263&#038;ssl=1" alt="Retard de PostgreSQL 19 : comment un bogue trouvé avec l'IA devient des rétroportages et des retraits tardifs" class="wp-image-5395" srcset="https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/retard-postgresql-19-ia-un-facteur-deux-committers-1.png?w=1720&amp;ssl=1 1720w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/retard-postgresql-19-ia-un-facteur-deux-committers-1.png?resize=300%2C126&amp;ssl=1 300w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/retard-postgresql-19-ia-un-facteur-deux-committers-1.png?resize=1024%2C429&amp;ssl=1 1024w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/retard-postgresql-19-ia-un-facteur-deux-committers-1.png?resize=768%2C321&amp;ssl=1 768w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/retard-postgresql-19-ia-un-facteur-deux-committers-1.png?resize=1536%2C643&amp;ssl=1 1536w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/retard-postgresql-19-ia-un-facteur-deux-committers-1.png?resize=900%2C377&amp;ssl=1 900w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/retard-postgresql-19-ia-un-facteur-deux-committers-1.png?resize=1280%2C536&amp;ssl=1 1280w" sizes="auto, (max-width: 629px) 100vw, 629px" /></a><figcaption class="wp-element-caption">Tel que le billet le décrit : un flux qualitatif, sans chiffres.</figcaption></figure>



<h3 class="wp-block-heading">Ce que Haas n&rsquo;affirme pas</h3>



<ul class="wp-block-list"><li>Il n&rsquo;affirme pas que les correctifs écrits par des LLM sont fiables.</li><li>Il ne donne pas de date de sortie ferme, et il ne compte pas combien de bogues les LLM ont trouvés au total.</li><li>Il n&rsquo;explique pas pourquoi chacune des trois fonctionnalités a été retirée.</li><li>Il juge toujours le code de PostgreSQL d&rsquo;une très grande qualité.</li></ul>



<h2 class="wp-block-heading">Ce que dit Eisentraut sur les retraits</h2>



<p class="wp-block-paragraph">Eisentraut a écrit plus tôt et donne un tableau plus large. Il dit qu&rsquo;environ 8 à 10 fonctionnalités importantes, selon la façon de compter, ont quitté la branche de PostgreSQL 19 après le début de la bêta, et qu&rsquo;il était développeur ou committer de certaines. Il présente son billet comme un ensemble d&rsquo;opinions personnelles, et il estime que les causes sont probablement une combinaison, différente pour chaque fonctionnalité.</p>



<ul class="wp-block-list"><li><strong>La qualité du code :</strong> peut-être que le code n&rsquo;était pas assez bon, et il dit que l&rsquo;équipe en tirera des leçons.</li><li><strong>Des fonctionnalités trop dures à terminer :</strong> certaines avaient des défauts d&rsquo;architecture difficiles à corriger pendant la bêta.</li><li><strong>Une longue traîne de bogues de cas limites :</strong> la relecture de code assistée par LLM les trouve désormais bien plus vite, et il donne l&rsquo;exemple d&rsquo;une liste de « une quarantaine de rapports de défauts ». Cette phrase illustre l&rsquo;idée. Ce n&rsquo;est pas une donnée mesurée.</li><li><strong>Moins de relecture :</strong> les développeurs seniors ont passé d&rsquo;avril à août sur des correctifs de sécurité, donc la relecture habituelle de la bêta a diminué.</li><li><strong>La complexité :</strong> dans PostgreSQL, « tout fonctionne avec tout », ce qui rend certaines fonctionnalités très difficiles à réussir.</li><li><strong>Pas de statut expérimental :</strong> la communauté en discute, mais elle n&rsquo;a pas encore de moyen de le faire.</li></ul>



<p class="wp-block-paragraph">Point important : il dit que la situation ne vient pas du « vibe coding », car le code retiré est plus ancien que cela. Selon lui, la relecture par IA a relevé la barre de qualité après l&rsquo;écriture du code. Il ajoute que cela devrait se résorber dans les prochains cycles, sauf si la relecture par LLM continue de progresser au rythme actuel. Il dit aussi que la communauté a préféré tenir le calendrier plutôt que de le repousser. Il s&rsquo;attend à ce que beaucoup de fonctionnalités retirées reviennent d&rsquo;ici peu. Enfin, il cite REPACK CONCURRENTLY, la réplication logique des séquences et pg_plan_advice parmi les points forts, et il évoque quelques fonctionnalités prêtes à environ 80 % pour PostgreSQL 20.</p>



<h2 class="wp-block-heading">Ce que le projet a publié lui-même</h2>



<p class="wp-block-paragraph">Pour le retard de PostgreSQL 19, le plus important est <a href="https://www.postgresql.org/about/news/postgresql-19-rc-1-released-3409/">l&rsquo;annonce de la version candidate du 15 octobre 2026</a>, qui donne un calendrier. La prochaine mise à jour planifiée, le 12 novembre, contiendra un grand nombre de correctifs, donc l&rsquo;équipe de publication a décidé de les inclure dans PostgreSQL 19 avant la disponibilité générale. Par conséquent, l&rsquo;équipe prévoit une seconde version candidate le 12 novembre, et la disponibilité générale le 19 novembre 2026, sauf si un problème exige un report ou une autre version candidate. Au 5 novembre 2026, ces dates sont des prévisions.</p>



<figure class="wp-block-image size-full"><a href="https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/retard-postgresql-19-ia-un-facteur-deux-committers-2.png?ssl=1"><img data-recalc-dims="1" loading="lazy" decoding="async" width="629" height="249" src="https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/retard-postgresql-19-ia-un-facteur-deux-committers-2.png?resize=629%2C249&#038;ssl=1" alt="Retard de PostgreSQL 19 : calendrier du gel du 8 avril à la sortie prévue du 19 novembre" class="wp-image-5396" srcset="https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/retard-postgresql-19-ia-un-facteur-deux-committers-2.png?w=1720&amp;ssl=1 1720w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/retard-postgresql-19-ia-un-facteur-deux-committers-2.png?resize=300%2C119&amp;ssl=1 300w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/retard-postgresql-19-ia-un-facteur-deux-committers-2.png?resize=1024%2C405&amp;ssl=1 1024w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/retard-postgresql-19-ia-un-facteur-deux-committers-2.png?resize=768%2C304&amp;ssl=1 768w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/retard-postgresql-19-ia-un-facteur-deux-committers-2.png?resize=1536%2C607&amp;ssl=1 1536w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/retard-postgresql-19-ia-un-facteur-deux-committers-2.png?resize=900%2C356&amp;ssl=1 900w, https://i0.wp.com/blog.neurozone.fr/wp-content/uploads/2026/11/retard-postgresql-19-ia-un-facteur-deux-committers-2.png?resize=1280%2C506&amp;ssl=1 1280w" sizes="auto, (max-width: 629px) 100vw, 629px" /></a><figcaption class="wp-element-caption">Seulement des dates citées par le billet ou par postgresql.org début novembre 2026.</figcaption></figure>



<p class="wp-block-paragraph">Certaines pages ne concordent pas avec cette annonce. Par exemple, <a href="https://www.postgresql.org/developer/roadmap/">la page de la feuille de route</a> indiquait encore début novembre que la prochaine version majeure était prévue pour octobre 2026. De son côté, <a href="https://www.postgresql.org/docs/devel/release-19.html">les notes de version de développement</a> affichaient une date de sortie « 2026-??-?? » au 9 octobre, et leur vue d&rsquo;ensemble ne mentionnait pas les trois fonctionnalités retirées. J&rsquo;ai cherché leurs noms dans cette page et je n&rsquo;en ai trouvé aucun.</p>



<p class="wp-block-paragraph">Deux autres pages comptent pour l&rsquo;exploitation. <a href="https://www.postgresql.org/support/versioning/">La politique de versions</a> dit que chaque version majeure reçoit cinq ans de support et au moins une version mineure tous les trois mois. Elle recommande aussi d&rsquo;exécuter la version mineure courante. De son côté, <a href="https://www.postgresql.org/developer/beta/">la page sur la bêta</a> déconseille fortement les versions bêta en production.</p>



<h2 class="wp-block-heading">Vérifier les chiffres</h2>



<p class="wp-block-paragraph">Haas donne des nombres de commits derrière le retard de PostgreSQL 19, du 8 avril au 25 septembre de chaque année. Je les ai reproduits sur un clone du <a href="https://github.com/postgres/postgres">dépôt de PostgreSQL</a> le 3 novembre 2026, avec la date du committer et le jour UTC. Mes comptes sont proches sans être identiques, et l&rsquo;écart reste sous 2,5 % environ. Mon hypothèse est que les limites de fuseau horaire et les règles de comptage l&rsquo;expliquent.</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Branche et année</th><th>Selon le billet</th><th>Mon comptage : atteignables depuis la pointe de la branche</th><th>Mon comptage : seulement sur la branche</th></tr></thead><tbody><tr><td>master, 2025</td><td>1 037</td><td>1 056</td><td>sans objet</td></tr><tr><td>master, 2026</td><td>1 443</td><td>1 477</td><td>sans objet</td></tr><tr><td>REL_18_STABLE, 2025 (version en préparation)</td><td>724</td><td>738</td><td>279</td></tr><tr><td>REL_19_STABLE, 2026 (version en préparation)</td><td>1 169</td><td>1 182</td><td>600</td></tr><tr><td>REL_17_STABLE, 2025 (branche précédente)</td><td>248</td><td>250</td><td>250</td></tr><tr><td>REL_18_STABLE, 2026 (branche précédente)</td><td>433</td><td>437</td><td>437</td></tr></tbody></table></figure>



<p class="wp-block-paragraph">Les deux définitions de mon tableau comptent. Un commit est atteignable depuis la pointe d&rsquo;une branche stable s&rsquo;il est sur cette branche ou avant le point de création sur master. Les chiffres du billet pour la version en préparation sont proches du comptage « atteignable », pas du comptage « seulement sur la branche », donc ils incluent très probablement des commits de master faits entre le 8 avril et la création de la branche. Les deux branches se sont séparées de master fin juin, en 2025 et en 2026. Les commits présents seulement sur la branche stable passent plutôt de 279 à 600. Dans les deux définitions, le sens se confirme : les branches stables ont reçu beaucoup plus de travail. Pour la branche précédente, qui existait déjà, les deux définitions concordent : de 250 à 437, soit environ 75 % de plus.</p>



<p class="wp-block-paragraph">Numerama reformule la première paire comme des modifications « reportées » sur la version en préparation. Cette reformulation se comprend, mais les chiffres incluent aussi des commits antérieurs de master. De plus, un nombre de commits mesure un volume de travail, pas sa cause. Il ne confirme ni ne réfute que l&rsquo;IA en soit la raison. Cette affirmation causale appartient aux auteurs, et je n&rsquo;ai pas revérifié que les LLM trouvent ces bogues.</p>



<p class="wp-block-paragraph">Pour reproduire mes comptes, clonez le dépôt sans le contenu des fichiers, puis lancez un petit script depuis ce dépôt.</p>



<pre class="EnlighterJSRAW" data-enlighter-language="shell">git clone --bare --filter=blob:none --no-tags https://github.com/postgres/postgres.git postgres.git
cd postgres.git
python pg_commit_count.py</pre>



<pre class="EnlighterJSRAW" data-enlighter-language="python">import datetime as dt, subprocess

W = {2025: (dt.date(2025, 4, 8), dt.date(2025, 9, 25)), 2026: (dt.date(2026, 4, 8), dt.date(2026, 9, 25))}
day = lambda s: dt.datetime.fromisoformat(s).astimezone(dt.timezone.utc).date()


def log(args):
    out = subprocess.run(["git", "log", "--format=%H|%cI"] + args, capture_output=True, text=True, check=True).stdout.strip().split("\n")
    return [l.split("|") for l in out if l]


def n(rows, y):
    a, b = W[y]
    return sum(1 for r in rows if a &lt;= day(r[1]) &lt;= b)


for y in (2025, 2026):
    print(f"master {y}: {n(log(['master']), y)}")
for br, y in (("REL_18_STABLE", 2025), ("REL_19_STABLE", 2026), ("REL_17_STABLE", 2025), ("REL_18_STABLE", 2026)):
    print(f"{br} {y}: atteignables depuis la pointe {n(log([br]), y)}; seulement sur la branche {n(log([br, '^master']), y)}")</pre>



<p class="wp-block-paragraph">Ces résultats valent pour le jour où je les ai obtenus. Le dépôt continue d&rsquo;avancer : les comptes de ces fenêtres closes devraient rester les mêmes, mais les pointes des branches changeront.</p>



<h2 class="wp-block-heading">Ce qu&rsquo;ajoute Numerama, et où les sources divergent</h2>



<p class="wp-block-paragraph">Le 2 novembre 2026, <a href="https://www.numerama.com/tech/2365629-comment-allons-nous-rattraper-notre-retard-lia-trouve-tant-de-bugs-que-postgresql-sinquiete-de-sa-capacite-a-innover.html">Numerama</a> a résumé le retard de PostgreSQL 19 en français. Il ajoute quelques éléments que j&rsquo;ai vérifiés. D&rsquo;abord, il donne un décompte de sécurité : 28 vulnérabilités corrigées dans la mise à jour du 13 août, contre 11 en mai. Les annonces du projet du <a href="https://www.postgresql.org/about/news/postgresql-186-1711-1615-1519-1424-and-19-beta-3-released-3365/">13 août</a> et du <a href="https://www.postgresql.org/about/news/postgresql-184-1710-1614-1518-and-1423-released-3297/">14 mai</a> donnent les mêmes deux nombres. Numerama note lui-même que ni ce décompte ni les chiffres de Haas ne disent combien de problèmes l&rsquo;IA a trouvés.</p>



<p class="wp-block-paragraph">Il présente aussi Eisentraut comme un membre de la direction du projet, alors que j&rsquo;ai trouvé les deux ingénieurs sur <a href="https://www.postgresql.org/developer/committers/">la page des committers</a> et que je n&rsquo;ai pas trouvé Eisentraut sur la page de l&rsquo;équipe centrale. Par ailleurs, il présente sa phrase sur « une quarantaine de rapports » comme une citation simple, alors qu&rsquo;il l&rsquo;écrit comme une illustration. Enfin, il liste d&rsquo;autres réactions de l&rsquo;open source aux rapports générés par IA : curl a mis fin à ses primes aux bogues en janvier, Linus Torvalds a parlé des doublons en mai, et Google a suspendu ses récompenses open source le 1er octobre. Je n&rsquo;ai pas vérifié ces éléments, ils restent donc des affirmations de Numerama.</p>



<h3 class="wp-block-heading">Là où les sources divergent</h3>



<ul class="wp-block-list"><li><strong>Le calendrier :</strong> Eisentraut écrit le 9 octobre que la communauté a préféré tenir le calendrier. L&rsquo;annonce du 15 octobre prévoit la disponibilité générale le 19 novembre, et Haas attend novembre. Numerama dit que l&rsquo;équipe de publication a repoussé la date début octobre, mais l&rsquo;annonce que j&rsquo;ai trouvée date du 15 octobre, et je n&rsquo;ai pas pu dater la décision. La page de la feuille de route disait toujours octobre.</li><li><strong>La cause des retraits :</strong> Haas relie les retraits tardifs et le retard au flot de bogues trouvés par des LLM. Eisentraut cite plusieurs causes dont la relecture par IA n&rsquo;est qu&rsquo;une, et il dit que le code écrit par IA n&rsquo;est pas en cause.</li><li><strong>Les chiffres :</strong> mes comptes diffèrent un peu du billet, et les chiffres de la « version en préparation » incluent des commits antérieurs de master.</li></ul>



<h2 class="wp-block-heading">L&rsquo;IA et les bases de données au-delà du calendrier</h2>



<h3 class="wp-block-heading">Les moteurs : la charge des mainteneurs</h3>



<p class="wp-block-paragraph">Les deux billets décrivent une charge de travail qui a grossi. Haas s&rsquo;attend à ce que chaque nouvelle fonctionnalité soit désormais examinée par l&rsquo;IA. Eisentraut propose du fuzzing assisté par LLM dans le développement des fonctionnalités, et il s&rsquo;attend à ce que la pression diminue si la relecture par IA cesse de progresser. Aucun des deux ne dit que les versions mineures deviendront plus grosses ou plus fréquentes. La politique de versions promet seulement au moins une tous les trois mois.</p>



<h3 class="wp-block-heading">Les fonctionnalités : la recherche vectorielle</h3>



<p class="wp-block-paragraph">Les charges de travail d&rsquo;IA changent aussi ce que les bases offrent. Selon son README, <a href="https://github.com/pgvector/pgvector">pgvector</a> ajoute à Postgres la recherche exacte et approchée des plus proches voisins, avec des index HNSW et IVFFlat, et prend en charge Postgres 13 et suivants. Dans PostgreSQL 19, les notes de version de développement ne listent aucune fonction vectorielle (j&rsquo;ai cherché le mot), donc la recherche vectorielle vient toujours de cette extension. MariaDB, au contraire, documente <a href="https://mariadb.com/docs/server/reference/sql-structure/vectors/vector-overview">un type et un index vectoriels intégrés</a> : les vecteurs sont arrivés dans MariaDB Community Server 11.7, une version continue, puis sont devenus disponibles de façon générale à partir de la 11.8, la première version à support long à les inclure. Son index initial utilise un algorithme HNSW modifié et prend la distance euclidienne par défaut. De plus, <a href="https://genai.owasp.org/llm-top-10/">l&rsquo;OWASP cite les « faiblesses des vecteurs et des embeddings »</a> parmi les dix risques des applications LLM pour 2025.</p>



<h3 class="wp-block-heading">L&rsquo;exploitation : le SQL assisté par LLM</h3>



<p class="wp-block-paragraph">Pour les DBA, la question principale est de savoir comment traiter du SQL qu&rsquo;un modèle écrit. La liste 2025 de l&rsquo;OWASP nomme l&rsquo;injection de prompt, la mauvaise gestion des sorties et l&rsquo;agentivité excessive parmi ses risques. La mauvaise gestion des sorties consiste à passer la sortie du modèle en aval sans validation suffisante. Les points suivants sont des considérations générales, pas des mesures. Relisez le SQL généré avant de l&rsquo;exécuter. Testez-le d&rsquo;abord sur une copie. Donnez à tout outil qui l&rsquo;exécute un rôle avec le moins de droits possible, de préférence en lecture seule sur un réplica.</p>



<h2 class="wp-block-heading">Mon avis</h2>



<p class="wp-block-paragraph">Sur le retard de PostgreSQL 19, je suis moins inquiète que les titres des articles. Corriger beaucoup de bogues et de vulnérabilités est plutôt une bonne chose, même quand cela coûte du temps. Si la base devient beaucoup plus stable, je pense que la course à l&rsquo;innovation peut attendre un peu. C&rsquo;est ma lecture, pas une mesure : rien de ce que j&rsquo;ai compté ne montre si la base est plus stable aujourd&rsquo;hui.</p>



<p class="wp-block-paragraph">Haas lui-même juge le retard modeste et estime toujours très haut la qualité du code. En revanche, il dit aussi que la question du long terme reste ouverte : comment le projet peut-il rattraper son retard de bogues et continuer d&rsquo;innover ? Il qualifie aussi le code de l&rsquo;un des plus propres sur lesquels il ait travaillé, et Eisentraut, de son côté, s&rsquo;attend à ce que beaucoup de fonctionnalités retirées reviennent d&rsquo;ici peu. Mon avis et les billets vont donc ensemble, mais l&rsquo;optimisme est le mien, pas le leur.</p>



<h2 class="wp-block-heading">Que faire en pratique</h2>



<ul class="wp-block-list"><li><strong>Traitez les dates comme des prévisions.</strong> Après le retard de PostgreSQL 19, le projet prévoit la disponibilité générale le 19 novembre 2026, et son propre texte admet un report.</li><li><strong>Gardez la production loin des versions candidates.</strong> La page sur la bêta le déconseille.</li><li><strong>Gardez la version mineure courante.</strong> La politique la recommande, et elle prévient que certaines mises à jour mineures demandent des étapes en plus : lisez donc les notes d&rsquo;abord.</li><li><strong>Testez une mise à jour majeure sur une copie.</strong> La politique exige <code>pg_upgrade</code> ou un export-import, et elle conseille de lire les notes de chaque version que vous sautez.</li><li><strong>Sauvegardez avant toute mise à jour.</strong> C&rsquo;est une règle générale.</li><li><strong>Traitez le SQL généré par un LLM comme une entrée non fiable.</strong></li></ul>



<h2 class="wp-block-heading">Ce que nous ignorons encore</h2>



<ul class="wp-block-list"><li>Si le retard de PostgreSQL 19 s&rsquo;arrête le 19 novembre, puisque l&rsquo;annonce admet un autre report.</li><li>Combien de bogues viennent de l&rsquo;IA. Numerama note qu&rsquo;aucun chiffre ne le dit.</li><li>Si les fonctionnalités retirées reviennent vite. Eisentraut s&rsquo;y attend, mais c&rsquo;est une attente, pas un engagement.</li><li>Comment le projet rattrapera son retard de bogues. Haas écrit qu&rsquo;il ne sait pas encore.</li><li>Si la pression s&rsquo;estompe ou reste. Eisentraut et Haas n&rsquo;ont pas la même perspective.</li><li>Si le projet a une politique écrite sur les contributions assistées par IA. Je n&rsquo;en ai trouvé aucune dans la FAQ des développeurs ni dans la page Submitting a Patch, mais je n&rsquo;ai pas fouillé les listes de diffusion.</li></ul>



<h2 class="wp-block-heading">Articles liés</h2>



<ul class="wp-block-list"><li><a href="https://blog.neurozone.fr/?p=2044">Pas à pas : les réglages MariaDB qui comptent vraiment, et comment les vérifier</a></li><li><a href="https://blog.neurozone.fr/?p=1419">CrowdSec 1.8 sur Debian : réinstaller depuis le dépôt officiel, piège du postinst et vérifications</a></li><li><a href="https://blog.neurozone.fr/?p=1396">Un /boot trop petit, des disques en miroir et des alertes : ménage de printemps… en octobre</a></li></ul>



<h2 class="wp-block-heading">Sources</h2>



<ul class="wp-block-list"><li><a href="https://neon.com/blog/postgres-19-is-late-and-its-ais-fault">Robert Haas, « Postgres 19 is late, and it&rsquo;s AI&rsquo;s fault », blog de Neon, 21 octobre 2026</a></li><li><a href="https://rhaas.blogspot.com/2026/10/postgres-19-is-late-and-its-ais-fault.html">Le même billet sur le blog de Robert Haas, 30 octobre 2026</a></li><li><a href="https://peter.eisentraut.org/blog/2026/10/09/the-reverts-will-continue-until-morale-improves">Peter Eisentraut, « The reverts will continue until morale improves », 9 octobre 2026</a></li><li><a href="https://www.postgresql.org/about/news/postgresql-19-rc-1-released-3409/">PostgreSQL 19 RC 1 Released!, postgresql.org, 15 octobre 2026</a></li><li><a href="https://www.postgresql.org/developer/roadmap/">Feuille de route de PostgreSQL</a></li><li><a href="https://www.postgresql.org/support/versioning/">Politique de versions de PostgreSQL</a></li><li><a href="https://www.postgresql.org/docs/devel/release-19.html">Notes de version de développement de PostgreSQL 19</a></li><li><a href="https://www.postgresql.org/developer/beta/">Informations sur la bêta de PostgreSQL</a></li><li><a href="https://www.postgresql.org/about/news/postgresql-186-1711-1615-1519-1424-and-19-beta-3-released-3365/">Annonce de la mise à jour du 13 août 2026</a></li><li><a href="https://www.postgresql.org/about/news/postgresql-184-1710-1614-1518-and-1423-released-3297/">Annonce de la mise à jour du 14 mai 2026</a></li><li><a href="https://www.postgresql.org/developer/committers/">Les committers de PostgreSQL</a></li><li><a href="https://wiki.postgresql.org/wiki/Developer_FAQ">FAQ des développeurs de PostgreSQL</a> et <a href="https://wiki.postgresql.org/wiki/Submitting_a_Patch">Submitting a Patch</a></li><li><a href="https://github.com/pgvector/pgvector">README de pgvector</a></li><li><a href="https://mariadb.com/docs/server/reference/sql-structure/vectors/vector-overview">Présentation de MariaDB Vector</a></li><li><a href="https://genai.owasp.org/llm-top-10/">OWASP Top 10 pour les applications LLM, 2025</a></li><li><a href="https://www.numerama.com/tech/2365629-comment-allons-nous-rattraper-notre-retard-lia-trouve-tant-de-bugs-que-postgresql-sinquiete-de-sa-capacite-a-innover.html">Numerama, Amine Baba Aissa, 2 novembre 2026</a></li><li><a href="https://github.com/postgres/postgres">Dépôt source de PostgreSQL (comptage des commits)</a></li></ul>


<p>L’article <a href="https://blog.neurozone.fr/2026/11/05/retard-postgresql-19-ia-un-facteur-deux-committers/">Le retard de PostgreSQL 19 : l&rsquo;IA est un facteur parmi d&rsquo;autres, selon deux committers</a> est apparu en premier sur <a href="https://blog.neurozone.fr">Quelques pensées</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.neurozone.fr/2026/11/05/retard-postgresql-19-ia-un-facteur-deux-committers/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">5397</post-id>	</item>
	</channel>
</rss>
