<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>#cloudflare — Blog d'Ange Bonkoski</title><link>https://journal.ange-bonkoski.fr/tags/cloudflare/</link><description>Journal de mon homelab et write-ups de labs de cybersécurité.</description><language>fr</language><copyright>Ange Bonkoski, licence CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/deed.fr)</copyright><lastBuildDate>Sun, 18 Oct 2026 10:00:00 +0200</lastBuildDate><atom:link href="https://journal.ange-bonkoski.fr/tags/cloudflare/index.xml" rel="self" type="application/rss+xml"/><item><title>Zéro port ouvert : Cloudflare Tunnel pour le public, Tailscale pour le privé</title><link>https://journal.ange-bonkoski.fr/journal/zero-port-ouvert/</link><pubDate>Sun, 18 Oct 2026 10:00:00 +0200</pubDate><guid>https://journal.ange-bonkoski.fr/journal/zero-port-ouvert/</guid><category>homelab</category><category>réseau</category><category>cloudflare</category><category>tailscale</category><category>sécurité</category><description>&lt;p&gt;Pendant des semaines, mon serveur maison était joignable depuis Internet&#10;de la façon la plus classique qui soit : deux redirections de ports sur ma&#10;box, le 80 et le 443, vers un reverse proxy. Ça marchait. Et c&amp;rsquo;est&#10;justement le problème : ça marchait &lt;strong&gt;pour tout le monde&lt;/strong&gt;.&lt;/p&gt;&#10;&lt;p&gt;Aujourd&amp;rsquo;hui, ma box n&amp;rsquo;a plus &lt;strong&gt;aucune&lt;/strong&gt; redirection. Mon portfolio est&#10;toujours en ligne, et j&amp;rsquo;accède toujours à mes services privés depuis mon&#10;téléphone en 4G. Voici comment, et surtout pourquoi.&lt;/p&gt;&#10;&lt;h2 id="le-point-de-départ"&gt;Le point de départ&lt;/h2&gt;&#10;&lt;p&gt;Mon homelab est un vieux PC portable sous ZimaOS, qui fait tourner une&#10;dizaine de services dans des conteneurs Docker : mon portfolio, un&#10;gestionnaire de mots de passe (Vaultwarden), de la supervision, un filtre&#10;DNS…&lt;/p&gt;&#10;&lt;p&gt;Pour les rendre accessibles de l&amp;rsquo;extérieur, j&amp;rsquo;avais fait ce que font la&#10;plupart des tutoriels :&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;un enregistrement DNS qui pointe vers l&amp;rsquo;&lt;strong&gt;IP publique&lt;/strong&gt; de ma box ;&lt;/li&gt;&#10;&lt;li&gt;une redirection des ports 80 et 443 de la box vers le serveur ;&lt;/li&gt;&#10;&lt;li&gt;un reverse proxy (Nginx Proxy Manager) qui répartit les requêtes entre&#10;les services selon le nom de domaine demandé.&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h2 id="pourquoi-ce-nétait-pas-acceptable"&gt;Pourquoi ce n&amp;rsquo;était pas acceptable&lt;/h2&gt;&#10;&lt;p&gt;Un port ouvert, c&amp;rsquo;est une porte d&amp;rsquo;entrée que n&amp;rsquo;importe qui peut essayer de&#10;pousser. Et sur Internet, quelqu&amp;rsquo;un essaie &lt;strong&gt;en permanence&lt;/strong&gt; : des robots&#10;balayent toutes les adresses IPv4 en continu, à la recherche d&amp;rsquo;un service&#10;mal configuré ou pas à jour.&lt;/p&gt;&#10;&lt;p&gt;Trois choses me gênaient en particulier :&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;mon coffre de mots de passe était joignable par le monde entier.&lt;/strong&gt; Il&#10;était protégé par un mot de passe maître et une double authentification,&#10;mais une seule faille dans Vaultwarden aurait suffi ;&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;mon IP publique apparaissait dans mon DNS.&lt;/strong&gt; N&amp;rsquo;importe qui pouvait&#10;savoir où se trouve mon serveur, et donc ma connexion personnelle ;&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;toute la sécurité reposait sur un seul logiciel&lt;/strong&gt;, le reverse proxy,&#10;en première ligne face à Internet.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Je me suis fixé une règle simple : &lt;strong&gt;ce qui doit être public l&amp;rsquo;est ; tout le&#10;reste ne doit même pas être visible.&lt;/strong&gt;&lt;/p&gt;&#10;&lt;h2 id="lidée--inverser-le-sens-de-la-connexion"&gt;L&amp;rsquo;idée : inverser le sens de la connexion&lt;/h2&gt;&#10;&lt;p&gt;Une redirection de port, c&amp;rsquo;est la box qui &lt;strong&gt;accepte&lt;/strong&gt; des connexions&#10;entrantes. Le pare-feu de la box, lui, bloque déjà tout ce qui entre sans&#10;avoir été demandé. Il suffit donc de ne rien lui demander d&amp;rsquo;accepter.&lt;/p&gt;&#10;&lt;p&gt;Le principe est le suivant : c&amp;rsquo;est le &lt;strong&gt;serveur&lt;/strong&gt; qui ouvre lui-même une&#10;connexion &lt;strong&gt;sortante&lt;/strong&gt; vers un service extérieur, et c&amp;rsquo;est par ce tuyau&#10;déjà ouvert que les visiteurs arrivent. Vu de la box, il n&amp;rsquo;y a qu&amp;rsquo;une&#10;connexion sortante, comme quand vous ouvrez un site web. Rien n&amp;rsquo;entre.&lt;/p&gt;&#10;&lt;p&gt;J&amp;rsquo;ai utilisé deux outils construits sur cette idée, un pour chaque usage.&lt;/p&gt;&#10;&lt;h2 id="le-chemin-public--cloudflare-tunnel"&gt;Le chemin public : Cloudflare Tunnel&lt;/h2&gt;&#10;&lt;p&gt;Mon portfolio doit rester accessible à n&amp;rsquo;importe quel recruteur, sans&#10;logiciel à installer. Pour lui, j&amp;rsquo;utilise &lt;strong&gt;Cloudflare Tunnel&lt;/strong&gt;.&lt;/p&gt;&#10;&lt;p&gt;Un petit conteneur, &lt;code&gt;cloudflared&lt;/code&gt;, tourne sur le serveur. Au démarrage, il&#10;ouvre une connexion chiffrée vers Cloudflare et la garde ouverte. Le&#10;domaine pointe vers Cloudflare, plus vers ma box :&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Visiteur → DNS → Cloudflare (pare-feu applicatif, cache)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → tunnel (connexion sortante ouverte par le serveur)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → cloudflared → serveur web du portfolio&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Le fichier Compose tient en quelques lignes. Le jeton qui identifie le&#10;tunnel n&amp;rsquo;y est pas écrit : il est lu dans un fichier &lt;code&gt;.env&lt;/code&gt; séparé, lisible&#10;seulement par son propriétaire.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;services&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;cloudflared&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;cloudflare/cloudflared:latest&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;restart&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;unless-stopped&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;tunnel --no-autoupdate run&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;TUNNEL_TOKEN=${TUNNEL_TOKEN} &lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# lu dans .env, jamais en clair ici&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Au passage, mon IP publique a disparu du DNS : on ne voit plus que celles&#10;de Cloudflare.&lt;/p&gt;&#10;&#10;&#10;&#10;&#10;&#10;&#10;&#10;&lt;div class="encadre encadre-attention" role="note"&gt;&#10; &lt;p class="encadre-titre"&gt;&#10; &#10; &#10; &lt;svg aria-hidden="true" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"&gt;&lt;path d="M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0z"/&gt;&lt;path d="M12 9v4"/&gt;&lt;path d="M12 17h.01"/&gt;&lt;/svg&gt;&#10; &#10; &lt;span&gt;Le prix de ce confort&lt;/span&gt;&#10; &lt;/p&gt;&#10; &#10; &lt;div class="encadre-contenu"&gt;&#10; &lt;p&gt;Cloudflare termine le chiffrement TLS : il voit passer le trafic du site&#10;en clair avant de le renvoyer dans le tunnel. Pour un portfolio public, ça&#10;ne me dérange pas. Pour un service qui manipule des données sensibles,&#10;c&amp;rsquo;est une vraie question de confiance. C&amp;rsquo;est d&amp;rsquo;ailleurs pour ça que mon&#10;coffre de mots de passe n&amp;rsquo;emprunte &lt;strong&gt;pas&lt;/strong&gt; ce chemin.&lt;/p&gt;&#10;&#10; &lt;/div&gt;&#10;&lt;/div&gt;&#10;&#10;&#10;&lt;h2 id="les-chemins-privés--tailscale"&gt;Les chemins privés : Tailscale&lt;/h2&gt;&#10;&lt;p&gt;Pour tout le reste (coffre de mots de passe, administration, partage de&#10;fichiers, SSH), il n&amp;rsquo;y a qu&amp;rsquo;un seul public : moi. Pas besoin de les&#10;exposer au monde ; il suffit que &lt;strong&gt;mes appareils&lt;/strong&gt; puissent les joindre.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;Tailscale&lt;/strong&gt; crée un réseau privé chiffré (basé sur WireGuard) entre mon&#10;serveur, mon PC, mon Mac et mon iPhone. Chaque appareil reçoit une adresse&#10;dans une plage réservée, qui n&amp;rsquo;a de sens que sur ce réseau.&#10;Et là encore, chaque appareil n&amp;rsquo;ouvre que des connexions sortantes : aucune&#10;redirection n&amp;rsquo;est nécessaire.&lt;/p&gt;&#10;&lt;p&gt;Restait un détail : je voulais garder une vraie adresse, avec un vrai&#10;certificat HTTPS, pour que les applications de mon gestionnaire de mots de&#10;passe fonctionnent comme avant. La solution tient en trois morceaux.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;1. Un DNS qui pointe vers une adresse privée.&lt;/strong&gt; Le sous-domaine du coffre&#10;renvoie l&amp;rsquo;adresse Tailscale du serveur. Tout le monde peut lire cet&#10;enregistrement, mais cette adresse ne mène nulle part si l&amp;rsquo;on n&amp;rsquo;est pas&#10;dans mon réseau Tailscale.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;2. Un certificat obtenu sans port ouvert.&lt;/strong&gt; Let&amp;rsquo;s Encrypt doit vérifier&#10;que je contrôle bien le domaine avant de délivrer un certificat. La méthode&#10;habituelle (le &lt;em&gt;défi HTTP&lt;/em&gt;) consiste à venir chercher un fichier sur mon&#10;serveur… ce qui est impossible, puisque plus rien n&amp;rsquo;est joignable. J&amp;rsquo;utilise&#10;donc le &lt;strong&gt;défi DNS&lt;/strong&gt; : le reverse proxy crée lui-même un enregistrement&#10;temporaire dans ma zone DNS, grâce à un jeton d&amp;rsquo;API Cloudflare limité à&#10;cette seule zone.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;3. Un relais pour le port 443.&lt;/strong&gt; Sur mon serveur, l&amp;rsquo;interface de ZimaOS&#10;occupe déjà le port 80, et le reverse proxy écoute sur d&amp;rsquo;autres ports. C&amp;rsquo;est&#10;Tailscale qui reçoit le 443 sur son interface et le relaie vers le reverse&#10;proxy :&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;tailscale serve --bg --tcp &lt;span class="m"&gt;443&lt;/span&gt; tcp://localhost:&amp;lt;port-https-du-proxy&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&#10;&#10;&#10;&#10;&#10;&#10;&#10;&lt;div class="encadre encadre-attention" role="note"&gt;&#10; &lt;p class="encadre-titre"&gt;&#10; &#10; &#10; &lt;svg aria-hidden="true" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"&gt;&lt;path d="M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0z"/&gt;&lt;path d="M12 9v4"/&gt;&lt;path d="M12 17h.01"/&gt;&lt;/svg&gt;&#10; &#10; &lt;span&gt;La commande à ne jamais lancer&lt;/span&gt;&#10; &lt;/p&gt;&#10; &#10; &lt;div class="encadre-contenu"&gt;&#10; &lt;p&gt;&lt;code&gt;tailscale serve reset&lt;/code&gt; efface &lt;strong&gt;tous&lt;/strong&gt; les relais configurés. Je l&amp;rsquo;ai&#10;appris en la lançant : mon coffre est devenu injoignable jusqu&amp;rsquo;à ce que je&#10;recrée le relais.&lt;/p&gt;&#10;&#10; &lt;/div&gt;&#10;&lt;/div&gt;&#10;&#10;&#10;&lt;h2 id="vérifier-plutôt-que-supposer"&gt;Vérifier, plutôt que supposer&lt;/h2&gt;&#10;&lt;p&gt;Une fois les redirections supprimées dans l&amp;rsquo;interface de la box, je n&amp;rsquo;ai&#10;pas considéré que c&amp;rsquo;était fini. J&amp;rsquo;ai vérifié chaque promesse depuis&#10;l&amp;rsquo;&lt;strong&gt;extérieur&lt;/strong&gt;, avec mon iPhone en 4G (donc hors de mon réseau local) :&lt;/p&gt;&#10;&#10;&lt;div class="tableau" role="region" aria-label="Tableau" tabindex="0"&gt;&#10;&lt;table&gt;&#10; &lt;thead&gt;&#10; &lt;tr&gt;&#10; &lt;th class="aligne-left"&gt;Test&lt;/th&gt;&#10; &lt;th class="aligne-center"&gt;Tailscale coupé&lt;/th&gt;&#10; &lt;th class="aligne-center"&gt;Tailscale actif&lt;/th&gt;&#10; &lt;/tr&gt;&#10; &lt;/thead&gt;&#10; &lt;tbody&gt;&#10; &lt;tr&gt;&#10; &lt;td class="aligne-left"&gt;Portfolio&lt;/td&gt;&#10; &lt;td class="aligne-center"&gt;accessible&lt;/td&gt;&#10; &lt;td class="aligne-center"&gt;accessible&lt;/td&gt;&#10; &lt;/tr&gt;&#10; &lt;tr&gt;&#10; &lt;td class="aligne-left"&gt;Coffre de mots de passe&lt;/td&gt;&#10; &lt;td class="aligne-center"&gt;&lt;strong&gt;injoignable&lt;/strong&gt;&lt;/td&gt;&#10; &lt;td class="aligne-center"&gt;accessible&lt;/td&gt;&#10; &lt;/tr&gt;&#10; &lt;tr&gt;&#10; &lt;td class="aligne-left"&gt;IP publique de la box&lt;/td&gt;&#10; &lt;td class="aligne-center"&gt;aucune réponse&lt;/td&gt;&#10; &lt;td class="aligne-center"&gt;—&lt;/td&gt;&#10; &lt;/tr&gt;&#10; &lt;tr&gt;&#10; &lt;td class="aligne-left"&gt;Partages de fichiers&lt;/td&gt;&#10; &lt;td class="aligne-center"&gt;&lt;strong&gt;injoignables&lt;/strong&gt;&lt;/td&gt;&#10; &lt;td class="aligne-center"&gt;accessibles&lt;/td&gt;&#10; &lt;/tr&gt;&#10; &lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;J&amp;rsquo;ai aussi balayé l&amp;rsquo;adresse &lt;strong&gt;IPv6&lt;/strong&gt; publique du serveur. Contrairement à&#10;l&amp;rsquo;IPv4, une adresse IPv6 désigne directement la machine, sans traduction&#10;d&amp;rsquo;adresse par la box : c&amp;rsquo;est un oubli classique. Résultat : tous les ports&#10;testés fermés ou filtrés.&lt;/p&gt;&#10;&#10;&#10;&#10;&#10;&#10;&#10;&#10;&lt;div class="encadre encadre-lecon" role="note"&gt;&#10; &lt;p class="encadre-titre"&gt;&#10; &#10; &#10; &lt;svg aria-hidden="true" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"&gt;&lt;path d="M9 18h6"/&gt;&lt;path d="M10 22h4"/&gt;&lt;path d="M12 2a7 7 0 0 0-4 12.7c.6.5 1 1.3 1 2.1V17h6v-.2c0-.8.4-1.6 1-2.1A7 7 0 0 0 12 2z"/&gt;&lt;/svg&gt;&#10; &#10; &lt;span&gt;L&amp;#39;échec qui prouve que ça marche&lt;/span&gt;&#10; &lt;/p&gt;&#10; &#10; &lt;div class="encadre-contenu"&gt;&#10; &lt;p&gt;Plus tard, en ajoutant un nouveau service, j&amp;rsquo;ai oublié de cocher le défi&#10;DNS : la demande de certificat en défi HTTP a échoué. Let&amp;rsquo;s Encrypt n&amp;rsquo;avait&#10;pas pu joindre mon serveur depuis Internet. Cette erreur était la&#10;meilleure preuve possible que l&amp;rsquo;isolement tenait.&lt;/p&gt;&#10;&#10; &lt;/div&gt;&#10;&lt;/div&gt;&#10;&#10;&#10;&lt;h2 id="ce-que-jen-retiens"&gt;Ce que j&amp;rsquo;en retiens&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;Ne pas confondre « accessible » et « exposé ».&lt;/strong&gt; Mes services privés&#10;sont accessibles partout où je vais, mais ne sont exposés à personne.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Séparer les chemins selon le public.&lt;/strong&gt; Un visiteur anonyme et moi&#10;n&amp;rsquo;avons pas besoin d&amp;rsquo;emprunter la même porte.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Tester depuis l&amp;rsquo;extérieur.&lt;/strong&gt; Depuis le Wi-Fi de la maison, tout semble&#10;toujours fonctionner. C&amp;rsquo;est en 4G qu&amp;rsquo;on voit ce qui est vraiment exposé.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Chaque choix a un coût.&lt;/strong&gt; Le tunnel me rend dépendant de Cloudflare :&#10;si le conteneur &lt;code&gt;cloudflared&lt;/code&gt; s&amp;rsquo;arrête, le portfolio tombe. Ma&#10;supervision me prévient dans ce cas.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Il reste une limite que j&amp;rsquo;assume : à l&amp;rsquo;intérieur de mon réseau local, les&#10;services écoutent encore sur toutes les interfaces. La prochaine étape est&#10;de réduire aussi cette surface-là.&lt;/p&gt;&#10;</description></item></channel></rss>