<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="fr"><generator uri="https://jekyllrb.com/" version="4.2.2">Jekyll</generator><link href="https://www.lprp.fr/feed.xml" rel="self" type="application/atom+xml" /><link href="https://www.lprp.fr/" rel="alternate" type="text/html" hreflang="fr" /><updated>2026-07-05T10:55:48+00:00</updated><id>https://www.lprp.fr/feed.xml</id><title type="html">LPRP.fr</title><subtitle>La Page de Rémi Peyronnet</subtitle><entry xml:lang="en"><title type="html">Start / stop OctoPrint docker with the 3D printer</title><link href="https://www.lprp.fr/2026/07/start-stop-octoprint-docker-with-the-3d-printer/" rel="alternate" type="text/html" title="Start / stop OctoPrint docker with the 3D printer" /><published>2026-07-04T11:00:00+00:00</published><updated>2026-07-04T11:00:00+00:00</updated><id>https://www.lprp.fr/2026/07/start-stop-octoprint-docker-with-the-3d-printer</id><content type="html" xml:base="https://www.lprp.fr/2026/07/start-stop-octoprint-docker-with-the-3d-printer/"><![CDATA[<p>This setup starts and stops an OctoPrint container automatically when the 3D printer is plugged in or unplugged. It uses <code class="language-shell highlighter-rouge">udev</code> rules to detect the USB device and a Docker container named <code class="language-shell highlighter-rouge">octoprint</code>.</p>

<p>The idea is simple:</p>
<ul>
  <li>When the printer is connected, the container starts.</li>
  <li>When the printer is disconnected, the container stops.</li>
  <li>A stable device symlink is created for the printer inside Docker.</li>
</ul>

<p>This is useful when you only want OctoPrint running while the printer is actually present.</p>

<h1 id="octoprint-docker-setup">OctoPrint Docker Setup</h1>

<p>First, make sure OctoPrint runs in Docker and that the printer device is exposed to the container.</p>

<p>A typical Docker setup looks like this:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">services</span><span class="pi">:</span>
  <span class="na">octoprint</span><span class="pi">:</span>
    <span class="c1"># You should not use :latest as it may cause problems on update with your data</span>
    <span class="c1"># prefer updating within the UI, and then update the version here </span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">octoprint/octoprint:1.10</span>
    <span class="na">container_name</span><span class="pi">:</span> <span class="s">octoprint</span>
    <span class="na">restart</span><span class="pi">:</span> <span class="s">unless-stopped</span>
    <span class="na">ports</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">5000:80</span>
    <span class="na">devices</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">/dev/ttyUSB0:/dev/ttyUSB0</span>
      <span class="pi">-</span> <span class="s">/dev/video0:/dev/video0</span>
    <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">./config:/octoprint</span>
      <span class="pi">-</span> <span class="s">./data:/octoprint/data</span>
</code></pre></div></div>

<p>The important part is the device mapping:</p>
<ul>
  <li><code class="language-shell highlighter-rouge">/dev/ttyUSB0</code> is the device name for the printer USB port</li>
  <li><code class="language-shell highlighter-rouge">/dev/video0</code> is the device name for the camera USB port ; remove it if you don’t need it</li>
</ul>

<p>You can adapt the internal device path if your OctoPrint configuration expects something else.</p>

<h1 id="rules-udev-on-host">Rules udev on host</h1>

<p>Create the file <code class="language-shell highlighter-rouge">/etc/udev/rules.d/90-docker-octoprint.rules</code> with the following content:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># This is the Creality Ender 3v2 printer mapped into the Octoprint container</span>
<span class="nv">SUBSYSTEM</span><span class="o">==</span><span class="s2">"tty"</span>, ATTRS<span class="o">{</span>idVendor<span class="o">}==</span><span class="s2">"1a86"</span>, ATTRS<span class="o">{</span>idProduct<span class="o">}==</span><span class="s2">"7523"</span>, <span class="nv">ACTION</span><span class="o">==</span><span class="s2">"add"</span>,  SYMLINK+<span class="o">=</span><span class="s2">"docker/ender"</span>
<span class="nv">SUBSYSTEM</span><span class="o">==</span><span class="s2">"tty"</span>, ATTRS<span class="o">{</span>idVendor<span class="o">}==</span><span class="s2">"1a86"</span>, ATTRS<span class="o">{</span>idProduct<span class="o">}==</span><span class="s2">"7523"</span>, <span class="nv">ACTION</span><span class="o">==</span><span class="s2">"add"</span>,  RUN+<span class="o">=</span><span class="s2">"/usr/bin/docker start octoprint"</span>
<span class="nv">SUBSYSTEM</span><span class="o">==</span><span class="s2">"tty"</span>, ATTRS<span class="o">{</span>idVendor<span class="o">}==</span><span class="s2">"1a86"</span>, ATTRS<span class="o">{</span>idProduct<span class="o">}==</span><span class="s2">"7523"</span>, <span class="nv">ACTION</span><span class="o">==</span><span class="s2">"remove"</span>, RUN+<span class="o">=</span><span class="s2">"/usr/bin/docker stop octoprint"</span>
</code></pre></div></div>

<p>The USB identifiers here match the common CH340-based adapter used by the Creality Ender 3 V2. You may need to change this according to your printer (use <code class="language-shell highlighter-rouge">lsusb</code> to see the vendor:product identifiers)</p>

<p>This rule does three things:</p>
<ul>
  <li>Creates <code class="language-shell highlighter-rouge">/dev/docker/ender</code> when the printer is detected.</li>
  <li>Starts the Docker container on plug-in.</li>
  <li>Stops the Docker container on unplug.</li>
</ul>

<p>After adding the rule, reload udev with <code class="language-shell highlighter-rouge">systemctl restart udev</code>, then reconnect the printer to test the rule.</p>

<p>If something does not work, enable debug logging and watch the journal: <code class="language-shell highlighter-rouge">udevadm control <span class="nt">--log-priority</span><span class="o">=</span>debug <span class="o">&amp;&amp;</span> journalctl <span class="nt">-f</span></code> ; plug and unplug the printer while watching the logs. This usually helps confirm whether the device is detected and whether the Docker commands are triggered.</p>

<h1 id="proxmox-usb-hotplug-setup">Proxmox USB hotplug setup</h1>
<p>You can use Proxmox USB device mapping to pass the printer and camera USB. Once the VM is launched, and if you have defined USB as hotplug, it will work as expected. But doing so requires that the USB devices exist to start the VM, else the VM will fail to start. In my case this is very annoying.</p>

<p>The solution is to use a hook script. I have used this script already in a few articles as it is very useful <a href="/2026/06/proxmox-usbip/">in this article to pass USB-IP devices</a> and <a href="/2025/09/acces-distant-vnc-avec-proxmox-qemu/">in this article to add a VNC password after the VM startup</a> ; here is the script to be created in the snippets folder with executable flag (<code class="language-shell highlighter-rouge"><span class="nb">chmod</span> +x</code>), create the file <code class="language-shell highlighter-rouge">/var/lib/vz/snippets/hook-hotplug.sh</code>   with the following contents:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/bin/bash</span>

<span class="c"># From Perplexity: hook script pour proxmox qui ajoute dans le qm monitor </span>
<span class="c"># après le démarrage le contenu du fichier à .hotplug.conf </span>
<span class="c"># à coté du fichier de configuration de la VM</span>

<span class="c"># Installation</span>
<span class="c"># - chmod +x </span>
<span class="c"># - copy dans /var/lib/vz/snippets</span>
<span class="c"># - ajouter dans la VM avec: qm set &lt;VMID&gt; --hookscript local:snippets/hook-hotplug.sh</span>

<span class="c"># Variables transmises par Proxmox à chaque appel hook</span>
<span class="nv">VMID</span><span class="o">=</span><span class="s2">"</span><span class="nv">$1</span><span class="s2">"</span>
<span class="nv">PHASE</span><span class="o">=</span><span class="s2">"</span><span class="nv">$2</span><span class="s2">"</span>

<span class="c"># Chemin du fichier de hotplug à appliquer si présent</span>
<span class="nv">HOTPLUG_CONF</span><span class="o">=</span><span class="s2">"/etc/pve/qemu-server/</span><span class="k">${</span><span class="nv">VMID</span><span class="k">}</span><span class="s2">.hotplug.conf"</span>

<span class="c"># Lancement après démarrage effectif</span>
<span class="k">if</span> <span class="o">[</span> <span class="s2">"</span><span class="nv">$PHASE</span><span class="s2">"</span> <span class="o">=</span> <span class="s2">"post-start"</span> <span class="o">]</span> <span class="o">&amp;&amp;</span> <span class="o">[</span> <span class="nt">-f</span> <span class="s2">"</span><span class="nv">$HOTPLUG_CONF</span><span class="s2">"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
    <span class="c"># Lire chaque ligne et l’injecter dans le monitor QEMU de la VM</span>
    <span class="k">while </span><span class="nv">IFS</span><span class="o">=</span> <span class="nb">read</span> <span class="nt">-r</span> line<span class="p">;</span> <span class="k">do</span>
        <span class="c"># On ignore les lignes vides ou commentaires (#)</span>
        <span class="o">[[</span> <span class="s2">"</span><span class="nv">$line</span><span class="s2">"</span> <span class="o">=</span>~ ^#.<span class="k">*</span><span class="nv">$ </span><span class="o">||</span> <span class="nt">-z</span> <span class="s2">"</span><span class="nv">$line</span><span class="s2">"</span> <span class="o">]]</span> <span class="o">&amp;&amp;</span> <span class="k">continue</span>
        <span class="c"># Injecte la commande dans qm monitor</span>
        <span class="nb">echo</span> <span class="s2">"</span><span class="nv">$line</span><span class="s2">"</span> | qm monitor <span class="s2">"</span><span class="nv">$VMID</span><span class="s2">"</span>
    <span class="k">done</span> &lt; <span class="s2">"</span><span class="nv">$HOTPLUG_CONF</span><span class="s2">"</span> <span class="o">&gt;&gt;</span> /var/log/hook-hotplug.log 2&gt;&amp;1
<span class="k">fi</span>
</code></pre></div></div>

<p>And to use it, put in each VM you want the following line:</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>hookscript: <span class="nb">local</span>:snippets/hook-hotplug.sh  
</code></pre></div></div>

<p>This script is triggered by the event<code class="language-shell highlighter-rouge">post-start</code> emitted at the end of the startup of the VM, will read the hotplug configuration file <code class="language-shell highlighter-rouge">&lt;vmid&gt;.hotplug.conf</code> next to the VM configuration, and will send it to the QEMU Monitor interface.</p>

<p>In our case:  <code class="language-shell highlighter-rouge">/etc/pve/qemu-server/&lt;vmid&gt;.hotplug.conf</code></p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>device_add usb-host,vendorid<span class="o">=</span>0x1a86,productid<span class="o">=</span>0x7523,id<span class="o">=</span>print3d
device_add usb-host,vendorid<span class="o">=</span>0x1908,productid<span class="o">=</span>0x2311,id<span class="o">=</span>webcamrouge
</code></pre></div></div>

<h1 id="conclusion">Conclusion</h1>

<p>With this setup:</p>
<ul>
  <li>Plug the printer in, and OctoPrint starts automatically.</li>
  <li>Unplug the printer, and OctoPrint stops cleanly.</li>
  <li>The printer is exposed through a stable device name.</li>
</ul>

<p>This keeps the system lightweight and avoids running OctoPrint when the printer is not connected.</p>]]></content><author><name>Rémi Peyronnet</name></author><category term="3D" /><category term="OctoPrint" /><category term="Proxmox" /><category term="USB" /><category term="Docker" /><category term="Script" /><summary type="html"><![CDATA[This setup starts and stops an OctoPrint container automatically when the 3D printer is plugged in or unplugged. It uses udev rules to detect the USB device and a Docker container named octoprint.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://www.lprp.fr/files/2026/octoprint_startstop.jpg" /><media:content medium="image" url="https://www.lprp.fr/files/2026/octoprint_startstop.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry xml:lang="fr"><title type="html">Migration vers Proxmox</title><link href="https://www.lprp.fr/2026/06/migration-vers-proxmox/" rel="alternate" type="text/html" title="Migration vers Proxmox" /><published>2026-06-19T11:30:00+00:00</published><updated>2026-06-19T11:30:00+00:00</updated><id>https://www.lprp.fr/2026/06/migration-vers-proxmox</id><content type="html" xml:base="https://www.lprp.fr/2026/06/migration-vers-proxmox/"><![CDATA[<p>Retour d’expérience sur la migration de mon serveur historique monolithique vers une infrastructure Proxmox et des composants containerisés. Cette transition aura duré plus d’un an, avec différentes phases progressives pour lisser les évolutions dans le temps, quand j’ai le temps de m’en occuper, et sans pour autant avoir de coupure de service.</p>

<h1 id="pourquoi-">Pourquoi ?</h1>
<p>Mon serveur sous Linux s’était construit au fil du temps, de mon premier PC il y a plus de 20 ans à l’école, mis dans un placard plus tard pour en faire en serveur, et en lui ajoutant au fil du temps différentes fonctions et logiciels. Le résultat était devenu une accumulation qui n’était plus très maîtrisée de scripts, de batch cron, de services que j’utilise tous les jours, au milieu de différentes expérimentations et de mon desktop Linux me servant également d’environnement de développement. Chaque montée de version de debian était un défi, et si généralement la première se passait bien, la suivante posait problème sur certaines subtilités et j’avais à réinstaller en urgence, avec la difficulté de ne rien oublier… Avec les solutions actuelles de virtualisation et de containérisation, il était temps de restructurer et rationaliser.</p>

<p>Cette situation n’avait cependant pas que des inconvénients, dans les avantages :</p>
<ul>
  <li>cette architecture “monolithique” était très performante pour optimiser l’usage des ressources physiques limitées de mon serveur, car l’intégralité des ressources CPU et mémoire était disponible pour tous les services, sans segmentation, sans overhead</li>
  <li>même chose pour les accès aux disques, tous les services ont accès au matériel avec des IOs optimales</li>
  <li>quasiment tous les services étaient installés depuis les paquets debian, la mise à jour était ainsi très simple, avec une gestion des mises à jour très simple à faire</li>
</ul>

<p>C’est ce qui a fait que la migration a déjà pris du temps à concevoir, pour trouver la bonne architecture cible, puis réaliser la transition. A noter que j’avais tenté une première tentative il y a quelques années, à l’arrache lors d’une réinstallation, et j’avais fini par revenir en arrière car je m’étais retrouvé bloqué par l’absence de réflexion préalable sur l’architecture.</p>

<h1 id="les-étapes-suivies">Les étapes suivies</h1>

<p>J’ai suivi les étapes suivantes :</p>
<ol>
  <li>Des expérimentations sur machine virtuelle sur mon PC pour choisir les solutions principales</li>
  <li>Recenser tous les services dont je me sers, choisir ce que je conserve et ce que j’abandonne, et voir dans quel environnement je les positionne (container, VM,…)</li>
  <li>Commencer à migrer en container petit à petit mes services en docker sur le serveur précédent</li>
  <li>Installer <a href="https://pve.proxmox.com/wiki/Install_Proxmox_VE_on_Debian_12_Bookworm">Proxmox sur mon serveur debian actuel</a>, Proxmox est basé sur debian et il est possible de l’ajouter sur une debian existante ; cette étape permet de poursuivre la migration petit à petit, en migrant sur VM</li>
  <li>Installer Proxmox en standalone sur un nouveau disque, en maintenant un double boot pour revenir en arrière tant que ce n’est pas finalisé</li>
  <li>Basculer sur la nouvelle architecture</li>
  <li>Finaliser avec toutes les améliorations (notamment l’ajout d’un idp, de l’authentification, du routage https, des backups…)</li>
</ol>

<p>On peut aussi prendre une image du serveur précédent et le faire tourner en VM (<code class="language-shell highlighter-rouge">qemu-img convert <span class="nt">-O</span> qcow2 /dev/nvme0n1p5 /mnt//images/100/old-server.qcow2</code>, désactiver le secure-boot, créer une nouvelle VM debian, ajouter le disque, le mettre en premier, booter, chroot, et réinstaller le boot avec pve-efiboot-tool)</p>

<h1 id="les-solutions-et-larchitecture-retenue">Les solutions et l’architecture retenue</h1>
<h2 id="hyperviseur-proxmox">Hyperviseur Proxmox</h2>

<p><a href="https://www.proxmox.com/en/products/proxmox-virtual-environment/overview">Proxmox VE</a>, solution open source populaire, basée sur debian dont j’ai l’habitude, avec un modèle économique entreprise et une bonne communauté ; en alternatives Xen ne m’avait pas convaincu en test, et coté VMware, même si plus facile à utiliser, n’est pas libre (avec les changements de politique suite au rachat par Broadcom, je suis content de ne pas avoir fait le choix de VMWare…)</p>

<h2 id="nas-et-docker-dans-une-vm-debian">NAS et Docker dans une VM Debian</h2>
<p>Eh oui, encore et toujours <a href="https://www.debian.org/">debian</a> depuis 25 ans :)  j’étais plutôt parti initialement sur utiliser <a href="https://www.openmediavault.org/">OpenMediaVault</a> ou <a href="https://www.truenas.com/">TrueNAS</a>, que j’ai essayé pendant plusieurs semaines, mais pas vraiment moderne et assez limité pour le premier, et également un peu rigide pour le second, avec des doutes sur la pérennité de la gratuité ; ce sera donc debian, mais avec un maximum sous docker et en limitant son paramétrage local (et en notant tout, dans la perspective de scripter l’installation de la VM plus tard) ; j’espère qu’une solution type <a href="https://nixos.org/">NixOS</a> se développera sur debian pour pouvoir avoir une configuration scriptée et complètement reproductible (en fait, il y a déjà des tentatives, mais qui ne me semblaient pas encore assez matures).</p>

<h2 id="stockage-en-passthrough-pci">Stockage en passthrough PCI</h2>

<p>Un point qui m’a bloqué pendant longtemps, j’ai en gros un stockage SSD pour le quotidien et des disques en raid logiciel pour le stockage. Mes exigences sont que ces stockages restent directement lisibles en cas de crash, sans utiliser des filesystems virtuels ou autres, et que les performances soient décentes.</p>

<p>Expérimental avec Proxmox 8, <a href="https://pve.proxmox.com/pve-docs/pve-admin-guide.html#qm_virtual_machines_settings">virtiofs</a> devient facile à utiliser avec Proxmox 9. J’étais donc parti sur cette option avec le hardware géré par Proxmox et les dossiers montés dans chaque VM suivant les besoins, mais la compatibilité avec notamment Docker, Samba, et NFS n’était pas bonne. Mauvaise piste… Par ailleurs le fait que le stockage soit directement traité par proxmox ne me plaisait pas non plus, car lorsque les disques en raid ont des problèmes (et ça arrive plus souvent qu’on voudrait…), cela bloque potentiellement tout le boot, avec besoin se connecter physiquement.</p>

<p>J’ai finalement pensé au passthrough PCI :</p>
<ul>
  <li>d’une part pour passer un SSD NVMe (et donc PCI), à la VM server ; comme il n’est pas possible de le couper en deux, j’ai donc ajouté un NVMe : 1 pour les VMs géré par Proxmox, et 1 autre pour le stockage quotidien géré par la VM Server</li>
  <li>d’autre part pour passer le contrôleur SATA de ma carte mère, et que les disques durs SATA soient directement gérés par la VM Server ; et après 15 ans d’usage du raid logiciel mdadm avec une très grande résilience (le raid m’a sauvé plusieurs fois, y compris les fonctions de récupération robustes de mdadm, par contre ext4fs a eu quelques corruptions…), j’ai cédé aux sirènes de ZFS. J’espère que je n’aurai pas à le regretter…</li>
</ul>

<p>La VM Server fera serveur de fichiers, avec Samba pour l’usage bureautique, et NFS pour partager avec les autres VMs.</p>

<h2 id="home-assistant-os-en-vm">Home Assistant OS en VM</h2>

<p>J’avais commencé ma migration de domoticz vers Home Assistant en docker, cependant on perd beaucoup à ne pas utiliser la version VM Home Assistant OS avec les mises à jour facile et les apps/addons qui permettent d’étendre les fonctions avec des dockers directement gérés par Home Assistant OS. Cette VM est parfaitement bien prévue, avec les mises à jour et les backups automatiques distants.</p>

<h2 id="routage-et-vpn-avec-opnsense">Routage et VPN avec OPNsense</h2>

<p>J’ai testé différents routeurs, soit en virtuel en VM ou en physique avec un firmware custom comme OpenWRT. <a href="https://opnsense.org/">OPNsense</a> correspond parfaitement à ce que je cherchais, très riche en fonctionnalités, très facile à utiliser avec son IHM simple, et extensible avec les plugins. Il permet de couvrir les fonctions de firewall, de VPN et de proxy dont j’ai besoin.</p>

<h2 id="le-maximum-de-services-en-docker">Le maximum de services en docker</h2>

<p>Pour que chaque service intègre ses propres dépendances et découpler au maximum les services, les containers sont l’idéal.</p>

<p>Pour en faciliter la gestion, j’ai fait le choix de :</p>
<ul>
  <li>déployer uniquement avec docker compose en fichier yaml, notamment à l’aide de <a href="https://www.portainer.io/">Portainer</a> au début, et <a href="https://dockhand.pro/">Dockhand</a> maintenant</li>
  <li>stocker les configurations et les volumes à conserver en répertoires absolus dans le système de fichier de l’hôte (et non perdu dans l’arborescence docker de la VM)</li>
  <li>sélectionner avec soin les images docker à utiliser, en évitant les exotiques non maintenues ou risquées</li>
  <li>pour mes scripts maison, de créer des scripts de setup à partir d’une base docker debian-slim plutôt que des Dockerfile à reconstruire, pour bénéficier des mises à jour sans avoir à mettre en place une CI complète sur les Dockerfile</li>
</ul>

<p>J’ai ainsi actuellement une cinquantaine de containers en une trentaine de stacks, et notamment :</p>
<ul>
  <li><a href="https://dockhand.pro/">Dockhand</a> pour l’administration de l’ensemble docker (stacks, logs, mises à jour automatiques,…)</li>
  <li><a href="https://github.com/mcuadros/ofelia">Ofelia</a> pour l’orchestration des batchs ; j’ai essayé beaucoup d’alternatives, sans trouver la solution qui me convienne parfaitement en simplicité et interface, mais après avoir appris à utiliser ofelia, j’en suis très content ; j’ai également quelques conteneurs customs qui intègrent directement leur cron locale</li>
  <li><a href="https://duplicati.com/">duplicati</a> pour les sauvegardes, avec <a href="https://rclone.org/">rclone</a> pour des destinations non supportées</li>
  <li><a href="https://syncthing.net/">syncthing</a> pour la synchronisation entre les différents PC et le serveur</li>
  <li><a href="https://traefik.io">Traefik</a>, <a href="https://pocketid.app">PocketID</a>, <a href="https://tinyauth.app">TinyAuth</a> pour la sécurisation (voir l’article dédié <a href="/2026/06/securisation-de-vos-serveurs/">sur la sécurisation de l’infrastructure</a>)</li>
  <li>et en vrac, <a href="https://gogs.io">Gogs</a>, <a href="https://languagetool.org">LanguageTool</a>, <a href="https://sourceforge.net/projects/minidlna">MiniDLNA</a>, <a href="https://github.com/AnalogJ/scrutiny">Scrutiny</a></li>
</ul>

<h2 id="des-dockers-vnc-pour-administrer">Des dockers VNC pour administrer</h2>

<p>La ligne de commande c’est sympa, mais les interfaces graphiques aussi. Pour pouvoir administrer le serveur, que ce soit pour l’édition des fichiers de configuration ou également pour gérer les fichiers en direct, j’avais initialement prévu une VM GUI, sous debian avec interface graphique et en montant les répertoires en NFS. C’est lourd et pas très pratique. Une excellente alternative est de créer un docker qui créé un serveur VNC.</p>

<p>Voici un exemple de docker-compose (il existe une image docker sur le même principe <a href="https://hub.docker.com/r/consol/debian-xfce-vnc">consol/debian-xfce-vnc</a>) :</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">services</span><span class="pi">:</span>
  <span class="na">vnc</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">debian</span>
    <span class="na">restart</span><span class="pi">:</span> <span class="s">unless-stopped</span>
    <span class="na">container_name</span><span class="pi">:</span> <span class="s">server-vnc</span>
    <span class="na">mem_limit</span><span class="pi">:</span> <span class="s">2g</span>
    <span class="na">environment</span><span class="pi">:</span>
      <span class="na">HOME_DIR</span><span class="pi">:</span> <span class="s">/home</span>
      <span class="na">USERNAME</span><span class="pi">:</span> <span class="s">username</span>
      <span class="na">PASSWORD</span><span class="pi">:</span> <span class="s">xxxxxx</span>
      <span class="na">UID</span><span class="pi">:</span> <span class="m">1000</span>
      <span class="na">GID</span><span class="pi">:</span> <span class="m">1000</span>
      <span class="na">VNC_PASSWORD</span><span class="pi">:</span> <span class="s">yyyyyyy</span>
      <span class="na">VNC_OPTIONS</span><span class="pi">:</span> <span class="s">-geometry 1600x1000</span>
    <span class="na">ports</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">5900:5900"</span>
    <span class="na">command</span><span class="pi">:</span> 
      <span class="pi">-</span> <span class="s">bash</span>
      <span class="pi">-</span> <span class="s">-c</span>
      <span class="pi">-</span> <span class="pi">|</span>
        <span class="s"># Escape $ to $$</span>
        <span class="s">if ! id -u "$${USERNAME}" &gt; /dev/null 2&gt;&amp;1; then</span>
          <span class="s">echo "Création de l'utilisateur '$${USERNAME}' ..."</span>
          <span class="s">groupadd -g "$${GID}" "$${USERNAME}"</span>
          <span class="s">useradd -m -u "$${UID}" -g "$${GID}" -d "$${HOME_DIR}" -s /bin/bash -p "$$(mkpasswd -m sha-512 $${PASSWORD})" "$${USERNAME}"</span>
          <span class="s">mkdir -p "$${HOME_DIR}"</span>
          <span class="s">chown -R "$${UID}:$${GID}" "$${HOME_DIR}"</span>
        <span class="s">else</span>
          <span class="s">echo "L'utilisateur $${USERNAME} existe déjà."</span>
        <span class="s">fi</span>
        <span class="s">unset PASSWORD</span>

        <span class="s">echo 'Setting up container...'</span>
        
        <span class="s">export DEBIAN_FRONTEND=noninteractive</span>
        
        <span class="s">echo "locales locales/locales_to_be_generated multiselect fr_FR.UTF-8 UTF-8" | debconf-set-selections</span>
        <span class="s">echo "locales locales/default_environment_locale select fr_FR.UTF-8" | debconf-set-selections</span>
        <span class="s">echo "localepurge localepurge/multiselect multiselect fr en" | debconf-set-selections</span>
        <span class="s">echo "keyboard-configuration  keyboard-configuration/xkb-keymap select fr" | debconf-set-selections</span>
        <span class="s">echo "keyboard-configuration  keyboard-configuration/layoutcode string fr" | debconf-set-selections</span>
        <span class="s">echo "keyboard-configuration  keyboard-configuration/layout toggle" | debconf-set-selections </span>
        
        <span class="s">apt-get update</span>
        <span class="s">apt-get install -y unattended-upgrades \</span>
        <span class="s">	tigervnc-standalone-server tigervnc-tools \</span>
        <span class="s">	locales localepurge keyboard-configuration tzdata \</span>
        <span class="s">	lxqt-core openbox dbus-x11 xdg-user-dirs \</span>
        <span class="s">	screen symlinks nano scite ncdu \</span>
        <span class="s">	qterminal pcmanfm-qt qdirstat \</span>
            <span class="s">\</span>
            <span class="s">sudo vlc wget baobab chromium</span>
        
        <span class="s">apt-get remove -y xfwm4 upower xscreensaver</span>
        <span class="s">apt-get autoremove -y</span>
        <span class="s">apt-get clean</span>

        <span class="s">ln -sf /usr/share/zoneinfo/Europe/Paris /etc/localtime</span>
        <span class="s">dpkg-reconfigure -f noninteractive tzdata</span>

        <span class="s">usermod -aG sudo $${USERNAME}</span>

        <span class="s">echo "Write xstartup"</span>
        <span class="s">echo '</span>
          <span class="s">export LANG=fr_FR.utf8</span>
          <span class="s">export LANGUAGE=fr_FR:fr</span>
          <span class="s">export LC_ALL=fr_FR.utf8</span>
          <span class="s">export SHELL=/bin/bash</span>
        <span class="no">  </span>
          <span class="s">unset SESSION_MANAGER</span>
          <span class="s">vncconfig -nowin &amp;</span>
          <span class="s">xhost +</span>
          <span class="s">setxkbmap fr</span>
        <span class="no">  </span>
          <span class="s">(sleep 5 &amp;&amp; pcmanfm-qt /home ) &amp;</span>
          <span class="s">startlxqt</span>
        <span class="s">' &gt; /xstartup</span>
        <span class="s">chmod a+rx xstartup</span>

        <span class="s">echo "Switching to user $${USERNAME}"</span>
        <span class="s">su --preserve-environment "$${USERNAME}" -c '</span>

          <span class="s">export HOME=$${HOME_DIR}</span>
  
          <span class="s">write_conf_file() {</span>
             <span class="s">mkdir -p $$(dirname "$$1")</span>
             <span class="s">[ ! -f "$$1" ] &amp;&amp; echo -e "$$2" &gt; "$$1"</span>
          <span class="s">}</span>
        <span class="no">  </span>
          <span class="s">write_conf_file "$${HOME_DIR}/.config/lxqt/lxqt.conf" "[General]\ntheme=light\n" </span>
        <span class="no">  </span>
          <span class="s">echo "Starting VNC server..."</span>
          <span class="s">echo -e "$${VNC_PASSWORD}\n$${VNC_PASSWORD}\n" | tigervncpasswd $${HOME_DIR}/.vnc_passwd</span>
          <span class="s">unset VNC_PASSWORD</span>
          <span class="s">tigervncserver :2 -fg -localhost no -passwd $${HOME_DIR}/.vnc_passwd -xstartup /xstartup $${VNC_OPTIONS}</span>
        <span class="s">'</span>
    <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">/docker/vnc/home:/home</span>
      <span class="pi">-</span> <span class="s">/:/mnt/host</span>
</code></pre></div></div>

<p>J’utilise comme client VNC  RealVNC Viewer en version 7 (version Classic dont le téléchargement semble malheureusement réduit maintenant), qui fonctionne très bien en enregistrement de sessions, restauration de connectivité en sortie de veille, et gestion des copier/coller.</p>

<p>Il est possible d’avoir le même principe sous RDP avec <code class="language-shell highlighter-rouge">xrdp</code>.</p>

<h2 id="autres-machines-virtuelles">Autres machines virtuelles</h2>

<ul>
  <li>une VM Linux pour avoir un desktop pour le dev, l’idée est d’en faire un template et d’instancier par projet ou réinitialisation</li>
  <li>une VM Lab pour avoir un environnement desktop qui me serve de lab pour tester des nouveautés sans pourrir le serveur</li>
  <li>une VM Windows pour avoir un desktop de secours, accessible avec Remote Desktop Protocol de Windows, en cas de problème avec mon PC portable (setup comparable)</li>
</ul>

<h2 id="autres-détails">Autres détails</h2>

<p>Pour des sauvegardes automatiques long terme, la VM Server expose en NFS à Proxmox un dossier qui est monté en storage pour être une destination de backup.</p>

<h1 id="ressources-utiles">Ressources utiles</h1>
<h2 id="autres-articles-lprpfr-liés-à-la-migration-proxmox">Autres articles LPRP.fr liés à la migration Proxmox</h2>
<ul>
  <li><a href="/2026/06/securisation-de-vos-serveurs/">Sécuriser votre infrastructure maison avec Proxmox, OPNsense, Traefik, Home Assistant et PKI XCA</a></li>
  <li><a href="/2026/06/proxmox-usbip/">USB d’un PC Windows distant dans une VM Proxmox via USB-IP</a></li>
  <li><a href="/2026/06/proxmox-resize-disks/">Proxmox - Réduire la taille d’un disque</a></li>
  <li><a href="/2025/09/acces-distant-vnc-avec-proxmox-qemu/">Accès distant VNC avec Proxmox/QEMU</a></li>
</ul>

<h2 id="documentations">Documentations</h2>
<ul>
  <li><a href="https://pve.proxmox.com/pve-docs/">Documentation Proxmox</a></li>
  <li><a href="https://community-scripts.org/">Proxmox Helper Scripts</a></li>
</ul>

<h2 id="trucs-et-astuce-pour-proxmox">Trucs et astuce pour Proxmox</h2>
<ul>
  <li>Pour utiliser la console xterm.js et ses copier/coller facilités, il faut ajouter une console série dans hardware et ajouter dans <code class="language-shell highlighter-rouge">/etc/default/grub</code>:   <code class="language-shell highlighter-rouge"><span class="nv">GRUB_CMDLINE_LINUX</span><span class="o">=</span><span class="s2">"console=tty0 console=ttyS0,115200"</span></code> et <code class="language-shell highlighter-rouge"><span class="nv">GRUB_TERMINAL</span><span class="o">=</span><span class="s2">"console serial"</span></code></li>
  <li>Hotplug automatique via Proxmox, voir le hookscript de <a href="/2026/06/proxmox-usbip/">cet article</a></li>
  <li>Eviter de passer par un cluster pour migrer d’une instance Proxmox à une autre ; le mode cluster n’est pas prévu pour fonctionner avec 2 serveurs ; il existe maintenant <a href="https://www.proxmox.com/en/products/proxmox-datacenter-manager/overview">Proxmox Datacenter Manager</a> qui devrait être plus adapté (sorti malheureusement après ma migration, donc non testé) ; si vous faites la même erreur que moi, pour forcer la sortie du mode cluster il faut suivre <a href="https://pve.proxmox.com/wiki/Cluster_Manager#_remove_a_cluster_node">Cluster_Manager / remove_a_cluster_node</a></li>
  <li>Pour le passthrough PCI il peut être nécessaire de faire des group iommu séparés :  <code class="language-shell highlighter-rouge">/etc/default/grub</code>: <code class="language-shell highlighter-rouge"><span class="nv">GRUB_CMDLINE_LINUX_DEFAULT</span><span class="o">=</span><span class="s2">"quiet amd_iommu=on iommu=pt pcie_acs_override=downstream,multifunction"</span></code> pour AMD, ou pour Intel  <code class="language-shell highlighter-rouge"><span class="nv">GRUB_CMDLINE_LINUX_DEFAULT</span><span class="o">=</span><span class="s2">"quiet intel_iommu=on iommu=pt pcie_acs_override=downstream,multifunction"</span></code> puis <code class="language-shell highlighter-rouge">update-grub <span class="o">&amp;&amp;</span> reboot</code></li>
  <li>Pour optimiser la consommation électrique : `
    <div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>apt <span class="nb">install </span>powertop linux-cpupower
cpupower frequency-set <span class="nt">-g</span> powersave
powertop <span class="nt">--auto-tune</span>
systemctl <span class="nb">enable </span>powertop
</code></pre></div>    </div>
  </li>
  <li>Pour installer via DKMS des modules noyaux signés compatible avec secure boot (typiquement ZFS pour debian): https://www.linuxtricks.fr/wiki/signer-les-modules-noyau-tiers-dkms-pour-le-secure-boot-sous-linux</li>
</ul>]]></content><author><name>Rémi Peyronnet</name></author><category term="Informatique" /><category term="Proxmox" /><category term="Docker" /><category term="Virtual" /><category term="Scripts" /><category term="Debian" /><category term="VNC" /><summary type="html"><![CDATA[Retour d’expérience sur la migration de mon serveur historique monolithique vers une infrastructure Proxmox et des composants containerisés. Cette transition aura duré plus d’un an, avec différentes phases progressives pour lisser les évolutions dans le temps, quand j’ai le temps de m’en occuper, et sans pour autant avoir de coupure de service.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://www.lprp.fr/files/2026/migration_proxmox.jpg" /><media:content medium="image" url="https://www.lprp.fr/files/2026/migration_proxmox.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry xml:lang="fr"><title type="html">Sécuriser votre infrastructure maison avec Proxmox, OPNsense, Traefik, Home Assistant et PKI XCA</title><link href="https://www.lprp.fr/2026/06/securisation-de-vos-serveurs/" rel="alternate" type="text/html" title="Sécuriser votre infrastructure maison avec Proxmox, OPNsense, Traefik, Home Assistant et PKI XCA" /><published>2026-06-19T10:30:00+00:00</published><updated>2026-06-19T10:30:00+00:00</updated><id>https://www.lprp.fr/2026/06/securisation-de-vos-serveurs</id><content type="html" xml:base="https://www.lprp.fr/2026/06/securisation-de-vos-serveurs/"><![CDATA[<p>Cette stack regroupe le routage, le firewall, la gestion des certificats, le reverse proxy et l’authentification autour d’une logique simple : un point d’entrée clair, des services bien isolés, et une séparation nette entre certificats internes et publics.</p>

<h1 id="présentation-de-la-stack">Présentation de la stack</h1>

<p>La stack s’articule autour de plusieurs briques complémentaires :</p>

<p><img src="/files/2026/Server-SecureStack.svg" alt="" class="img-center mw80" /></p>

<ul>
  <li><strong>Box opérateur</strong> : accès Internet avec NAT IPv4 et firewall IPv6</li>
  <li><strong>Proxmox Firewall</strong> : filtre le trafic au niveau des VM et de l’hyperviseur.</li>
  <li><strong>Routeur OPNsense</strong> : gère le routage, le proxy HTTPS externe avec LetsEncrypt, et une partie des certificats locaux.</li>
  <li><strong>XCA</strong> : sert à créer et administrer l’autorité de certification interne.</li>
  <li><strong>Traefik</strong> : reverse proxy pour les services Docker avec SSL.</li>
  <li><strong>TinyAuth + PocketId</strong> : brique d’authentification et d’OpenID Connect.</li>
  <li><strong>Nginx Proxy Manager</strong> : reverse proxy SSL pour Home Assistant.</li>
</ul>

<p>Il serait possible d’utiliser des certificats publics LetsEncrypt pour tous, ce qui simplifierait le setup, mais cela impose d’exposer les services internes à l’extérieur (en HTTP ou en DNS).  Une autorité de certification interne (AC) permet plus de souplesse, et également de générer des certificats pour des systèmes qui n’ont pas le support LetsEncrypt, avec l’inconvénient de devoir installer le certificat sur tous les appareils et dockers ayant besoin de valider une connexion https avec des certificats issus de cette AC.</p>

<h1 id="firewall">Firewall</h1>
<h2 id="box-opérateur">Box opérateur</h2>

<p>Les box opérateurs proposent généralement deux fonctions de protection à utiliser :</p>
<ul>
  <li>NAT (Network Address Translation, typiquement pour IPv4) : est une première forme de protection, car les ports non exposés sont de facto filtrés ; n’exposez que les ports utiles et sécurisés (typiquement https/443), sur une association fixe (les box modernes permettent souvent d’associer à l’adresse MAC de la cible, pour que ce soit indépendant de l’adresse IP donnée par le DHCP ; si ce n’est pas le cas, configurez votre DHCP ou votre cible pour avoir une IP fixe)</li>
  <li>Firewall (typiquement pour IPv6) : assurez-vous que le firewall est bien actif (souvent par défaut maintenant), sinon vos appareils sont directement  tous exposés sur internet en IPv6 ; les accès IPv6 sont maintenant plus courants, vous pouvez également ouvrir les ports que vous avez ouverts en NAT sur les IPv6 concernées</li>
</ul>

<h2 id="proxmox-firewall">Proxmox Firewall</h2>

<p>Le firewall Proxmox sert de premières barrières sur l’infrastructure virtuelle. Il permet de filtrer les accès aux nœuds Proxmox eux-mêmes, mais aussi aux VM comme le routeur OPNsense, le serveur Docker ou Home Assistant.</p>

<p>Il est nécessaire d’activer l’option Firewall à tous les niveaux (Datacenter, nœud et VM) pour que le firewall soit actif. Il est ensuite possible de définir (ou non) des règles de filtrage à chaque niveau. Vous pouvez également définir des politiques de filtrage par défaut, par exemple pour interdire tout trafic par défaut, ou au contraire laisser ouvert. Sur le nœud et le datacenter, Proxmox intègre automatiquement les règles pour ouvrir les ports utiles à la gestion de Proxmox.</p>

<p>Proxmox offre plusieurs options utiles pour définir efficacement vos règles :</p>
<ul>
  <li>les <strong>IPSets</strong> permettent de définir des groupes d’adresses IP ou réseaux qui seront utilisables dans les règles par la suite ; j’ai défini par exemple un IPset <code class="language-shell highlighter-rouge">LAN</code> et <code class="language-shell highlighter-rouge">LAN-broadcast</code>avec les réseaux IPv4 et IPv6 correspondant, ce qui facilite l’écriture des règles, et leur maintenance si par exemple le préfixe IPv6 change</li>
  <li>les <strong>Security Groups</strong> permettent de définir des groupes de règles, qu’on pourra ensuite ajouter dans chacun des firewall ; j’ai par exemple défini des groupes générique <code class="language-shell highlighter-rouge">lan-generic</code>et <code class="language-shell highlighter-rouge">lan-mdns</code> avec des règles par défaut, que je peux activer sur chaque VM si j’ai besoin d’un filtrage LAN classique, d’ajouter le support UPNP / discovery, etc. La prise en compte des modifications nécessite parfois de décocher / cocher le security group sur la VM.</li>
</ul>

<p>Enfin, sur une VM donnée, pour identifier les ports réellement écoutés et potentiellement candidats à être ouverts, une commande utile est  <code class="language-shell highlighter-rouge">ss <span class="nt">-tulnrp</span></code> qui permet de lister tous les ports TCP ou UDP exposés, et par quel processus (Si la commande n’est pas disponible, sous debian<code class="language-shell highlighter-rouge">apt <span class="nb">install </span>iproute2</code>). Tous n’ont bien sûr pas vocation à être exposés, mais cela permet d’étudier chaque port et d’éviter d’en oublier.</p>

<h2 id="routeur-opnsense">Routeur OPNsense</h2>

<p>En plus du premier filtrage de la box opérateur, j’ai choisi d’ajouter une seconde couche de sécurité via un routeur OPNsense. C’est notamment utile pour ajouter une couche de sécurité et utiliser des fonctions de filtrage IP par des blacklists communautaires, des règles de filtrage plus avancées, etc.  Et bien sûr, il intègre des fonctions de routeur avancé que nous ne traiterons pas ici.</p>

<p>Le principe est de déclarer deux interfaces réseaux sur la VM OPNsense :</p>
<ul>
  <li>une interface WAN : c’est sur cette interface qu’on fera pointer nos règles NAT ou notre exposition externe, et sur cette interface que nous définirons nos règles firewall</li>
  <li>une interface LAN : c’est via cette interface qu’on proxifiera les services exposés</li>
</ul>

<h1 id="proxy-https">Proxy HTTPS</h1>
<h2 id="gestion-des-certificats-avec-xca-et-opnsense">Gestion des certificats avec XCA et OPNsense</h2>

<p><a href="https://www.hohnstaedt.de/xca/">XCA</a> sert à gérer l’autorité de certification interne. C’est la brique qui permet de créer votre autorité de certification racine (AC), d’émettre les certificats internes et d’organiser leur cycle de vie. C’est une application desktop disponible pour Windows, Mac et Linux (selon la distribution, ou en compilation)</p>

<p>J’ai opté pour la logique suivante :</p>
<ul>
  <li>une AC root définies dans XCA ; qui permet de gérer certains certificats en direct dans XCA</li>
  <li>une AC secondaire gérée par OPNsense ; il faut créer cette AC dans XCA, puis l’importer dans OPNsense ; OPNsense pourra ensuite l’utiliser et signer de nouveaux certificats avec cette AC secondaire, qui sera automatiquement reconnu dans la mesure où on a installé l’autorité racine</li>
  <li>un certificat wildcard pour le server docker exposé par Traefik (ce n’est pas une bonne pratique, mais suffisant pour mon cas)</li>
</ul>

<p>Un point d’attention dans OPNsense dans la gestion des certificats, il faut déclarer uniquement les noms de domaines (en CN ou en SAN) que doivent gérer ces certificats, et ne pas inclure des noms de domaine destinés à Let’s Encrypt, car dès que Caddy trouve un certificat qui correspond, il prendra celui là et n’en demandera pas à Letsencrypt, même si les options “Auto HTTPS” sont activées.</p>

<p>Les champs à bien remplir pour des certificats correctement retenus :</p>
<ul>
  <li><strong>CN</strong> (Common Name) : l’adresse DNS principale sécurisée (l’adresse seule, sans le préfixe https)</li>
  <li><strong>Extension SAN</strong> (Subject Alternative Names), en cochant critical : toutes les adresses DNS et/ou IP qui peuvent être utilisées avec ce certificat (toujours les adresses seules, mais vous pouvez utiliser un wildcard <code class="language-shell highlighter-rouge"><span class="k">*</span>.madns.com</code>)</li>
  <li><strong>Key Usage</strong>, en cochant critical : Digital Signature, Key Encipherment</li>
  <li><strong>Extended Key Usage</strong> : TLS Web Server Authentication, TLS Web Client Authentication</li>
  <li>et n’oubliez pas bien sûr de créer une clé privée, sélectionner la signature par la bonne AC et ajuster la durée de validité selon votre convenance</li>
</ul>

<h2 id="caddy-pour-opnsense">Caddy pour OPNsense</h2>

<p>Comme proxy HTTPS, j’utilise caddy, qui est très bien intégré dans OPNsense, avec une interface complète et facile à utiliser, et la possibilité de demander automatiquement des certificats ACME/LetsEncrypt, ou des certificats locaux via la gestion des certificats intégrée à OPNsense, et très pratique également.</p>

<p>Si vous souhaitez rediriger vers des DNS pointant vers des IP locales définies avec un DNS qui ne serait pas géré par OPNsense (comme OVH, Cloudflare, etc. si vous avez un domaine personnalisé), il vous faudra les déclarer également dans OPNsense, car par défaut pour des raisons de sécurité, OPNsense interdit des adresses DNS qui se résolvent sur une IP locale. Il faut alors les déclarer dans <strong>Unbound DNS / Contournements</strong>.</p>

<h2 id="traefik-pour-les-containers-docker">Traefik pour les containers docker</h2>

<p>Il serait possible d’utiliser seulement Caddy comme proxy https, en exposant sur le réseau interne tous les ports http des containers docker. Le problème est que cela ne permettrait pas de les protéger des autres périphériques connectés sur votre réseau local (y compris les IoT type prises connectées ou autres qui ouvrent un tunnel avec leur cloud et peuvent présenter des risques de sécurité), ni de les protéger par l’authentification qui sera ajoutée ensuite.</p>

<p>L’usage de traefik permet ainsi d’avoir un autre étage de proxy SSL au niveau du server docker, et de n’ouvrir au niveau du firewall que le port https. Si on cumule avec caddy pour l’exposition externe on aura donc :</p>
<ul>
  <li>pour les services externes : Internet –&gt; Box (NAT / Firewall) –&gt; Caddy/OPNsense (Firewall + SSL avec Letsencrypt) –&gt; VM Docker (Firewall Proxmox) –&gt; Traefik (SSL avec AC interne et authentification) –&gt; service docker</li>
  <li>pour les services internes, on aura seulement : LAN –&gt; VM Docker (Firewall Proxmox) –&gt; Traefik (SSL avec AC interne et authentification) –&gt; service docker</li>
</ul>

<p>L’utilisation de Traefik est particulièrement pratique avec <code class="language-shell highlighter-rouge">docker compose</code>.  Pour sécuriser, on va déclarer un réseau <code class="language-shell highlighter-rouge">proxy</code> qui sera partagé entre traefik et les différents containers. Si on veut sécuriser encore plus, et éviter qu’un service docker compromis puisse accéder à d’autres services docker sans sécurisation, on peut créer un réseau dédié pour chaque service docker <code class="language-shell highlighter-rouge">proxy_xxx</code>.</p>

<p>Exemple de docker compose pour Traefik :</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">services</span><span class="pi">:</span>
  <span class="na">traefik</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">traefik</span>
    <span class="na">restart</span><span class="pi">:</span> <span class="s">always</span>
    <span class="na">command</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">--providers.docker=true"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">--providers.docker.watch=true"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">--providers.docker.exposedByDefault=false"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">--providers.docker.network=proxy"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">--providers.file.filename=/config/dynamic.yml"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">--entrypoints.websecure.address=:443"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">--entrypoints.web.address=:80"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">--entrypoints.web.http.redirections.entrypoint.to=websecure"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">--entrypoints.web.http.redirections.entrypoint.scheme=https"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">--api.dashboard=true"</span> <span class="c1"># Pour l'interface graphique</span>
    <span class="na">ports</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">80:80"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">443:443"</span>
    <span class="na">networks</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">proxy"</span><span class="pi">]</span>
    <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">/var/run/docker.sock:/var/run/docker.sock:ro</span>
      <span class="pi">-</span> <span class="s">/docker/auth/traefik-config:/config</span>
      <span class="pi">-</span> <span class="s">/docker/auth/traefik-certs:/certs</span>

<span class="na">networks</span><span class="pi">:</span>
  <span class="na">proxy</span><span class="pi">:</span>
    <span class="na">external</span><span class="pi">:</span> <span class="no">true</span>
</code></pre></div></div>
<p>Notez <code class="language-shell highlighter-rouge"><span class="s2">"--providers.docker.network=proxy"</span></code> qui permet d’indiquer à traefik quelle IP sélectionner pour les containers qui ont plusieurs networks.</p>

<p>Fichier `dynamic.yml:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">tls</span><span class="pi">:</span>
  <span class="na">stores</span><span class="pi">:</span>
    <span class="na">default</span><span class="pi">:</span>
      <span class="na">defaultCertificate</span><span class="pi">:</span>
        <span class="na">certFile</span><span class="pi">:</span> <span class="s">/certs/wildcard.crt</span>
        <span class="na">keyFile</span><span class="pi">:</span> <span class="s">/certs/wildcard.key</span>

  <span class="na">options</span><span class="pi">:</span>
    <span class="na">default</span><span class="pi">:</span>
      <span class="na">minVersion</span><span class="pi">:</span> <span class="s">VersionTLS12</span>

<span class="na">serversTransport</span><span class="pi">:</span>
  <span class="na">insecureSkipVerify</span><span class="pi">:</span> <span class="no">false</span>   <span class="c1"># Optionel, utile pour router vers du HTTPS autosigné</span>
</code></pre></div></div>

<p><code class="language-shell highlighter-rouge">wildcard.crt</code> et <code class="language-shell highlighter-rouge">wildcard.key</code>sont le certificat wildcard et la clé privée générée par XCA pour ce server, et exporté au format PEM.</p>

<p>Sur chaque docker, on pourra ajouter :</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># dans le service</span>
    <span class="na">networks</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">proxy</span>
    <span class="na">labels</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">traefik.enable=true</span>
      <span class="pi">-</span> <span class="s">traefik.http.routers.monservice.rule=Host(`monservice.customdns`)</span>
      <span class="pi">-</span> <span class="s">traefik.http.routers.monservice.tls=true</span>
      <span class="c1"># Si le port n'est pas directement déductible par Traefik</span>
      <span class="c1">#- traefik.http.services.monservice.loadbalancer.server.port=80</span>
      
<span class="na">networks</span><span class="pi">:</span>
  <span class="na">proxy</span><span class="pi">:</span>
    <span class="na">external</span><span class="pi">:</span> <span class="no">true</span>
</code></pre></div></div>
<p>Si plusieurs services doivent partager un réseau commun sans pour autant le partager avec tout le monde (comme par exemple entre un backend et sa base de données), vous pouvez ajouter un réseau <code class="language-shell highlighter-rouge">internal</code>, le déclarer sur chacun des containers concernés.</p>

<p>Et comme l’exposition est maintenant assurée par Traefik, on peut retirer ou commenter l’exposition des ports en commentait les options <code class="language-shell highlighter-rouge">ports:</code>. Il ne sera plus possible d’accéder au service par <code class="language-shell highlighter-rouge">http:///&lt;IP&gt;:&lt;PORT&gt;</code>mais seulement via <code class="language-shell highlighter-rouge">https://monservice.customdns</code></p>

<p>Traefik offre également une interface graphique permettant la lecture des configurations actives (lecture seule, la configuration ne se fait que via les labels ou les fichier yaml) ; vous pouvez également l’exposer et la sécuriser via Traefik en ajoutant</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="na">labels</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">traefik.enable=true</span>
      <span class="pi">-</span> <span class="s">traefik.http.routers.traefik.rule=Host(`traefik.customdns`)</span>
      <span class="pi">-</span> <span class="s">traefik.http.routers.traefik.tls=true</span>
      <span class="pi">-</span> <span class="s">traefik.http.routers.traefik.service=api@internal</span>
      <span class="pi">-</span> <span class="s">traefik.http.routers.traefik.middlewares=tinyauth</span>  <span class="c1"># Pour la sécurisation tinyauth qu'on verra par la suite</span>
</code></pre></div></div>

<p>Il est facile de faire une erreur dans la configuration de Traefik et que le service ne fonctionne pas ; dans les erreurs courantes :</p>
<ul>
  <li><strong>404</strong> :
    <ul>
      <li>vérifier que les noms de router et de service sont bien distincts dans le YAML.</li>
      <li>vérifier que le conteneur a bien terminé son démarrage (éventuellement forcer sa recréation)</li>
      <li>vérifier que le port exposé est correct et non déjà utilisé</li>
      <li>vérifier que Traefik voit bien les configurations (éventuellement forcer la relecture en redémarrant Traefik)</li>
    </ul>
  </li>
  <li><strong>Gateway Timeout</strong> :
    <ul>
      <li>le container peut avoir un problème, vérifier qu’il est bien <code class="language-shell highlighter-rouge">healthy</code>dans docker et que tout va bien dans les logs du container</li>
      <li>si le container a plusieurs réseaux, Traefik peut ne pas prendre la bonne IP ; il faut indiquer a Trafik quel réseau utiliser</li>
    </ul>
  </li>
</ul>

<h2 id="nginx-proxy-manager-pour-home-assistant">Nginx Proxy Manager pour Home Assistant</h2>

<p>Dans Home Assistant il existe deux addons pour sécuriser en SSL :</p>
<ul>
  <li><code class="language-shell highlighter-rouge">NGINX Home Assistant SSL proxy</code>est le plus simple, il est préconfiguré pour Home Assistant, vous pouvez fournir un certificat ou laisser un autosigné généré automatiquement</li>
  <li><code class="language-shell highlighter-rouge">Nginx Proxy Manager</code> est un reverse proxy simple et complet administrable avec une interface graphique efficace, vous pouvez le paramétrer pour Home Assistant (activer le support WebSockets), mais également d’autres services</li>
</ul>

<p>Dans tous les cas, suivez bien la documentation pour ajouter la configuration <code class="language-shell highlighter-rouge">http:</code>pour l’ajout du proxy de confiance.</p>

<h1 id="authentification">Authentification</h1>
<p>Deux mécanismes d’authentification sont possibles :</p>
<ul>
  <li>via un Identity Prodiver OIDC (OpenID Connect / OAuth2) : l’application supporte nativement l’authentification OIDC et on va créer pour chaque un client_id / client_secret et déclarer le server dans l’application ; on va utiliser <strong>PocketID</strong> pour cela</li>
  <li>via authentification dans Traefik : pour les applications sans authentification, on va demande à Traefik de s’en occuper, et de vérifier auprès d’un service que l’utilisateur a le droit d’utiliser le service ; on va utiliser <strong>TinyAuth</strong> pour ça</li>
</ul>

<h2 id="serveur-oidc-pocketid">Serveur OIDC PocketID</h2>
<p><a href="https://pocket-id.org/">PocketID</a> est un serveur OIDC hyper léger, et pourtant avec l’ensemble des fonctions utiles pour un homelab et une interface graphique très simple à utiliser pour le paramétrage. La spécificité de PocketID est qu’il gère seulement et exclusivement les passkeys. Donc notamment, pas de support de login / mot de passe.  Les passkeys sont cependant maintenant bien gérées par les navigateurs, et assez pratique à utiliser, mais définissez en autant que possible pour ne pas être bloqué (par exemple Windows Hello sur PC, Google sur android et chrome,…)</p>

<p>Il existe de nombreuses alternatives de serveur OIDC comme <a href="https://www.keycloak.org">Keycloak</a>, <a href="https://goauthentik.io">Authentik</a>, <a href="https://zitadel.com/">Zitadel</a>, <a href="https://lemonldap-ng.org/">LemonLDAP::NG</a> et beaucoup d’autres ; généralement nécessitant plus de ressources, et avec des interfaces plus ou moins abouties ou plus ou moins simples.</p>

<p>La configuration est également ultra-simple en docker :</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="na">pocketid</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">ghcr.io/pocket-id/pocket-id:latest</span>
    <span class="na">restart</span><span class="pi">:</span> <span class="s">always</span>
    <span class="na">networks</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">proxy"</span><span class="pi">]</span>
    <span class="na">environment</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">APP_URL=https://id.customdns</span>
      <span class="pi">-</span> <span class="s">ENCRYPTION_KEY=supersecret</span>
      <span class="pi">-</span> <span class="s">TRUST_PROXY=True</span>
      <span class="pi">-</span> <span class="s">PORT=3000</span>
      <span class="pi">-</span> <span class="s">POCKETID_CA_CERT=/certs/internal_ca.pem</span>
    <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">/docker/pocketid-data:/app/data</span>
      <span class="pi">-</span> <span class="s">/docker/crt:/certs</span>
    <span class="na">labels</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">traefik.enable=true</span>
      <span class="pi">-</span> <span class="s">traefik.http.routers.pocketid.rule=Host(`id.customdns`)</span>
      <span class="pi">-</span> <span class="s">traefik.http.routers.pocketid.tls=true</span>
      <span class="pi">-</span> <span class="s">traefik.http.services.pocketid.loadbalancer.server.port=3000</span>
      <span class="pi">-</span> <span class="s">traefik.http.services.pocketid.loadbalancer.server.scheme=http</span>
</code></pre></div></div>

<p>On en profite aussi pour l’exposer en https via Traefik. A la première connexion, on va pouvoir créer une passkey admin, en créer d’autre et configurer d’autres utilisateurs.</p>

<p>L’interface d’administration est très complète:
<img src="/files/2026/auth_pocketid_ui.png" alt="" class="img-center mw80" /></p>

<p>et permet de :</p>
<ul>
  <li>paramétrer de nouvelles applications OIDC (client_id / client_secret / redirect_uri / …    très simple et pourtant tout ce qui sert pour un homelab)</li>
  <li>paramétrer de nouveaux utilisateurs</li>
  <li>paramétrer des groupes, afin de restreindre des applications à des groupes utilisateurs</li>
</ul>

<p>L’interface de connexion quant à elle est hyper simple :</p>

<p><img src="/files/2026/auth_pocketid_login.png" alt="" class="img-center mw60" /></p>

<p>Le bouton permet ensuite la sélection de la passkey à utiliser, et la suite est gérée par le navigateur / l’OS.</p>

<h2 id="proxy-dauthentification-tinyauth">Proxy d’authentification TinyAuth</h2>

<p>PocketId n’intègre pas nativement d’intégration pour s’authentifier dans Traefik, et Traefik ne supporte pas nativement d’authentification OIDC (il existe un plugin tiers). Pour faire le pont entre les deux, on va utiliser un serveur d’authentification très léger <a href="https://tinyauth.app/">TinyAuth</a>. TinyAuth sait également faire server OIDC (et remplacer à ce titre PocketId), cependant :</p>
<ul>
  <li>il ne supporte pas les passkeys</li>
  <li>il ne dispose pas d’une interface graphique d’administration, la configuration doit passer par des fichiers de configuration ou labels docker, ce qui n’est pas le plus pratique au quotidien pour ajouter une application ou un utilisateur</li>
</ul>

<p>Pour le configurer sous docker</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="na">tinyauth</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">ghcr.io/tinyauthapp/tinyauth:latest</span>
    <span class="na">restart</span><span class="pi">:</span> <span class="s">always</span>
    <span class="na">networks</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">proxy"</span><span class="pi">]</span>
    <span class="c1"># entrypoint:  'sh -c "set -a; source /data/oidc_clients.env; set +a; exec /tinyauth/tinyauth" '</span>
    <span class="na">environment</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">TINYAUTH_APPURL=https://auth.customdns</span>
      <span class="c1">#- TINYAUTH_AUTH_USERSFILE=/data/users</span>
      <span class="pi">-</span> <span class="s">TINYAUTH_ANALYTICS_ENABLED=false</span>
      <span class="pi">-</span> <span class="s">TINYAUTH_AUTH_SESSIONEXPIRY=86400</span>
      <span class="pi">-</span> <span class="s">TINYAUTH_UI_TITLE=RP Auth</span>
      
      <span class="c1"># --- OIDC PocketID ---</span>
      <span class="pi">-</span> <span class="s">TINYAUTH_OAUTH_PROVIDERS_POCKETID_CLIENTID=&lt;client_id&gt;</span>
      <span class="pi">-</span> <span class="s">TINYAUTH_OAUTH_PROVIDERS_POCKETID_CLIENTSECRET=&lt;client_secret&gt;</span>
      <span class="pi">-</span> <span class="s">TINYAUTH_OAUTH_PROVIDERS_POCKETID_AUTHURL=https://id.customdns/authorize</span>
      <span class="pi">-</span> <span class="s">TINYAUTH_OAUTH_PROVIDERS_POCKETID_TOKENURL=http://pocketid:3000/api/oidc/token</span>
      <span class="pi">-</span> <span class="s">TINYAUTH_OAUTH_PROVIDERS_POCKETID_USERINFOURL=http://pocketid:3000/api/oidc/userinfo</span>
      <span class="pi">-</span> <span class="s">TINYAUTH_OAUTH_PROVIDERS_POCKETID_REDIRECTURL=https://auth.customdns/api/oauth/callback/pocketid</span>
      <span class="pi">-</span> <span class="s">TINYAUTH_OAUTH_PROVIDERS_POCKETID_SCOPES=openid email profile</span>
      <span class="pi">-</span> <span class="s">TINYAUTH_OAUTH_PROVIDERS_POCKETID_NAME=PocketID</span>
      <span class="pi">-</span> <span class="s">TINYAUTH_OAUTH_PROVIDERS_POCKETID_INSECURE=true</span>   <span class="c1"># CA privé</span>

      <span class="c1"># Redirection automatique vers PocketID (passkeys)</span>
      <span class="pi">-</span> <span class="s">TINYAUTH_OAUTH_AUTOREDIRECT=pocketid</span>
    <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">/docker/tinyauth-data:/data</span>
    <span class="na">labels</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">traefik.enable=true</span>
      <span class="pi">-</span> <span class="s">traefik.http.routers.tinyauth.rule=Host(`auth.customdns`)</span>
      <span class="pi">-</span> <span class="s">traefik.http.routers.tinyauth.tls=true</span>
      <span class="pi">-</span> <span class="s">traefik.http.services.tinyauth.loadbalancer.server.port=3000</span>

      <span class="c1"># Middleware ForwardAuth</span>
      <span class="pi">-</span> <span class="s">traefik.http.middlewares.tinyauth.forwardauth.address=http://tinyauth:3000/api/auth/traefik</span>
      <span class="pi">-</span> <span class="s">traefik.http.middlewares.tinyauth.forwardauth.trustForwardHeader=true</span>
      <span class="pi">-</span> <span class="s">traefik.http.middlewares.tinyauth.forwardauth.authResponseHeaders=remote-user,remote-email</span>

</code></pre></div></div>

<p>Quelques précisions :</p>
<ul>
  <li>TinyAuth permet aussi une gestion de login / mot de passe ; vous pouvez ajouter un fichier <code class="language-shell highlighter-rouge"><span class="nb">users</span></code> (voir ligne commentée + fichier avec des lignes <code class="language-shell highlighter-rouge">&lt;username&gt;:&lt;bcrypt-hash&gt;</code> et pour créer le hash <code class="language-shell highlighter-rouge">mkpasswd <span class="nt">--method</span><span class="o">=</span>bcrypt <span class="nt">--rounds</span><span class="o">=</span>12</code> ( <code class="language-shell highlighter-rouge">mkpasswd</code> dans <code class="language-shell highlighter-rouge">apt <span class="nb">install </span>whois</code> )</li>
  <li>Pour faciliter la configuration et éviter de tout mettre dans le docker compose, une option est de déplacer les variables dans un fichier d’environnement et de décommenter la ligne entrypoint ; il faut restart le container à chaque modification.</li>
  <li>Notez les subtilités dans la configuration OIDC PocketID (qui sera également à suivre avec les autres clients OIDC de PocketId) :
    <ul>
      <li>pour les URL qui passent par le navigateur de l’utilisateur (auth, redirect,…), il faut utiliser l’adresse externe <code class="language-shell highlighter-rouge">https://id.customdns</code></li>
      <li>pour les URL directes entre TinyAuth et PocketId, on utilise directement l’adresse interne docker via le nom du service et le port exposé en http <code class="language-shell highlighter-rouge">http://pocketid:3000</code></li>
    </ul>
  </li>
</ul>

<p>L’option <code class="language-shell highlighter-rouge"><span class="nv">TINYAUTH_OAUTH_AUTOREDIRECT</span><span class="o">=</span>pocketid</code> permet de se connecter automatiquement via PocketId, cependant en cas d’échec (ou sans cette option), l’écran de login ci-dessous est proposé et permet le choix entre PocketId ou le mot de passe si vous l’avez activé :</p>

<p><img src="/files/2026/auth_tinyauth_login.png" alt="" class="img-center mw60" /></p>

<h2 id="quelques-points-de-configuration-spécifiques">Quelques points de configuration spécifiques</h2>

<p>Certains clients OIDC peuvent avoir des contraintes particulières, notamment :</p>
<ul>
  <li>de devoir paramétrer les utilisateurs via OIDC (notamment le mapping pour déterminer quel champ OIDC utiliser pour le nom d’utilisateur), ou une création automatique des utilisateurs par OIDC</li>
  <li>d’exiger des emails validés ; il est préférable de systématiquement renseigner une adresse email dans l’utilisateur PocketId, et il est possible d’activer l’option Administration / Configuration de l’application / E-mail / E-mail vérifiés par défaut ; après l’activation de cette option, il faut mettre à jour les emails de vos utilisateurs pour que le paramètre fasse effet.</li>
  <li>de devoir paramétrer l’autorité de confiance interne SSL également coté client OIDC (certains vérifient que l’adresse d’authentification est valide) ;  voir les différentes méthodes ci-dessous</li>
</ul>

<h1 id="configuration-des-serverscontainers-pour-utilisation-de-lac-interne">Configuration des servers/containers pour utilisation de l’AC interne</h1>

<p>Pour que les applications fassent confiance à l’AC interne, il faut importer le certificat racine dans l’environnement concerné. Certaines applications le permettent directement dans leur configuration, soit globale, soit liée à la partie OIDC, d’autres nécessitent de l’ajouter en dehors de l’application, via les composants logiciels utilisés. Certains cas ne permettent pas d’importer un certificat AC, mais de désactiver la vérification SSL.</p>

<h2 id="debian">Debian</h2>

<p>Sur Debian, le certificat racine doit être copié dans le répertoire des autorités locales, puis le trust store doit être régénéré.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo cp </span>internal_ca.crt /usr/local/share/ca-certificates/internal_ca.crt
<span class="nb">sudo </span>update-ca-certificates
</code></pre></div></div>

<p>Après cela, le certificat est pris en compte par les outils système qui s’appuient sur le trust store Debian.</p>

<h2 id="java">Java</h2>

<p>Pour créer le keystore (une seule fois) :</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>keytool <span class="nt">-importcert</span> <span class="nt">-trustcacerts</span> <span class="nt">-alias</span> internal_ca <span class="nt">-file</span> /crt/internal_ca.crt <span class="nt">-keystore</span> /keystore/internal_ca.p12 <span class="nt">-storepass</span> changeme_not_that_secret <span class="nt">-storetype</span> PKCS12
</code></pre></div></div>

<p>Pour utiliser ce keystore au démarrage de l’application, ajouter en <code class="language-shell highlighter-rouge">JAVA_OPTS</code> :</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">-Djavax</span>.net.ssl.trustStore<span class="o">=</span>/keystore/internal_ca.p12
<span class="nt">-Djavax</span>.net.ssl.trustStorePassword<span class="o">=</span>changeme_not_that_secret
</code></pre></div></div>

<p>Si l’application utilise des variables d’environnement ou un fichier <code class="language-shell highlighter-rouge">java_opts</code>, il suffit d’y ajouter ces options.</p>

<h2 id="nodejs">Node.js</h2>

<p>Pour Node.js et les outils npm, le plus simple consiste à utiliser la variable d’environnement <code class="language-shell highlighter-rouge">NODE_EXTRA_CA_CERTS</code>.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">export </span><span class="nv">NODE_EXTRA_CA_CERTS</span><span class="o">=</span>/crt/internal_ca.crt
</code></pre></div></div>

<h2 id="go">Go</h2>

<p>Pour Go, la variable <code class="language-shell highlighter-rouge">SSL_CERT_FILE</code> peut être utilisée pour pointer vers le certificat racine de l’AC interne.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">export </span><span class="nv">SSL_CERT_FILE</span><span class="o">=</span>/crt/internal_ca.crt
</code></pre></div></div>

<p>Selon le contexte, <code class="language-shell highlighter-rouge">SSL_CERT_DIR</code> peut aussi être nécessaire si l’environnement attend un répertoire de certificats plutôt qu’un fichier unique.</p>

<h2 id="python">Python</h2>

<p>En Python, le plus simple est d’utiliser <code class="language-shell highlighter-rouge">REQUESTS_CA_BUNDLE</code> ou <code class="language-shell highlighter-rouge">SSL_CERT_FILE</code> selon l’outil utilisé.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">export </span><span class="nv">REQUESTS_CA_BUNDLE</span><span class="o">=</span>/crt/internal_ca.crt
</code></pre></div></div>

<p>Ou, pour un usage plus générique :</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">export </span><span class="nv">SSL_CERT_FILE</span><span class="o">=</span>/crt/internal_ca.crt
</code></pre></div></div>

<h1 id="appliquer-les-mises-à-jour">Appliquer les mises à jour</h1>

<p>Vouloir se sécuriser sans suivre les mises à jour serait complètement inutile. Il n’y a qu’à voir à quelle fréquence sortent les failles de sécurité à l’heure de l’IA. L’inconvénient d’avoir de nombreux applicatifs dans la stack est que cela peut faire beaucoup d’éléments à mettre à jour (OS, VM, dockers,…).</p>

<p>Il existe généralement des options de mise à jour automatique, comme :</p>
<ul>
  <li>Unattended upgrades pour Proxmox, <a href="https://slash-root.fr/debian-configuration-complete-dunattended-upgrades-pour-une-securite-sans-effort/">Debian</a>, <a href="https://docs.opnsense.org/manual/updates.html">OPNsense</a> ; n’oubliez cependant pas de redémarrer lors des mises à jour de kernel</li>
  <li>Mise à jour automatique des containers docker avec <a href="https://dockhand.pro/">Dockhand</a> ou <a href="https://github.com/containrrr/watchtower">watchtower</a></li>
  <li>Alors oui, ça vous expose aux attaques type “Supply chain”, mais si vous n’êtes pas admin au quotidien sur votre homelab, c’est sans doute le moins pire…</li>
</ul>]]></content><author><name>Rémi Peyronnet</name></author><category term="Informatique" /><category term="Proxmox" /><category term="Docker" /><category term="Securite" /><category term="LAN" /><category term="PKI" /><summary type="html"><![CDATA[Cette stack regroupe le routage, le firewall, la gestion des certificats, le reverse proxy et l’authentification autour d’une logique simple : un point d’entrée clair, des services bien isolés, et une séparation nette entre certificats internes et publics.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://www.lprp.fr/files/2026/auth_post_vignette.jpg" /><media:content medium="image" url="https://www.lprp.fr/files/2026/auth_post_vignette.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry xml:lang="fr"><title type="html">USB d’un PC Windows distant dans une VM Proxmox via USB-IP</title><link href="https://www.lprp.fr/2026/06/proxmox-usbip/" rel="alternate" type="text/html" title="USB d’un PC Windows distant dans une VM Proxmox via USB-IP" /><published>2026-06-14T11:00:00+00:00</published><updated>2026-06-14T11:00:00+00:00</updated><id>https://www.lprp.fr/2026/06/proxmox-usbip</id><content type="html" xml:base="https://www.lprp.fr/2026/06/proxmox-usbip/"><![CDATA[<p>Ce tutoriel explique comment partager un périphérique USB connecté à un PC Windows distant avec une machine virtuelle Linux sous Proxmox, en utilisant <strong>usbipd-win</strong> (serveur Windows) et le client USB/IP Linux.</p>

<p>Les différentes étapes :</p>
<ul>
  <li>installer <a href="https://github.com/dorssel/usbipd-win">usbipd-win</a> sur le PC Windows et exposer (bind) le device USB</li>
  <li>installer le client USB-IP sur Proxmox et attacher le périphérique</li>
  <li>ajouter le périphérique dans une VM</li>
  <li>rendre automatique l’attachement à Proxmox et l’ajout à la VM</li>
</ul>

<h1 id="installer-usbipd-win-sur-le-pc-windows">Installer usbipd-win sur le PC Windows</h1>
<p class="notice-note"><code class="language-shell highlighter-rouge">usbipd-win</code> utilise WSL (Windows Subsystem pour Linux), vous devez avoir WSL opérationnel, configuré et en cours d’exécution sur votre Windows</p>

<p><code class="language-shell highlighter-rouge">usbipd-win</code> est la déclinaison Windows du serveur USB-IP en utilisant WSL, vous pouvez l’installer facilement via <code class="language-shell highlighter-rouge">winget</code></p>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">winget</span><span class="w"> </span><span class="nx">install</span><span class="w"> </span><span class="nx">usbipd</span><span class="w">
</span></code></pre></div></div>
<p>ou manuellement en téléchargeant la dernière version <code class="language-shell highlighter-rouge">.msi</code> sur <a href="https://github.com/dorssel/usbipd-win">GitHub - dorssel/usbipd-win</a> et exécutant l’installateur.</p>

<p><strong>Ce que l’installation ajoute :</strong></p>
<ul>
  <li>Un service <code class="language-shell highlighter-rouge">usbipd</code> (USBIP Device Host)</li>
  <li>L’outil en ligne de commande <code class="language-shell highlighter-rouge">usbipd</code> (ajouté à PATH)</li>
  <li>Une règle de pare-feu pour autoriser les connexions locales sur TCP 3240</li>
</ul>

<blockquote>
  <p><strong>Note :</strong> Si vous utilisez un pare-feu tiers, reconfigurez pour autoriser les connexions entrantes sur <strong>TCP port 3240</strong>.</p>
</blockquote>

<h1 id="lister-et-lier-bind-le-périphérique-usb-sur-windows">Lister et lier (bind) le périphérique USB sur Windows</h1>
<h2 id="option-ligne-de-commande">Option ligne de commande</h2>
<p>Ouvrez <strong>PowerShell ou CMD en mode administrateur</strong> sur le PC Windows.</p>

<p>Pour lister les périphériques USB disponibles</p>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">usbipd</span><span class="w"> </span><span class="nx">list</span><span class="w">
</span></code></pre></div></div>

<p>Exemple de résultat :</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>BUSID DEVICE STATE
1-7 USB Input Device Not shared
4-4 STMicroelectronics STLink dongle Not shared
5-2 Surface Ethernet Adapter Not shared
</code></pre></div></div>

<p>Identifiez le <strong>BUSID</strong> de votre périphérique (ex: <code class="language-shell highlighter-rouge">4-4</code>).</p>

<p>Ensuite pour lier (bind) le périphérique au réseau</p>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">usbipd</span><span class="w"> </span><span class="nx">bind</span><span class="w"> </span><span class="nt">--busid</span><span class="o">=</span><span class="mi">4</span><span class="nt">-4</span><span class="w">
</span></code></pre></div></div>

<p>Remplacez <code class="language-shell highlighter-rouge">4-4</code> par le BUSID de votre appareil.</p>

<p>Vérifier que le périphérique est partagé</p>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">usbipd</span><span class="w"> </span><span class="nx">list</span><span class="w">
</span></code></pre></div></div>

<p>Le STATE doit maintenant être <strong>Shared</strong>.</p>

<blockquote>
  <p><strong>Important :</strong> Le <code class="language-shell highlighter-rouge"><span class="nb">bind</span></code> est <strong>persistant</strong> — il survit aux redémarrages. À faire une seule fois par périphérique.</p>
</blockquote>

<h2 id="option-gui-sous-windows---wsl-usb-manager">Option GUI sous Windows - WSL USB Manager</h2>

<p>Si vous préférez une interface graphique pour gérer les périphériques USB vers WSL, utilisez <a href="https://github.com/nickbeth/wsl-usb-manager">WSL USB Manager</a>, qui est une interface pratique et légère à utiliser.</p>

<p><img src="/files/2026/usbip_wsl_manager.png" alt="" class="img-center mw80" /></p>

<ul>
  <li>Dans l’onglet “Connected” sont listés tous les périphériques USB connectés au PC, pour exposer en USB-IP l’un d’entre eux, utiliser le bouton “Bind”</li>
  <li>La liste des périphériques “Binded” est visible dans l’onglet “Persisted”</li>
</ul>

<p>N’oubliez pas que cela utilise WSL sous le capot, donc pour que l’exposition soit active, il faut vous assurer que WSL soit bien lancé. Typiquement après un reboot Windows, dès que vous voulez exposer un device USB, ouvrez une ligne de commande WSL pour forcer.</p>

<h1 id="configuration-usbip-sur-proxmox">Configuration USB/IP sur Proxmox</h1>

<p>On va ici installer le client USB/IP directement sur Proxmox. Cela va permettre de pouvoir l’ajouter à n’importe quelle VM très simplement, qu’elle sous Windows ou Linux. Mais vous pouvez également configurer USB/IP directement dans les VMs, avec le même procédé.</p>

<h2 id="installation-des-binaires-et-modules-noyaux">Installation des binaires et modules noyaux</h2>
<p>Ouvrez une <strong>Shell</strong> sur le nœud Proxmox et installez les outils USB/IP :</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>apt update
apt <span class="nb">install </span>usbip usbip-utils
</code></pre></div></div>

<p>Chargez les modules noyaux nécessaires :</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>modprobe usbip-core
modprobe vhci-hcd
modprobe usbip-host
</code></pre></div></div>

<p>Ajoutez ces modules dans <code class="language-shell highlighter-rouge">/etc/modules</code> pour qu’ils soient chargés au démarrage :</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">echo</span> <span class="s2">"usbip-core"</span> <span class="o">&gt;&gt;</span> /etc/modules
<span class="nb">echo</span> <span class="s2">"vhci-hcd"</span> <span class="o">&gt;&gt;</span> /etc/modules
<span class="nb">echo</span> <span class="s2">"usbip-host"</span> <span class="o">&gt;&gt;</span> /etc/modules
</code></pre></div></div>

<h2 id="attacher-le-périphérique-usb-remote-à-proxmox">Attacher le périphérique USB remote à Proxmox</h2>

<p>Pour lister les périphériques distants disponibles, sur Proxmox (remplacez <code class="language-shell highlighter-rouge">IP_WINDOWS</code> par l’adresse IP ou DNS de votre PC Windows, par ex <code class="language-shell highlighter-rouge">192.168.1.50</code>) :</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>usbip list <span class="nt">--remote</span><span class="o">=</span>&lt;IP_WINDOWS&gt;
</code></pre></div></div>

<p>Exemple :</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>BUSID DEVICE STATE
4-4 STMicroelectronics STLink dongle Shared
</code></pre></div></div>

<p>Ensuite, pour attacher (attach) un des périphériques USB (Remplacez <code class="language-shell highlighter-rouge">IP_WINDOWS</code> par l’adresse IP ou DNS de votre PC Windows et <code class="language-shell highlighter-rouge">4-4</code> par le BUSID de votre appareil)</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>usbip attach <span class="nt">--remote</span><span class="o">=</span>&lt;IP_WINDOWS&gt; <span class="nt">--busid</span><span class="o">=</span>4-4
</code></pre></div></div>

<p>Vous pouvez ensuite vérifier que le périphérique est bien attaché avec <code class="language-shell highlighter-rouge">usbip port</code> et <code class="language-shell highlighter-rouge">lsusb</code>, et vous pouvez ensuite l’ajouter à une VM depuis Hardware / Add USB Device comme s’il était directement branché sur l’hôte Proxmox  (ou en ligne de commande avec <code class="language-shell highlighter-rouge">qm <span class="nb">set </span>100 <span class="nt">-usb0</span> <span class="nv">host</span><span class="o">=</span>0483:374b</code>)</p>

<h1 id="configurer-lattachement-automatique">Configurer l’attachement automatique</h1>
<p>Pour un attachement automatique du périphérique à Proxmox et à la VM dès qu’il est détecté sur le Windows distant, il faut configurer deux parties :</p>
<ul>
  <li>sur Proxmox, on va ajouter un script qui scanne régulièrement les devices exposés et disponibles, et les attache automatique à Proxmox</li>
  <li>sur la VM, on va rendre l’ajout USB dynamique pour qu’il soit ajouté dès que détecté, mais ne bloque pas le lancement de la VM</li>
</ul>

<h2 id="sur-proxmox---usbip-watchsh">Sur Proxmox - usbip-watch.sh</h2>

<p>Script <code class="language-shell highlighter-rouge">usbip-watch.sh</code> <em>(vibecodé avec Copilot)</em></p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/usr/bin/env bash</span>
<span class="nb">set</span> <span class="nt">-euo</span> pipefail

<span class="nv">SERVER</span><span class="o">=</span><span class="s2">"</span><span class="k">${</span><span class="nv">SERVER</span><span class="k">:-</span><span class="nv">votre_windows</span><span class="p">.lan</span><span class="k">}</span><span class="s2">"</span>
<span class="nv">INTERVAL</span><span class="o">=</span><span class="s2">"</span><span class="k">${</span><span class="nv">INTERVAL</span><span class="k">:-</span><span class="nv">60</span><span class="k">}</span><span class="s2">"</span>
<span class="nv">USBIP</span><span class="o">=</span><span class="s2">"</span><span class="k">${</span><span class="nv">USBIP</span><span class="k">:-</span><span class="p">/usr/sbin/usbip</span><span class="k">}</span><span class="s2">"</span>

log<span class="o">()</span> <span class="o">{</span>
  <span class="nb">echo</span> <span class="s2">"[</span><span class="si">$(</span><span class="nb">date</span> <span class="s1">'+%F %T'</span><span class="si">)</span><span class="s2">] </span><span class="nv">$*</span><span class="s2">"</span>
<span class="o">}</span>

load_modules<span class="o">()</span> <span class="o">{</span>
  modprobe usbip_core <span class="o">||</span> <span class="nb">true
  </span>modprobe vhci_hcd <span class="o">||</span> <span class="nb">true</span>
<span class="o">}</span>

remote_busids_available<span class="o">()</span> <span class="o">{</span>
  <span class="s2">"</span><span class="nv">$USBIP</span><span class="s2">"</span> list <span class="nt">--remote</span><span class="o">=</span><span class="s2">"</span><span class="nv">$SERVER</span><span class="s2">"</span> 2&gt;/dev/null <span class="se">\</span>
    | <span class="nb">sed</span> <span class="nt">-n</span> <span class="s1">'s/^[[:space:]]*-\{0,1\}[[:space:]]*\([0-9][0-9]*-[0-9.][0-9.]*\).*/\1/p'</span> <span class="se">\</span>
    | <span class="nb">sort</span> <span class="nt">-u</span>
<span class="o">}</span>

remote_busids_mounted<span class="o">()</span> <span class="o">{</span>
  <span class="s2">"</span><span class="nv">$USBIP</span><span class="s2">"</span> port 2&gt;/dev/null <span class="se">\</span>
    | <span class="nb">sed</span> <span class="nt">-n</span> <span class="s2">"s#.*usbip://</span><span class="nv">$SERVER</span><span class="s2">:3240/</span><span class="se">\(</span><span class="s2">[0-9][0-9]*-[0-9.][0-9.]*</span><span class="se">\)</span><span class="s2">.*#</span><span class="se">\1</span><span class="s2">#p"</span> <span class="se">\</span>
    | <span class="nb">sort</span> <span class="nt">-u</span>
<span class="o">}</span>

debug_state<span class="o">()</span> <span class="o">{</span>
  log <span class="s2">"Remote busids available:"</span>
  remote_busids_available | <span class="nb">sed</span> <span class="s1">'s/^/  - /'</span> <span class="o">||</span> <span class="nb">true
  </span>log <span class="s2">"Remote busids already mounted:"</span>
  remote_busids_mounted | <span class="nb">sed</span> <span class="s1">'s/^/  - /'</span> <span class="o">||</span> <span class="nb">true</span>
<span class="o">}</span>

attach_missing<span class="o">()</span> <span class="o">{</span>
  <span class="nb">mapfile</span> <span class="nt">-t</span> mounted &lt; &lt;<span class="o">(</span>remote_busids_mounted<span class="o">)</span>

  <span class="k">while </span><span class="nb">read</span> <span class="nt">-r</span> busid<span class="p">;</span> <span class="k">do</span>
    <span class="o">[[</span> <span class="nt">-z</span> <span class="s2">"</span><span class="nv">$busid</span><span class="s2">"</span> <span class="o">]]</span> <span class="o">&amp;&amp;</span> <span class="k">continue
    if </span><span class="nb">printf</span> <span class="s1">'%s\n'</span> <span class="s2">"</span><span class="k">${</span><span class="nv">mounted</span><span class="p">[@]</span><span class="k">:-}</span><span class="s2">"</span> | <span class="nb">grep</span> <span class="nt">-qx</span> <span class="s2">"</span><span class="nv">$busid</span><span class="s2">"</span><span class="p">;</span> <span class="k">then
      continue
    fi
    </span>log <span class="s2">"Attaching </span><span class="nv">$busid</span><span class="s2"> from </span><span class="nv">$SERVER</span><span class="s2">"</span>
    <span class="s2">"</span><span class="nv">$USBIP</span><span class="s2">"</span> attach <span class="nt">--remote</span><span class="o">=</span><span class="s2">"</span><span class="nv">$SERVER</span><span class="s2">"</span> <span class="nt">--busid</span><span class="o">=</span><span class="s2">"</span><span class="nv">$busid</span><span class="s2">"</span> <span class="o">||</span> <span class="nb">true
  </span><span class="k">done</span> &lt; &lt;<span class="o">(</span>remote_busids_available<span class="o">)</span>
<span class="o">}</span>

detach_disappeared<span class="o">()</span> <span class="o">{</span>
  <span class="nb">mapfile</span> <span class="nt">-t</span> mounted &lt; &lt;<span class="o">(</span>remote_busids_mounted<span class="o">)</span>
  <span class="nb">mapfile</span> <span class="nt">-t</span> available &lt; &lt;<span class="o">(</span>remote_busids_available<span class="o">)</span>

  <span class="nb">local </span>busid port
  <span class="k">for </span>busid <span class="k">in</span> <span class="s2">"</span><span class="k">${</span><span class="nv">mounted</span><span class="p">[@]</span><span class="k">:-}</span><span class="s2">"</span><span class="p">;</span> <span class="k">do
    if </span><span class="nb">printf</span> <span class="s1">'%s\n'</span> <span class="s2">"</span><span class="k">${</span><span class="nv">available</span><span class="p">[@]</span><span class="k">:-}</span><span class="s2">"</span> | <span class="nb">grep</span> <span class="nt">-qx</span> <span class="s2">"</span><span class="nv">$busid</span><span class="s2">"</span><span class="p">;</span> <span class="k">then
      continue
    fi
    </span><span class="nv">port</span><span class="o">=</span><span class="s2">"</span><span class="si">$(</span><span class="s2">"</span><span class="nv">$USBIP</span><span class="s2">"</span> port 2&gt;/dev/null | <span class="nb">sed</span> <span class="nt">-n</span> <span class="s2">"/usbip:</span><span class="se">\/\/</span><span class="nv">$SERVER</span><span class="s2">:3240</span><span class="se">\/</span><span class="nv">$busid</span><span class="s2">/,</span><span class="se">\$</span><span class="s2">p"</span> | <span class="nb">sed</span> <span class="nt">-n</span> <span class="s1">'s/^Port \([0-9][0-9]*\):.*/\1/p'</span> | <span class="nb">head</span> <span class="nt">-n1</span><span class="si">)</span><span class="s2">"</span>
    <span class="k">if</span> <span class="o">[[</span> <span class="nt">-n</span> <span class="s2">"</span><span class="k">${</span><span class="nv">port</span><span class="k">:-}</span><span class="s2">"</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then
      </span>log <span class="s2">"Detaching port </span><span class="nv">$port</span><span class="s2"> for missing remote busid </span><span class="nv">$busid</span><span class="s2">"</span>
      <span class="s2">"</span><span class="nv">$USBIP</span><span class="s2">"</span> detach <span class="nt">--port</span><span class="o">=</span><span class="s2">"</span><span class="nv">$port</span><span class="s2">"</span> <span class="o">||</span> <span class="nb">true
    </span><span class="k">fi
  done</span>
<span class="o">}</span>

log <span class="s2">"Loading kernel modules"</span>
load_modules
log <span class="s2">"Starting usbip watcher for server=</span><span class="nv">$SERVER</span><span class="s2"> interval=</span><span class="k">${</span><span class="nv">INTERVAL</span><span class="k">}</span><span class="s2">s"</span>

<span class="k">while </span><span class="nb">true</span><span class="p">;</span> <span class="k">do</span>
  <span class="c"># debug_state</span>
  attach_missing
  detach_disappeared
  <span class="nb">sleep</span> <span class="s2">"</span><span class="nv">$INTERVAL</span><span class="s2">"</span>
<span class="k">done</span>
</code></pre></div></div>

<p>et lancé au démarrage via la ligne crontab suivante (édité avec <code class="language-shell highlighter-rouge">crontab <span class="nt">-e</span></code>) :</p>
<pre><code class="language-crontab">@reboot         /usr/bin/bash /root/bin/usbip-watch.sh
</code></pre>

<p>Le script scanne régulièrement les périphériques disponibles à la fréquence <code class="language-shell highlighter-rouge">INTERVAL</code> à modifier selon votre convenance, et attache automatiquement les nouveaux périphériques exposés et détache ceux qui ne sont plus disponibles.</p>

<h2 id="sur-la-vm---en-hotplug">Sur la VM - en hotplug</h2>

<p>Il ne fait pas inclure le périphérique USB directement dans la liste du Hardware, car sinon la VM refusera de se lancer si le périphérique n’est pas trouvé. L’astuce pour la VM est d’utiliser le hotplug en ajoutant le périphérique juste après que la VM soit lancée. On va réutiliser le <code class="language-shell highlighter-rouge">hook</code> créé dans l’article <a href="/2025/09/acces-distant-vnc-avec-proxmox-qemu/">accès distant VNC avec Proxmox</a></p>

<p>On va créer le hook script dans le répertoire des snippets et le rendre exécutable : <code class="language-shell highlighter-rouge">/var/lib/vz/snippets/hook-hotplug.sh</code></p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/bin/bash</span>

<span class="c"># From Perplexity: hook script pour proxmox qui ajoute dans le qm monitor </span>
<span class="c"># après le démarrage le contenu du fichier à .hotplug.conf </span>
<span class="c"># à coté du fichier de configuration de la VM</span>

<span class="c"># Installation</span>
<span class="c"># - chmod +x </span>
<span class="c"># - copy dans /var/lib/vz/snippets</span>
<span class="c"># - ajouter dans la VM avec: qm set &lt;VMID&gt; --hookscript local:snippets/hook-hotplug.sh</span>

<span class="c"># Variables transmises par Proxmox à chaque appel hook</span>
<span class="nv">VMID</span><span class="o">=</span><span class="s2">"</span><span class="nv">$1</span><span class="s2">"</span>
<span class="nv">PHASE</span><span class="o">=</span><span class="s2">"</span><span class="nv">$2</span><span class="s2">"</span>

<span class="c"># Chemin du fichier de hotplug à appliquer si présent</span>
<span class="nv">HOTPLUG_CONF</span><span class="o">=</span><span class="s2">"/etc/pve/qemu-server/</span><span class="k">${</span><span class="nv">VMID</span><span class="k">}</span><span class="s2">.hotplug.conf"</span>

<span class="c"># Lancement après démarrage effectif</span>
<span class="k">if</span> <span class="o">[</span> <span class="s2">"</span><span class="nv">$PHASE</span><span class="s2">"</span> <span class="o">=</span> <span class="s2">"post-start"</span> <span class="o">]</span> <span class="o">&amp;&amp;</span> <span class="o">[</span> <span class="nt">-f</span> <span class="s2">"</span><span class="nv">$HOTPLUG_CONF</span><span class="s2">"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
    <span class="c"># Lire chaque ligne et l’injecter dans le monitor QEMU de la VM</span>
    <span class="k">while </span><span class="nv">IFS</span><span class="o">=</span> <span class="nb">read</span> <span class="nt">-r</span> line<span class="p">;</span> <span class="k">do</span>
        <span class="c"># On ignore les lignes vides ou commentaires (#)</span>
        <span class="o">[[</span> <span class="s2">"</span><span class="nv">$line</span><span class="s2">"</span> <span class="o">=</span>~ ^#.<span class="k">*</span><span class="nv">$ </span><span class="o">||</span> <span class="nt">-z</span> <span class="s2">"</span><span class="nv">$line</span><span class="s2">"</span> <span class="o">]]</span> <span class="o">&amp;&amp;</span> <span class="k">continue</span>
        <span class="c"># Injecte la commande dans qm monitor</span>
        <span class="nb">echo</span> <span class="s2">"</span><span class="nv">$line</span><span class="s2">"</span> | qm monitor <span class="s2">"</span><span class="nv">$VMID</span><span class="s2">"</span>
    <span class="k">done</span> &lt; <span class="s2">"</span><span class="nv">$HOTPLUG_CONF</span><span class="s2">"</span> <span class="o">&gt;&gt;</span> /var/log/hook-hotplug.log 2&gt;&amp;1
<span class="k">fi</span>
</code></pre></div></div>
<p>Et pour l’utiliser il faut ajouter dans chaque VM concernée la ligne suivante :</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>hookscript: <span class="nb">local</span>:snippets/hook-hotplug.sh  
</code></pre></div></div>

<p>Le principe de ce script va être de répondre à l’évènement <code class="language-shell highlighter-rouge">post-start</code> lancé à la fin du démarrage de la VM, de lire le fichier de configuration <code class="language-shell highlighter-rouge">&lt;vmid&gt;.hotplug.conf</code> à côté du fichier de configuration de la VM, et de l’injecter dans l’interface QEMU Monitor.</p>

<p>Pour notre cas, le fichier hotplug sera simplement :  <code class="language-shell highlighter-rouge">/etc/pve/qemu-server/&lt;vmid&gt;.hotplug.conf</code></p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>device_add usb-host,vendorid<span class="o">=</span>0x0549,productid<span class="o">=</span>0x0720,id<span class="o">=</span>remoteusb
</code></pre></div></div>

<p>Et voilà il ne reste plus qu’à redémarrer Proxmox (ou lancer manuellement le script et redémarrer la VM concernée)</p>

<h1 id="commandes-récapitulatives">Commandes récapitulatives</h1>

<table>
  <thead>
    <tr>
      <th>Action</th>
      <th>Commande Windows (admin)</th>
      <th>Commande Linux (Proxmox/VM)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Lister USB locaux</td>
      <td><code class="language-shell highlighter-rouge">usbipd list</code></td>
      <td><code class="language-shell highlighter-rouge">usbip list <span class="nt">-l</span></code></td>
    </tr>
    <tr>
      <td>Lister USB distants</td>
      <td>-</td>
      <td><code class="language-shell highlighter-rouge">usbip list <span class="nt">--remote</span><span class="o">=</span>IP_ou_DNS</code></td>
    </tr>
    <tr>
      <td>Lier (bind)</td>
      <td><code class="language-shell highlighter-rouge">usbipd <span class="nb">bind</span> <span class="nt">--busid</span><span class="o">=</span>X-X</code></td>
      <td><code class="language-shell highlighter-rouge">usbip <span class="nb">bind</span> <span class="nt">-b</span> X-X</code></td>
    </tr>
    <tr>
      <td>Attacher (attach)</td>
      <td><code class="language-shell highlighter-rouge">usbipd attach <span class="nt">--wsl</span> <span class="nt">--busid</span><span class="o">=</span>X-X</code></td>
      <td><code class="language-shell highlighter-rouge">usbip attach <span class="nt">--remote</span><span class="o">=</span>IP_ou_DNS <span class="nt">--busid</span><span class="o">=</span>X-X</code></td>
    </tr>
    <tr>
      <td>Détacher</td>
      <td><code class="language-shell highlighter-rouge">usbipd detach <span class="nt">--busid</span><span class="o">=</span>X-X</code></td>
      <td><code class="language-shell highlighter-rouge">usbip detach <span class="nt">-p</span> 0</code></td>
    </tr>
    <tr>
      <td>Vérifier attachement</td>
      <td><code class="language-shell highlighter-rouge">usbipd list</code></td>
      <td><code class="language-shell highlighter-rouge">usbip port</code></td>
    </tr>
  </tbody>
</table>]]></content><author><name>Rémi Peyronnet</name></author><category term="Informatique" /><category term="Proxmox" /><category term="USB" /><category term="Script" /><category term="IP" /><summary type="html"><![CDATA[Ce tutoriel explique comment partager un périphérique USB connecté à un PC Windows distant avec une machine virtuelle Linux sous Proxmox, en utilisant usbipd-win (serveur Windows) et le client USB/IP Linux.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://www.lprp.fr/files/2026/usbip_vignette.jpg" /><media:content medium="image" url="https://www.lprp.fr/files/2026/usbip_vignette.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry xml:lang="fr"><title type="html">Proxmox - Réduire la taille d’un disque</title><link href="https://www.lprp.fr/2026/06/proxmox-resize-disks/" rel="alternate" type="text/html" title="Proxmox - Réduire la taille d’un disque" /><published>2026-06-14T10:25:00+00:00</published><updated>2026-06-14T10:25:00+00:00</updated><id>https://www.lprp.fr/2026/06/proxmox-resize-disks</id><content type="html" xml:base="https://www.lprp.fr/2026/06/proxmox-resize-disks/"><![CDATA[<p>S’il est très facile d’augmenter la taille d’un disque virtuel sous Proxmox, l’opération inverse est nettement plus délicate. Lors de l’installation de Home Assistant, j’ai dû réduire un disque surdimensionné. Voici la procédure utilisée.</p>

<h1 id="démarrer-sur-gparted">Démarrer sur GParted</h1>

<ul>
  <li>Ajouter l’ISO <a href="https://gparted.org/livecd.php">GParted</a> en CDROM à la VM</li>
  <li>Modifier l’ordre de boot pour démarrer dessus</li>
  <li>Lancer la VM</li>
</ul>

<h1 id="réduire-la-partition">Réduire la partition</h1>

<p>Dans GParted :</p>
<ul>
  <li>Sélectionner le disque</li>
  <li>Réduire la partition à la taille souhaitée (ex: 16G)</li>
  <li>Appliquer les changements</li>
  <li>Éteindre la VM</li>
</ul>

<h1 id="réduire-le-disque-côté-proxmox">Réduire le disque côté Proxmox</h1>

<p>Sur l’hôte Proxmox :</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>lvreduce <span class="nt">-L</span> 16G /dev/mapper/pve-vm--102--disk--1
</code></pre></div></div>

<p>⚠️ Adapter le chemin du volume si nécessaire (<code class="language-shell highlighter-rouge">vm--ID--disk--X</code>).</p>

<h1 id="réparer-la-table-de-partition">Réparer la table de partition</h1>

<p>Toujours sur l’hôte :</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gdisk /dev/mapper/pve-vm--102--disk--1
</code></pre></div></div>

<p>Ensuite dans <code class="language-shell highlighter-rouge">gdisk</code> :</p>

<ul>
  <li><code class="language-shell highlighter-rouge">x</code> (mode expert)</li>
  <li><code class="language-shell highlighter-rouge">e</code> (réparer la table GPT)</li>
  <li><code class="language-shell highlighter-rouge">m</code> (retour menu principal)</li>
  <li><code class="language-shell highlighter-rouge">p</code> (vérifier)</li>
  <li><code class="language-shell highlighter-rouge">w</code> (écrire)</li>
  <li><code class="language-shell highlighter-rouge">q</code> (quitter)</li>
</ul>

<h1 id="redémarrer-la-vm">Redémarrer la VM</h1>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>qm start 102
</code></pre></div></div>

<p>Dans  Home Assistant, certains plugins peuvent échouer après modification du disque. Si c’est le cas désactiver temporairement SSL  ou reconfigurer correctement les certificats</p>

<h1 id="références">Références</h1>

<ul>
  <li>https://forum.proxmox.com/threads/decrease-a-vm-disk-size.122430/</li>
</ul>]]></content><author><name>Rémi Peyronnet</name></author><category term="Informatique" /><category term="Proxmox" /><category term="Shell" /><category term="Partition" /><category term="Home Assistant" /><summary type="html"><![CDATA[S’il est très facile d’augmenter la taille d’un disque virtuel sous Proxmox, l’opération inverse est nettement plus délicate. Lors de l’installation de Home Assistant, j’ai dû réduire un disque surdimensionné. Voici la procédure utilisée.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://www.lprp.fr/files/2026/proxmox_disk_reduce.jpg" /><media:content medium="image" url="https://www.lprp.fr/files/2026/proxmox_disk_reduce.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry xml:lang="fr"><title type="html">Optimiser la taille de la base de données de Home Assistant</title><link href="https://www.lprp.fr/2026/05/optimiser-la-taille-de-la-base-de-donnees-de-home-assistant/" rel="alternate" type="text/html" title="Optimiser la taille de la base de données de Home Assistant" /><published>2026-05-25T16:56:52+00:00</published><updated>2026-05-25T16:56:52+00:00</updated><id>https://www.lprp.fr/2026/05/optimiser-la-taille-de-la-base-de-donnees-de-home-assistant</id><content type="html" xml:base="https://www.lprp.fr/2026/05/optimiser-la-taille-de-la-base-de-donnees-de-home-assistant/"><![CDATA[<p>Si on ne fait pas attention, la base de données Home Assistant peut rapidement devenir obèse avec plusieurs centaines de méga-octets. Il est probable qu’elle stocke beaucoup d’information qui n’est pas utile à conserver. Voici quelques trucs pour identifier et réduire à ce qui vous intéresse.</p>

<h1 id="comprendre-ce-qui-est-stocké">Comprendre ce qui est stocké</h1>

<p>Il y a plusieurs espaces de stockage sous Home Assistant:</p>
<ul>
  <li>la configuration de vos entités et intégration gérée par Home Assistant, sous forme d’une collection de fichiers json dans <code class="language-shell highlighter-rouge">/config/.storage</code> (en complément des éventuels yaml de configuration dans <code class="language-shell highlighter-rouge">/config</code>)</li>
  <li>l’historique des états de vos entités, gérée par le <code class="language-shell highlighter-rouge">Recorder</code> de Home Assistant ; par défaut il va stocker dans une base SQLite dans le fichier <code class="language-shell highlighter-rouge">/config/home-assistant_v2.db</code> (d’autres bases sont supportées, ou des recorder spécialisées, mais c’est en dehors du scope de cet article). Ce recorder stocke plusieurs choses :
    <ul>
      <li>l’historique des états de vos entités (table <code class="language-shell highlighter-rouge">states</code>)</li>
      <li>lorsque les états ont des attributs, ceux-ci sont stockés dans une autre table <code class="language-shell highlighter-rouge">states_attributes</code> ; une nouvelle entrée n’est créée que lorsque les attributs changent pour optimiser le stockage ; c’est donc rarement un problème d’avoir de nombreux attributs lorsqu’ils sont tous stables, mais si un seul change (comme un simple timestamp) et c’est la porte ouverte à une forte inflation</li>
      <li>ces états et attributs sont stockés seulement quelque jours, souvent 10 jours, passé ce délai les états et attributs sont supprimés, et suivant le type de capteur seuls des statistiques sont conservées, par exemple pour les sensors une valeur moyenne, min et max sue chaque période. Il y a des statistiques court terme (avec une granularité 5mn pour un historique très détaillé) et long terme (agrégées à l’heure).</li>
    </ul>
  </li>
</ul>

<p>Les références pour tout comprendre :</p>
<ul>
  <li>la <a href="https://www.home-assistant.io/integrations/recorder/">documentation officielle Home Assistant du Recorder</a></li>
  <li>le <a href="https://www.hacf.fr/bd-recorder-statistiques/">tutoriel sur HACF</a></li>
</ul>

<p>Et pour suivre la taille de votre base, je vous conseille d’ajouter une entité <code class="language-shell highlighter-rouge">File Size</code> qui pointe sur votre fichier de base de données:</p>

<p><img src="/files/2026/dbusage_entity.jpg" alt="" class="img-center mw80" /></p>

<h1 id="laddon-dbstats">L’addon dbstats</h1>

<p>Il existe un <a href="https://github.com/jehy/hass-dbstats">addon dbstats</a> qui permet de faciliter la compréhension du contenu de la base. Cet addon est l’inspiration pour la suite de cet article, et déjà très utile, mais présente quelques inconvénients :</p>
<ul>
  <li>les graphiques sont des images, il n’est pas possible de copier les identifiants des libellés pour les exclure du recorder</li>
  <li>les tables s’arrêtent aux premières valeurs</li>
  <li>les tables présentent souvent un nombre de lignes sans qu’il soit possible de se rendre compte de la taille occupée</li>
  <li>les tables présentent des éléments unitaires, mais il manque une vision de synthèse</li>
</ul>

<h1 id="des-requêtes-custom-utiles">Des requêtes custom utiles</h1>

<p>J’ai donc créé quelques requêtes SQL pour me fournir une vision agrégée, à copier coller dans l’addon <code class="language-shell highlighter-rouge">SQLite Web</code></p>

<p><details>
    <summary>Une requête pour voir la synthèse des états et attributs</summary>

    <div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span> 
  <span class="n">entity</span><span class="p">,</span>
  <span class="k">SUM</span><span class="p">(</span><span class="n">states_count</span><span class="p">)</span> <span class="k">AS</span> <span class="n">states_count</span><span class="p">,</span>
  <span class="k">SUM</span><span class="p">(</span><span class="n">attributes_count</span><span class="p">)</span> <span class="k">AS</span> <span class="n">attributes_count</span><span class="p">,</span>
  <span class="k">SUM</span><span class="p">(</span><span class="n">attributes_size</span><span class="p">)</span> <span class="k">AS</span> <span class="n">attributes_size</span>
<span class="k">FROM</span> <span class="p">(</span>
  <span class="c1">-- Requête 1 : Nombre d'états par entité</span>
  <span class="k">SELECT</span> 
    <span class="n">sm</span><span class="p">.</span><span class="n">entity_id</span> <span class="k">AS</span> <span class="n">entity</span><span class="p">,</span> 
    <span class="k">COUNT</span><span class="p">(</span><span class="o">*</span><span class="p">)</span> <span class="k">AS</span> <span class="n">states_count</span><span class="p">,</span>
    <span class="mi">0</span> <span class="k">AS</span> <span class="n">attributes_count</span><span class="p">,</span>
    <span class="mi">0</span> <span class="k">AS</span> <span class="n">attributes_size</span>
  <span class="k">FROM</span> <span class="n">states</span> <span class="n">s</span>
  <span class="k">JOIN</span> <span class="n">states_meta</span> <span class="n">sm</span> <span class="k">ON</span> <span class="n">s</span><span class="p">.</span><span class="n">metadata_id</span> <span class="o">=</span> <span class="n">sm</span><span class="p">.</span><span class="n">metadata_id</span>
  <span class="k">WHERE</span> <span class="n">s</span><span class="p">.</span><span class="n">last_updated_ts</span> <span class="o">&gt;=</span> <span class="p">(</span><span class="n">strftime</span><span class="p">(</span><span class="s1">'%s'</span><span class="p">,</span> <span class="s1">'now'</span><span class="p">)</span> <span class="o">-</span> <span class="mi">24</span><span class="o">*</span><span class="mi">60</span><span class="o">*</span><span class="mi">60</span><span class="p">)</span>
  <span class="k">GROUP</span> <span class="k">BY</span> <span class="n">sm</span><span class="p">.</span><span class="n">entity_id</span>
  
  <span class="k">UNION</span> <span class="k">ALL</span>
  
  <span class="c1">-- Requête 2 : Attributs par entité </span>
  <span class="k">SELECT</span> 
    <span class="n">sm</span><span class="p">.</span><span class="n">entity_id</span> <span class="k">AS</span> <span class="n">entity</span><span class="p">,</span>
    <span class="mi">0</span> <span class="k">AS</span> <span class="n">states_count</span><span class="p">,</span>
    <span class="k">COUNT</span><span class="p">(</span><span class="o">*</span><span class="p">)</span> <span class="k">AS</span> <span class="n">attributes_count</span><span class="p">,</span>
    <span class="k">SUM</span><span class="p">(</span><span class="k">LENGTH</span><span class="p">(</span><span class="n">sa</span><span class="p">.</span><span class="n">shared_attrs</span><span class="p">))</span> <span class="k">AS</span> <span class="n">attributes_size</span>
  <span class="k">FROM</span> <span class="n">states</span> <span class="n">s</span>
  <span class="k">JOIN</span> <span class="n">states_meta</span> <span class="n">sm</span> <span class="k">ON</span> <span class="n">s</span><span class="p">.</span><span class="n">metadata_id</span> <span class="o">=</span> <span class="n">sm</span><span class="p">.</span><span class="n">metadata_id</span>
  <span class="k">JOIN</span> <span class="n">state_attributes</span> <span class="n">sa</span> <span class="k">ON</span> <span class="n">sa</span><span class="p">.</span><span class="n">attributes_id</span> <span class="o">=</span> <span class="n">s</span><span class="p">.</span><span class="n">attributes_id</span>
  <span class="k">WHERE</span> <span class="n">s</span><span class="p">.</span><span class="n">last_updated_ts</span> <span class="o">&gt;=</span> <span class="p">(</span><span class="n">strftime</span><span class="p">(</span><span class="s1">'%s'</span><span class="p">,</span> <span class="s1">'now'</span><span class="p">)</span> <span class="o">-</span> <span class="mi">24</span><span class="o">*</span><span class="mi">60</span><span class="o">*</span><span class="mi">60</span><span class="p">)</span>
  <span class="k">GROUP</span> <span class="k">BY</span> <span class="n">sm</span><span class="p">.</span><span class="n">entity_id</span>
<span class="p">)</span> <span class="k">AS</span> <span class="n">requetes_combinees</span>
<span class="k">GROUP</span> <span class="k">BY</span> <span class="n">entity</span>
<span class="k">ORDER</span> <span class="k">BY</span> <span class="n">states_count</span> <span class="o">+</span> <span class="n">attributes_count</span> <span class="k">DESC</span><span class="p">;</span>
</code></pre></div>    </div>

  </details></p>
<p><details>
    <summary>Une requête pour voir la taille des tables et index</summary>

    <div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- See what tables/indexes are taking space</span>
<span class="k">SELECT</span> <span class="n">name</span><span class="p">,</span> <span class="k">sum</span><span class="p">(</span><span class="n">pgsize</span><span class="p">)</span><span class="o">/</span><span class="mi">1024</span><span class="o">/</span><span class="mi">1024</span> <span class="k">as</span> <span class="k">size</span>
<span class="k">FROM</span> <span class="n">dbstat</span>
<span class="k">GROUP</span> <span class="k">BY</span> <span class="n">name</span>
<span class="k">ORDER</span> <span class="k">BY</span> <span class="k">size</span> <span class="k">DESC</span>
</code></pre></div>    </div>

  </details></p>
<p><details>
    <summary>Une requête pour voir les entités avec de nombreux états</summary>

    <div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- List entities with lot of states</span>
<span class="k">SELECT</span> <span class="n">sm</span><span class="p">.</span><span class="n">entity_id</span> <span class="k">as</span> <span class="n">entity</span><span class="p">,</span> <span class="k">count</span><span class="p">(</span><span class="o">*</span><span class="p">)</span> <span class="k">as</span> <span class="k">count</span>
<span class="k">FROM</span> <span class="n">states</span> <span class="n">s</span><span class="p">,</span> <span class="n">states_meta</span> <span class="n">sm</span>
<span class="k">WHERE</span> <span class="n">s</span><span class="p">.</span><span class="n">metadata_id</span> <span class="o">=</span> <span class="n">sm</span><span class="p">.</span><span class="n">metadata_id</span>
<span class="k">GROUP</span> <span class="k">BY</span> <span class="n">entity</span>
<span class="k">ORDER</span> <span class="k">BY</span> <span class="k">count</span> <span class="k">DESC</span>
</code></pre></div>    </div>

  </details></p>
<p><details>
    <summary>Une requête pour voir la taille des attributs</summary>

    <div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- Get size of state attributes by entities</span>
<span class="k">SELECT</span> <span class="n">sm</span><span class="p">.</span><span class="n">entity_id</span> <span class="k">as</span> <span class="n">entity</span><span class="p">,</span> <span class="k">sum</span><span class="p">(</span><span class="k">length</span><span class="p">(</span><span class="n">sa</span><span class="p">.</span><span class="n">shared_attrs</span><span class="p">))</span> <span class="k">as</span> <span class="k">count</span>
<span class="k">FROM</span> <span class="n">states</span> <span class="n">s</span><span class="p">,</span> <span class="n">states_meta</span> <span class="n">sm</span><span class="p">,</span> <span class="n">state_attributes</span> <span class="n">sa</span>
<span class="k">WHERE</span> <span class="n">s</span><span class="p">.</span><span class="n">metadata_id</span> <span class="o">=</span> <span class="n">sm</span><span class="p">.</span><span class="n">metadata_id</span>
<span class="k">AND</span> <span class="n">sa</span><span class="p">.</span><span class="n">attributes_id</span> <span class="o">=</span> <span class="n">s</span><span class="p">.</span><span class="n">attributes_id</span>
<span class="k">GROUP</span> <span class="k">BY</span> <span class="n">entity</span>
<span class="k">ORDER</span> <span class="k">BY</span> <span class="k">count</span> <span class="k">DESC</span>
</code></pre></div>    </div>
    <p>Cette requête n’est pas très exacte, car elle peut compter plusieurs fois un même groupe d’attributs, on verra plus tard comment compter de manière plus exacte.</p>

  </details></p>
<p><details>
    <summary>Une requête pour voir les attributs d'une entité pour comprendre ce qui est stocké</summary>

    <div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- Inspect attributes of an entity</span>
<span class="k">SELECT</span> <span class="k">DISTINCT</span> <span class="n">sm</span><span class="p">.</span><span class="n">entity_id</span> <span class="k">as</span> <span class="n">entity</span><span class="p">,</span> <span class="n">sa</span><span class="p">.</span><span class="n">attributes_id</span><span class="p">,</span> <span class="n">sa</span><span class="p">.</span><span class="n">shared_attrs</span>
<span class="k">FROM</span> <span class="n">states</span> <span class="n">s</span><span class="p">,</span> <span class="n">states_meta</span> <span class="n">sm</span><span class="p">,</span> <span class="n">state_attributes</span> <span class="n">sa</span>
<span class="k">WHERE</span> <span class="n">s</span><span class="p">.</span><span class="n">metadata_id</span> <span class="o">=</span> <span class="n">sm</span><span class="p">.</span><span class="n">metadata_id</span>
<span class="k">AND</span> <span class="n">sa</span><span class="p">.</span><span class="n">attributes_id</span> <span class="o">=</span> <span class="n">s</span><span class="p">.</span><span class="n">attributes_id</span>
<span class="k">AND</span> <span class="n">sm</span><span class="p">.</span><span class="n">entity_id</span> <span class="o">=</span> <span class="nv">"device_tracker.capteur_fuite_eau"</span>
</code></pre></div>    </div>

  </details></p>

<h1 id="retex-du-vibe-coding-dun-addon">RETEX du vibe-coding d’un addon</h1>

<p>Après avoir itéré plusieurs voir sur ma base avec les requètes manuelles ci-dessus, ça m’a donné envie d’avoir quelque chose d’un peu plus industriel, dont la première expression de fonctionnalité a été</p>
<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>1er tableau: Liste des tables : Mo (est.)
2ème tableau: Liste des entités : nombre / attributs Mo (est.)
- si clic sur attributs, affichage des 10 derniers attributs
- si clic sur nombre, lien page historique
- si clic sur entité, lien page entité
</code></pre></div></div>

<p>Par ailleurs comme je n’avais encore jamais fait d’addon Home Assistant, et que j’étais en train d’expérimenter les possibilités de Github Copilot (avec simplement l’offre gratuite), je me suis dit que c’était une bonne occasion de vibe-coder l’application à partir de cette liste de requirements et des SQL ci-dessus, que j’ai un peu plus étoffée et ajouté quelques contraintes techniques pour éviter des frameworks et fichiers trop complexes.  La première génération est assez bluffante, car on arrive littéralement à avoir en quelques minutes une première application qui ressemble à quelque chose, et quasi fonctionnelle. Comme je n’avais pas défini de tests, il y avait cependant quelques erreurs de calcul que j’ai du lui demande de refaire. Et c’est assez addictif, on se retrouve très facilement à ajouter des fonctionnalités, et itérer pour avoir exactement ce que l’on veut. Au fur et à mesure, le code perd en simplicité et en cohérence, car Github Copilot va plus facilement générer du spécifique que du générique réutilisé, ou pas forcément comme on l’aurait fait. J’imagine qu’il faudrait que je lui fasse faire des tests et réécrire intégralement sur la base des requirements actualisés et tests, mais c’est la limite de l’offre gratuite Github Copilot et ses quotas un peu bas. Bref, une première expérience de vibe-coding intéressante, je ne me serais sans doute pas lancé dans cet addon sans ça.</p>

<p>Et sinon <a href="https://developers.home-assistant.io/docs/apps/configuration/">la documentation officielle pour développer un addon/app HomeAssistant</a></p>

<h1 id="laddon-sqlite-ha-db-usage">L’addon SQLite HA DB Usage</h1>
<p>Le résultat est disponible dans ce repo <a href="https://github.com/rpeyron/ha-dbusage">github.com/rpeyron/ha-dbusage</a>. Il suffit de suivre les instructions d’installation sur la page.</p>

<p><img src="/files/2026/dbusage_entities.jpg" alt="" class="img-center mw80" /></p>

<h1 id="le-grand-ménage">Le grand ménage</h1>
<p>Cet addon vous permet maintenant de bien identifier les sources de ce qui prend de la place, et:</p>
<ul>
  <li>soit de désactiver les extensions qui ne vous seraient plus utiles mais génère beaucoup d’entités ou de grande tailles</li>
  <li>soit d’exclure ce qui prend de la place et qui n’est pas nécessaire à conserver</li>
</ul>

<p>A noter qu’à ce jour le paramétrage du recorder est assez basique, et qu’il n’est pas possible d’exclure simplement toute une extension, ou seulement le stockage long terme, ou d’avoir une durée de rétention différente par entités / type d’entité. Egalement, un petit piège, dès qu’on utilise <code class="language-shell highlighter-rouge">include</code> alors l’enregistrement par défaut disparait et seules les entités inclues sont enregistrées. Il n’est donc pas possible d’utiliser un <code class="language-shell highlighter-rouge">exclude_globs</code> et un <code class="language-shell highlighter-rouge">include</code> pour réinclure une entité qui aurait été exclue. Vous pouvez bien sûr choisir de n’inclure que les entités que vous souhaitez, mais c’est facile d’oublier donc j’ai préféré conserver l’enregistrement par défaut et des exclusions.</p>

<p>L’usage du plugin permet de selectionner les entités et de générer le code yaml de configuration à compier coller dans <code class="language-shell highlighter-rouge">configuration.yaml</code>.</p>

<p>Pour ma part je généralise souvent les entités trouvées dans des globs sur les patterns associés ; par exemple un extrait de ma configuration:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">recorder</span><span class="pi">:</span>
  <span class="na">exclude</span><span class="pi">:</span>
    <span class="na">entity_globs</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">media_player.*</span>
      <span class="pi">-</span> <span class="s">camera.*_stream</span>
      <span class="pi">-</span> <span class="s">sensor.*_uptime*</span>
</code></pre></div></div>

<p><details>
    <summary>et d'autres exemples</summary>

    <div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>      <span class="c1"># Portainer</span>
      <span class="pi">-</span> <span class="s">sensor.*_cpu_usage_total*</span>
      <span class="pi">-</span> <span class="s">sensor.*_memory_limit*</span>
      <span class="pi">-</span> <span class="s">sensor.*_memory_usage*</span>
      <span class="pi">-</span> <span class="s">sensor.*_memory_usage_percentage*</span>
      <span class="pi">-</span> <span class="s">sensor.*_du_processeur*</span>
      <span class="pi">-</span> <span class="s">sensor.*_du_disque*</span>
      <span class="pi">-</span> <span class="s">sensor.*_de_la_memoire*</span>
      <span class="pi">-</span> <span class="s">sensor.*_image</span>
      <span class="c1"># ZHA</span>
      <span class="pi">-</span> <span class="s">sensor.*_rssi</span>
      <span class="pi">-</span> <span class="s">sensor.*_niveau_de_signal</span>
      <span class="pi">-</span> <span class="s">sensor.*_battery</span>
      <span class="pi">-</span> <span class="s">sensor.*_battery_level</span>
      <span class="pi">-</span> <span class="s">sensor.*_batterie</span>
      <span class="c1"># Proxmox</span>
      <span class="pi">-</span> <span class="s">sensor.*_utilisation_maximale_du_disque</span>
      <span class="pi">-</span> <span class="s">sensor.*_utilisation_maximale_de_la_memoire</span>
      <span class="pi">-</span> <span class="s">sensor.*_backup_duration</span>
      <span class="pi">-</span> <span class="s">sensor.storage_*_usage_percentage</span>
      <span class="pi">-</span> <span class="s">sensor.storage_*_storage</span>
      <span class="pi">-</span> <span class="s">sensor.storage_*_available_storage</span>
      <span class="pi">-</span> <span class="s">sensor.storage_*_total_storage</span>
      <span class="pi">-</span> <span class="s">sensor.*_network_input</span>
      <span class="pi">-</span> <span class="s">sensor.*_network_output</span>
</code></pre></div>    </div>
    <p>À noter la très agaçante traduction des identifiants d’entités, qui n’est pas non plus consistante, il faut souvent déclarer les patterns dans les deux langues.</p>

  </details></p>

<p>Pour aller jusqu’au bout du cleanup, il faut quelques étapes manuelles, pour d’une part purger les entités, et d’autre part supprimer également les statistiques.</p>

<p>Pour prendre en compte vos nouvelles exclusions, soit vous êtes patients et vous attendez 15 jours que les purges automatiques fasse le travail, soit vous le faites manuellement, via les outils de développement et l’action <code class="language-shell highlighter-rouge">recorder.purge</code> ; il faut bien cocher sur les 2 cases à cocher appliquer le filtre (supprimer directement vos nouvelles exclusions), et le reconditionnement (ce qui va réécrire le fichier SQLite pour le réduire à la taille minimum des données utilisées)</p>

<p><img src="/files/2026/dbusage_ha_purge.jpg" alt="" class="img-center mw80" /></p>

<p>Enfin, la purge, qu’elle soit manuelle ou automatique, ne supprime pas les statistiques. Pour ce faire, on va utiliser le menu Statistiques dans les outils de développements, et sélectionner toutes les entités avec “problèmes”. Comme la purge supprime les états des entités, elles vont automatiquement apparaitre ici. Bien relire la liste des entités sélectionner pour vérifier qu’il n’y a pas d’autres entités avec des problèmes d’intégration passagers et qu’on ne voudrait pas supprimer.</p>

<p><img src="/files/2026/dbusage_ha_stats.jpg" alt="" class="img-center mw80" /></p>

<h1 id="résultat-une-base-divisée-par-cinq-">Résultat, une base divisée par cinq !</h1>

<p>Au fur et à mesure de mes optimisations, installations de nouvelles intégrations, nouvelles optimisations… j’ai pu diviser la taille de ma base de donnée par 5 !</p>

<p><img src="/files/2026/dbusage_history.jpg" alt="" class="img-center mw80" /></p>

<p>Au quotidien ça ne change pas grand-chose, mais c’est notamment appréciable pour les sauvegardes multiples qui avaient fini par saturer le disque.</p>

<h1 id="bonus--sql-pour-fusionner-lhistorique-de-deux-capteurs">Bonus : SQL pour fusionner l’historique de deux capteurs</h1>
<p>La meilleure solution pour fusionner l’historique est de renommer l’entité du nouveau capteur en celui de l’ancien capteur via le paramétrage de l’entité. Cela permettra à cette nouvelle entité d’hériter automatiquement de l’historique de l’entité précédente. Si toutefois ce n’est pas possible, typiquement pour celles générées automatiquement depuis des intégrations type MQTT, alors un petit script SQL peut résoudre le problème.</p>

<p>Attention, le script correspond à l’usage dont j’avais besoin, sur les statistiques long terme, mais ne couvre pas toutes les tables de la base de données. À utiliser avec précautions !!  Notamment il faut que les plages de dates des statistiques à merger soient disjointes. Si ce n’est pas le cas, vous pouvez supprimer la partie de redondance avant le merge.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- Step 1: identifier les statistics_id source et cible (et via l'IHM)</span>
<span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="nv">"statistics_meta"</span> <span class="k">WHERE</span> <span class="n">statistic_id</span> <span class="k">LIKE</span> <span class="s1">'%puissance%'</span>

<span class="c1">-- Step 2: vérifier</span>
<span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">states</span> 
<span class="k">WHERE</span> <span class="n">last_updated_ts</span> <span class="k">BETWEEN</span> <span class="n">strftime</span><span class="p">(</span><span class="s1">'%s'</span><span class="p">,</span> <span class="s1">'2025-12-07 19:00:00'</span><span class="p">)</span> <span class="k">AND</span> <span class="n">strftime</span><span class="p">(</span><span class="s1">'%s'</span><span class="p">,</span><span class="s1">'2026-01-24 17:22:00'</span><span class="p">)</span>
<span class="k">AND</span> <span class="n">metadata_id</span> <span class="o">=</span> <span class="mi">895</span>

<span class="c1">-- Step 3: changer l'identifiant</span>
<span class="k">UPDATE</span> <span class="n">states</span> <span class="k">SET</span> <span class="n">metadata_id</span> <span class="o">=</span> <span class="o">&lt;</span><span class="n">id_cible</span><span class="o">&gt;</span>
<span class="k">WHERE</span> <span class="n">last_updated_ts</span> <span class="k">BETWEEN</span> <span class="n">strftime</span><span class="p">(</span><span class="s1">'%s'</span><span class="p">,</span> <span class="s1">'2025-12-07 19:00:00'</span><span class="p">)</span> <span class="k">AND</span> <span class="n">strftime</span><span class="p">(</span><span class="s1">'%s'</span><span class="p">,</span><span class="s1">'2026-01-24 17:22:00'</span><span class="p">)</span>
<span class="k">AND</span> <span class="n">metadata_id</span> <span class="o">=</span> <span class="o">&lt;</span><span class="n">id_source</span><span class="o">&gt;</span>
</code></pre></div></div>]]></content><author><name>Rémi Peyronnet</name></author><category term="Domotique" /><category term="Home Assistant" /><category term="Plugin" /><category term="GitHub" /><category term="Copilot" /><summary type="html"><![CDATA[Si on ne fait pas attention, la base de données Home Assistant peut rapidement devenir obèse avec plusieurs centaines de méga-octets. Il est probable qu’elle stocke beaucoup d’information qui n’est pas utile à conserver. Voici quelques trucs pour identifier et réduire à ce qui vous intéresse.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://www.lprp.fr/files/2026/dbusage_post.jpg" /><media:content medium="image" url="https://www.lprp.fr/files/2026/dbusage_post.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry xml:lang="fr"><title type="html">Un écran tactile autonome Home Assistant avec un Raspberry Pi Zero 1</title><link href="https://www.lprp.fr/2026/04/un-ecran-tactile-autonome-home-assistant-avec-un-raspberry-pi-zero-1/" rel="alternate" type="text/html" title="Un écran tactile autonome Home Assistant avec un Raspberry Pi Zero 1" /><published>2026-04-18T15:23:00+00:00</published><updated>2026-04-18T15:23:00+00:00</updated><id>https://www.lprp.fr/2026/04/un-ecran-tactile-autonome-home-assistant-avec-un-raspberry-pi-zero-1</id><content type="html" xml:base="https://www.lprp.fr/2026/04/un-ecran-tactile-autonome-home-assistant-avec-un-raspberry-pi-zero-1/"><![CDATA[<p>Je souhaitais un moniteur Home Assistant indépendant et tactile. Il existe de nombreuses méthodes plus ou moins simples. Les méthodes qui semblent les plus simples passent par l’utilisation d’une tablette Android et d’un navigateur en mode Kiosque (<a href="https://www.domo-blog.fr/pilotage-home-assistant-avec-tablette-mur-blackview-tab90/">exemple sur cet article avec la tablette Blackview Tab90, 140€ pour 11 pouces</a>). C’est simple à mettre en place, mais c’est clairement un budget.</p>

<p>La vieille tablette Android peut aussi être une option, mais je souhaite également fonctionner uniquement avec des logiciels mis à jour, même si ce choix pourrait être discutable pour un usage réduit à un réseau local.</p>

<h1 id="matériel-utilisé">Matériel utilisé</h1>

<p>J’ai voulu suivre l’approche inverse, en partant de ce que j’avais à disposition :</p>
<ul>
  <li>Un écran tactile 14” acheté 31€ en promo sur <a href="https://fr.aliexpress.com/item/1005009341035178.html?spm=a2g0o.order_list.order_list_main.41.57765e5bbIXhVr&amp;gatewayAdapt=glo2fra">AliExpress</a></li>
  <li>Un raspberry Pi Zero 1 acheté il y a quelques années 8 euros, et que je n’utilise plus depuis quelque temps, ayant upgradé depuis vers un Zero 2WH</li>
  <li>Une clé USB Wifi compatible Linux</li>
  <li>Une alimentation USB suffisante pour alimenter le Raspberry Pi et l’écran en USB Type C ; un seul fil à brancher pour allumer le tout.</li>
</ul>

<p>J’ai complété avec la connectique adaptée pour que ce soit pratique à utiliser et suffisamment compact. Le Raspberry Pi Zero est connecté en HDMI à l’écran, alimenté avec un port USB, et l’autre port USB est relié à un Hub USB pour brancher la clé USB Wifi et le volet tactile de l’écran aussi en USB. Pendant le setup, j’ai également branché un clavier et souris sans fil pour faciliter.</p>

<h1 id="incompatibilité-logicielle">Incompatibilité logicielle</h1>

<p>Dans l’absolu, il suffit faire en sorte que le Pi lance un navigateur en plein écran sur la bonne adresse au démarrage. Cependant l’usage du Pi Zero 1 s’est avéré être une grosse contrainte, car le processeur utilisé, sur ARMv6, ne comporte pas l’unité NEON (Advanced SIMD) et Chromium a supprimé la compatibilité avec cette ancienne architecture. Depuis 2023 il n’est donc plus possible d’utiliser Chromium sur Pi Zero 1. J’ai essayé les navigateurs alternatifs disponibles pour Raspberry Pi, mais Home Assistant nécessitant une bonne compatibilité sur les technologies web modernes, aucun n’a réussi à faire fonctionner Home Assistant.</p>

<p>J’ai donc opté pour une approche un peu plus compliquée avec un partage d’écran en VNC, mais qui répond à l’ensemble de mes contraintes : le Raspberry Pi Zero 1 lance en mode kiosque un viewer VNC vers un serveur VNC en docker dont la seule fonction est d’afficher un Chromium sur la bonne page en plein écran</p>

<h1 id="mise-en-place">Mise en place</h1>
<h2 id="stack-docker">Stack docker</h2>
<p>Je suis parti du container <a href="https://hub.docker.com/r/mrcolorrain/vnc-browser">mrcolorrain/vnc-browser</a> qui correspond à mon usage et semble bien maintenu.  La stack correspondante est :</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">version</span><span class="pi">:</span> <span class="s2">"</span><span class="s">3.9"</span>
<span class="na">services</span><span class="pi">:</span>
  <span class="na">vnc-browser</span><span class="pi">:</span>
    <span class="na">container_name</span><span class="pi">:</span> <span class="s">vnc-homeassistant</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">mrcolorrain/vnc-browser:debian</span>
    <span class="na">restart</span><span class="pi">:</span> <span class="s">unless-stopped</span>
    <span class="na">ports</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">5900:5900"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">6080:6080"</span>
    <span class="na">environment</span><span class="pi">:</span>
      <span class="na">VNC_PASSWORD</span><span class="pi">:</span> <span class="s2">"</span><span class="s">YOUR_PASSWORD"</span>
      <span class="na">VNC_RESOLUTION</span><span class="pi">:</span> <span class="s2">"</span><span class="s">1920x1200"</span>
      <span class="na">STARTING_WEBSITE_URL</span><span class="pi">:</span> <span class="s2">"</span><span class="s">https://homeassistant.lan/dashboard-home/0"</span>
      <span class="na">LANG</span><span class="pi">:</span> <span class="s">fr_FR.UTF-8</span>
      <span class="na">LC_ALL</span><span class="pi">:</span> <span class="s">fr_FR</span>
      <span class="na">AUTO_START_BROWSER</span><span class="pi">:</span> <span class="no">true</span>
      <span class="na">AUTO_START_XTERM</span><span class="pi">:</span> <span class="no">true</span>
      <span class="na">BROWSER_OPTIONS</span><span class="pi">:</span> <span class="s2">"</span><span class="s">--start-fullscreen</span><span class="nv"> </span><span class="s">--touch-events=enabled</span><span class="nv">  </span><span class="s">--force-dark-mode</span><span class="nv"> </span><span class="s">--enable-features=WebUIDarkMode</span><span class="nv"> </span><span class="s">--test-type=ui"</span>
      <span class="c1">#CUSTOMIZE: true</span>
    <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">vnc_ha_chrome_data:/root/.config/chromium/Default</span>
      <span class="pi">-</span> <span class="s">vnc_ha_pki:/root/.locale/share/pki</span>
<span class="na">volumes</span><span class="pi">:</span>
  <span class="na">vnc_ha_chrome_data</span><span class="pi">:</span>
  <span class="na">vnc_ha_pki</span><span class="pi">:</span>
</code></pre></div></div>

<p>Ici un volume est déclaré pour les données Chromium et un autre pour la partie pki (utile par exemple si vous ajoutez une autorité de certification locale dans Chromium par exemple).</p>

<h2 id="client-vnc">Client VNC</h2>

<p>J’ai opté pour TigerVNC qui est maintenant le défaut du paquet <code class="language-shell highlighter-rouge">xvnc4viewer</code></p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>apt <span class="nb">install </span>tigervnc-viewer tigervnc-tools
</code></pre></div></div>

<p>Puis un script pour lancer automatiquement le viewer en plein écran :</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/bin/bash</span>
<span class="nb">sleep </span>10
<span class="k">while </span><span class="nb">true</span><span class="p">;</span> <span class="k">do</span>
  <span class="c"># Use tigervncpasswd  installed by apt install tigervnc-tools</span>
  xvncviewer <span class="nt">-passwd</span> ~/.vnc/pwd_ha <span class="nt">-fullscreen</span> 192.168.x.x:5900
  <span class="nb">echo</span> <span class="s2">"Stopped, restart"</span>
  <span class="nb">sleep </span>5
<span class="k">done</span>
</code></pre></div></div>

<p>Et enfin, il faut enregistrer le mot de passe VNC avec  <code class="language-shell highlighter-rouge">vncpasswd <span class="nt">-f</span> ~/.vnc/pwd_ha</code></p>

<p>Lancez le script pour vérifier que ça fonctionne correctement.</p>

<h2 id="démarrage-automatique">Démarrage automatique</h2>

<p>On va maintenant faire en sorte que ça se lance automatiquement. Dans les réglages Raspberry (Settings / Control Center), activez la connexion automatique de l’utilisateur (Desktop Auto login). Puis on va définir un fichier de lancement automatique</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">mkdir</span> <span class="nt">-p</span> ~/.config/autostart
nano ~/.config/autostart/vnc-client.desktop
</code></pre></div></div>
<p>avec le contenu</p>
<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">[Desktop Entry]</span>
<span class="py">Type</span><span class="p">=</span><span class="s">Application</span>
<span class="py">Name</span><span class="p">=</span><span class="s">VNC Client Fullscreen</span>
<span class="py">Exec</span><span class="p">=</span><span class="s">/home/remi/start_vnc.sh</span>
<span class="py">Hidden</span><span class="p">=</span><span class="s">false</span>
<span class="py">NoDisplay</span><span class="p">=</span><span class="s">false</span>
<span class="py">X-GNOME-Autostart-enabled</span><span class="p">=</span><span class="s">true</span>
</code></pre></div></div>

<h2 id="finalisation">Finalisation</h2>

<p>Je vous conseille également :</p>
<ul>
  <li>d’activer VNC : vous pourrez ensuite vous connecter depuis votre PC sur le Raspberry, lui même connecté en VNC sur votre serveur :-) ; plus sérieusement ça sera utile pour re-rentrer votre mot de passe Home Assistant si besoin de le changer plus facilement que via le clavier tactile, de modifier la configuration, etc ; pour sortir du client VNC en plein écran et accéder à l’interface raspberry OS, il faut dans le menu de votre client VNC selectionner l’option “Envoyer la touche F8” ; cela va faire apparaitre un menu dans lequel vous pourrez sortir du mode plein écran</li>
  <li>faire un backup de la carte SD</li>
  <li>activer le mode overlayfs (Settings / Control Center / Performances / Overlayfs) : permet notamment d’éviter les corruptions sur les coupures de courant</li>
</ul>]]></content><author><name>Rémi Peyronnet</name></author><category term="Domotique" /><category term="Home Assistant" /><category term="Linux" /><category term="Raspberry" /><category term="VNC" /><summary type="html"><![CDATA[Je souhaitais un moniteur Home Assistant indépendant et tactile. Il existe de nombreuses méthodes plus ou moins simples. Les méthodes qui semblent les plus simples passent par l’utilisation d’une tablette Android et d’un navigateur en mode Kiosque (exemple sur cet article avec la tablette Blackview Tab90, 140€ pour 11 pouces). C’est simple à mettre en place, mais c’est clairement un budget.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://www.lprp.fr/files/2026/vnc_homeassistant.jpg" /><media:content medium="image" url="https://www.lprp.fr/files/2026/vnc_homeassistant.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry xml:lang="en"><title type="html">Compare some free web analytics and migrate from Piwik Pro to Umami</title><link href="https://www.lprp.fr/2026/03/compare-some-free-web-analytics-and-migrate-from-piwik-to-umami/" rel="alternate" type="text/html" title="Compare some free web analytics and migrate from Piwik Pro to Umami" /><published>2026-03-15T18:00:00+00:00</published><updated>2026-03-15T18:00:00+00:00</updated><id>https://www.lprp.fr/2026/03/compare-some-free-web-analytics-and-migrate-from-piwik-to-umami</id><content type="html" xml:base="https://www.lprp.fr/2026/03/compare-some-free-web-analytics-and-migrate-from-piwik-to-umami/"><![CDATA[<p>For several years this site used <a href="https://piwik.pro/">Piwik Pro</a> for web analytics.  The service had a convenient free plan, privacy‑friendly defaults, and a simple integration. Perfect for my low traffic website. However, the free plan has recently been discontinued, and the remaining offers are not suitable for a small personal &amp; free blog like my website. So this forced a migration to another analytics solution.</p>

<h1 id="compare-solutions">Compare solutions</h1>
<p>My needs are very basic:</p>
<ul>
  <li>free for a low traffic website</li>
  <li>privacy‑friendly analytics (GDPR-compliant)</li>
  <li>lightweight script</li>
  <li>easy integration with my Jekyll static website</li>
  <li>an API allowing to access data and reuse metrics in other tools (for instance in an Home Assistant dashboard)</li>
</ul>

<p>Several alternatives were evaluated during a few weeks to compare some results:</p>
<ul>
  <li>Umami has a <a href="https://umami.is/">cloud-hosted free plan for low traffic</a> and also an <a href="https://github.com/umami-software/umami">open source version that you can self-host for free</a></li>
  <li><a href="https://www.goatcounter.com/">GoatCounter</a> is also free for low traffic, and the origin of this tool is exactly what I am looking for</li>
  <li>As I am using Cloudflare as CDN, I have also tested the free <a href="https://www.cloudflare.com/web-analytics/">Cloudflare Web Analytics</a> ; there is also a GraphQL API, but I discovered a bit late that it was exluded from the free plan</li>
</ul>

<h2 id="main-collection-techniques-and-major-issues">Main collection techniques and major issues</h2>
<p>Web analytics has become quite complex:</p>

<ol>
  <li><strong>Website logs</strong>: some years ago, the easiest way to get data was from access logs from the web server ; you were certain at that time to get all the requests, but this method have now several problems: Content Delivery Network will cache and serve contents without soliciting your own web server, so you will miss a good part of your traffic ; also, I am using GitHub Pages, and GitHub does not make available the server logs, also you won’t be able to differentiate users/sessions. This method is fairly limited and no more usable</li>
  <li><strong>JavaScript snippet</strong> to collect data: the most common one, you include a small JavaScript snippet that will run on your page, in the browser of the user, and it will call an API to record usages ; this can record a lot of information, including personal information ; the choice of the solution you use is very important to record only the information you need, in my case without personal information, and without the need of cookies ; the drawback of this method is that it depends on the user browser settings regarding JavaScript, or ad-blockers browser extension that may filter those snippets out</li>
  <li><strong>pixel images</strong> are 1-pixel transparent image, that will trigger the record of the usage ; you can pass some basic information with the query parameters of the image ; as this is included as an image, it is less likely to be deactivated by the browser or extensions, but you will usually get less information</li>
</ol>

<p>Also, there are now a lot of crawlers and search engines to be known to filter out crawlers from usage, and find the relevant source of traffic.</p>

<p>I have activated both JavaScript snippet and pixel image for Umami and Goatcounter to be able to compare the difference between all the methods.</p>

<h2 id="test-dashboard-comparison">Test Dashboard Comparison</h2>

<p>Due to the short notice of Piwik Pro end of free plan, I have only tested the different methods on a week, but still we can see some interesting elements.
The following dashboards show the collected metrics during the evaluation period.</p>

<p><img src="/files/2026/Web%20stats%20-%20Comparaison%20lprp.jpg" alt="Analytics comparison" /></p>

<p>From those dashboards we can see:</p>
<ul>
  <li>we have large differences between all the results!</li>
  <li>Goatcounter is clearly having some troubles recording the usages, quite consistently for JavaScript snippet or pixel image ; it seems that the totals are correct, but maybe some collected usages were treated lately at the time of the test ; remember this is a non-professional solution and is totally free, so anyway, it is very good for what you pay</li>
  <li>Umami Cloud and Cloudflare display quite comparable information, with the same curve as Piwik, but counts more users than Piwik</li>
  <li>Umami Self-hosted is counting more visits, and with a curve looking a bit different ; this can be explained in different ways: the script and URL is unknown from adblockers and do not get blocked, or the self-hosted version is less accurate in crawlers filtering</li>
  <li>anyway, do not expect exact values from any web analytics engine, but use it as rough estimates and trends</li>
</ul>

<h2 id="final-choice-umami-cloud">Final Choice: Umami Cloud</h2>

<p>After testing the different solutions, I decided to go mainly with Umami Cloud. Main reasons are simple setup, privacy-friendly, lightweight, clean dashboard with everything that I need,  good API, no infrastructure to maintain, and all that for free for my low traffic website. That was also the solution with the most consistent numbers in the test period. I think I will keep the other solution for some time to see webstats differences in a longer period, and also in the case Umami Cloud have in the future the same bad idea than Piwik of removing the free plan.</p>

<h1 id="the-migration-part">The migration part</h1>

<h2 id="jekyll-data-collection">Jekyll data collection</h2>

<p>I included in my Jekyll build the required files and lines for data collection</p>
<div class="language-liquid highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="p">{%</span><span class="w"> </span><span class="kr">when</span><span class="w"> </span><span class="s1">'umami'</span><span class="w"> </span><span class="p">%}</span>
        &lt;!-- Umami cloud or self-hosted https://github.com/umami-software/umami --&gt;
        <span class="p">{%</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="nv">analytic</span><span class="p">[</span><span class="nv">env</span><span class="p">].</span><span class="nv">token</span><span class="w"> </span><span class="p">%}</span>
            &lt;script defer src="<span class="p">{{</span><span class="w"> </span><span class="nv">analytic</span><span class="p">.</span><span class="nv">script</span><span class="w"> </span><span class="p">|</span><span class="nf">default</span><span class="p">:</span><span class="w"> </span><span class="s1">'https://cloud.umami.is/script.js'</span><span class="w"> </span><span class="p">}}</span>" 
                    data-website-id="<span class="p">{{</span><span class="w"> </span><span class="nv">analytic</span><span class="p">[</span><span class="nv">env</span><span class="p">].</span><span class="nv">token</span><span class="w"> </span><span class="p">}}</span>"
                    <span class="p">{%</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="nv">analytic</span><span class="p">.</span><span class="nv">host</span><span class="w"> </span><span class="p">%}</span>data-host-url="<span class="p">{{</span><span class="w"> </span><span class="nv">analytic</span><span class="p">.</span><span class="nv">host</span><span class="w"> </span><span class="p">}}</span>"<span class="p">{%</span><span class="w"> </span><span class="kr">endif</span><span class="w"> </span><span class="p">%}</span>
                    data-tag="page_<span class="p">{{</span><span class="w"> </span><span class="nv">include</span><span class="p">.</span><span class="nv">lang</span><span class="w"> </span><span class="p">}}</span>"&gt;&lt;/script&gt;
        <span class="p">{%</span><span class="w"> </span><span class="kr">endif</span><span class="w"> </span><span class="p">%}</span>
        <span class="p">{%</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="nv">analytic</span><span class="p">[</span><span class="nv">env</span><span class="p">].</span><span class="nv">noscript-pixel</span><span class="w"> </span><span class="p">%}</span>
            &lt;noscript&gt;
            &lt;img src="<span class="p">{{</span><span class="w"> </span><span class="nv">analytic</span><span class="p">[</span><span class="nv">env</span><span class="p">].</span><span class="nv">noscript-pixel</span><span class="w"> </span><span class="p">}}</span>" loading="lazy" style="position: absolute; top: 0; right: 0; width: 4px;"&gt;
            &lt;/noscript&gt;
        <span class="p">{%</span><span class="w"> </span><span class="kr">endif</span><span class="w"> </span><span class="p">%}</span>
        <span class="p">{%</span><span class="w"> </span><span class="kr">if</span><span class="w"> </span><span class="nv">analytic</span><span class="p">[</span><span class="nv">env</span><span class="p">].</span><span class="nv">pixel</span><span class="w"> </span><span class="p">%}</span>
            &lt;img src="<span class="p">{{</span><span class="w"> </span><span class="nv">analytic</span><span class="p">[</span><span class="nv">env</span><span class="p">].</span><span class="nv">pixel</span><span class="w"> </span><span class="p">}}</span>" loading="lazy" style="position: absolute; top: 0; right: 0; width: 4px;"&gt;
        <span class="p">{%</span><span class="w"> </span><span class="kr">endif</span><span class="w"> </span><span class="p">%}</span>
</code></pre></div></div>
<p><a href="https://github.com/rpeyron/rpeyron.github.io/blob/20260315/_includes/privacy-consent.html">Full script for all services on my GitHub</a></p>

<h2 id="jekyll-use-of-umami-results">Jekyll use of Umami results</h2>

<p>I use top pages on the front page to display most viewed pages in the corresponding order, and in the blog view to display top ten. To make the data available, here is what I am using:</p>
<ul>
  <li>I have created a share URL for my Umami ; this enables to share a readonly view of all the data ; this URL gives a readonly UI on your data, and also enables to get a read-only token and use the allowed API with that token</li>
  <li>This URL can be shared as is, as it is readonly, but I added another layer to avoid making available the full data ; a script is run weekly as action workflow on my github repository, run a script that authenticate to Umami share URL (given as github “secret”), and save the result in a gist.</li>
  <li>The URL of the corresponding GIST in included in my jekyll website, and a javascript code fetchs the gist to display the posts or widget, the corresponding code is:
    <ul>
      <li><a href="https://github.com/rpeyron/rpeyron.github.io/blob/20260315/_includes/top-article-script.html">top-article-script.html</a></li>
      <li><a href="https://github.com/rpeyron/rpeyron.github.io/blob/20260315/_includes/top-article-widget.html">top-article-widget.html</a></li>
    </ul>
  </li>
</ul>

<h2 id="a-dashboard-in-home-assistant-with-umami-web-stats">A dashboard in Home Assistant with Umami web stats</h2>

<p>I also have created a dashboard in my Home Assistant to integrate directly Umami visitors graphs and top pages.
I have a dedicated GitHub repository <a href="https://github.com/rpeyron/homeassistant_umami">rpeyron/homeassistant_umami</a> with all the elements and a usage guide.</p>

<p><img src="/files/2026/ha_umami_cards.png" alt="" class="img-center mw80" /></p>

<p>This ends the last feature I wanted with my new Umami web analytics.</p>

<h1 id="annexes">Annexes</h1>
<h2 id="cloudflare-graphql-explorer-for-web-analytics">Cloudflare GraphQL Explorer for Web Analytics</h2>

<p>One last thing about Cloudflare, the GraphQL API is not available with the free plan, but you still can use the Cloudflare GraphQL Explorer to query your data (and see what you could get with the paid plan). Anyway, this is a good solution to be able to <strong>manually query</strong> your data.</p>

<p>You first have to login to the {Cloudflare GraphQL Explorer](https://graphql.cloudflare.com/explorer)</p>

<p>You will need to configure the identifiers you will use: <code class="language-shell highlighter-rouge"><span class="o">{</span><span class="s2">"accountTag"</span>:<span class="s2">"&lt;your account id&gt;"</span>,<span class="s2">"siteTag"</span>:<span class="s2">"&lt;your site id&gt;"</span>,<span class="s2">"dateFrom"</span>:<span class="s2">"2026-02-01T00:00:00Z"</span><span class="o">}</span></code></p>

<p>And then use the following GraphQL to get visitors number:</p>
<div class="language-graphql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">query</span><span class="p">(</span><span class="nv">$accountTag</span><span class="p">:</span><span class="w"> </span><span class="n">string</span><span class="p">!,</span><span class="w"> </span><span class="nv">$siteTag</span><span class="p">:</span><span class="w"> </span><span class="n">string</span><span class="p">!,</span><span class="w"> </span><span class="nv">$dateFrom</span><span class="p">:</span><span class="w"> </span><span class="n">Time</span><span class="p">!)</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="n">viewer</span><span class="w"> </span><span class="p">{</span><span class="w">
          </span><span class="n">accounts</span><span class="p">(</span><span class="n">filter</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="n">accountTag</span><span class="p">:</span><span class="w"> </span><span class="nv">$accountTag</span><span class="p">})</span><span class="w"> </span><span class="p">{</span><span class="w">
            </span><span class="n">rumPageloadEventsAdaptiveGroups</span><span class="p">(</span><span class="w">
              </span><span class="n">filter</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
                </span><span class="n">datetime_geq</span><span class="p">:</span><span class="w"> </span><span class="nv">$dateFrom</span><span class="w">
                </span><span class="n">siteTag</span><span class="p">:</span><span class="w"> </span><span class="nv">$siteTag</span><span class="w">
              </span><span class="p">}</span><span class="w">
              </span><span class="n">limit</span><span class="p">:</span><span class="w"> </span><span class="mi">31</span><span class="w">
              </span><span class="n">orderBy</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="n">date_ASC</span><span class="p">]</span><span class="w">
            </span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
              </span><span class="n">dimensions</span><span class="w"> </span><span class="p">{</span><span class="w">
                </span><span class="n">date</span><span class="w">
              </span><span class="p">}</span><span class="w">
              </span><span class="n">sum</span><span class="w"> </span><span class="p">{</span><span class="w">
                </span><span class="n">visits</span><span class="w">
              </span><span class="p">}</span><span class="w">
            </span><span class="p">}</span><span class="w">
          </span><span class="p">}</span><span class="w">
        </span><span class="p">}</span><span class="w">
      </span><span class="p">}</span><span class="w">
    
</span></code></pre></div></div>]]></content><author><name>Rémi Peyronnet</name></author><category term="Informatique" /><category term="Web" /><category term="Website" /><category term="Jekyll" /><category term="Umami" /><category term="Piwik" /><category term="Goatcounter" /><category term="Cloudflare" /><summary type="html"><![CDATA[For several years this site used Piwik Pro for web analytics. The service had a convenient free plan, privacy‑friendly defaults, and a simple integration. Perfect for my low traffic website. However, the free plan has recently been discontinued, and the remaining offers are not suitable for a small personal &amp; free blog like my website. So this forced a migration to another analytics solution.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://www.lprp.fr/files/2026/vignette-piwik-vers-umami.jpg" /><media:content medium="image" url="https://www.lprp.fr/files/2026/vignette-piwik-vers-umami.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry xml:lang="fr"><title type="html">Remplacer un verrou MPM cassé en 3D avec Fusion360</title><link href="https://www.lprp.fr/2026/02/remplacer-un-verrou-mpm-casse-en-3d-avec-fusion360/" rel="alternate" type="text/html" title="Remplacer un verrou MPM cassé en 3D avec Fusion360" /><published>2026-02-07T15:30:00+00:00</published><updated>2026-02-07T15:30:00+00:00</updated><id>https://www.lprp.fr/2026/02/remplacer-un-verrou-mpm-casse-en-3d-avec-fusion360</id><content type="html" xml:base="https://www.lprp.fr/2026/02/remplacer-un-verrou-mpm-casse-en-3d-avec-fusion360/"><![CDATA[<p>Après de nombreuses années de service, la partie fixe du verrou plastique d’une de mes persiennes s’est cassé en deux, sans doute suite à la fatigue du plastique soumis à de fortes températures lorsqu’il est exposé au soleil, et de fortes contraintes mécaniques lors de la fermeture. Suite à quelques recherches, il semblerait que ce soit un verrou assez courant de modèle “MPM”.</p>

<p><img src="/files/2026/MPM%20_%20Casse%20et%20imprime.jpg" alt="" class="img-center mw60" /></p>

<h1 id="tentative-à-la-colle">Tentative à la colle</h1>
<p><img src="/files/2026/MPM%20_%20Tentative%20bicarbonate.jpg" alt="" class="img-right mw30" />
Ma première tentative a été de recoller la pièce avec de la colle cyanoacrylate. J’ai tenté en même temps de consolider avec du bicarbonate collé, une technique dont on peut voir plusieurs vidéos impressionnantes.</p>

<p>C’est un échec complet à la première tentative de fermeture, la contrainte mécanique casse toute la colle instantanément :</p>

<p><img src="/files/2026/MPM%20_%20Echec%20bicarbonate.jpg" alt="" class="img-left mw40" /></p>

<h1 class="clear-float" id="modélisation-3d-avec-inkscape-et-fusion360">Modélisation 3D avec Inkscape et Fusion360</h1>
<h2 id="scan">Scan</h2>
<p>La première étape est de scanner le modèle à reproduire. La meilleure solution pour la 3D est le scan 3D, soit avec un appareil dédié (mais assez cher), soit avec de la photogrammétrie (à recommander si vous avez un iPhone avec capteur à détection de profondeur, il y a des applications dédiées). Cependant pour cet objet c’est tout à fait surdimensionné, et un simple scan 2D suffit amplement.</p>

<p>Je l’ai fait assez simplement avec mon scanner, en positionnant les pièces directement sur la vitre et en ajoutant des règles pour pouvoir garder des repères de taille qui seront très utiles lors de l’étape de calibration. J’ai scanné une pièce cassée, et une autre en bon état d’une autre persienne.</p>

<p><img src="/files/2026/MPM%20_%20SCAN1869.JPG" alt="" class="img-center mw60" /></p>

<p>C’est également possible sans scanner, en prenant en photo les pièces ; il faut prévoir de traiter l’image par la suite pour supprimer la déformation, par exemple avec Darktable et ses fonctionnalités de <a href="https://docs.darktable.org/usermanual/4.6/fr/module-reference/processing-modules/lens-correction/">correction de lentille</a>, ou de <a href="https://docs.darktable.org/usermanual/4.6/fr/module-reference/processing-modules/rotate-perspective/">correction de perspective</a>. Une alternative pour minimiser la déformation sans post-traitement consiste à prendre les objets assez loin et à la verticale, puis de zoomer ensuite sur l’image.</p>

<h2 id="vectorisation">Vectorisation</h2>
<p>Cette partie est facultative, vous pouvez importer l’image directement dans Fusion360. Mais j’ai voulu tester la vectorisation dans Inkscape et la possibilité d’utiliser le profil correspondant dans Fusion360. Ce n’est finalement pas ce que j’ai utilisé.</p>

<p>Cette photo montre les différentes étapes, de gauche à droite, en vectorisant via détection de contours, puis nettoyant les imperfections et en simplifiant un maximum sans affecter la forme globale :</p>

<p><img src="/files/2026/MPM%20_%20Vectorisation%20191505.png" alt="" class="img-center mw80" /></p>

<h2 id="modélisation-sous-fusion360">Modélisation sous Fusion360</h2>

<p>Dans Fusion360, on peut ensuite insérer l’image:</p>

<p><img src="/files/2026/MPM%20-%20Fusion360%20_%20Inserer%20SVG.png" alt="" class="img-center mw80" /></p>

<p>Puis la calibrer en utilisant le clic droit sur l’image, puis option Calibrer ; il suffit ensuite de tracer une cote et la dimension associée. Parfait pour utiliser notre règle repère.</p>

<p><img src="/files/2026/MPM%20-%20Fusion360%20_%20Calibration%20_%202025-12-27%20180135.png" alt="" class="img-center mw80" /></p>

<p>J’ai ensuite créé une esquisse au-dessus de l’image, puis créé les volumes 3D à partie de l’esquisse.</p>

<p><img src="/files/2026/MPM%20Fusion360%20_%20Esquisse%20_%202026-02-07%20195158.jpg" alt="" class="img-center mw80" /></p>

<p><img src="/files/2026/MPM%20Fusion360%20_%203D%20_%202026-02-07%20195158.jpg" alt="" class="img-center mw80" /></p>

<h2 id="impression">Impression</h2>

<p>Enfin la dernière étape est l’impression ; j’ai utilisé les réglages suivants :</p>
<ul>
  <li>utilisation d’un filament PETG pour plus de résistance mécanique et thermique (et moins cassant que le PLA), à 240°C et 80°C pour le plateau</li>
  <li>un remplissage de 50% en Cubic  (un remplissage plus important ne donne pas plus de résistance mécanique)</li>
  <li>3 épaisseurs de murs, et 4 couches en haut et en bas</li>
  <li>et bien sûr des supports</li>
</ul>

<p><img src="/files/2026/3dprint_verroumpm.gif" alt="" class="img-center" /></p>

<p>Le résultat est plutôt OK, et semble résister correctement aux tests que j’ai faits à confirmer dans le temps :</p>

<p><img src="/files/2026/MPM%20_%20Resultat%20ferme.jpg" alt="" class="img-center mw60" /></p>

<p>J’avais oublié à ce stade les coins cassés, j’ai corrigé sur le dernier modèle. Les modèles sont disponibles sous <a href="https://www.thingiverse.com/thing:7290146">Thingiverse</a> ou <a href="https://www.printables.com/model/1589582-verrou-mpm-pour-persiennes">Printables</a>.</p>]]></content><author><name>Rémi Peyronnet</name></author><category term="3D" /><category term="Verrou" /><category term="3D" /><category term="Fusion 360" /><category term="MPM" /><category term="Printables" /><category term="Thingiverse" /><summary type="html"><![CDATA[Après de nombreuses années de service, la partie fixe du verrou plastique d’une de mes persiennes s’est cassé en deux, sans doute suite à la fatigue du plastique soumis à de fortes températures lorsqu’il est exposé au soleil, et de fortes contraintes mécaniques lors de la fermeture. Suite à quelques recherches, il semblerait que ce soit un verrou assez courant de modèle “MPM”.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://www.lprp.fr/files/2026/vignette_verrou_mpm.jpg" /><media:content medium="image" url="https://www.lprp.fr/files/2026/vignette_verrou_mpm.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry xml:lang="fr"><title type="html">Faux Hubs USB 3.0 chinois</title><link href="https://www.lprp.fr/2026/01/faux-hubs-usb-30-chinois/" rel="alternate" type="text/html" title="Faux Hubs USB 3.0 chinois" /><published>2026-01-31T18:11:33+00:00</published><updated>2026-01-31T18:11:33+00:00</updated><id>https://www.lprp.fr/2026/01/faux-hubs-usb-30-chinois</id><content type="html" xml:base="https://www.lprp.fr/2026/01/faux-hubs-usb-30-chinois/"><![CDATA[<p>Par curiosité j’ai ouvert un hub USB 3.0 qui a récemment cramé suite au branchement d’un dongle Wifi, et je n’ai pas été déçu, ce qui est vendu comme un hub à quatre ports USB 3.0 est en réalité un hub USB avec 4 ports USB 2.0, dont un qui peut fonctionner en USB 3.0. Tout dans son apparence laisse à penser que c’est bien un hub USB 3.0, et pourtant si on lit attentivement l’annonce, quelques incohérences permettent d’identifier cette “subtilité”. Difficile alors de parler complètement d’arnaque, mais les formulations sont clairement a minima trompeuses.</p>

<h1 id="exemple-dune-annonce-aliexpress">Exemple d’une annonce AliExpress</h1>
<p>Il existe énormément d’annonces qui présente cette “particularité”, prenons en exemple <a href="https://fr.aliexpress.com/item/1005006064840808.html">cette annonce AliExpress</a> :</p>

<p>La présentation rapide parle d’un “Hub USB 3.0 à 4 ports haute vitesse” :</p>

<p><img src="/files/2026/AliExpress_Example_Hub_USB3_2026-02-07%20163819.png" alt="" class="img-center mw80" /></p>

<p>On voit bien une mention à la fin du titre mentionnant “4 ports USB 3.0, 2.0”, mais à ce stade on peut comprendre que le titre veuille dire que les ports sont également compatibles USB 2.0</p>

<p>La présentation détaillée donne des indications plus précises, et contradictoires !</p>

<p><img src="/files/2026/AliExpress_Example_Hub_USB3_Description_2026-02-07%20163625.png" alt="" class="img-center mw80" /></p>

<p>Une première phrase dans la description (mis en évidence par le premier rectangle en rouge) mentionne sans équivoque possible 4 ports USB 3.0. Cette partie est fausse.
Mais un deuxième passage dans la description (mis en évidence par le premier rectangle en vert) donne quant à lui l’information exacte, à savoir un seul port en USB3.0 et les autres en USB2.0.</p>

<h1 id="le-piège-des-couleurs-des-ports">Le “piège” des couleurs des ports</h1>

<p>Je me suis rendu compte à cette occasion que je faisais un peu trop confiance à la couleur des ports USB. En effet, j’avais en tête “connecteur bleu = USB 3.0”. Certain sites, comme <a href="https://www.corsair.com/fr/fr/explorer/diy-builder/storage/usb-port-colors-explained/">celui de Corsair</a>, mentionnent d’ailleurs toute une palette de couleurs de ports :</p>

<p><img src="/files/2026/Corsair_Couleurs_USB_2026-02-07%20170401.png" alt="" class="img-center mw80" /></p>

<p>Dans notre exemple, que ce soit les 4 connecteurs ou le câble, tout est bleu, impossible de se douteur que les 4 ports ne sont pas identiques :</p>

<p><img src="/files/2026/IMG_20260207_162256455_HDR.jpg" alt="" /></p>

<p>Cependant, j’ai cherché dans la norme USB, et je n’ai trouvé nulle part de norme portant sur la couleur des ports USB. Je ne sais pas si je n’ai pas bien cherché ou si ce n’est effectivement pas normé. Il est probable que ce ne soit qu’une convention, très répandue, mais que le fabricant ne soit pas réellement “non conforme” s’il ne suit pas la convention…</p>

<h1 id="la-surprise-au-démontage">La surprise au démontage</h1>

<p>Le démontage du Hub est très instructif pour comprendre comment cela fonctionne :</p>

<p><img src="/files/2026/IMG_20260125_212902786_HDR.jpg" alt="" class="img-center mw80" /></p>

<p>On voit effectivement assez clairement d’après le câblage que les 3 ports USB de gauches n’ont que 4 fils et plots (rectangle blanc), et ne peuvent donc pas physiquement aller au-delà de la compatibilité USB 2.0 (2 fils d’alimentation, et 2 fils pour les données).</p>

<p>Le premier connecteur quant à lui, comporte le bon nombre de fils, avec 2 fils provenant de la puce qui doit être un hub USB 2.0, et les 5 fils USB 3.0 relié en directe depuis le câble (rectangle bleu).</p>

<p>Je n’ai pas pu vérifier car le hub n’étant plus fonctionnel, les résultats ne seraient pas exacts, et je n’ai pas envie de cramer un port USB, mais il est probable que le firmware de la puce hub USB 2.0 soit modifié pour annoncer le premier port en USB 3.0.</p>

<h1 id="les-hubs-à-7-ou-10-ports">Les Hubs à 7 ou 10 ports</h1>

<p>La norme USB permet des hubs de 4 ports. Lorsqu’il y a plus que 4 ports, c’est qu’il y a en réalité plusieurs hubs en cascade :</p>
<ul>
  <li>7 ports : un premier hub 4 ports, dont un des ports est utilisé par un autre hub, ce qui donne 7 ports disponibles ( 4 - 1 pour le premier hub, et 4 pour le deuxième)</li>
  <li>10 ports : un premier hub 4 ports, dont deux des ports sont utilisés par deux autres hubs (4 - 2 + 2 x 4)</li>
</ul>

<p>J’ai ainsi pu constater sur un Hub USB 3.0 à 7 ports, qu’en réalisé, seuls 3 ports supportent réellement l’USB 3.0, et les 4 ports restants sont seulement en USB 2.0. Comme il fonctionne encore, je ne l’ai pas démonté, mais il est certainement constitué d’un premier Hub 3.0, dont l’un des ports est ensuite utilisé par un second hub en cascade uniquement en 2.0.  Encore une fois, de l’extérieur rien de ne permet de distinguer les ports. C’est un peu dommage, ce format 3 USB 3.0 + 4 USB 2.0 est plutôt intéressante, on a rarement besoin de plus en USB 3.0, et si c’était le cas il y aurait probablement d’autres limitations de bande passante ou d’alimentation, mais c’est dommage qu’aucune indication visuelle ne permette de comprendre la différence entre les ports, pour brancher les bons périphériques sur les bons ports.</p>

<p><img src="/files/2026/AliExpress_Exemple_Hub_7ports_2026-02-07%20175813.png" alt="" class="img-center mw80" /></p>

<h1 id="conclusion">Conclusion</h1>
<p>En conclusion, méfiez-vous des hubs marqués USB 3.0 de marque inconnue (qu’ils ne soient pas chers ou non…) et ne faites pas une confiance aveugle à la couleur des connecteurs…</p>

<p>Dans la jungle des câbles USB, faîtes également très attention aux spécifications des câbles Type C, qui peuvent être très différentes sur la compatibilité avec le transport de vidéo, la bande passante disponible, ou encore la puissance maximum supportée.</p>]]></content><author><name>Rémi Peyronnet</name></author><category term="Avis Conso" /><category term="USB" /><category term="AliExpress" /><category term="Elec" /><summary type="html"><![CDATA[Par curiosité j’ai ouvert un hub USB 3.0 qui a récemment cramé suite au branchement d’un dongle Wifi, et je n’ai pas été déçu, ce qui est vendu comme un hub à quatre ports USB 3.0 est en réalité un hub USB avec 4 ports USB 2.0, dont un qui peut fonctionner en USB 3.0. Tout dans son apparence laisse à penser que c’est bien un hub USB 3.0, et pourtant si on lit attentivement l’annonce, quelques incohérences permettent d’identifier cette “subtilité”. Difficile alors de parler complètement d’arnaque, mais les formulations sont clairement a minima trompeuses.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://www.lprp.fr/files/2026/vignette_faux_usb3.jpg" /><media:content medium="image" url="https://www.lprp.fr/files/2026/vignette_faux_usb3.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>