Durcir SSH : contrôle d'accès et surveillance
SSH est la porte d’entrée de presque tous les parcs et c’est justement pourquoi c’est la première chose à bien régler. Une configuration lâche n’expose pas un seul hôte, elle offre à l’attaquant les trois choses qu’il convoite le plus: un moyen d’entrer, un moyen de se déplacer, un moyen de rester. Le but de cette note n’est pas de réciter une liste de conformité, c’est de décrire la configuration que je déploie vraiment, les raisons de chaque choix et la surveillance qui transforme “quelqu’un s’est connecté” en “cette personne, depuis là, à cette heure”.
Trois principes portent l’ensemble. Aucun compte ne détient plus de privilège que son travail ne l’exige, root le premier. Chaque action humaine remonte à un individu nommé, ce qui exclut les clés partagées. Et les comptes de service, ceux que personne ne surveille, sont les plus étroitement cadrés de tous.
Ce que coûte une configuration lâche
Root par SSH est l’évidence et il doit disparaître entièrement, parce que chaque variante plus douce laisse encore une porte entrouverte:
# aucun de ces réglages n'a sa place sur un hôte durci
PermitRootLogin yes
PermitRootLogin without-password # toujours dangereux
PermitRootLogin prohibit-password # autorise encore root par clé
Une compromission directe de root contourne tous les contrôles en dessous, se déplace latéralement sans friction, persiste via une clé déposée et peut réécrire les logs mêmes qui l’auraient attrapée. Il n’existe aucune version de “root par SSH” qui survive au contact d’un vrai modèle de menace.
Les clés partagées sont l’échec plus subtil. Le jour où deux personnes s’authentifient avec la même clé privée, l’imputabilité disparaît: les logs montrent une connexion, pas qui l’a faite et un seul portable négligent compromet toute l’équipe d’un coup. Une clé appartient à exactement une personne.
Les comptes de service qui s’authentifient par mot de passe sont le troisième classique. Un mot de passe vit dans un script ou un fichier de conf, se réutilise d’une machine à l’autre et répond au brute force. Les comptes de service s’authentifient par clé, avec une restriction de commande ou ne s’authentifient pas du tout.
La configuration du serveur
Voici le sshd_config dont je pars, débarrassé de tout ce qui n’est pas un choix délibéré:
# /etc/ssh/sshd_config
# root n'est jamais atteignable par SSH
PermitRootLogin no
# clés uniquement, imposé
PasswordAuthentication no
PubkeyAuthentication yes
AuthenticationMethods publickey
PermitEmptyPasswords no
KbdInteractiveAuthentication no
# clés d'hôte: les modernes d'abord
HostKey /etc/ssh/ssh_host_ed25519_key
HostKey /etc/ssh/ssh_host_ecdsa_key
# surface de connexion
Port 2222
MaxAuthTries 3
LoginGraceTime 30
MaxStartups 10:30:60
# cryptographie: AEAD et ETM seulement, échange de clés curve25519
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com
MACs hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com
KexAlgorithms sntrup761x25519-sha512@openssh.com,curve25519-sha256,curve25519-sha256@libssh.org,diffie-hellman-group16-sha512
# fonctions coupées tant que rien n'en a besoin
AllowTcpForwarding no
AllowStreamLocalForwarding no
PermitTunnel no
X11Forwarding no
AllowAgentForwarding no
# sessions
ClientAliveInterval 300
ClientAliveCountMax 2
TCPKeepAlive no
# journalisation
LogLevel VERBOSE
SyslogFacility AUTHPRIV
PrintLastLog yes
Deux mots sur ce qui est délibérément absent.
Protocol 2 a disparu: SSHv1 a été retiré d’OpenSSH il y a des années, donc la directive est un poids mort que les démons récents rejettent.
Et ChallengeResponseAuthentication a été renommée KbdInteractiveAuthentication depuis OpenSSH 8.7; l’ancien nom fonctionne encore comme alias, mais il n’y a aucune raison de continuer à écrire une orthographe dépréciée dans un fichier censé durer.
Les restrictions de fonctions sont coupées par défaut à dessein. Le forwarding, le tunneling et l’agent forwarding sont puissants, parfois nécessaires et presque jamais utiles à tout le monde, donc ils s’accordent par groupe plutôt que globalement.
Contrôles par groupe et par source
Une politique plate a la mauvaise forme, parce qu’un administrateur, un développeur et un compte de sauvegarde n’ont pas les mêmes besoins.
Les blocs Match laissent la politique suivre le rôle:
Match Group users
AllowTcpForwarding no
AllowStreamLocalForwarding no
PermitTunnel no
Match Group sysadmins
AllowTcpForwarding local
AllowStreamLocalForwarding yes
Match Group service_accounts
AllowTcpForwarding no
PermitTTY no
ForceCommand /opt/scripts/service_wrapper.sh
Match Address 192.168.100.0/24
AllowGroups dev_team
Match Address 10.0.0.0/8
AllowGroups internal_users
Les blocs par source ne sont pas une frontière de sécurité à eux seuls, une adresse se spoofe sur un réseau plat, mais posés sur l’authentification par clé ils rabotent la surface exposée aux seuls réseaux qui ont une raison d’atteindre un compte donné.
Les comptes de service, mis en boîte
Un compte de service reçoit un shell restreint, une clé par humain qui se tient derrière lui et un wrapper qui refuse tout ce qui sort de son mandat. Le commentaire de la clé porte l’attribution, de sorte que les logs nomment une personne même quand le compte est partagé:
useradd -r -s /usr/sbin/nologin -d /opt/backup backup_svc
install -d -m 700 -o backup_svc -g backup_svc /opt/backup/.ssh
# authorized_keys: une ligne par humain, chacune cadrée sur le wrapper
command="/opt/backup/wrapper.sh",no-pty,no-port-forwarding,no-X11-forwarding,no-agent-forwarding,restrict ssh-ed25519 AAAA...alice@company-backup
command="/opt/backup/wrapper.sh",no-pty,no-port-forwarding,no-X11-forwarding,no-agent-forwarding,restrict ssh-ed25519 AAAA...bob@company-backup
L’option restrict est le raccourci moderne qui désactive d’un coup toute fonction de forwarding et de tty, de sorte qu’une future fonction d’OpenSSH est refusée par défaut plutôt qu’autorisée en silence.
Le wrapper liste ensuite les commandes exactes que le compte peut lancer et journalise le reste comme un incident:
#!/bin/bash
# /opt/backup/wrapper.sh
logger -t backup_service "cmd='$SSH_ORIGINAL_COMMAND' from $(echo "$SSH_CLIENT" | cut -d' ' -f1)"
case "$SSH_ORIGINAL_COMMAND" in
"rsync --server"*) exec $SSH_ORIGINAL_COMMAND ;;
"backup_database") exec /opt/backup/db_backup.sh ;;
*)
logger -t backup_service "DENY: $SSH_ORIGINAL_COMMAND"
echo "commande non autorisée" >&2
exit 1 ;;
esac
Les certificats, quand le parc dépasse authorized_keys
Passé une poignée d’hôtes, recopier des clés publiques cesse de passer à l’échelle et personne ne nettoie jamais les traînards. Une autorité de certification SSH remplace cette dispersion par une signature qui porte sa propre expiration, de sorte que l’accès se périme de lui-même au lieu de s’attarder des années dans un fichier:
# l'autorité, gardée hors ligne
ssh-keygen -t ed25519 -f ssh_ca -C "SSH CA"
# un certificat utilisateur court et contraint
ssh-keygen -s ssh_ca -I alice.johnson -n alice.johnson \
-O source-address=192.168.1.0/24 \
-O permit-pty \
-V +1w \
alice.johnson.pub
# les serveurs font simplement confiance à l'autorité
echo "TrustedUserCAKeys /etc/ssh/ssh_ca.pub" >> /etc/ssh/sshd_config
Le gain, c’est la révocation par expiration plutôt que par ménage: un certificat valable une semaine est un problème qui se résout tout seul, là où une ligne authorized_keys orpheline est une porte dérobée qui attend.
Second facteur, avec la bonne directive
Une clé matérielle (une FIDO2 ed25519-sk) est le second facteur le plus propre, parce que la moitié privée ne quitte jamais le jeton.
Là où un second facteur TOTP est exigé en plus de la clé, il passe par le keyboard-interactive et la directive à activer est l’actuelle, pas l’alias déprécié:
apt-get install libpam-google-authenticator
echo "auth required pam_google_authenticator.so" >> /etc/pam.d/sshd
# dans sshd_config
KbdInteractiveAuthentication yes
AuthenticationMethods publickey,keyboard-interactive
AuthenticationMethods publickey,keyboard-interactive est ce qui en fait un vrai second facteur: la clé est exigée d’abord et le code seulement ensuite, de sorte que l’un sans l’autre ne mène nulle part.
Le bastion
SSH exposé droit sur internet est un service que chaque scanner de la planète voit, brute-force sans limite et prend en empreinte pour une version à exploiter. Un bastion réduit cette surface à un unique point d’entrée durci, plus strict que les hôtes derrière lui:
# sshd_config du bastion, resserré pour l'exposition
PermitRootLogin no
AllowGroups bastion_users
ForceCommand /usr/local/bin/bastion_menu.sh
MaxAuthTries 2
LoginGraceTime 15
MaxStartups 3:50:10
AllowTcpForwarding no
X11Forwarding no
AllowAgentForwarding no
LogLevel VERBOSE
Derrière lui, les hôtes internes n’acceptent de connexion que depuis l’adresse du bastion, de sorte que la seule voie d’entrée passe par la seule machine qui est surveillée, enregistrée et patchée en premier.
Fail2ban, pour le bruit
Fail2ban n’arrête pas un attaquant déterminé avec un botnet et ce n’est pas son rôle. Ce qu’il fait, c’est assécher le flot de brute force automatisé pour que les logs restent lisibles et que les contrevenants évidents tombent au pare-feu:
# /etc/fail2ban/jail.local
[DEFAULT]
bantime = 3600
findtime = 600
maxretry = 3
backend = systemd
[sshd]
enabled = true
port = 2222
maxretry = 3
bantime = 86400
Sur un serveur en clés seules il n’y a pas de mot de passe à brute-forcer au départ, donc fail2ban ici tient de l’hygiène de logs et de la pression sur le pare-feu, pas de ce qui tient les attaquants dehors. Ce rôle revient à la clé.
Une surveillance qui nomme
La journalisation verbeuse ne vaut rien si personne ne la lit, donc les connexions acceptées sont analysées en quelque chose qui peut lever une alerte et nourrir un SIEM:
#!/bin/bash
# ssh_monitor.sh
tail -F /var/log/auth.log | while read -r line; do
case "$line" in
*"sshd"*"Accepted"*)
user=$(sed -n 's/.*Accepted [a-z]* for \([A-Za-z0-9._-]*\).*/\1/p' <<<"$line")
ip=$(sed -n 's/.*from \([0-9.]*\).*/\1/p' <<<"$line")
logger -t ssh_monitor -p auth.notice "login user=$user src=$ip"
[ "$user" = root ] && echo "connexion root depuis $ip" | \
mail -s "SSH ALERT" security@company.example
;;
esac
done
L’audit de l’usage des clés est l’autre moitié, parce que les fichiers authorized_keys s’accumulent et personne ne les taille.
Un passage périodique qui compte les clés par compte, signale celles sans restriction de commande et fait remonter les clés privées traînant dans les répertoires personnels, transforme une dispersion invisible en rapport relisable:
#!/bin/bash
# ssh_key_audit.sh
for home in /home/*; do
ak="$home/.ssh/authorized_keys"
[ -f "$ak" ] || continue
printf '%s: %s clés' "$(basename "$home")" "$(wc -l < "$ak")"
grep -q 'command=' "$ak" && printf ' (restreintes)' || printf ' (NON RESTREINTES)'
printf '\n'
done
Où s’arrêter
La note honnête, c’est qu’OpenSSH lui-même a un palmarès exceptionnel: les vulnérabilités critiques de protocole sont rares et les sérieuses récentes, regreSSHion (CVE-2024-6387) en tête, ont touché le build portable plutôt que celui d’OpenBSD. Ce palmarès est une raison de garder le démon à jour, pas une raison de sauter le durcissement, parce que presque tout ce qui précède défend contre la mauvaise configuration et l’abus d’identifiants plutôt que contre une faille dans SSH. Le démon a rarement été le maillon faible. La configuration autour l’est le plus souvent.
Un dernier mot sur les astuces. Le port knocking et la Single Packet Authorization ajoutent une couche et aussi une façon de se verrouiller dehors quand une séquence casse; l’obscurité complète le durcissement, elle ne le remplace jamais. Multiplexer SSH et HTTPS sur un même port avec sslh est commode à l’exploitation et faible en stratégie, pour les raisons que son propre article détaille longuement. Rien de tout cela ne remplace les trois choses qui portent vraiment le poids: pas de root, une clé par personne et des logs que quelqu’un lit.
[edit: 2026] L’implémentation Ansible de tout ceci: les rôles ssh et ssh-users .