Une lame de serveur noire dans un centre de données plongé dans l’obscurité, enveloppée d’un bouclier hexagonal émeraude lumineux, des particules de lumière vertes qui viennent se briser contre la barrière
Guide de sécurité

Durcir un nouveau VPS pendant la première heure

Une adresse IPv4 publique est sondée quelques minutes après son premier paquet, par des machines qui n’ont jamais entendu parler de vous et qui n’en entendront jamais parler. C’est une bonne nouvelle : presque tout ce qui vise un serveur neuf est générique, et une heure menée avec méthode en déjoue la quasi-totalité. La même heure, faite dans le mauvais ordre, vous enferme dehors d’une machine où personne ne peut se connecter à votre place.

Nous vous remettons un shell root environ 47 secondes après la validation du paiement, puis nous nous arrêtons, délibérément. Nous n’installons aucun agent, nous ne gérons pas votre pare-feu, et nous ne conservons pas de copie de vos identifiants — une clé que nous détenons est une clé qu’on peut nous contraindre à produire, et ce principe traverse toute la plateforme. La conséquence est simple et mérite d’être dite sans détour : la sécurité de votre serveur, c’est l’état dans lequel vous le laissez pendant la première heure.

Ce qui suit, c’est cette heure, dans l’ordre où nous l’exécutons nous-mêmes, sur Debian 13 et Ubuntu 24.04. Deux réglages font l’essentiel du travail. Le reste de cette page existe à cause de trois choses qui paraissent correctes, qui passent la vérification finale de n’importe quel tutoriel, et qui sont pourtant discrètement fausses : une configuration SSH en drop-in silencieusement écrasée, un réglage de port qu’un sshd activé par socket ignore, et un runtime de conteneurs qui publie des ports sous votre pare-feu. Chacune de ces failles a mordu quelqu’un qui avait pourtant bien fait tout le reste.

Ce qui frappe réellement à la porte

Observez journalctl -u ssh sur un serveur en ligne depuis une heure, et le volume a de quoi alarmer la première fois qu’on le voit. Il ne devrait pas. Ce que vous regardez, c’est le bruit de fond de l’internet : une poignée d’opérations de scan, certaines académiques, certaines commerciales, certaines criminelles, qui recensent en continu tout l’espace IPv4 et transmettent les résultats à des robots qui devinent des identifiants. Votre adresse a été atteinte parce qu’elle existe, dans l’ordre numérique, et elle sera de nouveau atteinte dans quelques heures, quoi que vous fassiez.

Cela change utilement la nature du problème. Vous ne vous défendez pas contre un adversaire qui vous a choisi et qui s’adaptera ; vous vous défendez contre un script au répertoire figé — root, admin, ubuntu, test, git, oracle, postgres, et les vingt mille mots de passe apparus dans des fuites de données. Il n’a ni patience, ni créativité, ni le moindre intérêt pour une machine qui ne répond pas du premier coup. Désactiver l’authentification par mot de passe ne ralentit pas cet attaquant. Cela le retire entièrement du jeu.

Deux détails méritent d’être connus. D’abord, IPv6 est nettement plus silencieux, parce qu’un /64 ne peut pas être balayé exhaustivement — mais dès que votre enregistrement AAAA devient public, ou que l’adresse de votre machine apparaît dans un en-tête de mail ou un journal de certificate transparency, le silence prend fin. Ne traitez jamais IPv6 comme une cachette ; traitez-le comme une botte de foin plus petite. Ensuite, une adresse IPv4 a un passé. Elle avait un locataire avant vous, et si ce locataire a mal géré un serveur mail ou hébergé quelque chose qui a fini blacklisté, vous héritez de la réputation jusqu’à ce qu’elle s’efface. Si le mail compte pour vous, vérifiez l’adresse sur les listes noires habituelles avant de construire dessus — cinq minutes de vérification qui évitent une quinzaine de jours à déboguer la délivrabilité.

Les deux réglages qui font quatre-vingt-dix pour cent du travail

Presque toute compromission réelle d’un petit serveur commence à l’un de ces deux endroits : un mot de passe qui pouvait être deviné, ou un service qui écoutait alors qu’il n’aurait pas dû. Les correctifs correspondants sont PasswordAuthentication no et un pare-feu dont la politique par défaut est drop. Ce n’est pas glamour, cela prend quinze minutes à eux deux, et cela vaut plus que tous les autres contrôles de cette page réunis.

La raison est structurelle, pas seulement statistique. Les deux sont fermés par défaut — ils échouent du côté de la sécurité. Un sshd en clés uniquement ne peut pas être attaqué par force brute, quel que soit le nombre de tentatives, parce qu’aucun chemin du code n’accepte un mot de passe. Un pare-feu à refus par défaut protège des services que vous n’avez pas encore installés, y compris la base de données que vous ajouterez dans trois mois en oubliant de la lier à localhost. Tout le reste d’une checklist de durcissement consiste à énumérer les problèmes : une liste de choses précises à désactiver, aussi complète que la liste elle-même — et pas plus.

Alors si vous ne devez lire qu’une seule section avant de fermer l’onglet, lisez celle-ci, effectuez les étapes 2 à 5 ci-dessous, et considérez l’heure comme bien employée. Le reste est réellement utile, et réellement secondaire.

Où vit la configuration SSH aujourd’hui, et le piège qu’elle contient

Sur Debian 13 et Ubuntu 24.04, /etc/ssh/sshd_config s’ouvre sur une ligne Include /etc/ssh/sshd_config.d/*.conf, et les images cloud livrent un fichier dans ce répertoire — généralement 50-cloud-init.conf — qui définit déjà PasswordAuthentication. Modifier le fichier principal et ajouter vos propres directives à la fin semble naturel, et produit une configuration qui ne fait pas ce qu’elle prétend faire.

La règle est la chose qui, à propos de sshd, surprend presque tout le monde : pour chaque mot-clé, c’est la première valeur obtenue qui l’emporte. C’est l’inverse de presque tous les autres systèmes de fusion de configuration que vous avez utilisés. L’Include se trouve près du début du fichier principal, si bien qu’un drop-in est lu avant le corps de sshd_config — et entre les drop-in, c’est l’ordre alphabétique qui tranche. Un fichier nommé 10-hardening.conf l’emporte sur 50-cloud-init.conf. Un fichier nommé 60-hardening.conf perd face à lui, silencieusement, et un redémarrage qui annonce un succès ne vous l’apprendra pas.

C’est pourquoi il ne faut jamais faire confiance au fichier que vous venez d’écrire. Demandez au démon ce qu’il a réellement retenu :

sshd -t                 # syntax check — silence means valid
sshd -T | grep -Ei '^(permitrootlogin|passwordauthentication|pubkeyauthentication|kbdinteractive|allowusers|port) '

sshd -T affiche la configuration effective une fois chaque inclusion, chaque surcharge et chaque valeur par défaut résolues. Si cette sortie indique passwordauthentication yes, alors l’authentification par mot de passe est active, quoi que prétende n’importe quel fichier sur le disque. Rien d’autre ne compte comme vérification.

Le second piège appartient à la même famille. Ubuntu 24.04 démarre sshd par activation par socket : ssh.socket possède le port d’écoute, et Port dans sshd_config est entièrement ignoré. Vérifiez avec systemctl is-enabled ssh.socket ; s’il est activé et que vous voulez un port différent, changez-le avec systemctl edit ssh.socket et une surcharge ListenStream= — pas dans sshd_config, où le réglage aura l’air correct sans rien faire du tout.

Changer le port 22 : du théâtre, mais un théâtre pas cher

Soyons honnêtes sur ce point, parce que l’internet, lui, ne l’est pas. Déplacer sshd sur le port 2222 ou 47000 n’arrête aucun attaquant compétent. Un scan TCP complet d’un hôte prend quelques secondes, le service s’annonce lui-même dans sa bannière, et quiconque a décidé de vous attaquer vous en particulier le trouvera avant d’avoir fini son café.

Ce que cela fait réellement, c’est réduire votre journal d’authentification d’environ quatre-vingt-quinze pour cent, et cela a une vraie valeur : c’est la différence entre des journaux que vous survolez et des journaux que vous lisez pour de bon. Un signal visible vaut mieux qu’un signal enterré. Si vous surveillez quoi que ce soit, un auth.log silencieux est le moyen le plus économique de rendre une anomalie visible.

Les coûts sont faibles mais réels, et c’est pour cela que nous appelons cela optionnel plutôt que recommandé. Vous oublierez le port quand vous reviendrez dans huit mois. Un port non standard au-delà de 1024 peut, en principe, être accaparé par un processus non privilégié si sshd venait à ne plus écouter. Des pare-feux sortants restrictifs sur les réseaux clients bloquent les ports inhabituels, si bien que vous serez parfois incapable de joindre votre propre serveur depuis un bureau ou un hôtel. Et sur un système activé par socket, il faut le changer au bon endroit, comme vu dans la section précédente. Faites-le si des journaux silencieux comptent pour vous. Ne le faites pas en pensant que cela suffit à vous sécuriser, et ne le faites jamais à la place du clés-uniquement.

Refus par défaut, et le jeu de règles que nous utilisons réellement

Un pare-feu qui liste ce qu’il faut bloquer est un système de classement. Un pare-feu qui liste ce qu’il faut autoriser est un contrôle de sécurité. Cette distinction, c’est tout le jeu, parce que seul le second couvre le service que vous installerez le mois prochain, le port de debug ouvert un vendredi, et le conteneur qui a décidé de s’exposer tout seul.

Sur notre plateforme, vous disposez de deux couches, indépendantes l’une de l’autre. Le filtre en périphérie est optionnel, configuré par serveur dans le panneau et appliqué au niveau de l’hyperviseur, si bien que le trafic bloqué n’atteint jamais votre machine invitée — utile pour les règles de couche 4 que vous voulez appliquer avant que votre noyau n’y consacre le moindre cycle, et pour empêcher qu’une machine compromise reste joignable pendant que vous travaillez dessus. Le pare-feu invité est entièrement le vôtre : nftables, iptables, pf, ce que votre image fournit. Nous n’y touchons jamais. Utilisez les deux si la machine compte vraiment ; utilisez au moins le second, toujours.

Deux points du jeu de règles de l’étape 5 méritent une explication, car la plupart des jeux de règles copiés-collés se trompent dessus. Ne rejetez pas tout l’ICMP. Cela paraît propre, mais cela casse la découverte du MTU de chemin, ce qui produit la pire catégorie de bug qui soit : les petites requêtes fonctionnent, les grandes réponses restent bloquées, et rien dans vos logs ne mentionne le pare-feu. Acceptez au minimum destination-unreachable, time-exceeded et parameter-problem. Ne filtrez pas l’ICMPv6 par type sans connaître précisément la liste. IPv6 s’appuie sur l’ICMPv6 pour la découverte de voisins et l’annonce de routeurs ; le bloquer largement tue votre connectivité IPv6 d’une manière qui ressemble à un problème de routage. Accepter tout l’ICMPv6 sur un hôte unique est un compromis raisonnable, et c’est exactement ce que fait le jeu de règles ci-dessous.

Un avertissement avant d’exécuter flush ruleset : si Docker est installé, cette ligne supprime les règles NAT et filter que Docker a écrites, et la mise en réseau des conteneurs s’arrête jusqu’à ce que systemctl restart docker les remette en place. Chargez d’abord votre jeu de règles, redémarrez Docker ensuite, et lisez la section suivante avant de publier le moindre port de conteneur.

Les ports dont vous ignoriez qu’ils étaient ouverts

Demandez à la machine ce qu’elle écoute. Pas ce que vous croyez avoir installé — ce qui est réellement lié en ce moment :

ss -tulpn | grep -vE '127\.0\.0\.1|\[::1\]'

Tout ce qui survit à ce filtre est joignable depuis un autre endroit que la machine elle-même. Sur une image standard, on trouve généralement rpcbind sur le port 111 (rien de ce que vous faites tourner n’en a besoin), un exim4 resté à l’écoute depuis l’installation de base, et un stub systemd-resolved inoffensif sur loopback mais transformé en résolveur ouvert s’il ne l’est pas. Les cas dangereux arrivent plus tard, avec les logiciels que vous avez installés volontairement : PostgreSQL sur 0.0.0.0:5432 parce qu’un tutoriel disait de modifier listen_addresses, Redis sans mot de passe parce que Redis n’en a pas eu par défaut pendant la majeure partie de son existence, un nœud Elasticsearch, un exporter Prometheus, un notebook Jupyter. Chacun de ces cas a servi, de très nombreuses fois, de première étape à une compromission réelle.

Voici maintenant le piège qui attrape les gens prudents. Docker ne demande pas la permission à votre pare-feu. Quand vous écrivez -p 5432:5432, Docker insère des règles DNAT dans la chaîne PREROUTING de la table nat, que le noyau évalue avant même que votre chaîne input ne voie le paquet ; le trafic est ensuite redirigé vers le conteneur. Votre politique input à refus par défaut n’est jamais consultée, ufw status affiche le port comme fermé, et la base de données se retrouve sur l’internet public. C’est un comportement de Docker documenté, en place depuis dix ans, et il a exposé un nombre considérable de bases de données.

Le correctif tient en une chaîne de caractères, et c’est l’habitude à prendre :

# wrong — reachable from the entire internet, whatever your firewall says
docker run -p 5432:5432 postgres

# right — bound to loopback, reachable only through an SSH tunnel or a proxy
docker run -p 127.0.0.1:5432:5432 postgres

Enfin, demandez un avis extérieur. Un scan de port depuis le serveur vous dit ce que pense le noyau ; un scan depuis ailleurs vous dit ce que voit le monde, et c’est le seul chiffre qui compte. Lancez nmap -Pn -p- <your-ip> depuis votre ordinateur portable, pas depuis la machine, et comparez le résultat à la liste que vous attendiez.

Mises à jour automatiques : activez-les, et pour de bon

La fenêtre qui fait tomber les petits serveurs n’est pas le zero-day. C’est les quatre semaines qui séparent la publication d’une CVE accompagnée d’une preuve de concept fonctionnelle, et le prochain moment où vous vous connecterez par hasard. Les outils d’attaque absorbent une nouvelle faille distante en quelques jours ; une machine que vous corrigez « quand vous aurez le temps » reste exposée pendant tout cet intervalle, et tout opérateur honnête sait combien cet intervalle dure réellement.

L’objection aux mises à jour automatiques, c’est la peur de la casse, et elle mérite d’être prise au sérieux — puis d’être circonscrite. Limitez l’automatisation à la section sécurité de votre distribution, là où les mainteneurs rétroportent des correctifs dans la version déjà packagée au lieu de livrer de nouvelles versions amont. Une mise à jour de sécurité Debian pour openssl est une build corrigée de la même version que celle que vous faites déjà tourner ; le risque qu’elle change le comportement est bien plus faible que celui de laisser une brèche distante ouverte pendant un mois. Les montées en version fonctionnelles restent manuelles, là où elles doivent rester.

Ce que les gens omettent, c’est le redémarrage. Une libssl corrigée sur le disque ne change rien pour le processus qui a chargé l’ancienne au démarrage, et une mise à jour du noyau ne sert à rien tant que vous n’avez pas redémarré dessus. Acceptez soit un redémarrage sans surveillance dans une fenêtre que vous choisissez, soit installez needrestart et laissez-le vous dire quels services tournent encore avec des bibliothèques supprimées — mais faites l’un des deux. « Les mises à jour automatiques sont activées » alors que uptime affiche 340 jours est une illusion confortable, et c’est la plus fréquente que nous rencontrions.

fail2ban, CrowdSec, ou rien du tout

fail2ban lit vos journaux, repère les échecs répétés depuis une adresse et la bannit pour un moment. CrowdSec fait la même chose et partage les verdicts au sein d’un réseau de participants, si bien que vous pouvez bloquer une adresse qui s’est mal comportée ailleurs avant même qu’elle ne vous atteigne. Ce sont deux bons logiciels. Aucun des deux ne fait ce que la plupart des gens croient sur un serveur SSH en clés uniquement.

Une fois PasswordAuthentication à no, une attaque par force brute sur SSH ne peut pas réussir. Pas « elle a peu de chances de réussir » : il n’existe aucun chemin de code qui le permette. Bannir une adresse après cinq échecs ne prévient donc rien ; cela réduit le volume des journaux et une quantité négligeable de CPU. C’est un bénéfice réel, ce n’est simplement pas un bénéfice de sécurité, et le compromis n’est pas gratuit : une jail mal écrite qui surveille le mauvais journal a verrouillé davantage d’administrateurs hors de leurs propres serveurs qu’elle n’a jamais bloqué d’attaquants.

Ces outils trouvent réellement leur place un cran plus haut, sur les services que vous exposez pour de bon : un formulaire de connexion, une administration WordPress, une API avec limitation de débit, un serveur mail avec SMTP AUTH. Ceux-là acceptent bel et bien des mots de passe, ils sont vulnérables à la force brute, et une liste de bannissement est exactement le bon contrôle. Notre verdict est donc précis et restreint. Faites l’impasse sur la jail SSH. Placez CrowdSec ou fail2ban devant ce qui possède un champ mot de passe. Et quel que soit votre choix, mettez d’abord votre propre adresse de gestion en liste blanche — ignoreip existe pour la soirée dont vous vous souviendriez sinon pour de mauvaises raisons.

Faire en sorte que la machine vous prévienne quand elle change

La prévention, c’est ce que vous pouvez faire en une heure. La détection, c’est ce qui vous dit que cette heure n’a pas suffi. Elle n’a pas besoin d’être sophistiquée, et sur un serveur unique, trois mesures peu coûteuses couvrent l’essentiel du terrain.

Conservez les journaux. Sur de nombreuses images, le journal vit dans /run et s’évapore au redémarrage, ce qui signifie que la trace d’un incident disparaît au redémarrage qui le suit. Créer /var/log/journal est un correctif d’une seule ligne, et c’est la mesure la plus rentable de cette section.

Soyez averti des connexions. Une ligne dans /etc/ssh/sshrc qui déclenche logger à chaque ouverture de session ne coûte rien et vous donne une trace propre, facile à grepper, séparée du bavardage habituel de sshd. Un piège attrape souvent les gens : sshd exécute ~/.ssh/rc à la place de /etc/ssh/sshrc quand l’utilisateur en a un, si bien que le fichier global du système est sauté précisément pour le compte le plus susceptible d’avoir un dotfile personnalisé. Si vous avez besoin d’un hook qui ne puisse pas être masqué, utilisez plutôt pam_exec.

Sachez à quoi ressemblait le système de fichiers le premier jour. AIDE enregistre les empreintes de vos binaires, bibliothèques et fichiers unit, et signale ce qui a changé depuis. Le piège est fondamental et généralement ignoré : une base stockée sur la machine qu’elle surveille peut être régénérée par celui qui l’a compromise, et le contrôle indiquera alors « aucun changement » pour toujours. Copiez la base hors de la machine, ou à défaut enregistrez son empreinte ailleurs, et l’outil devient digne de ses vingt minutes. Laissé en l’état, ce n’est qu’un réconfort illusoire.

Le principe général mérite d’être énoncé à part : le journal auquel vous ne pouvez pas faire confiance est celui qui se trouve sur la machine compromise. Tout ce sur quoi vous comptez réellement — un journal, une base d’intégrité, une sauvegarde — devrait avoir une copie quelque part que ce même attaquant ne contrôle pas. Un second petit serveur dans une autre juridiction est une réponse légitime à cela, et un serveur à cinq dollars suffit.

Ce que cela ne fait pas

Tout ce qui précède relève de la sécurité périmétrique, et il vaut la peine d’en préciser les limites pour ne pas confondre une porte d’entrée verrouillée avec un coffre-fort.

Cela ne protège pas les données sur une machine en fonctionnement. Un noyau durci et un pare-feu fermé n’ont aucune importance pour quiconque a accès à l’hyperviseur, ni pour le contenu de la RAM tant que la machine tourne. Si votre préoccupation est le disque quand le serveur est éteint, saisi ou retiré du service, cela relève du chiffrement de disque, une procédure différente avec un mode d’échec différent — voir chiffrer un disque VPS avec LUKS.

Cela ne vous rend pas anonyme. Un serveur peut être parfaitement durci et pourtant révéler qui le possède, via un enregistrement WHOIS, une clé SSH réutilisée, un tag analytics, un client SSH qui se connecte depuis chez vous sans rebond, ou un certificat qui relie deux identités entre elles. Payer en Monero puis se connecter depuis sa propre adresse annule ce paiement. Ce mode d’échec a sa propre page : les erreurs qui vous déanonymisent.

Cela ne corrige pas votre application. Une injection SQL, un point d’accès admin non authentifié ou une dépendance contenant une porte dérobée se moquent totalement de vos réglages sshd. La plupart des compromissions de serveurs par ailleurs bien tenus passent par le port que vous avez ouvert délibérément, exprès, pour un service que vous avez écrit ou installé.

Ce n’est pas une sauvegarde. Un ransomware, un rm -rf avec une variable qui était vide, et une mise à jour ratée finissent tous de la même façon. Faites une copie, mettez-la ailleurs, et restaurez-la une fois avant d’en avoir besoin — la même discipline qui rend une migration vers un autre hébergeur supportable rend un mauvais mardi supportable.

Rien de tout cela ne plaide contre cette heure. Cela plaide pour savoir exactement ce que cette heure vous a acheté.

  1. Déployez avec une clé, jamais avec un mot de passe

    Générez la paire de clés sur votre propre machine et collez la partie publique dans le formulaire de déploiement ; l’image l’installera et vous n’aurez jamais de mot de passe root à perdre. Si vous déployez depuis une image marquée cloud-init, vous pouvez à la place transmettre toute la configuration de la première heure en tant que user-data — jusqu’à 64 Kio — et la machine démarre déjà durcie.

    # on your laptop, not on the server
    ssh-keygen -t ed25519 -a 100 -C "$(whoami)@laptop-cvh"
    cat ~/.ssh/id_ed25519.pub        # paste this into the deploy form

    Utilisez ed25519, sauf si un élément de votre chaîne d’outils le refuse, auquel cas RSA en 4096 bits convient. Protégez la clé privée par une phrase de passe et chargez-la dans un agent ; un fichier de clé sans phrase de passe sur un ordinateur portable revient à un mot de passe écrit sur l’écran. Le même jeu de clés est injecté dans le mode rescue, ce qui est précisément ce qui rend l’étape 4 récupérable.

  2. Ouvrez la seconde session avant de toucher à quoi que ce soit

    Ce n’est ni optionnel, ni de la paranoïa. À partir d’ici, vous allez modifier le démon par lequel vous êtes connecté et le pare-feu qui vous permet de l’atteindre. Gardez une session ouverte et inactive comme corde de rappel vers la machine, et faites chaque modification dans une autre. Si une modification est mauvaise, la session déjà ouverte reste authentifiée et peut l’annuler ; une nouvelle connexion, elle, serait refusée.

    # terminal A — the rope. Log in and leave it alone.
    ssh -i ~/.ssh/id_ed25519 root@<server-ip>
    
    # terminal B — where every command below runs.
    ssh -i ~/.ssh/id_ed25519 root@<server-ip>

    Testez chaque modification en ouvrant une troisième connexion, jamais en vous reconnectant à celle sur laquelle vous travaillez. Si la troisième connexion échoue, il vous reste tout de même deux shells fonctionnels et un problème, pas une crise.

  3. Créez le compte que vous utiliserez pour de bon

    Root en SSH est pratique pendant exactement une heure. Ensuite, un compte nominatif avec sudo vous donne une piste d’audit, vous protège d’une commande mal tapée qui s’exécuterait avec tous les privilèges, et vous permet de désactiver un compte compromis sans désactiver la machine.

    adduser --disabled-password --gecos "" deploy
    install -d -m 0700 -o deploy -g deploy /home/deploy/.ssh
    cp /root/.ssh/authorized_keys /home/deploy/.ssh/authorized_keys
    chown deploy:deploy /home/deploy/.ssh/authorized_keys
    chmod 0600 /home/deploy/.ssh/authorized_keys
    usermod -aG sudo deploy
    
    # a long random password, stored in your manager, used only by sudo
    passwd deploy

    Définissez ce mot de passe. Un compte sudo dont le mot de passe n’a jamais été défini ne peut pas utiliser sudo, et le découvrir après avoir désactivé la connexion root est la façon classique de se retrouver enfermé dehors alors que tous les fichiers sont pourtant corrects. Vérifiez avant d’aller plus loin : depuis un nouveau terminal, ssh deploy@<server-ip>, puis sudo -v. Les deux doivent fonctionner.

  4. Écrivez le drop-in SSH, puis demandez au démon ce qu’il a lu

    Un drop-in numéroté qui se trie tôt l’emporte sur ce que livre l’image cloud, conformément à la règle d’ordre vue plus haut. Écrivez-le, validez la syntaxe, relisez la configuration effective, et rechargez seulement ensuite.

    cat > /etc/ssh/sshd_config.d/10-hardening.conf <<'EOF'
    PermitRootLogin no
    PasswordAuthentication no
    KbdInteractiveAuthentication no
    PubkeyAuthentication yes
    AuthenticationMethods publickey
    PermitEmptyPasswords no
    AllowUsers deploy
    MaxAuthTries 3
    LoginGraceTime 20
    X11Forwarding no
    AllowAgentForwarding no
    AllowTcpForwarding no      # remove this line if you use ssh -L / -D tunnels
    ClientAliveInterval 300
    ClientAliveCountMax 2
    EOF
    chmod 0644 /etc/ssh/sshd_config.d/10-hardening.conf
    
    sshd -t                                    # silence means the syntax is valid
    sshd -T | grep -Ei '^(permitrootlogin|passwordauthentication|allowusers) '
    systemctl reload ssh

    Lisez cette sortie de sshd -T avant de recharger, pas après. Elle doit indiquer permitrootlogin no et passwordauthentication no. Si ce n’est pas le cas, votre fichier est écrasé par un autre qui se trie avant lui — faites ls /etc/ssh/sshd_config.d/ et renommez le vôtre plus bas dans l’ordre. Ouvrez maintenant un troisième terminal et connectez-vous en tant que deploy. Ce n’est que lorsque cela fonctionne que vous devez refermer la corde de rappel.

  5. Chargez un pare-feu à refus par défaut

    nftables est fourni avec les deux distributions et remplace le jeu de règles iptables par un fichier unique et lisible. Ajustez les ports acceptés aux services que vous faites réellement tourner — la liste ci-dessous suppose SSH et un serveur web, rien d’autre.

    apt install -y nftables
    cat > /etc/nftables.conf <<'EOF'
    #!/usr/sbin/nft -f
    flush ruleset
    
    table inet filter {
      chain input {
        type filter hook input priority filter; policy drop;
    
        ct state established,related accept
        ct state invalid drop
        iif lo accept
    
        # ICMP must live: dropping it blackholes path-MTU discovery.
        ip protocol icmp icmp type { echo-request, echo-reply, destination-unreachable, time-exceeded, parameter-problem } accept
        ip6 nexthdr icmpv6 accept
    
        tcp dport 22 ct state new accept
        tcp dport { 80, 443 } ct state new accept
    
        limit rate 5/minute burst 5 packets log prefix "nft-drop: " level info
      }
      chain forward { type filter hook forward priority filter; policy drop; }
      chain output  { type filter hook output  priority filter; policy accept; }
    }
    EOF
    
    nft -c -f /etc/nftables.conf              # check before you commit to it
    systemctl enable --now nftables
    nft list ruleset | head -40

    Si Docker est installé, flush ruleset supprime aussi les règles de Docker : lancez ensuite systemctl restart docker et vérifiez que vos conteneurs répondent toujours. Puis, depuis votre ordinateur portable, nmap -Pn -p- <server-ip> et vérifiez que la liste des ports ouverts correspond exactement à ceux que vous venez d’autoriser — ni plus, ni moins.

  6. Activez les mises à jour de sécurité sans surveillance

    Uniquement la section sécurité, avec une fenêtre de redémarrage que vous avez choisie plutôt qu’une que vous repousserez indéfiniment. Choisissez une heure calme pour vos utilisateurs, et pas pile à l’heure, pour ne pas entrer en collision avec le cron de tout le monde.

    apt install -y unattended-upgrades needrestart
    cat > /etc/apt/apt.conf.d/51-cvh-auto <<'EOF'
    // Debian. On Ubuntu, replace the two Origins-Pattern lines with:
    //   "${distro_id}:${distro_codename}-security";
    Unattended-Upgrade::Origins-Pattern {
      "origin=Debian,codename=${distro_codename}-security,label=Debian-Security";
      "origin=Debian,codename=${distro_codename},label=Debian-Security";
    };
    Unattended-Upgrade::Automatic-Reboot "true";
    Unattended-Upgrade::Automatic-Reboot-WithUsers "false";
    Unattended-Upgrade::Automatic-Reboot-Time "04:17";
    Unattended-Upgrade::Remove-Unused-Kernel-Packages "true";
    APT::Periodic::Update-Package-Lists "1";
    APT::Periodic::Unattended-Upgrade "1";
    EOF
    
    unattended-upgrade --dry-run --debug | tail -25
    systemctl status unattended-upgrades --no-pager

    Si un redémarrage sans surveillance est réellement inacceptable, réglez-le sur "false" et laissez needrestart -b indiquer quels services tournent encore avec des bibliothèques supprimées et si un noyau plus récent est installé — puis agissez en conséquence. Ce qui n’est pas acceptable, c’est de ne faire ni l’un ni l’autre.

  7. Fermez ce qui écoute, et vérifiez depuis l’extérieur

    Recensez ce qui est lié à autre chose que loopback, supprimez ce que vous n’utilisez pas, et confirmez le résultat depuis une machine qui n’est pas celle-ci.

    ss -tulpn | grep -vE '127\.0\.0\.1|\[::1\]'
    
    # the usual leftovers on a base image
    systemctl disable --now rpcbind.socket rpcbind 2>/dev/null
    apt purge -y exim4-base exim4-config exim4-daemon-light 2>/dev/null
    apt autoremove --purge -y
    
    # what each running unit is allowed to reach
    systemd-analyze security --no-pager | head -20

    Pour tout ce qui doit continuer à tourner mais n’a besoin d’aucun public — une base de données, un cache, un panneau d’administration, un exporter de métriques — liez-le à 127.0.0.1 dans sa propre configuration et atteignez-le via un tunnel SSH : ssh -L 5432:127.0.0.1:5432 deploy@<server-ip>. C’est strictement meilleur que d’ouvrir un port en espérant que l’authentification propre au service tienne la route, et c’est le réflexe à adopter par défaut.

  8. Enregistrez la référence et armez les alarmes

    Dix minutes qui ne rapportent que le jour où quelque chose tourne mal — soit précisément le jour où vous serez incapable de reconstituer quoi que ce soit de mémoire.

    # persistent journal, so a reboot stops erasing the evidence
    mkdir -p /var/log/journal
    systemd-tmpfiles --create --prefix /var/log/journal
    systemctl restart systemd-journald
    
    # a clean, greppable line per SSH session
    cat > /etc/ssh/sshrc <<'EOF'
    logger -t ssh-login "user=$USER from=${SSH_CONNECTION%% *} tty=${SSH_TTY:-none}"
    EOF
    
    # file-integrity baseline — then get the database off the machine
    apt install -y aide
    aideinit
    mv /var/lib/aide/aide.db.new /var/lib/aide/aide.db
    sha256sum /var/lib/aide/aide.db     # copy this hash somewhere else
    
    last -n 20 ; lastb -n 20            # who got in, and who tried

    Terminez en notant, hors du serveur, quatre choses : l’empreinte de la clé d’hôte SSH obtenue via ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub, l’empreinte AIDE ci-dessus, les ports que vous avez délibérément ouverts, et l’endroit où vit la clé privée. Cette note est ce qui transforme une mauvaise matinée en simple checklist plutôt qu’en enquête.

Comparatif

Les contrôles, classés selon ce qu’ils vous apportent réellement

Chaque contrôle de la première heure confronté aux deux questions qui décident s’il mérite sa place : ce qu’il arrête vraiment, et ce qu’il n’arrête pas — quoi qu’en laissent entendre les tutoriels.
ContrôleCe qu’il arrêteCe qu’il n’arrête pasCoûtVerdict
SSH en clés uniquement (PasswordAuthentication no)Tous les robots qui devinent des identifiants sur internet, en permanence et par constructionQuiconque détient votre clé privée, ou une faille dans sshd lui-même5 minutes, une foisNon négociable
Pare-feu à refus par défaut (nftables)Les services dont vous aviez oublié qu’ils écoutaient, et tous ceux que vous installerez par accident plus tardLes attaques contre les ports que vous avez ouverts délibérément10 minutes, une foisNon négociable
Mises à jour de sécurité sans surveillanceLa fenêtre n-day — les semaines entre un exploit public et votre prochaine connexionLes zero-days, et tout ce qui nécessite un redémarrage que vous ne planifiez jamais5 minutes plus une fenêtre de redémarrageNon négociable
Utilisateur nominatif avec sudo plutôt que rootLes commandes mal tapées exécutées avec tous les privilèges, et les processus qui tournent en root sans raisonUne élévation de privilèges locale entre les mains de quelqu’un déjà présent sur la machine3 minutesÇa vaut le coup
Référence d’intégrité des fichiers (AIDE)Les modifications silencieuses des binaires système, bibliothèques et fichiers unitAbsolument rien, si la base reste sur la machine qu’elle surveille20 minutes plus un stockage externeÇa vaut le coup si la machine compte
Déplacer SSH hors du port 22Environ 95 % du volume de votre journal d’authentificationQuiconque lance un scan de port, c’est-à-dire tous ceux qui comptent2 minutes, plus la soirée où vous oublierez le portOptionnel, pour des journaux silencieux
fail2ban / CrowdSec sur sshdLes récidivistes, et le CPU qu’ils vous font perdreRien, une fois l’authentification par mot de passe désactivée10 minutes, plus un vrai risque de verrouillageÀ éviter sur SSH ; à utiliser sur votre application
FAQ

Questions qui méritent une réponse

Combien de temps après le déploiement les scans commencent-ils ?

Quelques minutes, en général. Des scanners balayent en continu tout l’espace IPv4, si bien qu’une adresse neuve est atteinte au passage suivant, quoi qu’elle fasse tourner. Considérez que vous êtes sondé dès l’instant où la machine répond à son premier paquet — c’est précisément pour cela que le durcissement se joue dans la première heure, pas le premier week-end.

Ai-je encore besoin de fail2ban si la connexion par mot de passe est désactivée ?

Sur SSH, non. Avec PasswordAuthentication no, aucun chemin de code ne peut être gagné par force brute, donc bannir après cinq échecs ne prévient rien — cela réduit le bruit dans les journaux, ce qui a une valeur, mais ce n’est pas de la sécurité. Ces outils trouvent réellement leur place devant tout ce qui accepte véritablement un mot de passe : un formulaire de connexion web, un panneau d’administration, SMTP AUTH. Si vous en installez un, mettez d’abord votre propre adresse en liste blanche.

ufw, firewalld ou nftables ?

N’importe lequel des trois, tant que la politique par défaut est drop et que vous comprenez ce que vous avez déployé. ufw est le plus convivial, et écrit en dessous des règles nftables sur les versions actuelles de Debian et Ubuntu. nftables seul tient dans un fichier unique et lisible, facile à comparer et à versionner, c’est pourquoi nous l’utilisons. Le choix compte bien moins que la politique par défaut — et aucun des trois ne change le fait que Docker publie ses ports en dessous d’eux tous.

Faut-il déplacer SSH hors du port 22 ?

Uniquement pour des journaux plus silencieux. Cela n’arrête aucun attaquant compétent — un scan complet prend quelques secondes et la bannière identifie le service. Cela retire en revanche l’essentiel du bruit de votre journal d’authentification, ce qui rend les vraies anomalies visibles. Traitez cela comme de l’hygiène de journaux, jamais comme un contrôle, et vérifiez si votre sshd est activé par socket avant de le changer : sur Ubuntu 24.04, la directive Port dans sshd_config est ignorée, et le port appartient à ssh.socket.

Les mises à jour automatiques vont-elles casser mon site à quatre heures du matin ?

Limitées à la section sécurité, très rarement. Ces paquets sont des correctifs rétroportés sur la version que vous faites déjà tourner, pas de nouvelles versions amont. Le risque réaliste, c’est le redémarrage, pas le correctif — choisissez donc vous-même la fenêtre, réglez Automatic-Reboot-WithUsers sur false pour qu’il attende tant que quelqu’un est connecté, et si le service ne peut vraiment pas redémarrer sans surveillance, lancez needrestart -b sur un calendrier régulier et agissez selon ce qu’il rapporte. La seule position indéfendable, c’est des mises à jour automatiques avec un uptime qui se compte en années.

Je me suis enfermé dehors. Quelles sont mes options ?

Démarrez le serveur en mode rescue depuis le panneau. Cela lance un environnement Alpine en RAM avec les mêmes clés SSH que votre compte, et votre disque démonté sur /dev/vda ; vous pouvez donc le monter, corriger le drop-in sshd ou le fichier du pare-feu, le démonter et redémarrer. Le démarrage en rescue lui-même ne touche à rien sur le disque. L’empreinte de la clé d’hôte du rescue diffère de celle habituelle — c’est normal, pas une interception.

Est-ce une bonne idée de faire tourner Lynis ou un script de durcissement CIS à la place de tout ceci ?

Comme audit, oui ; comme substitut, non. Lynis fournit un second avis réellement utile et trouvera des choses que cette page ne mentionne pas. Les scripts de remédiation CIS automatisés relèvent d’une tout autre proposition : ils appliquent des centaines de modifications conçues pour un parc de postes de travail en entreprise, dont plusieurs cassent un serveur de façons difficiles à retracer des semaines plus tard. Faites d’abord cette heure à la main, pour comprendre ce que fait votre machine, puis lancez un outil d’audit et lisez ses résultats un par un.

Le durcissement rend-il mon serveur anonyme ?

Non, et confondre les deux est une erreur courante et coûteuse. Le durcissement contrôle qui peut entrer ; l’anonymat contrôle qui peut savoir que le serveur est le vôtre. Un serveur parfaitement verrouillé laisse quand même fuiter son propriétaire via un enregistrement WHOIS, une clé SSH réutilisée, un tag analytics ou une connexion admin depuis votre adresse personnelle. Payer en Monero puis se connecter directement depuis votre propre connexion annule entièrement ce paiement — c’est traité dans les erreurs qui vous déanonymisent.

Deploy your offshore server.

Choisissez une région. Choisissez un plan. Collez une clé. Payez. Les 47 prochaines secondes sont pour nous.