Pendant la première heure sur un VPS Ubuntu ou Debian tout neuf, fermez les portes que les scanners automatiques testent quelques minutes après la mise en ligne d'une adresse IP. Mettez le système à jour, connectez-vous avec un utilisateur non root doté de sudo et d'une clé Ed25519, désactivez les connexions par mot de passe et en root dans un fichier drop-in SSH, puis activez un pare-feu qui refuse tout par défaut et n'expose que SSH et les ports web, idéalement au seul CDN. Activez ensuite les mises à jour de sécurité automatiques avec des redémarrages planifiés, ajoutez fail2ban ou CrowdSec, placez l'application dans une sandbox systemd et mettez en place sauvegardes et alertes avant que le DNS ne pointe vers le serveur.
Les commandes prennent Ubuntu 24.04 LTS comme référence et signalent les différences de Debian 12 et 13. Gardez la console web de l'hébergeur ouverte pendant le travail. C'est votre porte de secours si une modification de SSH ou du pare-feu vous enferme dehors.
Mettre à jour le système et créer un utilisateur sudo
Beaucoup d'hébergeurs fournissent un accès root par mot de passe : remplacez d'abord les deux. Appliquez les mises à jour en attente, puis créez un compte personnel avec les droits sudo :
apt update && apt full-upgrade -y
adduser deploy
usermod -aG sudo deploy
Les images cloud d'Ubuntu contiennent déjà un compte ubuntu doté de sudo. Sur une installation Debian minimale avec un mot de passe root, sudo peut manquer : lancez d'abord apt install sudo. Donnez à chaque personne un compte nominatif pour que la piste d'audit renvoie à un humain. L'UTC simplifie la corrélation des journaux entre serveurs et tableaux de bord du CDN : timedatectl set-timezone Etc/UTC.
Ce qui change sous Debian par rapport à Ubuntu 24.04
| Domaine | Ubuntu 24.04 LTS | Debian 12 et 13 |
|---|---|---|
| Écoute SSH | ssh.socket lance le démon à la demande |
en général ssh.service ; vérifiez avec systemctl status |
| Pare-feu | ufw installé mais inactif |
rien d'activé ; nftables est le framework par défaut |
| Mises à jour automatiques | unattended-upgrades installé et activé |
peut manquer ; installez-le et activez-le |
| Journal d'authentification | /var/log/auth.log et le journal systemd |
journal seulement ; rsyslog n'est plus installé par défaut depuis Debian 12 |
sudo |
installé | peut manquer sur une installation minimale |
| OpenSSH | 9.6 | 9.2 sous Debian 12, 10.0 sous Debian 13 |
SSH avec des clés Ed25519 et un drop-in de durcissement
Créer et installer la clé
Générez la clé sur votre poste de travail, jamais sur le serveur. Les clés Ed25519 sont courtes, rapides et prises en charge par toutes les versions actuelles d'OpenSSH. Protégez la clé privée par une phrase de passe et confiez-la à ssh-agent. Avec une clé de sécurité FIDO2, ssh-keygen -t ed25519-sk lie la clé au matériel.
ssh-keygen -t ed25519 -C "deploy@laptop-2026"
ssh-copy-id -i ~/.ssh/id_ed25519.pub [email protected]
ssh-copy-id exige une connexion par mot de passe. Si root accepte déjà votre clé et que les mots de passe sont désactivés, copiez plutôt le fichier de clés sur le serveur :
sudo install -d -m 700 -o deploy -g deploy /home/deploy/.ssh
sudo install -m 600 -o deploy -g deploy /root/.ssh/authorized_keys /home/deploy/.ssh/authorized_keys
Les permissions comptent : avec StrictModes activé, ce qui est le cas par défaut, sshd ignore les fichiers de clés modifiables par d'autres utilisateurs.
Écrire le drop-in
Sous Ubuntu, /etc/ssh/sshd_config commence par Include /etc/ssh/sshd_config.d/*.conf, et le manuel sshd_config précise que pour chaque mot-clé, la première valeur obtenue est retenue. Les drop-ins sont lus dans l'ordre lexical et l'emportent sur le reste du fichier principal. Les images cloud livrent souvent leur propre fichier, comme 50-cloud-init.conf ou 60-cloudimg-settings.conf, qui définit PasswordAuthentication : donnez donc au vôtre un préfixe bas.
# /etc/ssh/sshd_config.d/00-hardening.conf
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
AuthenticationMethods publickey
AllowUsers deploy
MaxAuthTries 3
LoginGraceTime 30
X11Forwarding no
KbdInteractiveAuthentication no ferme la voie keyboard-interactive de PAM, qui peut encore demander un mot de passe quand PasswordAuthentication est désactivé. AllowUsers limite les connexions aux comptes nommés. Le fichier principal d'Ubuntu définit X11Forwarding yes, et le drop-in l'emporte parce qu'il est lu en premier. Si personne n'a besoin de redirection, ajoutez AllowAgentForwarding no et AllowTcpForwarding no.
Validez, affichez les valeurs effectives et appliquez :
sudo sshd -t
sudo sshd -T | grep -Ei 'permitrootlogin|passwordauthentication|kbdinteractive|authenticationmethods|allowusers'
sudo systemctl restart ssh
sshd -t détecte les erreurs de syntaxe, et sshd -T affiche la configuration réellement appliquée, y compris si un fichier cloud-init a pris le dessus. Redémarrer ssh conserve les sessions établies. Sous Ubuntu 24.04, le socket d'écoute appartient à ssh.socket : changer Port ou ListenAddress exige aussi sudo systemctl daemon-reload et sudo systemctl restart ssh.socket, une fois le nouveau port autorisé dans le pare-feu. Le guide OpenSSH server d'Ubuntu couvre les autres options.
Tester dans une seconde session avant de fermer la première
Gardez la session actuelle ouverte. Depuis un nouveau terminal, connectez-vous en deploy avec la clé et lancez sudo -v. Vérifiez ensuite que les chemins fermés échouent :
ssh -o PubkeyAuthentication=no -o PreferredAuthentications=password [email protected]
ssh [email protected]
Les deux tentatives doivent se terminer par Permission denied (publickey). Ne fermez la session d'origine qu'après cette vérification. Déplacer SSH sur un autre port réduit le bruit dans les journaux, mais ce n'est pas une mesure de sécurité.
Un pare-feu qui refuse tout par défaut avec ufw ou nftables
Le pare-feu de l'hôte est la deuxième couche après le pare-feu réseau de l'hébergeur, s'il en propose un. Rejetez tout le trafic entrant sauf SSH et les ports web, autorisez le trafic sortant et couvrez IPv6 autant qu'IPv4.
Ubuntu : ufw
Ajoutez la règle SSH avant d'activer le pare-feu, sinon ufw enable coupe votre session :
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw limit OpenSSH
sudo ufw allow proto tcp from any to any port 80,443 comment 'web'
sudo ufw enable
sudo ufw status verbose
Selon le manuel ufw, une règle limit bloque une adresse qui ouvre 6 connexions ou plus en 30 secondes. Cela ralentit les scanners, mais peut gêner une automatisation qui ouvre beaucoup de connexions SSH en parallèle ; un simple allow convient alors mieux. Avec une adresse d'administration fixe ou un VPN, n'autorisez SSH que depuis cette source : sudo ufw allow from 198.51.100.7 to any port 22 proto tcp. Les règles s'appliquent aussi à IPv6 tant que IPV6=yes reste défini dans /etc/default/ufw, ce qui est la valeur par défaut.
La documentation Docker avertit que les ports publiés par Docker contournent les règles d'ufw et de firewalld. Publiez les conteneurs sur l'adresse de bouclage, par exemple -p 127.0.0.1:3000:3000, et laissez le reverse proxy faire face à Internet.
Debian : nftables
Debian utilise nftables comme framework de pare-feu. Ce /etc/nftables.conf applique la même politique et prépare des ensembles nommés pour les plages du CDN de la section suivante :
#!/usr/sbin/nft -f
flush ruleset
table inet filter {
set cloudflare_v4 {
type ipv4_addr
flags interval
}
set cloudflare_v6 {
type ipv6_addr
flags interval
}
chain input {
type filter hook input priority filter; policy drop;
ct state established,related accept
ct state invalid drop
iif "lo" accept
meta l4proto { icmp, ipv6-icmp } accept
tcp dport 22 accept
ip saddr @cloudflare_v4 tcp dport { 80, 443 } accept
ip6 saddr @cloudflare_v6 tcp dport { 80, 443 } accept
}
chain forward {
type filter hook forward priority filter; policy drop;
}
}
Vérifiez-le avec sudo nft -c -f /etc/nftables.conf, puis lancez sudo systemctl enable --now nftables. Laissez ICMPv6 ouvert, car la découverte des voisins et la découverte du MTU de chemin en IPv6 en dépendent. flush ruleset supprime aussi les tables créées par fail2ban ou Docker : redémarrez-les après un rechargement. Utilisez un seul outil de gestion du pare-feu par hôte.
Ne laisser que le CDN atteindre les ports web
Derrière Cloudflare ou un autre CDN, l'origine ne doit répondre à personne d'autre sur les ports 80 et 443. Sinon, quiconque trouve l'adresse de l'origine, via d'anciens enregistrements DNS, un serveur de messagerie sur la même IP ou un scan d'Internet, contourne le WAF, les limites de débit et le cache du CDN. Cloudflare publie ses plages sur https://www.cloudflare.com/ips-v4 et https://www.cloudflare.com/ips-v6, et sa documentation sur les adresses IP indique que les nouvelles plages sont ajoutées à la liste avant leur mise en production.
Avec ufw, ajoutez une règle par plage. Les fichiers n'ont pas de saut de ligne final : la boucle doit traiter explicitement la dernière ligne, sinon elle saute une plage sans rien dire.
#!/usr/bin/env bash
set -euo pipefail
for list in ips-v4 ips-v6; do
curl -fsS "https://www.cloudflare.com/$list" |
while read -r cidr || [ -n "$cidr" ]; do
ufw allow proto tcp from "$cidr" to any port 80,443 comment 'cloudflare'
done
done
Supprimez ensuite la règle ouverte avec sudo ufw delete allow proto tcp from any to any port 80,443. Avec nftables, rafraîchissez les deux ensembles dans une seule transaction, car nft -f applique tout le lot ou rien. Si un téléchargement échoue, set -e arrête le script et les anciens ensembles restent en place :
#!/usr/bin/env bash
set -euo pipefail
v4=$(curl -fsS https://www.cloudflare.com/ips-v4 | paste -sd, -)
v6=$(curl -fsS https://www.cloudflare.com/ips-v6 | paste -sd, -)
nft -f - <<EOF
flush set inet filter cloudflare_v4
flush set inet filter cloudflare_v6
add element inet filter cloudflare_v4 { $v4 }
add element inet filter cloudflare_v6 { $v6 }
EOF
Exécutez-le au démarrage après nftables.service, puis chaque semaine avec un timer systemd. Nginx a ensuite besoin de set_real_ip_from pour les mêmes plages et de real_ip_header CF-Connecting-IP;, sinon journaux et limites de débit voient les adresses de Cloudflare au lieu de celles des visiteurs. Le guide sur le cache avec Nginx et Cloudflare couvre le côté serveur web.
Une liste d'IP autorisées prouve qu'une connexion vient du réseau de Cloudflare, pas qu'elle concerne votre zone. Le guide de protection de l'origine de Cloudflare la juge vulnérable à l'usurpation d'IP et décrit des options plus solides : Authenticated Origin Pulls, qui exige le mode de chiffrement Full ou Full (strict), et Cloudflare Tunnel, qui ne nécessite aucun port web entrant.
Mises à jour automatiques et redémarrages du noyau
Ubuntu Server installe unattended-upgrades et active les mises à jour de sécurité par défaut, comme le décrit le guide des mises à jour automatiques. Vérifiez-le :
cat /etc/apt/apt.conf.d/20auto-upgrades
sudo unattended-upgrade --dry-run -v
Le fichier doit contenir APT::Periodic::Update-Package-Lists "1"; et APT::Periodic::Unattended-Upgrade "1";. Sous Debian, lancez sudo apt install unattended-upgrades puis sudo dpkg-reconfigure -plow unattended-upgrades.
Les correctifs du noyau ne prennent effet qu'après un redémarrage. Sous Ubuntu 24.04, needrestart redémarre les services qui utilisent des bibliothèques mises à jour, mais il ne peut pas remplacer le noyau en cours d'exécution. Quand un paquet exige un redémarrage, le fichier /var/run/reboot-required apparaît. Placez vos réglages locaux dans un fichier trié après le 50unattended-upgrades fourni par le paquet :
// /etc/apt/apt.conf.d/52unattended-upgrades-local
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "04:30";
Choisissez une heure creuse et assurez-vous que l'application redémarre au boot sans intervention manuelle. Les journaux se trouvent dans /var/log/unattended-upgrades/. Canonical Livepatch, gratuit pour un usage personnel sur cinq machines au plus, corrige en mémoire les vulnérabilités critiques et élevées du noyau, mais Canonical précise qu'il ne remplace pas le redémarrage.
fail2ban ou CrowdSec
Avec la connexion par mot de passe désactivée, une attaque par force brute sur SSH ne peut pas réussir. Un outil de bannissement est une seconde couche : il réduit le bruit des journaux, économise le CPU consommé par les négociations de connexion et ralentit les scanners. Choisissez-en un.
fail2ban est empaqueté dans les deux distributions et ne demande aucun compte. Sous Ubuntu 24.04, le paquet active la prison sshd avec le backend du journal systemd et les actions nftables, et ce backend convient aussi à Debian, qui n'a pas de auth.log. Installez-le avec sudo apt install fail2ban, puis réglez la prison dans un fichier local plutôt que dans jail.conf :
# /etc/fail2ban/jail.d/sshd.local
[sshd]
enabled = true
backend = systemd
maxretry = 5
findtime = 10m
bantime = 1h
bantime.increment = true
Appliquez le fichier avec sudo systemctl restart fail2ban et inspectez la prison avec sudo fail2ban-client status sshd.
CrowdSec analyse les mêmes journaux, partage des signaux avec une liste de blocage communautaire et applique ses décisions via un composant de remédiation distinct. Ubuntu 24.04 fournit la version 1.4.6, alors que le guide d'installation de CrowdSec demande une version plus récente issue du dépôt de l'éditeur. Lisez le script d'installation avant de l'exécuter :
curl -s https://install.crowdsec.net | sudo sh
sudo apt install crowdsec crowdsec-firewall-bouncer-nftables
sudo cscli collections list
sudo cscli decisions list
Derrière un CDN, les requêtes web arrivent depuis les adresses du CDN. Bannir un attaquant HTTP dans le pare-feu de l'hôte bloque soit un nœud de Cloudflare, soit une adresse qui ne se connecte jamais directement. Réservez les bannissements de l'hôte à SSH et utilisez les règles WAF et de limitation de débit du CDN pour HTTP.
Isoler l'application avec systemd
Faites tourner le backend sous son propre utilisateur système dans une unité systemd, pas dans un shell ni avec nohup, et laissez systemd retirer tout ce dont le processus n'a pas besoin. Créez l'utilisateur avec sudo useradd --system --no-create-home --shell /usr/sbin/nologin webapp. Cette unité lance un service Node.js sur localhost derrière Nginx :
# /etc/systemd/system/webapp.service
[Unit]
Description=Example web application
After=network-online.target
Wants=network-online.target
[Service]
User=webapp
Group=webapp
WorkingDirectory=/opt/webapp
ExecStart=/usr/bin/node /opt/webapp/server.js
Environment=HOST=127.0.0.1 PORT=3000
LoadCredential=db_password:/etc/webapp/db_password
Restart=on-failure
StateDirectory=webapp
UMask=0077
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
PrivateDevices=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectKernelLogs=yes
ProtectControlGroups=yes
ProtectClock=yes
ProtectHostname=yes
ProtectProc=invisible
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
RestrictNamespaces=yes
RestrictRealtime=yes
RestrictSUIDSGID=yes
LockPersonality=yes
CapabilityBoundingSet=
SystemCallArchitectures=native
SystemCallFilter=@system-service
SystemCallErrorNumber=EPERM
[Install]
WantedBy=multi-user.target
NoNewPrivileges=yes empêche le processus et ses enfants d'acquérir des privilèges via des binaires setuid ou des capabilities de fichier. ProtectSystem=strict monte le système de fichiers en lecture seule pour le service, sauf /dev, /proc et /sys : le seul emplacement inscriptible est /var/lib/webapp, créé par StateDirectory=. Ajoutez ReadWritePaths= pour tout le reste. ProtectHome=yes masque /home, /root et /run/user, et PrivateTmp=yes donne au service ses propres /tmp et /var/tmp. Un CapabilityBoundingSet= vide retire toutes les capabilities, et @system-service est la liste d'appels autorisés que le manuel systemd.exec recommande comme point de départ.
MemoryDenyWriteExecute=yes est volontairement absent : le manuel indique qu'il est incompatible avec les moteurs JIT, et V8 dans Node.js génère du code à l'exécution. Ajoutez-le aux services écrits en Go, Rust ou C, puis testez.
Sous Ubuntu 24.04 avec systemd 255, systemd-analyze security --offline=true a noté ce fichier 1.5, contre 9.0 pour la même unité avec seulement User=. Démarrez le service et vérifiez :
sudo systemctl daemon-reload
sudo systemctl enable --now webapp
systemd-analyze security webapp.service
journalctl -u webapp -b -n 50 --no-pager
Si le service ne démarre pas, le journal nomme en général le chemin ou l'appel système bloqué. Assouplissez une option à la fois au lieu de supprimer tout le bloc. Les services empaquetés comme Nginx démarrent en root et ont besoin de capabilities précises : durcissez-les via sudo systemctl edit nginx, là aussi une option à la fois.
Synchronisation de l'heure, journald et rétention des journaux
La validation TLS, les codes TOTP, les plannings de sauvegarde et la corrélation des journaux supposent une horloge juste. Ubuntu 24.04 et Debian 12 utilisent systemd-timesyncd par défaut. Lancez timedatectl status et cherchez System clock synchronized: yes et NTP service: active. Si aucun service NTP ne tourne, installez systemd-timesyncd ou chrony.
Avec la valeur par défaut Storage=auto, le journal n'est conservé sur disque que si /var/log/journal existe, et il peut occuper jusqu'à 10 % du système de fichiers, plafonnés à 4 Go. Fixez des limites explicites pour qu'un service bavard ne remplisse pas un petit disque et que la rétention corresponde à votre politique de confidentialité :
# /etc/systemd/journald.conf.d/50-retention.conf
[Journal]
Storage=persistent
SystemMaxUse=1G
MaxRetentionSec=30day
Appliquez-le avec sudo systemctl restart systemd-journald et contrôlez journalctl --disk-usage. Les journaux de Nginx et de l'application dans /var/log sont tournés par logrotate : alignez /etc/logrotate.d/ sur la même durée. Les notes de publication de Debian 12 confirment que rsyslog n'est plus installé par défaut, si bien que sous Debian le journal systemd est le seul journal système. Les journaux d'accès contiennent des adresses IP, qui peuvent constituer des données personnelles au sens du RGPD : une rétention plus courte est souvent la bonne.
Secrets et permissions de fichiers
Sur un VPS, les secrets fuient le plus souvent par des chemins banals : un fichier .env lisible par tous, une clé dans l'historique Git ou une variable d'environnement imprimée dans un rapport de plantage. Le manuel systemd affirme que les variables d'environnement ne conviennent pas aux secrets, car elles sont exposées aux clients non privilégiés via D-Bus et héritées par les processus enfants. Utilisez LoadCredential=, comme dans l'unité ci-dessus. Le fichier source reste lisible par root seul, et le service reçoit une copie en lecture seule sous $CREDENTIALS_DIRECTORY :
sudo install -d -m 0700 -o root -g root /etc/webapp
sudo install -m 0600 -o root -g root /dev/null /etc/webapp/db_password
sudoedit /etc/webapp/db_password
systemd-creds encrypt associé à LoadCredentialEncrypted= conserve en plus le fichier chiffré au repos avec une clé de l'hôte ou le TPM. Le code de l'application doit appartenir à root ou à un utilisateur de déploiement, pas à l'utilisateur du service, pour qu'un processus compromis ne puisse pas se réécrire. Après l'installation d'un logiciel tiers, cherchez les fichiers modifiables par tous et les binaires setuid inattendus :
sudo find / -xdev -type f -perm -0002 -print
sudo find / -xdev -type f -perm -4000 -print
Donnez à la clé de déploiement de la CI sa propre ligne dans authorized_keys, avec l'option restrict et, si possible, une liste d'adresses from=. Les agents de programmation qui exécutent des commandes sur le serveur exigent la même discipline : un utilisateur dédié, aucun secret de production et une approbation pour les commandes destructrices. Le guide du développement agentique compare les contrôles de permissions des CLI actuelles.
Sauvegardes et points d'accroche pour la supervision
Le durcissement réduit la probabilité d'un incident, tandis que les sauvegardes et les alertes décident de sa durée. Les instantanés de l'hébergeur facilitent un retour arrière rapide, mais ils vivent dans le même compte que le serveur et ne constituent donc pas une copie indépendante. Conservez une copie chiffrée et versionnée hors de l'hébergeur, et testez une restauration. Le guide de sauvegarde 3-2-1 avec restic détaille l'organisation du dépôt, la rétention et les exercices de restauration.
Un VPS isolé a besoin de quelques signaux : un contrôle HTTP externe à travers le CDN, un contrôle vérifiant que l'origine refuse les connexions directes, l'occupation du disque, l'expiration des certificats, les unités systemd en échec et un heartbeat de chaque tâche planifiée, pour qu'une sauvegarde arrêtée en silence déclenche une alerte. systemd peut appeler un notificateur dès qu'une unité échoue :
# /etc/systemd/system/[email protected]
[Unit]
Description=Alert on failure of %i
[Service]
Type=oneshot
ExecStart=/usr/local/bin/notify-failure %i
Ajoutez OnFailure=notify-failure@%n.service à la section [Unit] des unités de l'application et des sauvegardes. Le script peut envoyer le nom de l'unité et les dernières lignes du journal à un webhook de messagerie ou d'astreinte. Les exportateurs de métriques, comme le node exporter de Prometheus, doivent écouter sur localhost ou un réseau privé. Si le serveur exécute des agents IA ou des outils MCP avec accès au shell, donnez-leur leur propre utilisateur et leur sandbox, et traitez leurs entrées comme non fiables, comme l'explique le guide sur la prompt injection et la sécurité MCP.
La checklist de la première heure
- Appliquez les mises à jour, créez un utilisateur sudo nominatif et gardez la console de l'hébergeur ouverte.
- Installez une clé Ed25519 et ajoutez
00-hardening.confavec les connexions root et par mot de passe désactivées. - Lancez
sshd -tetsshd -T, redémarrezsshet testez une nouvelle session avant de fermer l'ancienne. - Activez un pare-feu qui refuse tout par défaut en IPv4 et IPv6 et autorise SSH, 80 et 443.
- Limitez les ports web aux plages de Cloudflare ou utilisez Authenticated Origin Pulls ou Tunnel, et restaurez les IP des visiteurs dans Nginx.
- Vérifiez les mises à jour de sécurité automatiques et planifiez les redémarrages automatiques.
- Activez fail2ban ou CrowdSec pour SSH et utilisez le WAF du CDN pour HTTP.
- Faites tourner l'application sous son propre utilisateur dans une unité systemd isolée et vérifiez
systemd-analyze security. - Vérifiez la synchronisation de l'heure et fixez la taille et la rétention du journal.
- Déplacez les secrets dans des fichiers credentials lisibles par root seul et vérifiez les permissions.
- Mettez en place des sauvegardes hors de l'hébergeur, testez une restauration et ajoutez alertes d'échec et heartbeats.
- Redémarrez une fois volontairement et vérifiez que chaque service revient sans intervention manuelle.