{"id":39,"date":"2026-08-07T19:43:20","date_gmt":"2026-08-07T17:43:20","guid":{"rendered":"http:\/\/127.0.0.1:8086\/?p=39"},"modified":"2026-09-11T22:22:17","modified_gmt":"2026-09-11T20:22:17","slug":"securiser-vps-ubuntu-docker-ufw-fail2ban-tailscale","status":"publish","type":"post","link":"https:\/\/homelab.nvsd.ch\/?p=39","title":{"rendered":"S\u00e9curiser mon VPS Ubuntu : audit r\u00e9seau, Docker, UFW, Fail2Ban et Tailscale"},"content":{"rendered":"\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<figure class=\"wp-block-image\"><img decoding=\"async\" src=\"images\/article-securite-vps-hero-1600x900.png\" alt=\"S\u00e9curisation de mon VPS Ubuntu et de ses services Docker\"\/><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Introduction : un serveur fonctionnel n\u2019est pas forc\u00e9ment un serveur ma\u00eetris\u00e9<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Au fil de la construction de mon Homelab, mon VPS est devenu une vraie petite infrastructure. Il h\u00e9berge Docker, plusieurs applications web, Nginx Proxy Manager pour les services publics, Dockge pour l\u2019administration des stacks et Tailscale pour les acc\u00e8s priv\u00e9s.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Tout fonctionnait. C\u2019\u00e9tait justement le bon moment pour v\u00e9rifier une chose plus importante : <strong>est-ce que je savais exactement ce qui \u00e9tait expos\u00e9 sur Internet ?<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">J\u2019ai donc consacr\u00e9 une session compl\u00e8te \u00e0 l\u2019audit et \u00e0 la s\u00e9curisation du VPS.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Mon objectif n\u2019\u00e9tait pas d\u2019installer le plus grand nombre possible d\u2019outils de s\u00e9curit\u00e9. Je voulais d\u2019abord comprendre la situation r\u00e9elle :<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>quels ports \u00e9coutaient sur le serveur ;<\/li>\n\n\n\n<li>quels ports \u00e9taient publi\u00e9s par Docker ;<\/li>\n\n\n\n<li>quels services devaient r\u00e9ellement rester publics ;<\/li>\n\n\n\n<li>quel r\u00f4le jouait UFW ;<\/li>\n\n\n\n<li>comment prot\u00e9ger SSH ;<\/li>\n\n\n\n<li>si Fail2Ban fonctionnait correctement ;<\/li>\n\n\n\n<li>comment d\u00e9placer l\u2019administration vers Tailscale ;<\/li>\n\n\n\n<li>et si Docker occupait inutilement de l\u2019espace.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Cette session m\u2019a surtout rappel\u00e9 une r\u00e8gle simple : <strong>la s\u00e9curit\u00e9 n\u2019est pas un produit \u00e0 installer, mais une architecture \u00e0 comprendre.<\/strong><\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Commencer par observer avant de modifier<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Sur un serveur distant, je pr\u00e9f\u00e8re toujours commencer par le diagnostic.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Une mauvaise r\u00e8gle de pare-feu ou une modification SSH appliqu\u00e9e trop vite peut me couper compl\u00e8tement du VPS. Avant de fermer quoi que ce soit, je v\u00e9rifie donc l\u2019\u00e9tat du syst\u00e8me.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Je commence par les services actifs :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo systemctl --type=service --state=running<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Je regarde ensuite les ports r\u00e9ellement en \u00e9coute :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo ss -tulpn<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Puis les ports publi\u00e9s par Docker :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>docker ps --format \"table {{.Names}}\\t{{.Status}}\\t{{.Ports}}\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Ces trois commandes ne montrent pas exactement la m\u00eame chose.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>systemctl<\/code> d\u00e9crit les services g\u00e9r\u00e9s par Ubuntu. <code>ss<\/code> montre les sockets r\u00e9seau ouvertes sur l\u2019h\u00f4te. <code>docker ps<\/code> permet de rep\u00e9rer les ports publi\u00e9s par les conteneurs.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Dans un Homelab Docker, cette derni\u00e8re v\u00e9rification est essentielle. Une application peut \u00eatre parfaitement isol\u00e9e dans son conteneur tout en devenant accessible depuis Internet si son fichier Compose publie un port sur toutes les interfaces.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Distinguer les services publics des services priv\u00e9s<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Avant de configurer le pare-feu, j\u2019ai class\u00e9 les services selon leur r\u00f4le.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Les services destin\u00e9s aux visiteurs doivent \u00eatre accessibles publiquement. Dans mon architecture, il s\u2019agit principalement du trafic web :<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>HTTP sur le port <code>80<\/code> ;<\/li>\n\n\n\n<li>HTTPS sur le port <code>443<\/code> ;<\/li>\n\n\n\n<li>les sites et applications publi\u00e9s derri\u00e8re Nginx Proxy Manager.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">\u00c0 l\u2019inverse, les outils d\u2019administration n\u2019ont aucune raison d\u2019\u00eatre visibles par n\u2019importe qui sur Internet :<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Dockge ;<\/li>\n\n\n\n<li>les interfaces de maintenance ;<\/li>\n\n\n\n<li>les API internes ;<\/li>\n\n\n\n<li>les bases de donn\u00e9es ;<\/li>\n\n\n\n<li>les outils de d\u00e9veloppement ;<\/li>\n\n\n\n<li>les services techniques utilis\u00e9s uniquement par moi.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Cette distinction devient ma r\u00e8gle de base :<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\"><strong>Si un service n\u2019a aucune raison d\u2019\u00eatre public, je cherche d\u2019abord \u00e0 supprimer son exposition plut\u00f4t qu\u2019\u00e0 ajouter une r\u00e8gle complexe pour le prot\u00e9ger.<\/strong><\/p>\n<\/blockquote>\n\n\n\n<figure class=\"wp-block-image\"><img decoding=\"async\" src=\"images\/article-securite-vps-architecture-1600x900.png\" alt=\"Architecture publique et priv\u00e9e de mon VPS\"\/><\/figure>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"576\" src=\"https:\/\/homelab.nvsd.ch\/wp-content\/uploads\/2026\/08\/article-securite-vps-architecture-1600x900-1-1024x576.png\" alt=\"\" class=\"wp-image-44\" srcset=\"https:\/\/homelab.nvsd.ch\/wp-content\/uploads\/2026\/08\/article-securite-vps-architecture-1600x900-1-1024x576.png 1024w, https:\/\/homelab.nvsd.ch\/wp-content\/uploads\/2026\/08\/article-securite-vps-architecture-1600x900-1-300x169.png 300w, https:\/\/homelab.nvsd.ch\/wp-content\/uploads\/2026\/08\/article-securite-vps-architecture-1600x900-1-768x432.png 768w, https:\/\/homelab.nvsd.ch\/wp-content\/uploads\/2026\/08\/article-securite-vps-architecture-1600x900-1-1536x864.png 1536w, https:\/\/homelab.nvsd.ch\/wp-content\/uploads\/2026\/08\/article-securite-vps-architecture-1600x900-1.png 1600w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">V\u00e9rifier UFW sans lui faire une confiance aveugle<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">UFW reste un outil pratique sous Ubuntu pour d\u00e9finir une politique simple de filtrage.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Je commence par v\u00e9rifier son \u00e9tat :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo ufw status verbose<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Je contr\u00f4le \u00e9galement le service :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo systemctl status ufw<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Et, lorsque le comportement me para\u00eet incoh\u00e9rent, je regarde directement les r\u00e8gles de filtrage du syst\u00e8me :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo iptables -L -n\nsudo nft list ruleset<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Lors de cet audit, le comportement d\u2019UFW m\u2019a oblig\u00e9 \u00e0 ne pas m\u2019arr\u00eater au simple message <code>active<\/code> ou <code>inactive<\/code>. Ce qui m\u2019int\u00e9resse r\u00e9ellement est le trafic qui passe ou qui ne passe pas.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Pour un VPS web classique, la logique que je veux conserver est simple :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo ufw default deny incoming\nsudo ufw default allow outgoing<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Les connexions entrantes sont refus\u00e9es par d\u00e9faut, puis j\u2019autorise uniquement les services r\u00e9ellement n\u00e9cessaires.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Pour le web :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo ufw allow 80\/tcp\nsudo ufw allow 443\/tcp<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Si SSH doit encore rester temporairement accessible publiquement :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo ufw allow 22\/tcp<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Et pour le trafic provenant de l\u2019interface Tailscale :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo ufw allow in on tailscale0<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Apr\u00e8s modification :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo ufw reload\nsudo ufw status numbered<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Je garde toujours une session SSH ouverte pendant ces changements afin de pouvoir revenir en arri\u00e8re si une r\u00e8gle bloque une nouvelle connexion.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Le point critique : Docker peut publier un port ind\u00e9pendamment de ce que j\u2019imagine<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">L\u2019audit m\u2019a surtout oblig\u00e9 \u00e0 regarder Docker autrement.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Lorsqu\u2019un fichier Compose contient :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>ports:\n  - \"5001:5001\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Docker publie g\u00e9n\u00e9ralement le port sur toutes les interfaces de l\u2019h\u00f4te.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Je peux alors voir quelque chose comme :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>0.0.0.0:5001-&gt;5001\/tcp<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Ce <code>0.0.0.0<\/code> est important : il signifie que le service \u00e9coute sur toutes les interfaces disponibles.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Je ne consid\u00e8re donc pas UFW comme l\u2019unique m\u00e9canisme de contr\u00f4le de l\u2019exposition des conteneurs. <strong>La s\u00e9curit\u00e9 commence directement dans les fichiers Compose.<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Pour chaque stack, je peux v\u00e9rifier sa configuration finale :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>docker compose config<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Puis contr\u00f4ler les ports actifs :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>docker ps --format \"table {{.Names}}\\t{{.Ports}}\"<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Restreindre un port \u00e0 la machine locale<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Si un service ne doit \u00eatre accessible que depuis le VPS lui-m\u00eame, je peux publier le port uniquement sur <code>127.0.0.1<\/code> :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>ports:\n  - \"127.0.0.1:5001:5001\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Le service reste joignable localement mais n\u2019est plus publi\u00e9 directement sur l\u2019adresse publique.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Ne publier aucun port quand le reverse proxy suffit<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Pour une application web desservie uniquement par Nginx Proxy Manager, une meilleure solution consiste souvent \u00e0 ne publier aucun port h\u00f4te.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Le conteneur rejoint simplement un r\u00e9seau Docker partag\u00e9 avec le reverse proxy :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>services:\n  app:\n    image: mon-image\n    networks:\n      - proxy\n\nnetworks:\n  proxy:\n    external: true<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Nginx Proxy Manager peut alors joindre l\u2019application par le r\u00e9seau Docker interne.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Cette approche simplifie \u00e9norm\u00e9ment l\u2019architecture :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Internet\n   \u2502\n   \u251c\u2500\u2500 80 \/ 443\n   \u2502\n   \u25bc\nNginx Proxy Manager\n   \u2502\n   \u25bc\nr\u00e9seau Docker \"proxy\"\n   \u2502\n   \u251c\u2500\u2500 site A\n   \u251c\u2500\u2500 site B\n   \u2514\u2500\u2500 application C<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Les applications ne doivent plus chacune poss\u00e9der leur propre port public.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Utiliser Tailscale comme r\u00e9seau d\u2019administration<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Pour l\u2019administration du VPS, je pr\u00e9f\u00e8re s\u00e9parer le trafic priv\u00e9 du trafic destin\u00e9 aux visiteurs.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Tailscale me permet de cr\u00e9er ce deuxi\u00e8me r\u00e9seau.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Je v\u00e9rifie d\u2019abord le service :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo systemctl status tailscaled<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Puis l\u2019\u00e9tat du r\u00e9seau :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>tailscale status<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Je r\u00e9cup\u00e8re l\u2019adresse Tailscale du VPS :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>tailscale ip -4<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Et je peux v\u00e9rifier les caract\u00e9ristiques de la connexion :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>tailscale netcheck<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">L\u2019objectif est de d\u00e9placer progressivement les services d\u2019administration vers ce r\u00e9seau priv\u00e9.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Mon architecture cible devient alors :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>INTERNET PUBLIC\n    \u2502\n    \u2514\u2500\u2500 80 \/ 443\n          \u2502\n          \u25bc\n    Nginx Proxy Manager\n          \u2502\n          \u25bc\n    services web publics\n\n\nTAILSCALE\n    \u2502\n    \u251c\u2500\u2500 SSH\n    \u251c\u2500\u2500 Dockge\n    \u251c\u2500\u2500 maintenance\n    \u2514\u2500\u2500 services priv\u00e9s<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Cette s\u00e9paration est plus facile \u00e0 comprendre qu\u2019une longue liste de ports publics prot\u00e9g\u00e9s par des r\u00e8gles diff\u00e9rentes.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Tester Tailscale avant de fermer SSH public<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Je ne ferme jamais l\u2019acc\u00e8s SSH public avant d\u2019avoir valid\u00e9 un autre chemin d\u2019administration.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Depuis une deuxi\u00e8me machine connect\u00e9e \u00e0 Tailscale, je teste par exemple :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>ssh utilisateur@IP_TAILSCALE_DU_VPS<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Je garde la session SSH existante ouverte et j\u2019ouvre une deuxi\u00e8me session.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Si la seconde connexion fonctionne correctement via Tailscale, je sais que je dispose d\u2019un chemin de secours avant de r\u00e9duire l\u2019exposition du port <code>22<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Cette mani\u00e8re de proc\u00e9der est moins spectaculaire, mais elle \u00e9vite le sc\u00e9nario classique : s\u00e9curiser son serveur au point de ne plus pouvoir s\u2019y connecter soi-m\u00eame.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Renforcer SSH avec une cl\u00e9<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">M\u00eame si SSH doit finalement passer par Tailscale, je veux que son authentification reste solide.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Sur mon poste de travail, je peux g\u00e9n\u00e9rer une cl\u00e9 moderne :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>ssh-keygen -t ed25519<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Puis copier la cl\u00e9 publique sur le serveur :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>ssh-copy-id utilisateur@serveur<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Je teste ensuite la connexion dans une nouvelle session.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Lorsque la connexion par cl\u00e9 est confirm\u00e9e, je peux renforcer la configuration SSH.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Dans <code>\/etc\/ssh\/sshd_config<\/code>, les param\u00e8tres importants peuvent notamment \u00eatre :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>PermitRootLogin no\nPasswordAuthentication no\nPubkeyAuthentication yes<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Avant de recharger SSH, je v\u00e9rifie toujours la syntaxe :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo sshd -t<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Puis :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo systemctl reload ssh<\/code><\/pre>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\"><strong>Important :<\/strong> je ne d\u00e9sactive jamais l\u2019authentification par mot de passe avant d\u2019avoir prouv\u00e9 qu\u2019une nouvelle session fonctionne correctement avec la cl\u00e9.<\/p>\n<\/blockquote>\n\n\n\n<h2 class=\"wp-block-heading\">Ajouter Fail2Ban comme couche suppl\u00e9mentaire<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Fail2Ban surveille les journaux du syst\u00e8me et peut bannir temporairement les adresses qui multiplient les tentatives de connexion \u00e9chou\u00e9es.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Installation :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo apt update\nsudo apt install fail2ban<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Je cr\u00e9e ensuite un fichier de configuration local :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo nano \/etc\/fail2ban\/jail.local<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Configuration minimale :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&#91;DEFAULT]\nbantime = 1h\nfindtime = 10m\nmaxretry = 3<\/code><\/pre>\n\n\n<p>[sshd]<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">enabled = true port = ssh maxretry = 3<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Puis j\u2019active le service :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo systemctl enable --now fail2ban<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Je v\u00e9rifie son \u00e9tat :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo fail2ban-client status\nsudo fail2ban-client status sshd<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Au cours de ma session de v\u00e9rification, Fail2Ban avait d\u00e9j\u00e0 enregistr\u00e9 des tentatives de connexion et appliqu\u00e9 un bannissement.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Cela rappelle une r\u00e9alit\u00e9 assez simple : d\u00e8s qu\u2019un service SSH est visible sur Internet, les scans automatis\u00e9s finissent par le trouver.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Fail2Ban reste utile, mais je le consid\u00e8re comme une couche suppl\u00e9mentaire. <strong>Le meilleur trafic hostile est celui qui n\u2019atteint jamais le service parce que celui-ci n\u2019est pas expos\u00e9 publiquement.<\/strong><\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Auditer les conteneurs Docker<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">La s\u00e9curit\u00e9 ne concerne pas uniquement le r\u00e9seau.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Je v\u00e9rifie aussi les conteneurs actifs :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>docker ps<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Puis leur consommation de ressources :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>docker stats --no-stream<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Et l\u2019espace occup\u00e9 par Docker :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>docker system df<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Pour davantage de d\u00e9tails :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>docker system df -v<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Lors de cette session, le cache de build repr\u00e9sentait plusieurs gigaoctets. Un nettoyage cibl\u00e9 permet de r\u00e9cup\u00e9rer de l\u2019espace sans toucher aux donn\u00e9es persistantes.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Je commence de pr\u00e9f\u00e9rence par :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>docker builder prune<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">La commande me demande confirmation avant de supprimer le cache de build inutilis\u00e9.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Il existe \u00e9galement :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>docker system prune<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">mais je l\u2019utilise avec davantage de prudence.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Je n\u2019ex\u00e9cute pas automatiquement une commande agressive comme :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>docker system prune -a --volumes<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">sur un VPS de production.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Les volumes peuvent contenir pr\u00e9cis\u00e9ment les donn\u00e9es que je veux conserver.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">V\u00e9rifier une deuxi\u00e8me fois les ports r\u00e9ellement expos\u00e9s<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Apr\u00e8s les changements, je reprends le diagnostic depuis le d\u00e9but.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Je regarde les ports de l\u2019h\u00f4te :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo ss -tulpn<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Puis ceux de Docker :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>docker ps --format \"table {{.Names}}\\t{{.Ports}}\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Et enfin UFW :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo ufw status verbose<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Je cherche particuli\u00e8rement les publications du type :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>0.0.0.0:PORT-&gt;PORT\/tcp<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Si je ne sais pas imm\u00e9diatement pourquoi un port doit \u00eatre public, je le consid\u00e8re comme un point \u00e0 v\u00e9rifier.<\/p>\n\n\n\n<figure class=\"wp-block-image\"><img decoding=\"async\" src=\"images\/article-securite-vps-audit-1600x900.png\" alt=\"Audit des ports et des couches de s\u00e9curit\u00e9 du VPS\"\/><\/figure>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"576\" src=\"https:\/\/homelab.nvsd.ch\/wp-content\/uploads\/2026\/08\/article-securite-vps-audit-1600x900-1-1024x576.png\" alt=\"\" class=\"wp-image-43\" srcset=\"https:\/\/homelab.nvsd.ch\/wp-content\/uploads\/2026\/08\/article-securite-vps-audit-1600x900-1-1024x576.png 1024w, https:\/\/homelab.nvsd.ch\/wp-content\/uploads\/2026\/08\/article-securite-vps-audit-1600x900-1-300x169.png 300w, https:\/\/homelab.nvsd.ch\/wp-content\/uploads\/2026\/08\/article-securite-vps-audit-1600x900-1-768x432.png 768w, https:\/\/homelab.nvsd.ch\/wp-content\/uploads\/2026\/08\/article-securite-vps-audit-1600x900-1-1536x864.png 1536w, https:\/\/homelab.nvsd.ch\/wp-content\/uploads\/2026\/08\/article-securite-vps-audit-1600x900-1.png 1600w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n\n\n<h2 class=\"wp-block-heading\">La cible que je veux conserver<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>Service<\/th><th>Exposition souhait\u00e9e<\/th><\/tr><\/thead><tbody><tr><td>Nginx Proxy Manager HTTP<\/td><td>Public<\/td><\/tr><tr><td>Nginx Proxy Manager HTTPS<\/td><td>Public<\/td><\/tr><tr><td>Sites web<\/td><td>Via Nginx Proxy Manager<\/td><\/tr><tr><td>Dockge<\/td><td>Priv\u00e9<\/td><\/tr><tr><td>SSH<\/td><td>Tailscale ou acc\u00e8s fortement restreint<\/td><\/tr><tr><td>Bases de donn\u00e9es<\/td><td>R\u00e9seau Docker uniquement<\/td><\/tr><tr><td>API internes<\/td><td>Docker interne ou Tailscale<\/td><\/tr><tr><td>Outils de maintenance<\/td><td>Tailscale<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Ce tableau est volontairement simple. Il m\u2019aide davantage qu\u2019une liste de r\u00e8gles de pare-feu sans contexte.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"576\" src=\"https:\/\/homelab.nvsd.ch\/wp-content\/uploads\/2026\/08\/article-securite-vps-couches-1600x900-1-1024x576.png\" alt=\"\" class=\"wp-image-42\" srcset=\"https:\/\/homelab.nvsd.ch\/wp-content\/uploads\/2026\/08\/article-securite-vps-couches-1600x900-1-1024x576.png 1024w, https:\/\/homelab.nvsd.ch\/wp-content\/uploads\/2026\/08\/article-securite-vps-couches-1600x900-1-300x169.png 300w, https:\/\/homelab.nvsd.ch\/wp-content\/uploads\/2026\/08\/article-securite-vps-couches-1600x900-1-768x432.png 768w, https:\/\/homelab.nvsd.ch\/wp-content\/uploads\/2026\/08\/article-securite-vps-couches-1600x900-1-1536x864.png 1536w, https:\/\/homelab.nvsd.ch\/wp-content\/uploads\/2026\/08\/article-securite-vps-couches-1600x900-1.png 1600w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Penser la s\u00e9curit\u00e9 en couches<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">\u00c0 la fin de cette session, je ne cherche plus un hypoth\u00e9tique \u00e9tat \u00ab serveur s\u00e9curis\u00e9 \u00bb.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Je pr\u00e9f\u00e8re penser en plusieurs couches compl\u00e9mentaires.<\/p>\n\n\n\n<figure class=\"wp-block-image\"><img decoding=\"async\" src=\"images\/article-securite-vps-couches-1600x900.png\" alt=\"Les couches de s\u00e9curit\u00e9 de mon VPS\"\/><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\">Couche 1 \u2014 Infrastructure de l\u2019h\u00e9bergeur<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Le VPS b\u00e9n\u00e9ficie d\u00e9j\u00e0 des m\u00e9canismes d\u2019isolation et des protections r\u00e9seau disponibles chez l\u2019h\u00e9bergeur.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Je ne consid\u00e8re toutefois jamais cette couche comme une excuse pour n\u00e9gliger la configuration du serveur.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Couche 2 \u2014 Ubuntu et UFW<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">UFW applique une politique simple \u00e0 l\u2019h\u00f4te : refuser par d\u00e9faut les connexions entrantes et autoriser uniquement ce qui est n\u00e9cessaire.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Couche 3 \u2014 Docker<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Les fichiers Compose doivent eux-m\u00eames limiter les ports publi\u00e9s.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">C\u2019est une couche fondamentale parce que Docker participe directement \u00e0 la configuration r\u00e9seau du serveur.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Couche 4 \u2014 Nginx Proxy Manager<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Nginx Proxy Manager devient le point d\u2019entr\u00e9e principal des applications web publiques.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Il centralise l\u2019acc\u00e8s HTTP\/HTTPS au lieu d\u2019exposer un port diff\u00e9rent pour chaque application.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Couche 5 \u2014 Tailscale<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Le r\u00e9seau Tailscale transporte les acc\u00e8s administratifs et les services priv\u00e9s.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Couche 6 \u2014 SSH<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">L\u2019authentification par cl\u00e9 r\u00e9duit fortement le risque li\u00e9 aux mots de passe.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Couche 7 \u2014 Fail2Ban et les journaux<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Fail2Ban ajoute une d\u00e9tection simple des tentatives r\u00e9p\u00e9t\u00e9es sur les services qui restent accessibles.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Aucune de ces couches n\u2019est parfaite. Ensemble, elles r\u00e9duisent les cons\u00e9quences d\u2019une erreur isol\u00e9e.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Mon script d\u2019audit rapide<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Pour \u00e9viter d\u2019oublier une v\u00e9rification, j\u2019ai pr\u00e9par\u00e9 un petit script.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>#!\/usr\/bin\/env bash\n\nset -u\n\necho \"======================================\"\necho \"       AUDIT SECURITE DU VPS\"\necho \"======================================\"\n\necho\necho \"&#91;1] UFW\"\nsudo ufw status verbose || true\n\necho\necho \"&#91;2] PORTS EN ECOUTE\"\nsudo ss -tulpn || true\n\necho\necho \"&#91;3] PORTS DOCKER\"\ndocker ps --format \"table {{.Names}}\\t{{.Status}}\\t{{.Ports}}\" || true\n\necho\necho \"&#91;4] FAIL2BAN\"\nsudo fail2ban-client status sshd || true\n\necho\necho \"&#91;5] TAILSCALE\"\ntailscale status || true\n\necho\necho \"&#91;6] ESPACE DOCKER\"\ndocker system df || true\n\necho\necho \"======================================\"\necho \"              FIN AUDIT\"\necho \"======================================\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Je peux l\u2019enregistrer dans :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo nano \/usr\/local\/bin\/security-audit.sh<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Puis le rendre ex\u00e9cutable :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo chmod +x \/usr\/local\/bin\/security-audit.sh<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Et le lancer :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo \/usr\/local\/bin\/security-audit.sh<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Pour l\u2019instant, je pr\u00e9f\u00e8re l\u2019ex\u00e9cuter manuellement. L\u2019objectif n\u2019est pas uniquement de produire un rapport, mais de me forcer \u00e0 lire et \u00e0 comprendre les r\u00e9sultats.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Ce que cette session m\u2019a appris<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Cette journ\u00e9e de v\u00e9rification m\u2019a surtout appris \u00e0 ne pas confondre plusieurs notions.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Un conteneur <code>healthy<\/code> n\u2019est pas n\u00e9cessairement un conteneur correctement expos\u00e9.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Un pare-feu indiqu\u00e9 comme actif ne remplace pas la v\u00e9rification r\u00e9elle des ports.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Un service prot\u00e9g\u00e9 par un mot de passe n\u2019a pas n\u00e9cessairement besoin d\u2019\u00eatre accessible depuis Internet.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Et une interface graphique d\u2019administration peut \u00eatre tr\u00e8s pratique sans pour autant devoir \u00eatre publique.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">J\u2019ai \u00e9galement retenu quatre r\u00e8gles simples :<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>observer avant de modifier ;<\/strong><\/li>\n\n\n\n<li><strong>publier le moins de ports possible ;<\/strong><\/li>\n\n\n\n<li><strong>s\u00e9parer les services publics et l\u2019administration ;<\/strong><\/li>\n\n\n\n<li><strong>tester un nouveau chemin d\u2019acc\u00e8s avant de fermer l\u2019ancien.<\/strong><\/li>\n<\/ol>\n\n\n\n<h2 class=\"wp-block-heading\">Conclusion : r\u00e9duire l\u2019exposition avant d\u2019ajouter de la complexit\u00e9<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Mon VPS n\u2019est pas devenu \u00ab invuln\u00e9rable \u00bb apr\u00e8s cette session.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">En revanche, son architecture est devenue beaucoup plus compr\u00e9hensible.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Je sais mieux quels services doivent \u00eatre publics, lesquels doivent rester priv\u00e9s, pourquoi les ports Docker m\u00e9ritent une attention particuli\u00e8re et comment Tailscale peut devenir le r\u00e9seau d\u2019administration du serveur.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Cette approche me semble plus durable qu\u2019une accumulation de r\u00e8gles.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Pour chaque nouveau service, je peux maintenant me poser les m\u00eames questions :<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Ce port doit-il vraiment \u00eatre public ?<\/li>\n\n\n\n<li>Ce conteneur a-t-il r\u00e9ellement besoin d\u2019un <code>ports:<\/code> ?<\/li>\n\n\n\n<li>Puis-je le joindre uniquement depuis le r\u00e9seau Docker ?<\/li>\n\n\n\n<li>Puis-je l\u2019administrer via Tailscale ?<\/li>\n\n\n\n<li>SSH fonctionne-t-il correctement par cl\u00e9 ?<\/li>\n\n\n\n<li>Est-ce que je v\u00e9rifie la configuration r\u00e9elle apr\u00e8s chaque modification ?<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">La s\u00e9curit\u00e9 de mon Homelab devient ainsi une partie normale de son architecture, et non une op\u00e9ration exceptionnelle r\u00e9alis\u00e9e uniquement lorsqu\u2019un probl\u00e8me appara\u00eet.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Mon VPS fonctionnait, mais je voulais savoir exactement ce qui \u00e9tait r\u00e9ellement expos\u00e9 sur Internet. J\u2019ai donc audit\u00e9 les ports, Docker, UFW, SSH, Fail2Ban et Tailscale avant de r\u00e9duire la surface d\u2019attaque.<\/p>\n","protected":false},"author":1,"featured_media":41,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[11,22,13],"tags":[3,4,24,7,29,28,27,26,23,25,5],"class_list":["post-39","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-docker","category-securite","category-tutoriels","tag-docker","tag-dockge","tag-fail2ban","tag-nginx-proxy-manager","tag-pare-feu","tag-securite","tag-ssh","tag-tailscale","tag-ubuntu-server","tag-ufw","tag-vps"],"_links":{"self":[{"href":"https:\/\/homelab.nvsd.ch\/index.php?rest_route=\/wp\/v2\/posts\/39","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/homelab.nvsd.ch\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/homelab.nvsd.ch\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/homelab.nvsd.ch\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/homelab.nvsd.ch\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=39"}],"version-history":[{"count":4,"href":"https:\/\/homelab.nvsd.ch\/index.php?rest_route=\/wp\/v2\/posts\/39\/revisions"}],"predecessor-version":[{"id":119,"href":"https:\/\/homelab.nvsd.ch\/index.php?rest_route=\/wp\/v2\/posts\/39\/revisions\/119"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/homelab.nvsd.ch\/index.php?rest_route=\/wp\/v2\/media\/41"}],"wp:attachment":[{"href":"https:\/\/homelab.nvsd.ch\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=39"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/homelab.nvsd.ch\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=39"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/homelab.nvsd.ch\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=39"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}