Aller au contenu

Préparation d'un serveur Debian 13 (Trixie)

État du document

Rédigé le 19/09/2026 pour Debian 13 « Trixie », révisé le 20/09/2026 après relecture croisée avec les guides de référence du domaine. Il remplace l'ancienne fiche Debian 11 : ce qui a changé est résumé en fin de page.

Avant de commencer

Cette procédure part d'une installation minimale de Debian 13 « Trixie » fraîchement livrée par un hébergeur, et s'arrête au moment où le serveur est prêt à recevoir ses services.

Comptez une heure en lisant vraiment, une demi-heure si vous connaissez déjà le terrain. Rien ici ne se rattrape en vitesse : les quatre étapes qui touchent à l'accès peuvent vous enfermer dehors.

Contre quoi vous vous protégez

Un serveur qui vient d'être livré n'est pas tranquille : son adresse IP appartient à des plages publiques que des robots balaient en permanence. Les premières tentatives de connexion arrivent dans les minutes qui suivent la mise en ligne, avant même que vous ayez fini de le configurer. Elles ne vous visent pas : elles essaient root, admin, test, avec les mots de passe des fuites connues, sur des millions d'adresses.

Trois familles d'ennuis, et ce que cette fiche oppose à chacune :

Ce qui arrive vraiment Ce qui l'en empêche
Un robot devine un mot de passe faible et ouvre une session Authentification par clé seule, root refusé, comptes nominatifs (étapes 7 à 9)
Un service oublié écoute sur le réseau et se fait exploiter Inventaire de ce qui écoute, pare-feu fermé par défaut, AppArmor (étapes 2, 10, 13)
Une faille publiée reste non corrigée pendant des semaines Correctifs de sécurité automatiques, alerte de fin de support (étape 12)

Et quand tout cela échoue quand même : un instantané, une sauvegarde restaurable et des journaux qui existent ailleurs (étapes 14 et 15).

Règle d'or : ne fermez jamais la seconde session

Tant que vous touchez à SSH ou au pare-feu, gardez une deuxième session SSH ouverte sur le serveur. Chaque modification se teste depuis une troisième session, jamais en refermant celle qui marche.

Vérifiez aussi, avant de commencer, que vous disposez d'un accès de secours hors SSH : console KVM/VNC, console série ou mode rescue de l'hébergeur. Sans lui, une erreur de pare-feu signifie une réinstallation. En cas de problème, la marche à suivre est en fin de page : Si vous êtes verrouillé dehors.

Les niveaux, et le parcours court

Chaque étape porte un niveau :

Niveau Ce que cela veut dire
Essentiel À faire toujours, même en urgence. Sans cela, la machine est exposée ou inexploitable.
Recommandé Ce qui distingue un serveur tenu d'un serveur qui tient par chance. À faire le jour même.
Avancé Utile selon l'usage et l'hébergeur. À lire, à appliquer si le contexte s'y prête.

Vous devez sécuriser un serveur ce soir, en un quart d'heure ? Faites, dans l'ordre : 2 (constat et empreinte), 4 (mise à jour), 7 (compte admin), 8 (clés SSH), 10 (pare-feu), 12 (mises à jour automatiques). Le reste peut attendre le lendemain — mais pas la semaine suivante.

Variables utilisées dans ce document

Remplacez systématiquement ces valeurs par les vôtres.

Variable Exemple Signification
SRV web01 Nom court du serveur (sans domaine)
DOMAINE exemple.fr Le domaine qui porte le serveur
FQDN web01.exemple.fr Nom complet, celui du DNS et du reverse
IPV4 203.0.113.10 Adresse IPv4 publique
IPV6 2001:db8::10 Adresse IPv6 publique, si fournie
USER alix Votre futur compte d'administration
VENDOR debian Compte créé par l'hébergeur, à neutraliser
SSHPORT 2222 Port SSH final, si vous le déplacez

Plutôt que de remplacer web01 à la main dans quarante blocs — c'est là que naissent les erreurs — posez-les une fois en début de session, sur le serveur comme sur votre poste :

export SRV=web01 DOMAINE=exemple.fr USER=alix SSHPORT=2222
export FQDN="$SRV.$DOMAINE"
export IPV4=203.0.113.10

Les commandes de cette page s'écrivent alors telles quelles, et echo $FQDN vérifie ce que le shell a vraiment compris.

Deux pièges des variables d'environnement

Elles disparaissent à la déconnexion : reposez-les à chaque nouvelle session, ou écrivez-les dans votre ~/.bashrc le temps de l'installation.

Et sudo ne les transmet pas (env_reset est le défaut Debian) : une commande comme sudo echo $FQDN fonctionne — le shell remplace avant d'appeler sudo — mais sudo sh -c 'echo $FQDN' renvoie du vide. Utilisez sudo -E pour conserver l'environnement, ou passez par sudo tee avec un heredoc.

Ordre des opérations

L'ordre compte : le DNS se propage pendant que vous travaillez, et le pare-feu ne se ferme qu'une fois le nouvel accès SSH prouvé.

flowchart LR A[DNS + reverse] --> B[Identité<br/>hostname, hosts] B --> C[Système<br/>MAJ, paquets, locale] C --> D[Compte admin<br/>+ sudo] D --> E[SSH<br/>clés + durcissement] E --> F[Pare-feu<br/>+ fail2ban] F --> G[Automatismes<br/>MAJ, supervision] G --> H[Sauvegarde<br/>+ instantané]

Étape 1 — DNS direct et reverse DNS

Recommandé — le serveur tourne sans, mais ses mails partent en indésirables et ses journaux sont illisibles.

Pour comprendre : FQDN, PTR et FCrDNS

Le FQDN est le nom complet de la machine, web01.exemple.fr : un nom court (web01) suivi du domaine. C'est lui, et lui seul, qui identifie le serveur pour le reste d'Internet.

L'enregistrement A traduit ce nom en adresse IP — c'est l'aller. L'enregistrement PTR, le reverse, fait le chemin retour : il traduit l'adresse IP en nom. Les deux sont gérés à des endroits différents : le A chez le gestionnaire du domaine, le PTR chez l'hébergeur qui possède l'adresse IP.

web01.exemple.fr  ──(A)──▶  203.0.113.10  ──(PTR)──▶  web01.exemple.fr
                └──────────── la boucle doit se refermer ────────────┘

Quand la boucle se referme sur le même nom, on parle de FCrDNS valide. Les serveurs de messagerie s'en servent comme premier filtre anti-spam : un expéditeur dont le reverse ne correspond à rien est rejeté ou classé indésirable avant même d'être lu.

Faites cette étape en premier : la propagation DNS prend de quelques minutes à quelques heures, et elle tournera pendant que vous configurez le reste. Un reverse DNS cohérent conditionne l'acceptation de vos mails et la lisibilité de vos journaux.

1.1 Enregistrements directs (zone DNS du domaine)

Dans l'interface DNS de votre registrar ou de votre hébergeur, créez :

Type Nom Valeur TTL
A web01 203.0.113.10 300 pendant la mise en place, puis 3600
AAAA web01 2001:db8::10 idem, si IPv6 fournie

Un TTL court (300 s) pendant l'installation vous laisse corriger une erreur sans attendre. Remontez-le une fois le serveur stabilisé.

1.2 Reverse DNS (PTR)

Le PTR ne se déclare pas dans la zone du domaine, mais chez le propriétaire de l'adresse IP, c'est-à-dire votre hébergeur. Cherchez dans son panneau une entrée nommée « reverse », « rDNS » ou « PTR », sur la fiche de l'IP et non sur celle du domaine.

La valeur doit être exactement le FQDN déclaré en 1.1 : web01.exemple.fr. Faites-le pour l'IPv4 et pour l'IPv6 ; un reverse IPv6 manquant est une cause classique de mails rejetés.

1.3 Vérification

Depuis votre poste, une fois la propagation faite :

dig +short web01.exemple.fr A
dig +short web01.exemple.fr AAAA
dig +short -x 203.0.113.10
dig +short -x 2001:db8::10

Les quatre réponses doivent se refermer sur elles-mêmes : le A pointe vers l'IP, le PTR de cette IP renvoie le même FQDN. C'est ce qu'on appelle un FCrDNS valide (forward-confirmed reverse DNS).

Si dig manque sur votre poste

Si dig n'est pas installé sur votre poste : sudo apt install dnsutils sous Debian/Ubuntu, brew install bind sous macOS.

Étape 2 — Première connexion

Essentiel — constater l'état de livraison et valider l'identité de la machine.

Connectez-vous avec le compte fourni par l'hébergeur. Sur les images Debian officielles, c'est debian ; d'autres utilisent admin, ubuntu ou root.

ssh debian@203.0.113.10

Utilisez l'IP et non le FQDN pour cette première connexion : le DNS n'est peut-être pas encore propagé.

Vérifier l'empreinte avant de répondre « yes »

SSH affiche l'empreinte de la clé d'hôte et demande confirmation. C'est le seul moment de toute la vie du serveur où une interception est possible : une fois l'empreinte enregistrée dans votre known_hosts, toute substitution déclenche une alerte. Répondre « yes » sans regarder, c'est accepter n'importe quel serveur qui se présente à cette adresse.

Comparez l'empreinte affichée avec celle que l'hébergeur publie — panneau d'administration, courriel de livraison, ou console KVM :

# Sur la console KVM de l'hébergeur, ou après une première connexion de confiance
for f in /etc/ssh/ssh_host_*_key.pub; do ssh-keygen -lf "$f"; done

Les deux empreintes doivent être identiques, caractère pour caractère. Si l'hébergeur ne publie rien et que vous avez une console, relevez-la là : c'est le seul canal qui ne passe pas par le réseau que vous cherchez à valider.

Régénérer les clés d'hôte d'une image clonée

Certains fournisseurs distribuent des images où les clés SSH ont été générées une seule fois, avant le clonage : des milliers de machines partagent alors la même clé privée, et qui possède cette image peut se faire passer pour votre serveur.

ls -l --time-style=full-iso /etc/ssh/ssh_host_*_key

Si la date est antérieure à la livraison de votre machine, régénérez-les :

sudo rm /etc/ssh/ssh_host_*
sudo dpkg-reconfigure openssh-server
sudo systemctl restart ssh

Votre prochaine connexion signalera un changement de clé, ce qui est normal ici : ssh-keygen -R 203.0.113.10 sur votre poste, puis revérifiez la nouvelle empreinte.

Le même raisonnement vaut pour l'identifiant de la machine, qui sert de base au DUID DHCPv6 et à la corrélation des journaux :

cat /etc/machine-id
# S'il est identique sur deux machines issues de la même image :
sudo rm -f /etc/machine-id && sudo systemd-machine-id-setup

Vérifier ce qu'on a vraiment reçu

Avant toute modification, prenez trois minutes pour constater l'état initial.

cat /etc/os-release            # confirme Debian 13 (trixie)
uname -r                       # version du noyau
ip -brief address              # interfaces et adresses
lsblk                          # disques et partitions
free -h                        # RAM et swap
sudo systemctl list-units --type=service --state=running
sudo ss -tulpn                 # ce qui écoute déjà sur le réseau

Le disque est-il entier ?

Beaucoup de VPS démarrent sur une image dont la partition est plus petite que le volume facturé : lsblk montre alors un disque de 80 Go dont la partition n'en fait que 20. L'espace est payé, mais inutilisable tant qu'on ne l'a pas réclamé.

sudo apt install -y cloud-guest-utils      # fournit growpart
sudo growpart /dev/vda 1                   # adaptez le disque et le numéro
sudo resize2fs /dev/vda1                   # xfs_growfs si le système est en XFS
df -h /

Faites-le maintenant, avant d'avoir des données : l'opération est sûre mais se déroule mieux sur une machine vide.

La dernière commande est la plus instructive : elle montre les services exposés dès la livraison. Tout ce qui écoute sur 0.0.0.0 ou :: et que vous ne reconnaissez pas mérite une décision explicite — désactiver ou garder.

Si la connexion est refusée

Symptôme Cause fréquente
Permission denied (publickey) La clé publique n'a pas été injectée à la commande, ou mauvais nom d'utilisateur
Connection refused Le serveur n'a pas fini de démarrer, ou SSH écoute sur un autre port
Connection timed out Pare-feu amont côté hébergeur (security group, firewall network)
REMOTE HOST IDENTIFICATION HAS CHANGED Réinstallation ou réattribution d'IP → ssh-keygen -R 203.0.113.10

Étape 3 — Nom d'hôte et /etc/hosts

Recommandé — une identité stable, sur laquelle s'appuient mail, journaux et certificats.

3.1 Définir le nom d'hôte

Debian attend ici le nom court, pas le FQDN. Le FQDN est reconstitué via /etc/hosts.

sudo hostnamectl set-hostname web01

Vous pouvez aussi renseigner deux champs purement informatifs, utiles quand on gère plusieurs machines :

sudo hostnamectl set-hostname "Front web — Paris" --pretty
sudo hostnamectl set-chassis vm        # ou: server, desktop

3.2 Corriger /etc/hosts

C'est ce fichier, et non hostnamectl, qui donne au système son FQDN. La règle : sur la ligne 127.0.1.1, le FQDN vient en premier, le nom court ensuite.

sudo nano /etc/hosts

Ciblez ce résultat :

127.0.0.1       localhost
127.0.1.1       web01.exemple.fr web01

# The following lines are desirable for IPv6 capable hosts
::1             localhost ip6-localhost ip6-loopback
ff02::1         ip6-allnodes
ff02::2         ip6-allrouters

Certains hébergeurs pré-remplissent la ligne 127.0.1.1 avec le nom d'usine de la machine. Deux options : le supprimer, ou le laisser après vos deux entrées — la ligne compte alors quatre références.

127.0.1.1       web01.exemple.fr web01 vps-1a2b3c.exemple-hebergeur.net vps-1a2b3c

Les deux formes fonctionnent. Supprimer l'ancien nom est plus propre ; le conserver évite de casser un script de l'hébergeur qui s'y référerait encore.

Ne touchez pas à localhost

Ne touchez jamais à la ligne 127.0.0.1 localhost. Et n'associez pas le FQDN à l'IP publique dans /etc/hosts : sur une machine à IP dynamique ou derrière NAT, cela crée des résolutions fantomatiques.

3.3 Vérifier

hostname          # → web01
hostname -f       # → web01.exemple.fr
hostname -d       # → exemple.fr
hostnamectl status

Si hostname -f répond autre chose que le FQDN attendu, la ligne 127.0.1.1 est mal ordonnée. Le changement est immédiat, mais reconnectez-vous pour que l'invite de commande l'affiche.

Le nom d'hôte revient après un redémarrage ?

Sur certaines images cloud, cloud-init le réécrit à chaque démarrage. Dites-lui de respecter le vôtre :

sudo sed -i 's/^preserve_hostname:.*/preserve_hostname: true/' /etc/cloud/cloud.cfg
grep preserve_hostname /etc/cloud/cloud.cfg

Ajoutez la ligne si elle est absente.

Étape 4 — Sources APT et mise à jour

Essentiel — une machine livrée n'est jamais à jour.

4.1 Passer les sources au format deb822

Debian 13 introduit le format deb822 pour les dépôts. L'ancien format une-ligne est officiellement déprécié, même s'il restera supporté par Debian 14 « Forky » (détail du changement). Autant partir sur le bon format tout de suite.

sudo apt modernize-sources

La commande convertit /etc/apt/sources.list vers /etc/apt/sources.list.d/debian.sources et laisse au passage des sauvegardes /etc/apt/sources.list.bak et /etc/apt/sources.list.save, ce qui permet de revenir en arrière (procédure détaillée).

Vérifiez le résultat :

cat /etc/apt/sources.list.d/debian.sources

Vous devez y trouver trois blocs : trixie, trixie-updates et trixie-security, chacun avec un champ Signed-By.

Types: deb
URIs: http://deb.debian.org/debian/
Suites: trixie trixie-updates
Components: main contrib non-free-firmware
Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg

Types: deb
URIs: http://security.debian.org/debian-security/
Suites: trixie-security
Components: main contrib non-free-firmware
Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg

Sans trixie-security, aucun correctif

Si le dépôt trixie-security manque, ajoutez-le à la main : sans lui, vous ne recevez aucun correctif de sécurité. C'est l'erreur la plus coûteuse de toute cette procédure.

non-free-firmware est nécessaire sur matériel physique (cartes réseau, RAID). contrib et non-free sont optionnels ; ne les ajoutez que si un paquet précis l'exige.

4.2 Mettre à jour

sudo apt update
sudo apt full-upgrade -y
sudo apt autoremove --purge -y
sudo apt clean

full-upgrade et non upgrade : le premier accepte d'installer ou de retirer des paquets quand une dépendance a changé, ce qui est exactement ce qu'on veut sur une machine neuve. upgrade bloquerait sur ces cas.

4.3 Redémarrer si le noyau a changé

ls /var/run/reboot-required 2>/dev/null && echo "Redémarrage nécessaire"
sudo reboot

Après redémarrage, reconnectez-vous et vérifiez que tout est remonté :

systemctl is-system-running        # attendu : running
systemctl --failed                 # attendu : 0 loaded units listed
uptime

Un état degraded signale au moins un service en échec ; systemctl --failed vous dit lequel.

Étape 5 — Paquets de base

Recommandé — de quoi diagnostiquer sans improviser.

sudo apt install -y \
  vnstat iftop htop nano git \
  curl wget ca-certificates gnupg \
  dnsutils net-tools lsof tcpdump \
  rsync tree zip unzip \
  ncdu sysstat needrestart
Paquet À quoi il sert
vnstat Compteur de trafic réseau historisé, léger (vnstat -d, vnstat -m)
iftop Trafic en temps réel par connexion
htop Processus, CPU, mémoire, en interactif
ncdu Trouve ce qui remplit le disque, en navigation
sysstat sar, iostat, pidstat : historique de charge et d'E/S
needrestart Signale après chaque apt les services à redémarrer
dnsutils dig, nslookup pour déboguer la résolution
lsof / tcpdump Qui tient un fichier, que passe-t-il sur le réseau

Activer vnstat

sudo systemctl enable --now vnstat
vnstat --iflist            # vérifie l'interface détectée

Il faut attendre quelques heures avant que vnstat -d affiche des données utiles : le compteur démarre à zéro.

Ce que vous ne voulez probablement pas

Sur un serveur, allez au plus maigre. Chaque paquet installé est une surface d'attaque et une mise à jour de plus. Évitez les méta-paquets task-*, les environnements de bureau, et tout serveur (apache2, nginx, mariadb) tant que vous n'êtes pas à l'étape de déploiement des services.

sudo apt-mark showmanual | head -50    # ce qui a été installé explicitement

Étape 6 — Locale, fuseau horaire et clavier

Recommandé — une heure juste est la condition d'un journal exploitable.

6.1 Locale

Attention à l'orthographe : c'est fr_FR.UTF-8 avec un tiret, pas fr_FR.UTF_8. Un underscore produit une locale invalide, silencieusement ignorée.

Générez d'abord la locale, puis déclarez-la comme défaut système :

sudo sed -i 's/^# *\(fr_FR.UTF-8\)/\1/' /etc/locale.gen
sudo locale-gen
sudo localectl set-locale LANG=fr_FR.UTF-8

Variante non interactive, adaptée à un script :

echo "locales locales/locales_to_be_generated multiselect fr_FR.UTF-8 UTF-8, en_US.UTF-8 UTF-8" | sudo debconf-set-selections
echo "locales locales/default_environment_locale select fr_FR.UTF-8" | sudo debconf-set-selections
sudo rm -f /etc/locale.gen
sudo dpkg-reconfigure --frontend=noninteractive locales

Pourquoi localectl et pas LANG=

LANG=fr_FR.UTF-8 seul dans un terminal ne change que la session en cours. localectl écrit dans /etc/default/locale, lu par tous les services et toutes les futures connexions.

Recommandation : gardez les messages en anglais même avec une interface en français. Les messages d'erreur anglais sont cherchables et correspondent à la documentation ; les scripts qui parsent une sortie de commande cassent en français.

sudo localectl set-locale LANG=fr_FR.UTF-8 LC_MESSAGES=C.UTF-8

Reconnectez-vous, puis vérifiez :

locale              # aucune ligne "Cannot set LC_*"
localectl status

6.2 Fuseau horaire

sudo timedatectl set-timezone Europe/Paris
timedatectl list-timezones | grep Paris     # pour trouver un autre fuseau

UTC ou heure locale ?

Sur un parc international ou pour simplifier la lecture des journaux entre machines, UTC reste un choix défendable. Le seul vrai critère : que toutes vos machines aient le même fuseau.

6.3 Synchronisation de l'heure

Debian 13 utilise systemd-timesyncd par défaut. Une heure décalée casse TLS, les tokens et la corrélation des journaux.

sudo timedatectl set-ntp true
timedatectl status

Attendu : System clock synchronized: yes et NTP service: active.

Pour utiliser les serveurs français :

sudo mkdir -p /etc/systemd/timesyncd.conf.d
printf '[Time]\nNTP=0.fr.pool.ntp.org 1.fr.pool.ntp.org\nFallbackNTP=2.fr.pool.ntp.org 3.fr.pool.ntp.org\n' \
  | sudo tee /etc/systemd/timesyncd.conf.d/pool-fr.conf
sudo systemctl restart systemd-timesyncd
timedatectl timesync-status

6.4 Clavier (console uniquement)

Utile seulement si vous passez par la console KVM de l'hébergeur ; sans effet sur SSH.

sudo localectl set-keymap fr
sudo localectl set-x11-keymap fr

Étape 7 — Compte administrateur et sudo

Essentiel — ne plus travailler avec le compte du fournisseur.

7.1 Créer l'utilisateur

sudo adduser alix

Choisissez un mot de passe long et unique. Les champs « Nom complet », « N° de bureau », etc. peuvent rester vides : validez avec Entrée.

7.2 Accorder les droits sudo

Sous Debian, l'appartenance au groupe sudo suffit. Nul besoin d'éditer les sudoers pour un administrateur standard.

sudo usermod -aG sudo alix
id alix        # vérifie que "sudo" apparaît dans les groupes

Le -a n'est pas facultatif

Le -a de usermod -aG est obligatoire : sans lui, l'utilisateur est retiré de tous ses autres groupes. C'est une erreur classique et difficile à diagnostiquer.

Si vous avez besoin d'une règle spécifique — par exemple autoriser un redémarrage de service sans mot de passe — n'éditez jamais /etc/sudoers directement. Créez un fichier dédié dans /etc/sudoers.d/, ce qui isole la panne en cas d'erreur de syntaxe :

sudo visudo -f /etc/sudoers.d/10-alix
# Redémarrage de nginx sans mot de passe
alix ALL=(root) NOPASSWD: /usr/bin/systemctl restart nginx
sudo chmod 0440 /etc/sudoers.d/10-alix
sudo visudo -c                  # contrôle global de la syntaxe

Toujours visudo, jamais l'éditeur seul

visudo valide la syntaxe avant d'écrire, ce que nano /etc/sudoers ne fait pas. Une erreur dans les sudoers non validée rend sudo inutilisable pour tout le monde, y compris vous.

Évitez NOPASSWD: ALL : cela transforme toute exécution de code sous votre compte en accès root immédiat.

7.3 Tester avant de couper quoi que ce soit

Depuis une nouvelle session (en gardant l'ancienne ouverte) :

ssh alix@203.0.113.10
sudo -v              # doit demander VOTRE mot de passe et rendre la main
sudo whoami          # → root

Tant que ces trois commandes ne fonctionnent pas, n'allez pas plus loin : les étapes suivantes retirent votre accès actuel.

7.4 Confort de connexion (optionnel)

# Depuis votre poste : alias de connexion
cat >> ~/.ssh/config <<'EOF'
Host web01
    HostName web01.exemple.fr
    User alix
    Port 22
    IdentityFile ~/.ssh/id_ed25519
EOF

Vous vous connecterez ensuite avec ssh web01.

Le port change en cours de route

À ce stade, SSH écoute encore sur 22 : c'est le port à écrire ici. Si vous le déplacez à l'étape 8.4, revenez mettre $SSHPORT — la suite de cette page emploie 2222 comme exemple de port final, y compris dans les règles de pare-feu de l'étape 10. Tant que la bascule n'est pas faite et vérifiée, gardez les deux ports ouverts.

Étape 8 — Clés SSH et durcissement

Essentiel — c'est l'étape qui ferme la porte la plus attaquée.

C'est l'étape la plus risquée. Gardez une session ouverte du début à la fin.

8.1 Créer une paire de clés (sur votre poste, pas sur le serveur)

ssh-keygen -t ed25519 -C "alix@poste-bureau" -f ~/.ssh/id_ed25519

Ed25519 est le bon choix par défaut : court, rapide, sûr. Mettez une passphrase — elle protège la clé si votre poste est compromis, et ssh-agent évite de la retaper.

Pour comprendre : pourquoi une clé plutôt qu'un mot de passe

ssh-keygen produit deux fichiers : une clé privée, qui ne quitte jamais votre poste, et une clé publique, que vous déposez sur tous les serveurs où vous allez. À la connexion, le serveur envoie un défi que seule la clé privée sait résoudre — la clé privée elle-même ne transite jamais.

Un mot de passe de douze caractères se devine en quelques milliards d'essais, et les robots qui scannent le port 22 en font des milliers par heure. Une clé Ed25519 ne se devine pas : il n'y a rien à essayer. C'est pour cela que l'on peut couper l'authentification par mot de passe sans rien perdre.

Option avancée : une clé sur support matériel

Avec une clé de sécurité FIDO2 (YubiKey, Nitrokey, Token2), le secret ne quitte jamais le support physique — voler votre ordinateur ne suffit plus, il faut aussi la clé dans votre poche :

ssh-keygen -t ed25519-sk -C "alix@yubikey"

Le suffixe -sk (security key) demande une confirmation physique à chaque connexion. Gardez une seconde clé matérielle, ou une clé logicielle de secours dans authorized_keys : une clé FIDO2 perdue ne se duplique pas.

8.2 Déposer la clé publique

ssh-copy-id -i ~/.ssh/id_ed25519.pub alix@203.0.113.10

En manuel si ssh-copy-id n'est pas disponible :

ssh alix@203.0.113.10 "mkdir -p ~/.ssh && chmod 700 ~/.ssh"
cat ~/.ssh/id_ed25519.pub | ssh alix@203.0.113.10 "cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"

Testez dans une nouvelle session : ssh alix@203.0.113.10 doit passer sans demander le mot de passe du compte.

8.3 Configurer sshd par fichier drop-in

Debian 13 lit /etc/ssh/sshd_config.d/*.conf. Écrivez votre configuration là plutôt que dans sshd_config : vos choix survivent aux mises à jour du paquet et se retirent en supprimant un seul fichier.

Regardez d'abord ce qui s'y trouve déjà :

ls -l /etc/ssh/sshd_config.d/

Le premier fichier qui parle a raison

Pour une directive donnée, la première valeur rencontrée l'emporte, et les fichiers sont lus dans l'ordre alphabétique. Sous Debian, l'Include est en tête de sshd_config, donc n'importe quel drop-in gagne contre le corps du fichier — mais un 50-cloud-init.conf, fréquent sur les images d'hébergeurs, gagne contre votre 99-durcissement.conf. Or il force souvent PasswordAuthentication yes.

Si un tel fichier est présent : nommez le vôtre 00-durcissement.conf, ou corrigez directement le fichier fautif. Dans tous les cas, c'est sshd -T qui tranche, pas la lecture des fichiers.

sudo nano /etc/ssh/sshd_config.d/99-durcissement.conf
# Authentification
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
AuthenticationMethods publickey
MaxAuthTries 3
MaxSessions 5
LoginGraceTime 30

# Limiter qui peut se connecter
AllowGroups ssh-users

# Réduire la surface
X11Forwarding no
AllowAgentForwarding no
AllowTcpForwarding no
PermitTunnel no
PermitUserEnvironment no

# Sessions mortes
ClientAliveInterval 300
ClientAliveCountMax 2

Le groupe doit exister avant le rechargement, sans quoi plus personne ne peut se connecter :

sudo groupadd -f ssh-users
sudo usermod -aG ssh-users alix
id alix                              # ssh-users doit apparaître

Pourquoi un groupe plutôt qu'une liste de noms

Une liste blanche vaut mieux qu'un DenyUsers debian : elle couvre aussi les comptes que vous n'avez pas anticipés. Mais AllowUsers alix désigne une personne, et un serveur dont une seule personne détient l'accès est un serveur perdu le jour où cette personne ne répond plus.

Avec un groupe, ouvrir l'accès à quelqu'un devient sudo usermod -aG ssh-users bea, et le fermer sudo gpasswd -d bea ssh-users — sans jamais rouvrir la configuration de sshd. Prévoyez un second administrateur dès maintenant : c'est le moment où cela coûte le moins cher.

L'ordre des opérations n'est pas négociable

PasswordAuthentication no vous verrouille dehors si la clé de l'étape 8.2 ne fonctionne pas. Vérifiez la connexion par clé dans une session séparée avant de recharger sshd, et gardez la session courante ouverte jusqu'au succès du test.

Vérifiez maintenant la configuration réellement appliquée, jamais le fichier seul :

sudo sshd -t                         # syntaxe ; silence = OK
sudo sshd -T | grep -Ei 'permitrootlogin|passwordauth|allowgroups|^port'

Puis rechargez et testez depuis une nouvelle session :

sudo systemctl reload ssh

8.4 Changer le port : la spécificité Debian 13

Depuis Debian 12, SSH est démarré par activation à la demande via systemd (ssh.socket). Conséquence : une directive Port dans sshd_config peut rester sans effet, car c'est le socket systemd qui décide de l'écoute.

Pour comprendre : l'activation par socket

Dans le modèle classique, sshd démarre au boot, ouvre lui-même le port 22 et attend. Avec l'activation par socket, c'est systemd qui ouvre le port et écoute à sa place ; sshd ne démarre qu'à l'arrivée d'une connexion, et systemd lui passe la main.

Le port n'appartient donc plus à sshd mais à l'unité ssh.socket. C'est pourquoi Port 2222 dans sshd_config est lu, accepté, et sans effet : au moment où sshd s'exécute, l'écoute est déjà établie par quelqu'un d'autre. Le gain — quelques mégaoctets de mémoire tant que personne ne se connecte — explique ce choix par défaut ; c'est aussi le piège le plus déroutant de Debian 13.

D'abord, constatez votre situation :

systemctl is-active ssh.socket       # active → c'est le socket qui commande
systemctl is-active ssh.service
ss -tlpn | grep -E 'ssh|:22'

Si ssh.socket est actif, modifiez le socket :

sudo systemctl edit ssh.socket
[Socket]
ListenStream=
ListenStream=0.0.0.0:2222
ListenStream=[::]:2222

La ligne ListenStream= vide est indispensable : elle efface le port 22 hérité. Sans elle, vous ajoutez 2222 sans retirer 22.

sudo systemctl daemon-reload
sudo systemctl restart ssh.socket
ss -tlpn | grep 2222

Alternative — revenir au modèle classique, où sshd_config reprend la main :

sudo systemctl disable --now ssh.socket
sudo systemctl enable --now ssh.service

8.5 Faut-il vraiment changer de port ?

Ce n'est pas une mesure de sécurité, seulement de réduction du bruit : elle élimine les scans automatisés des journaux, pas un attaquant ciblé qui scannera vos 65 535 ports. Avec PasswordAuthentication no et fail2ban, le port 22 est déjà très défendable. Un port non standard complique en revanche le pare-feu, les sondes de supervision et les collaborateurs.

8.6 Vérification finale

Ouvrez une session neuve, sans fermer l'ancienne :

ssh -p 2222 alix@web01.exemple.fr
ssh -p 2222 debian@web01.exemple.fr     # doit être refusé
ssh -p 2222 root@web01.exemple.fr       # doit être refusé

Résultat attendu — la première commande ouvre une session sans demander de mot de passe ; les deux autres échouent en Permission denied (publickey). Et la configuration appliquée confirme les quatre directives qui comptent :

sudo sshd -T | grep -Ei '^port|permitrootlogin|passwordauthentication|allowgroups'

Si ça échoue

Symptôme Cause la plus fréquente
Permission denied (publickey) sur votre compte La clé n'est pas dans authorized_keys, ou les droits sont trop larges (chmod 700 ~/.ssh, 600 authorized_keys)
Connection refused après un changement de port ssh.socket n'a pas été rechargé, ou le pare-feu n'ouvre pas le nouveau port
Le mot de passe est toujours demandé Un drop-in antérieur gagne : relisez ls /etc/ssh/sshd_config.d/ et tranchez avec sshd -T
Plus personne ne peut entrer AllowGroups pointe un groupe vide ou inexistant

Revenir en arrière, depuis la session restée ouverte :

sudo rm /etc/ssh/sshd_config.d/99-durcissement.conf
sudo systemctl reload ssh
# Si le port avait été déplacé :
sudo rm -f /etc/systemd/system/ssh.socket.d/override.conf
sudo systemctl daemon-reload && sudo systemctl restart ssh.socket

Étape 9 — Neutraliser le compte fournisseur

Essentiel — un compte connu de tous, au nom prévisible.

Le compte debian livré par l'hébergeur est connu de tous et ciblé en premier par les scans. Une fois votre accès personnel prouvé, retirez-lui l'accès SSH.

9.1 Choisir le niveau de neutralisation

Niveau Commande Effet Quand
SSH bloqué AllowGroups ssh-users (étape 8.3) Le compte existe, ne peut plus se connecter par SSH Cas général
Mot de passe verrouillé sudo passwd -l debian Plus d'authentification par mot de passe À combiner avec le précédent
Shell retiré sudo usermod -s /usr/sbin/nologin debian Plus aucune session interactive Compte purement technique
Compte supprimé sudo deluser --remove-home debian Disparu Seulement si rien ne dépend de lui

La combinaison recommandée est la plus conservatrice :

sudo passwd -l debian
sudo usermod -s /usr/sbin/nologin debian
sudo rm -f /home/debian/.ssh/authorized_keys

Ne supprimez pas le compte du fournisseur trop vite

Ne supprimez pas le compte avant d'avoir vérifié qu'aucun processus ne tourne sous son identité (ps -u debian) et qu'il n'est pas utilisé par l'outillage de l'hébergeur (agent de sauvegarde, cloud-init, console de secours). Certains hébergeurs recréent le compte à chaque redémarrage.

9.2 Le cas de root

sudo passwd -l root                 # verrouille le mot de passe root
sudo rm -f /root/.ssh/authorized_keys

Sur Debian, root n'a généralement pas de mot de passe utilisable en SSH dès lors que PermitRootLogin no est posé. Le verrouillage est une ceinture en plus des bretelles.

Ne retirez pas le shell de root

N'utilisez pas usermod -s /usr/sbin/nologin root : vous perdriez l'accès par la console de secours de l'hébergeur, votre dernier recours.

9.3 Inventaire des comptes

# Comptes capables de se connecter
awk -F: '$3 >= 1000 && $3 < 65534 {print $1, $7}' /etc/passwd

# Membres du groupe sudo
getent group sudo

# Comptes sans mot de passe (doit être vide)
sudo awk -F: '$2 == "" {print $1}' /etc/shadow

# Clés SSH autorisées sur toute la machine
sudo find /home /root -name authorized_keys -exec ls -l {} \; -exec cat {} \;

La dernière commande mérite un passage attentif : une clé publique inconnue dans un authorized_keys est une porte dérobée.

Étape 10 — Pare-feu

Essentiel — tout ce qui écoute est joignable tant que rien ne filtre.

Debian 13 utilise nftables comme moteur de filtrage. Deux approches : nftables directement, ou ufw qui l'utilise en coulisse avec une syntaxe plus simple.

10.1 Option A — ufw (recommandé si vous débutez)

sudo apt install -y ufw

# Politique par défaut : tout entrant bloqué, tout sortant autorisé
sudo ufw default deny incoming
sudo ufw default allow outgoing

# SSH EN PREMIER — sinon vous vous coupez l'accès à l'activation
sudo ufw limit 2222/tcp comment 'SSH'      # $SSHPORT : le port réellement en écoute
# Pendant une bascule de port, autorisez les deux le temps de vérifier :
# sudo ufw limit 22/tcp comment 'SSH ancien port'

# Services web, à n'ouvrir que le jour où vous les déployez
# sudo ufw allow 80/tcp comment 'HTTP'
# sudo ufw allow 443/tcp comment 'HTTPS'

sudo ufw enable
sudo ufw status verbose

limit plutôt que allow pour SSH : ufw bloque une IP qui dépasse six tentatives de connexion en 30 secondes. C'est une première barrière avant fail2ban.

10.2 Option B — nftables directement

Plus verbeux, mais sans couche intermédiaire et lisible d'un seul coup d'œil.

sudo nano /etc/nftables.conf
#!/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

        ip protocol icmp accept
        ip6 nexthdr icmpv6 accept

        tcp dport 2222 ct state new limit rate 6/minute accept
        # tcp dport { 80, 443 } accept

        counter
    }

    chain forward {
        type filter hook forward priority filter; policy drop;
    }

    chain output {
        type filter hook output priority filter; policy accept;
    }
}
sudo nft -c -f /etc/nftables.conf          # vérifie la syntaxe sans appliquer
sudo systemctl enable --now nftables
sudo nft list ruleset

Jamais de policy drop sans règle SSH

Ne laissez jamais policy drop sur input sans règle SSH au-dessus. Testez toujours avec nft -c -f avant d'appliquer.

10.3 Filet de sécurité pendant les tests

Programmez une désactivation automatique du pare-feu dans 10 minutes. Si vous vous verrouillez dehors, l'accès revient seul ; si tout va bien, vous annulez la tâche.

Si vous avez choisi ufw :

# Avant d'activer les règles
sudo systemd-run --on-active=10min --unit=ufw-panic systemctl stop ufw

# Une fois la connexion reconfirmée depuis une nouvelle session
sudo systemctl stop ufw-panic.timer

Si vous avez choisi nftablessystemctl stop ufw n'existe pas, il faut vider le jeu de règles :

# Avant d'activer les règles
sudo systemd-run --on-active=10min --unit=nft-panic nft flush ruleset

# Une fois la connexion reconfirmée depuis une nouvelle session
sudo systemctl stop nft-panic.timer

Ici, l'unité transitoire est exactement ce qu'on veut : elle doit disparaître d'elle-même.

10.4 Le pare-feu de l'hébergeur

Beaucoup d'hébergeurs interposent leur propre filtrage en amont (security group, « firewall network »). Un port ouvert sur la machine et fermé chez eux reste inaccessible — et l'inverse est vrai. Tenez les deux couches alignées, et documentez-les ensemble.

# Depuis un autre poste : ce qui est réellement joignable
nmap -Pn -p- web01.exemple.fr

10.5 Contrôler, réparer, annuler

Résultat attendu — la politique d'entrée est en refus, SSH est la seule exception, et une nouvelle session s'ouvre toujours :

sudo ufw status verbose        # ou : sudo nft list ruleset
ss -tlpn                       # ce qui écoute, à comparer avec ce qui est ouvert

Si ça échoue

Symptôme Cause la plus fréquente
Session en cours figée juste après l'activation Règle SSH absente ou placée après la politique de refus
Le port est ouvert, le service reste injoignable Le pare-feu de l'hébergeur ne l'ouvre pas de son côté (§10.4)
ufw semble sans effet Des règles nftables concurrentes existent : sudo nft list ruleset pour voir qui filtre réellement
Tout tombe après un redémarrage Le pare-feu n'est pas activé au démarrage : systemctl is-enabled ufw ou nftables

Revenir en arrière — depuis la session restée ouverte, ou par la console de l'hébergeur :

sudo ufw disable                       # option A
sudo nft flush ruleset                 # option B, vide tout le filtrage
sudo systemctl disable --now nftables  # et empêche son retour au redémarrage

Un pare-feu vidé est un serveur nu

nft flush ruleset laisse la machine sans aucun filtrage. C'est une manœuvre de dépannage, pas un état dans lequel on laisse un serveur : remettez les règles dans la foulée, une fois l'accès retrouvé.

Étape 11 — fail2ban

Recommandé — écarte le bruit de fond avant qu'il ne remplisse vos journaux.

fail2ban lit les journaux et bannit temporairement les IP qui échouent trop souvent. Avec PasswordAuthentication no, son utilité sur SSH est surtout de nettoyer les journaux ; elle redevient essentielle dès que vous exposez un service authentifié par mot de passe.

sudo apt install -y fail2ban

Configuration

Ne modifiez jamais jail.conf : il est écrasé aux mises à jour. Créez jail.local.

sudo nano /etc/fail2ban/jail.local
[DEFAULT]
bantime  = 1h
findtime = 10m
maxretry = 4
backend  = systemd
banaction = nftables-multiport
# Ne bannissez jamais votre propre IP fixe
ignoreip = 127.0.0.1/8 ::1 198.51.100.7

[sshd]
enabled = true
port    = 2222
mode    = aggressive

# Bannit longuement ceux qui reviennent après chaque bannissement
[recidive]
enabled  = true
bantime  = 1w
findtime = 1d
maxretry = 3

La jail recidive lit les bannissements de fail2ban lui-même : une adresse bannie trois fois en vingt-quatre heures l'est ensuite pour une semaine. C'est ce qui sépare le robot opportuniste, qui repasse toutes les heures, du visiteur qui s'est trompé de mot de passe.

backend = systemd est important sous Debian 13 : les journaux passent par journald, pas par un fichier /var/log/auth.log garanti. banaction = nftables-multiport évite un conflit avec le moteur nftables du système.

Avec ufw plutôt que nftables

Si vous avez choisi ufw à l'étape 10, utilisez plutôt banaction = ufw.

sudo systemctl enable --now fail2ban
sudo systemctl restart fail2ban

Exploitation au quotidien

sudo fail2ban-client status              # prisons actives
sudo fail2ban-client status sshd         # IP bannies
sudo fail2ban-client set sshd unbanip 198.51.100.7   # débannir
sudo fail2ban-client set sshd banip 203.0.113.99     # bannir à la main

Votre IP dans ignoreip, avant de démarrer

Mettez votre IP fixe dans ignoreip avant de démarrer le service. Sinon, une passphrase mal tapée quatre fois vous bannit une heure de votre propre serveur.

Étape 12 — Mises à jour de sécurité automatiques

Essentiel — un serveur à jour aujourd'hui ne l'est plus dans trois semaines.

Un serveur non mis à jour devient vulnérable en quelques semaines. unattended-upgrades applique les correctifs de sécurité sans intervention.

sudo apt install -y unattended-upgrades apt-listchanges
sudo dpkg-reconfigure -plow unattended-upgrades

Configuration

sudo nano /etc/apt/apt.conf.d/50unattended-upgrades

Lignes à vérifier ou décommenter :

Unattended-Upgrade::Origins-Pattern {
        "origin=Debian,codename=${distro_codename},label=Debian-Security";
        "origin=Debian,codename=${distro_codename}-security,label=Debian-Security";
};

Unattended-Upgrade::Mail "alix@exemple.fr";
Unattended-Upgrade::MailReport "on-change";
Unattended-Upgrade::Remove-Unused-Kernel-Packages "true";
Unattended-Upgrade::Remove-Unused-Dependencies "true";
Unattended-Upgrade::Automatic-Reboot "false";
Unattended-Upgrade::Automatic-Reboot-Time "04:00";

Le choix structurant est Automatic-Reboot :

Valeur Conséquence
"false" Les correctifs noyau attendent un redémarrage manuel. Le serveur reste vulnérable en attendant, mais ne tombe jamais seul.
"true" Redémarrage automatique à 04:00 si nécessaire. Correctifs réellement actifs, au prix d'une coupure imprévisible.

Pour un serveur de production isolé, "true" avec une heure creuse est souvent le meilleur compromis. Pour un serveur critique sans redondance, gardez "false" et redémarrez lors d'une fenêtre planifiée.

Le fichier qui décide vraiment

dpkg-reconfigure écrit dans /etc/apt/apt.conf.d/20auto-upgrades, et c'est ce fichier qui commande l'automatisation — pas 50unattended-upgrades, qui n'en règle que le détail. Vérifiez-le explicitement :

cat /etc/apt/apt.conf.d/20auto-upgrades

Il doit contenir ces quatre directives :

APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Download-Upgradeable-Packages "1";
APT::Periodic::AutocleanInterval "7";
APT::Periodic::Unattended-Upgrade "1";

Sans la dernière à "1", tout le reste est configuré pour rien : les index se rafraîchissent, les paquets se téléchargent, et rien ne s'installe.

Rendre needrestart non interactif

Installé à l'étape 5, needrestart pose une question quand des services doivent être relancés. Sur un serveur, cette question bloque unattended-upgrades au lieu d'être posée à quelqu'un :

echo "\$nrconf{restart} = 'a';" | sudo tee /etc/needrestart/conf.d/50-auto.conf

'a' redémarre automatiquement les services concernés. Gardez 'l' (lister seulement) si vous préférez décider vous-même — mais alors n'activez pas le redémarrage automatique d'unattended-upgrades, vous cumuleriez le pire des deux.

Savoir ce qui n'est plus suivi

Tous les paquets d'une version stable ne sont pas couverts par l'équipe de sécurité jusqu'au bout — certains sortent du périmètre en cours de vie.

sudo apt install -y debian-security-support
check-support-status

À relancer après chaque gros apt : la commande liste ce qui est installé chez vous et n'est plus, ou bientôt plus, suivi.

Vérifier que ça tourne

sudo unattended-upgrade --dry-run --debug    # simulation détaillée
systemctl list-timers | grep -E 'apt|unattended'
cat /var/log/unattended-upgrades/unattended-upgrades.log

Deux minuteries doivent apparaître : apt-daily.timer (rafraîchissement des index) et apt-daily-upgrade.timer (installation).

Savoir quand redémarrer

cat /var/run/reboot-required.pkgs 2>/dev/null    # paquets concernés
sudo needrestart -b                              # services à relancer

Pensez à intégrer cette vérification à votre routine hebdomadaire.

Contrôler, réparer, annuler

Résultat attendu — les deux minuteries APT sont actives, et le journal montre des installations réelles et non un simulacre :

systemctl list-timers apt-daily.timer apt-daily-upgrade.timer
grep -c "Packages that will be upgraded" /var/log/unattended-upgrades/unattended-upgrades.log

Si ça échoue

Symptôme Cause la plus fréquente
Le journal reste vide des jours durant APT::Periodic::Unattended-Upgrade est à "0" dans 20auto-upgrades
« No packages found that can be upgraded unattended » en permanence Origins-Pattern ne couvre pas trixie-security, ou le dépôt de sécurité manque (étape 4)
Les mises à jour s'installent mais rien ne redémarre needrestart en mode interactif : il attend une réponse que personne ne donnera
Le serveur redémarre à des heures imprévues Automatic-Reboot "true" sans Automatic-Reboot-Time adapté

Revenir en arrière — pour suspendre l'automatisme sans désinstaller :

sudo sed -i 's/^APT::Periodic::Unattended-Upgrade.*/APT::Periodic::Unattended-Upgrade "0";/' \
  /etc/apt/apt.conf.d/20auto-upgrades
sudo systemctl disable --now apt-daily-upgrade.timer

Suspendre, c'est accepter de faire à la main

Un serveur sans mises à jour automatiques redevient vulnérable en quelques semaines. Si vous coupez l'automatisme, inscrivez la mise à jour manuelle dans une routine datée — pas dans vos bonnes intentions.

Étape 13 — Swap, /tmp et paramètres noyau

Recommandé — la stabilité au quotidien, et quelques durcissements noyau.

13.1 Swap

Beaucoup de VPS sont livrés sans swap. Un peu de swap évite qu'un pic de mémoire déclenche le tueur de processus du noyau (OOM killer).

free -h                    # vérifier ce qui existe déjà
swapon --show

Si la ligne Swap est à zéro :

sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab

Dimensionnement raisonnable : 2 Go jusqu'à 4 Go de RAM, puis 4 Go au-delà. Inutile d'aller au-delà sur un serveur : si vous swappez beaucoup, il faut de la RAM, pas du disque.

# Réduire l'agressivité du swap sur un serveur
echo 'vm.swappiness=10' | sudo tee /etc/sysctl.d/99-swappiness.conf

Le coût du swap dépend du stockage

Sur SSD NVMe, le swap est peu coûteux. Sur un disque réseau facturé aux IOPS, il peut coûter cher et ralentir fortement.

13.2 /tmp est désormais en RAM

Changement notable de Debian 13 : /tmp est monté en tmpfs, donc en mémoire, et vidé à chaque redémarrage (note de version).

Pour comprendre : un système de fichiers en mémoire

Un tmpfs ressemble à un répertoire ordinaire, mais son contenu n'est jamais écrit sur le disque : il vit dans la RAM. D'où ses deux propriétés, qui expliquent tout le reste — il est très rapide, et il disparaît intégralement à l'extinction.

Cela veut aussi dire qu'un fichier de 3 Go déposé dans /tmp occupe 3 Go de mémoire vive, au détriment des services. Sur un serveur à 2 Go de RAM, quelques fichiers temporaires volumineux suffisent à provoquer un arrêt par manque de mémoire.

df -h /tmp
findmnt /tmp

Deux conséquences à connaître :

  1. Écrire un gros fichier dans /tmp consomme de la RAM. Les traitements volumineux doivent utiliser /var/tmp, qui reste sur disque.
  2. Sur une machine migrée depuis Debian 12, les anciens fichiers de /tmp sont toujours sur le disque, masqués par le montage. Ils occupent de l'espace invisible.
# Retrouver et nettoyer l'ancien /tmp masqué
sudo mkdir -p /mnt/racine
sudo mount --bind / /mnt/racine
ls -lha /mnt/racine/tmp/
# ... supprimez ce qui doit l'être ...
sudo umount /mnt/racine && sudo rmdir /mnt/racine

Pour repasser /tmp sur disque si votre usage le demande :

sudo systemctl mask tmp.mount
sudo reboot

13.3 Paramètres réseau et noyau

sudo nano /etc/sysctl.d/99-durcissement.conf
# Anti-usurpation d'adresse
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1

# Ignorer le routage par la source et les redirections ICMP
net.ipv4.conf.all.accept_source_route = 0
net.ipv6.conf.all.accept_source_route = 0
net.ipv4.conf.all.accept_redirects = 0
net.ipv6.conf.all.accept_redirects = 0
net.ipv4.conf.all.send_redirects = 0

# Journaliser les paquets manifestement usurpés
net.ipv4.conf.all.log_martians = 1

# Résistance aux SYN flood
net.ipv4.tcp_syncookies = 1

# Limiter la lecture des informations noyau
kernel.dmesg_restrict = 1
kernel.kptr_restrict = 2

# Protection des liens en répertoires partagés
fs.protected_hardlinks = 1
fs.protected_symlinks = 1
sudo sysctl --system
sudo sysctl -a --pattern 'tcp_syncookies|rp_filter'   # contrôle

Ne coupez pas le routage à l'aveugle

N'appliquez pas net.ipv4.ip_forward = 0 sans réfléchir : Docker, les conteneurs et les VPN en ont besoin.

13.4 AppArmor : vérifier qu'il travaille

AppArmor est actif par défaut depuis Debian 12, et personne ne le regarde jamais. Il confine chaque programme profilé à ce dont il a besoin : un nginx compromis ne peut pas lire /home, même en tant que root, parce que le noyau le lui refuse.

sudo apt install -y apparmor-utils
sudo aa-status

Résultat attendu — des profils chargés, et surtout des profils en mode enforce :

apparmor module is loaded.
XX profiles are loaded.
XX profiles are in enforce mode.
0 profiles are in complain mode.

Un profil en complain journalise les violations sans les bloquer : c'est le mode de mise au point, pas un mode d'exploitation. Repassez-le en application avec sudo aa-enforce /etc/apparmor.d/<profil>.

Retenez surtout que les services installés plus tard apportent leurs profils : après avoir déployé nginx ou MariaDB, un aa-status confirme qu'ils sont bien confinés.

Étape 14 — Journaux, supervision et alertes

Recommandé — savoir ce qui se passe, et être prévenu quand plus rien ne se passe.

14.1 Plafonner les journaux

Par défaut, journald peut occuper jusqu'à 10 % du système de fichiers. Sur un petit VPS, c'est une cause classique de disque plein.

Le répertoire des drop-ins n'existe pas par défaut : créez-le avant d'ouvrir l'éditeur, sinon l'enregistrement échoue.

sudo mkdir -p /etc/systemd/journald.conf.d
sudo nano /etc/systemd/journald.conf.d/00-limites.conf
[Journal]
Storage=persistent
SystemMaxUse=500M
SystemMaxFileSize=50M
MaxRetentionSec=1month
sudo systemctl restart systemd-journald
journalctl --disk-usage

14.2 Commandes de lecture utiles

journalctl -p err -b                     # erreurs depuis le démarrage
journalctl -u ssh --since "1 hour ago"   # un service, une fenêtre de temps
journalctl -f                            # en direct
journalctl --since yesterday | grep -i "failed password"
last -20                                 # dernières connexions réussies
lastb -20                                # dernières tentatives échouées

14.3 Recevoir les alertes par mail

Sans MTA, les rapports de unattended-upgrades, cron et fail2ban sont écrits mais jamais lus. Un relais léger suffit — n'installez pas un serveur de mail complet.

sudo apt install -y msmtp-mta bsd-mailx
sudo nano /etc/msmtprc
defaults
auth           on
tls            on
tls_trust_file /etc/ssl/certs/ca-certificates.crt
logfile        /var/log/msmtp.log

account        default
host           smtp.exemple.fr
port            587
from           web01@exemple.fr
user           web01@exemple.fr
password       VOTRE_MOT_DE_PASSE
sudo chmod 600 /etc/msmtprc
echo "Test depuis web01" | mail -s "Test alerte" alix@exemple.fr

Un mot de passe en clair sur le disque

Le fichier contient un mot de passe en clair : chmod 600 est obligatoire. Préférez un mot de passe d'application dédié, révocable, plutôt que votre mot de passe principal.

Redirigez ensuite le courrier de root vers vous :

echo "root: alix@exemple.fr" | sudo tee -a /etc/aliases
sudo newaliases 2>/dev/null || true

14.4 Surveiller l'essentiel

# Trafic réseau
vnstat -d                    # par jour
vnstat -m                    # par mois, utile face à un quota

# Disque
df -h
sudo ncdu /                  # exploration interactive

# Charge et E/S
uptime
iostat -x 2 5
sar -u 1 5

Une alerte simple sur le disque, via cron, vaut mieux qu'une supervision compliquée jamais installée :

sudo tee /etc/cron.daily/alerte-disque >/dev/null <<'EOF'
#!/bin/sh
SEUIL=85
USAGE=$(df / --output=pcent | tail -1 | tr -dc '0-9')
[ "$USAGE" -ge "$SEUIL" ] && echo "Disque / à ${USAGE}% sur $(hostname -f)" \
  | mail -s "ALERTE disque $(hostname)" root
exit 0
EOF
sudo chmod +x /etc/cron.daily/alerte-disque

14.5 Sortir les journaux de la machine

Tout ce qui précède repose sur des journaux locaux. Or un attaquant qui obtient root efface /var/log et l'historique de journald : il ne reste rien pour comprendre ce qui s'est passé. Un journal n'a de valeur d'enquête que s'il existe ailleurs.

Le principe : journald recopie vers syslog, et rsyslog expédie vers un collecteur distant.

# Sur la machine : autoriser le relais vers syslog
sudo mkdir -p /etc/systemd/journald.conf.d
echo -e "[Journal]\nForwardToSyslog=yes" \
  | sudo tee /etc/systemd/journald.conf.d/10-syslog.conf
sudo systemctl restart systemd-journald

# Puis, dans rsyslog, une ligne de destination (TCP, port 514 par défaut)
# *.* @@collecteur.exemple.fr:514

La mise en place complète — collecteur, chiffrement TLS, rétention — dépasse cette fiche. Le minimum utile, si vous n'avez pas de collecteur : envoyer les alertes par courriel (§14.3), dont la copie sort de la machine.

14.6 Une sonde depuis l'extérieur

L'alerte disque ci-dessus part de la machine. Le jour où celle-ci est éteinte, saturée ou injoignable, elle n'envoie plus rien — et le silence ressemble à s'y méprendre au bon fonctionnement.

C'est la brique qui manque à presque tous les petits serveurs : une sonde externe, qui teste depuis ailleurs et vous prévient quand la réponse ne vient pas.

  • Uptime Kuma, auto-hébergé sur une autre machine — idéalement chez un autre hébergeur, sans quoi une panne de datacentre emporte la sonde avec le serveur ;
  • ou un service de supervision gratuit, si vous n'avez qu'une machine.

Surveillez au minimum le port SSH et, dès qu'ils existent, les ports 80 et 443 avec la date d'expiration du certificat.

Étape 15 — Sauvegarde et instantané de référence

Essentiel — la seule étape qui rattrape l'échec de toutes les autres.

15.1 Instantané immédiat

Vous venez de construire une base propre. Prenez-en un instantané (snapshot) chez votre hébergeur maintenant, avant de déployer le moindre service. C'est votre point de retour : une mauvaise manipulation plus tard se résoudra en trois minutes au lieu d'une réinstallation complète.

15.2 Archiver la configuration

sudo tar czf /root/config-initiale-$(date +%F).tar.gz \
  /etc/ssh/sshd_config.d /etc/hosts /etc/hostname \
  /etc/apt/sources.list.d /etc/sudoers.d \
  /etc/fail2ban/jail.local /etc/nftables.conf \
  /etc/sysctl.d /etc/default/locale 2>/dev/null

Récupérez l'archive sur votre poste :

scp -P 2222 alix@web01.exemple.fr:/root/config-initiale-*.tar.gz ./

Mieux : versionner la configuration

Mieux encore : versionnez ces fichiers dans un dépôt Git privé — sans les secrets. git est déjà installé à l'étape 5.

15.3 Mettre en place une vraie sauvegarde

Une sauvegarde qu'on n'a jamais restaurée n'est pas une sauvegarde. Principe des 3-2-1 : trois copies, deux supports, une hors site.

sudo apt install -y restic

Les paramètres de restic se passent par variables d'environnement. Comme sudo les efface (env_reset est le défaut sous Debian), déposez-les dans un fichier lu par root plutôt que de compter sur un export dans votre session :

sudo install -m 600 /dev/null /root/.restic-env
sudo tee /root/.restic-env >/dev/null <<'EOF'
RESTIC_REPOSITORY=sftp:sauvegarde@stockage.exemple.fr:/backups/web01
RESTIC_PASSWORD_FILE=/root/.restic-pass
EOF
sudo install -m 600 /dev/null /root/.restic-pass
sudo nano /root/.restic-pass        # la phrase de passe du dépôt, seule sur sa ligne

Cette phrase de passe n'est récupérable nulle part

Un dépôt restic dont on a perdu la phrase de passe est définitivement illisible. Recopiez-la hors du serveur — gestionnaire de mots de passe ou coffre de l'association — avant d'aller plus loin.

Initialisation et premières commandes, en chargeant explicitement ce fichier :

# Initialisation du dépôt distant (une seule fois)
sudo env $(cat /root/.restic-env | xargs) restic init

# Sauvegarde
sudo env $(cat /root/.restic-env | xargs) restic backup /etc /home /var/www --exclude-caches

# Vérification et purge
sudo env $(cat /root/.restic-env | xargs) restic snapshots
sudo env $(cat /root/.restic-env | xargs) restic forget \
  --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prune

Automatisez avec une minuterie systemd plutôt qu'un cron : les échecs remontent dans journalctl.

N'utilisez pas systemd-run pour une sauvegarde

systemd-run --on-calendar crée une unité transitoire : elle disparaît au redémarrage, et la sauvegarde cesse silencieusement. On ne s'en aperçoit que le jour où l'on en a besoin. Écrivez de vraies unités.

sudo tee /etc/systemd/system/sauvegarde.service >/dev/null <<'EOF'
[Unit]
Description=Sauvegarde restic
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
EnvironmentFile=/root/.restic-env
ExecStart=/usr/bin/restic backup /etc /home /var/www --exclude-caches
ExecStartPost=/usr/bin/restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prune
EOF

sudo tee /etc/systemd/system/sauvegarde.timer >/dev/null <<'EOF'
[Unit]
Description=Sauvegarde restic quotidienne

[Timer]
OnCalendar=*-*-* 03:00:00
RandomizedDelaySec=30m
Persistent=true

[Install]
WantedBy=timers.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable --now sauvegarde.timer

Persistent=true rattrape l'exécution manquée si la machine était éteinte à 3 h. EnvironmentFile règle le problème des variables : le service les lit lui-même, sans dépendre de votre session.

Résultat attendu — la minuterie apparaît avec sa prochaine échéance, et la première exécution se déclenche à la demande :

systemctl list-timers sauvegarde.timer
sudo systemctl start sauvegarde.service
journalctl -u sauvegarde.service -n 20

15.4 Tester la restauration

À faire une fois, tout de suite, puis tous les trimestres :

sudo restic restore latest --target /tmp/test-restauration --include /etc/hosts
cat /tmp/test-restauration/etc/hosts

Notez la date du dernier test réussi quelque part de visible. C'est la seule métrique qui compte vraiment.

Étape 16 — Auditer ce qui a été fait

Recommandé — vérifier que ce qui est écrit ici a réellement pris.

Vous avez appliqué quinze étapes ; un outil d'audit vous dira lesquelles ont réellement pris. Lynis parcourt la machine et note ce qu'il trouve.

sudo apt install -y lynis
sudo lynis audit system

Le rapport se termine par un index de durcissement sur 100, et surtout par une liste de suggestions numérotées, chacune avec sa justification.

Résultat attendu — sur une Debian fraîchement installée, l'index tourne autour de 55 à 60 ; après un parcours comme celui-ci, il monte typiquement vers 70 à 75.

Ne courez pas après le 100

Les points restants dépendent de ce que la machine fait : durcir un noyau au-delà du raisonnable, ou désactiver des modules dont vos services auront besoin, se paie en pannes incompréhensibles six mois plus tard. Lisez les suggestions, appliquez celles qui ont un sens pour votre usage, et notez pourquoi vous écartez les autres.

Le rapport complet reste dans /var/log/lynis-report.dat. Relancez l'audit après chaque déploiement de service : c'est la meilleure façon de voir ce qu'un apt install a ouvert.

Checklist récapitulative

Préparatifs

  • Accès de secours hors SSH vérifié (console KVM, mode rescue)
  • Enregistrement A (et AAAA) créé vers le FQDN
  • Reverse DNS (PTR) aligné sur le FQDN, IPv4 et IPv6
  • dig confirme la correspondance dans les deux sens

Identité et système

  • Première connexion réussie, état initial relevé (ss -tulpn)
  • Empreinte de la clé d'hôte comparée avec celle publiée par l'hébergeur
  • Clés d'hôte régénérées si l'image était clonée, machine-id vérifié
  • Partition étendue à tout le volume facturé (lsblk, growpart)
  • Nom d'hôte court défini avec hostnamectl set-hostname
  • /etc/hosts corrigé : FQDN avant nom court sur la ligne 127.0.1.1
  • hostname -f renvoie bien le FQDN
  • Sources APT converties en deb822, trixie-security présent
  • apt full-upgrade passé, machine redémarrée si le noyau a changé
  • systemctl --failed ne renvoie rien
  • Paquets de base installés, vnstat activé
  • Locale fr_FR.UTF-8 générée et définie (avec un tiret)
  • Fuseau horaire posé, timedatectl indique l'horloge synchronisée

Accès

  • Compte administrateur créé et ajouté au groupe sudo (avec -aG)
  • sudo -v et sudo whoami fonctionnent depuis le nouveau compte
  • Clé Ed25519 générée et déposée, connexion par clé vérifiée
  • Drop-in sshd_config.d créé : root refusé, mot de passe désactivé, AllowGroups posé
  • Groupe ssh-users créé et peuplé — avec un second administrateur
  • sudo sshd -T confirme la configuration réellement appliquée
  • Port SSH vérifié côté ssh.socket si vous l'avez changé
  • Compte fournisseur verrouillé, ses clés SSH retirées
  • Aucune clé inconnue dans les authorized_keys de la machine

Protection

  • Pare-feu actif, SSH autorisé avant activation
  • Ports ouverts alignés entre la machine et le pare-feu de l'hébergeur
  • fail2ban actif, votre IP fixe dans ignoreip
  • unattended-upgrades actif, politique de redémarrage décidée
  • Paramètres sysctl appliqués (sysctl --system)
  • 20auto-upgrades contrôlé : Unattended-Upgrade "1"
  • needrestart passé en mode non interactif
  • check-support-status consulté (paquets hors support sécurité)
  • aa-status : profils AppArmor chargés et en mode enforce
  • Swap présent si la RAM est limitée

Exploitation

  • Journaux plafonnés (journalctl --disk-usage)
  • Envoi de mail testé, courrier de root redirigé
  • Sauvegarde configurée et restauration testée une fois
  • Instantané de référence pris chez l'hébergeur
  • Archive de configuration récupérée hors du serveur
  • Minuterie de sauvegarde persistante (.timer + Persistent=true), pas un systemd-run
  • Sonde externe en place, sur une autre machine que celle-ci
  • lynis audit system passé, suggestions lues et arbitrées
  • TTL DNS remonté à sa valeur normale

Vérification finale d'un seul bloc

hostname -f; echo '---'
localectl status | head -3; echo '---'
timedatectl | grep -E 'Time zone|synchronized'; echo '---'
sudo sshd -T | grep -E '^(port|permitrootlogin|passwordauthentication|allowgroups)'; echo '---'
sudo ufw status verbose 2>/dev/null || sudo nft list ruleset | head -20; echo '---'
sudo fail2ban-client status; echo '---'
systemctl --failed; echo '---'
free -h; df -h /

Si vous êtes verrouillé dehors

Cela arrive, malgré la session de secours — elle finit par expirer, ou l'on referme la mauvaise fenêtre. Voici l'ordre dans lequel chercher.

1. Est-ce vraiment le serveur ? Depuis un autre réseau (partage de connexion du téléphone), testez le port :

nc -vz 203.0.113.10 2222

Si la connexion passe d'ailleurs mais pas de chez vous, c'est fail2ban ou le pare-feu de l'hébergeur qui a banni votre adresse, pas la configuration du serveur.

2. La console de l'hébergeur. KVM, VNC ou console série : elle donne un accès clavier indépendant de SSH et du réseau. C'est la voie la plus rapide, et la raison pour laquelle cette fiche demande de vérifier son existence avant de commencer.

Une fois connecté, retirez ce qui bloque :

sudo rm /etc/ssh/sshd_config.d/99-durcissement.conf
sudo systemctl restart ssh
sudo ufw disable            # ou : sudo nft flush ruleset
sudo fail2ban-client unban --all

3. Le mode rescue. Si même la console ne répond plus, l'hébergeur propose un démarrage sur un système de secours. Le disque de la machine s'y monte, et l'on répare de l'extérieur :

lsblk                                  # identifier la partition racine
sudo mount /dev/vda1 /mnt
sudo mount --bind /dev  /mnt/dev
sudo mount --bind /proc /mnt/proc
sudo mount --bind /sys  /mnt/sys
sudo chroot /mnt /bin/bash

# Dans le chroot : annuler la modification fautive
rm /etc/ssh/sshd_config.d/99-durcissement.conf
sed -i 's/^Port .*/Port 22/' /etc/ssh/sshd_config
systemctl disable nftables

exit
sudo umount -R /mnt

Redémarrez sur le disque, reconnectez-vous, et reprenez la configuration en gardant cette fois deux sessions ouvertes.

Ce qui verrouille, dans l'ordre de fréquence

Un port SSH changé sans ouvrir le nouveau dans le pare-feu ; un AllowGroups posé avant que le groupe n'existe ; PasswordAuthentication no avec une clé qui n'a jamais été testée ; une politique drop activée sans règle SSH au-dessus. Les quatre se préviennent en testant depuis une seconde session avant de fermer la première.

Et après ?

Le serveur est prêt à recevoir ses services — c'est là que cette fiche s'arrête et que les autres commencent :

  • Docker — installer le moteur de conteneurs sur cette base ;
  • Nextcloud et Matrix — deux services que l'association héberge ;
  • Déploiement du wiki — l'exemple d'un service statique, webhook compris.

Ouvrez les ports au moment du déploiement, pas avant, et repassez un lynis audit system derrière.

Le deuxième serveur ne se prépare pas à la main

Cette checklist a exactement la forme d'un playbook. Le jour où vous installez une deuxième machine, transposez-la dans Ansible : chaque étape devient une tâche idempotente, l'ordre est écrit noir sur blanc, et la configuration cesse de vivre dans la mémoire de celui qui l'a faite. Des rôles publics existent pour Debian 13 — lisez-les avant de les appliquer, ils durcissent parfois plus que vous ne le souhaitez.

Ce que vous avez construit

En cinq lignes, pour consolider ce que la checklist vérifie point par point :

  1. Une identité cohérente — un FQDN qui se résout dans les deux sens, un nom d'hôte stable, une heure juste. Sans cela, les journaux mentent et les mails partent en indésirables.
  2. Un accès nominatif et robuste — une clé par personne, un groupe qui décide qui entre, le compte du fournisseur neutralisé, root inaccessible depuis le réseau.
  3. Une surface réduite et surveillée — un pare-feu qui ferme par défaut, fail2ban qui écarte les robots, AppArmor qui confine les services, des journaux plafonnés et lisibles.
  4. Une machine qui se maintient seule — les correctifs de sécurité s'installent, ce qui n'est plus suivi se signale, et vous êtes prévenu de ce qui demande un redémarrage.
  5. Une porte de sortie — un instantané de référence, une sauvegarde dont la restauration a été testée, et une sonde extérieure pour vous dire que la machine vit.

Le reste — les services, leurs ports, leurs bases de données — se construit par-dessus.

Ce qui change par rapport à l'ancienne fiche Debian 11

L'ancienne fiche couvrait l'essentiel de la mise en route. Voici ce qui y était inexact ou incomplet, et pourquoi c'est corrigé ici.

Ancienne fiche Correction Pourquoi
LANG=fr_FR.UTF_8 fr_FR.UTF-8 Tiret et non underscore ; sinon la locale est invalide. Et LANG= seul ne dure que le temps de la session : utilisez localectl set-locale
Ordre dans /etc/hosts « avant les noms déjà renseignés » FQDN puis nom court, dans cet ordre hostname -f prend le premier nom de la ligne. Inversé, il renvoie le nom court
adduser nom sudo adduser nom La création d'utilisateur demande les privilèges root
Droits supplémentaires via visudo usermod -aG sudo d'abord ; visudo -f /etc/sudoers.d/10-nom si besoin spécifique L'appartenance au groupe suffit dans 95 % des cas. Un fichier dédié isole une erreur de syntaxe au lieu de casser sudo pour tout le monde
apt upgrade -y apt full-upgrade -y upgrade refuse d'installer ou de retirer des paquets, donc bloque sur des transitions courantes d'une machine neuve
« Désactiver ssh pour l'utilisateur debian » AllowGroups ssh-users + verrouillage du compte Une liste blanche couvre aussi les comptes non anticipés ; un groupe survit à l'arrivée d'un second administrateur ; le verrouillage ferme les autres voies que SSH

Ce que l'ancienne fiche ne couvrait pas

  1. Les clés SSH avant la désactivation du mot de passe. C'est la seule séquence qui ne pardonne pas l'inversion.
  2. Le pare-feu. Sans lui, chaque service déployé est exposé par défaut.
  3. Les mises à jour automatiques. Un serveur à jour le jour de l'installation ne l'est plus trois semaines après.
  4. La sauvegarde. Y compris le test de restauration, qui est la partie qu'on saute toujours.

Pièges spécifiques à Debian 13

Piège Symptôme Solution
SSH en activation par socket Port 2222 dans sshd_config ignoré, SSH reste sur 22 systemctl edit ssh.socket avec un ListenStream= vide puis le nouveau port (étape 8.4)
/tmp en tmpfs Espace disque « disparu » après migration ; gros fichiers qui saturent la RAM Nettoyer l'ancien /tmp masqué, utiliser /var/tmp pour le volumineux (étape 13.2)
Sources au format deb822 Un script qui écrit dans /etc/apt/sources.list semble sans effet Écrire dans /etc/apt/sources.list.d/*.sources
cloud-init Le nom d'hôte revient à sa valeur d'origine après redémarrage preserve_hostname: true (étape 3.3)

Sources