Retour au blog
    Tutoriels2026-05-0315 min de lecture

    Hardening OpenSSH 9.x : guide complet 2026 (PAM, FIDO2, auditd, fail2ban)

    Configuration durcie d'OpenSSH 9.x : authentification FIDO2, PAM anti-bruteforce, journalisation auditd, fail2ban, certificate authority SSH.

    sshhardeningfido2pamauditdfail2banansible

    #1. Pourquoi durcir OpenSSH en 2026 et pré-requis

    OpenSSH reste en 2026 la porte d'entrée privilégiée vers les fleets Linux : VM cloud, bastions on-prem, conteneurs LXC, équipements réseau. Les statistiques publiques d'observatoires de bots (Shadowserver, GreyNoise, AbuseIPDB) montrent qu'un service sshd exposé sur Internet absorbe entre 5 000 et 80 000 tentatives d'authentification par jour selon le préfixe IP. Les campagnes de credential stuffing visent désormais en priorité les clés faibles, les versions vulnérables (CVE-2023-38408 sur ssh-agent, CVE-2024-6387 alias regreSSHion sur sshd < 9.8) et les configurations PAM laxistes.

    OpenSSH 9.x a apporté plusieurs ruptures importantes : suppression définitive de ssh-rsa SHA-1 dans le négoce par défaut depuis 9.0, désactivation de scp historique au profit de SFTP, support natif des clés FIDO2 résidentes (-O resident), et durcissement des KEX avec sntrup761x25519-sha512 (post-quantique hybride) activé par défaut depuis 9.0. La 9.6 a corrigé la faille Terrapin (CVE-2023-48795) en introduisant la "strict KEX". La 9.8 a corrigé regreSSHion. Toute fleet figée en OpenSSH 8.x cumule donc un retard de sécurité significatif.

    Ce guide cible les sysadmins, DevOps et SRE qui exploitent un parc de serveurs Linux (Debian 12, Ubuntu 22.04 / 24.04, RHEL 9, Rocky 9, AlmaLinux 9). Pré-requis :

    • OpenSSH 9.6 minimum côté serveur (9.8 recommandée pour regreSSHion)
    • Un démon PAM fonctionnel (/etc/pam.d/sshd)
    • auditd installé et démarré
    • fail2ban >= 1.0 (ou sshguard, ou crowdsec selon préférence)
    • Pour FIDO2 : libfido2 >= 1.13 et une clé matérielle (YubiKey 5, SoloKey, NitroKey, OnlyKey, Titan)
    • Un canal de bascule : console série, IPMI/iDRAC/iLO, ou accès cloud "out of band". Toute manipulation de sshd_config doit être effectuée avec une session ouverte de secours pour éviter le lockout.

    Les contrôles ISO 27001:2022 directement adressés sont A.8.5 (secure authentication), A.8.16 (monitoring activities) et A.8.20 (networks security). Pour une mise en conformité globale, voir /services/iso-27001.

    #2. Configuration sshd_config durcie

    Le fichier /etc/ssh/sshd_config.d/00-hardening.conf (préféré à l'édition directe de sshd_config sur les distributions modernes qui utilisent Include /etc/ssh/sshd_config.d/*.conf). Toute directive ci-dessous est commentée pour expliquer le choix.

    # /etc/ssh/sshd_config.d/00-hardening.conf
    # Durcissement OpenSSH 9.x — base 2026
    
    # --- Réseau et écoute ---
    Port 2222                                    # Réduit le bruit des bots qui scannent le 22 ; n'est pas une mesure de sécurité en soi
    AddressFamily inet                           # IPv4 seul si IPv6 non utilisé ; sinon "any"
    ListenAddress 10.42.0.5                      # Bind explicite sur l'IP de management
    Protocol 2                                   # SSHv1 mort depuis 2006, mais on l'écrit explicitement
    
    # --- Versions et bannière ---
    VersionAddendum none                         # Pas de fingerprinting facilité
    Banner /etc/ssh/banner.txt                   # Avertissement légal (mention RGPD + journalisation)
    DebianBanner no                              # Sur Debian/Ubuntu : masque la sous-version
    
    # --- Authentification ---
    PermitRootLogin no                           # Root n'a aucune raison de se logger directement
    PasswordAuthentication no                    # Clés uniquement
    PermitEmptyPasswords no                      # Ceinture + bretelles
    ChallengeResponseAuthentication no           # Désactive l'auth keyboard-interactive sauf via PAM ciblé
    KbdInteractiveAuthentication no              # Synonyme moderne de la directive ci-dessus
    UsePAM yes                                   # Indispensable pour faillock, TOTP, etc.
    AuthenticationMethods publickey,keyboard-interactive:pam   # 2FA matériel + TOTP PAM
    PubkeyAuthentication yes
    HostbasedAuthentication no
    IgnoreRhosts yes
    IgnoreUserKnownHosts yes                     # Empêche un user d'auto-trustifier des hôtes
    
    # --- Algorithmes (KEX, cipher, MAC, host keys) ---
    KexAlgorithms [email protected],curve25519-sha256,[email protected]
    HostKeyAlgorithms ssh-ed25519,[email protected],[email protected],rsa-sha2-512,[email protected]
    PubkeyAcceptedAlgorithms ssh-ed25519,[email protected],[email protected],rsa-sha2-512,[email protected]
    Ciphers [email protected],[email protected],[email protected]
    MACs [email protected],[email protected],[email protected]
    
    # --- Clés d'hôte ---
    HostKey /etc/ssh/ssh_host_ed25519_key
    HostKey /etc/ssh/ssh_host_rsa_key            # Conservée pour rétro-compat clients legacy ; à retirer si le parc est homogène
    
    # --- Sessions ---
    ClientAliveInterval 300                      # Ping toutes les 5 min
    ClientAliveCountMax 2                        # Déconnecte après 10 min d'inactivité réseau
    LoginGraceTime 30                            # 30s pour s'authentifier, sinon coupe (mitige slowloris SSH)
    MaxAuthTries 3                               # 3 tentatives par session avant coupure
    MaxSessions 4                                # Limite les sessions multiplexées par connexion
    MaxStartups 10:30:60                         # Throttle des connexions concurrentes en cours d'auth
    
    # --- Forwardings et features ---
    AllowAgentForwarding no                      # On n'autorise que sur les bastions, via Match
    AllowTcpForwarding no                        # Idem
    GatewayPorts no
    PermitTunnel no
    X11Forwarding no                             # X11 = surface d'attaque, dépréciée
    PrintMotd no                                 # MotD géré par PAM
    TCPKeepAlive no                              # On préfère ClientAlive (chiffré)
    Compression no                               # Mitige CRIME-like sur SSH
    PermitUserEnvironment no                     # Empêche un user de surcharger l'env via ~/.ssh/environment
    PermitUserRC no
    
    # --- Subsystems ---
    Subsystem sftp internal-sftp -f AUTHPRIV -l INFO   # SFTP intégré, journalisé
    
    # --- Restriction d'accès ---
    AllowGroups ssh-users sre admins
    DenyUsers root toor admin guest
    
    # --- Logging ---
    SyslogFacility AUTHPRIV
    LogLevel VERBOSE                             # VERBOSE log les fingerprints des clés utilisées (utile pour audit)
    
    # --- Match blocs ---
    Match Group bastion-jumphost
        AllowTcpForwarding yes
        PermitOpen 10.0.0.0/8:22 192.168.0.0/16:22
        AllowAgentForwarding no
        ForceCommand /usr/local/bin/ssh-bastion-shell
    
    Match Address 10.42.0.0/24
        AuthenticationMethods publickey
    

    Validation et rechargement :

    sshd -t -f /etc/ssh/sshd_config             # Test de syntaxe
    sshd -T | grep -Ei 'kex|cipher|mac'         # Affiche la conf effective
    systemctl reload sshd                       # Reload sans couper les sessions actives
    

    LogLevel VERBOSE est essentiel pour la corrélation : il loggue le fingerprint SHA-256 de la clé publique présentée à chaque connexion réussie, ce qui permet de tracer précisément quelle paire de clés a été utilisée même si plusieurs clés sont autorisées pour un même compte.

    #3. FIDO2 SSH : ed25519-sk, verify-required, clés résidentes, Touch ID

    OpenSSH 8.2 a introduit les types de clés ecdsa-sk et ed25519-sk qui délèguent la signature à un authentificateur FIDO2 matériel. La clé privée ne quitte jamais le token, ce qui élimine la classe entière des vols de clés par exfiltration de fichier ou compromission d'agent.

    #Génération d'une clé FIDO2

    # Clé non-résidente (par défaut) : un handle est stocké côté client
    ssh-keygen -t ed25519-sk -O verify-required -O application=ssh:prod -C "alice@laptop"
    
    # Clé résidente (stockée dans la mémoire flash du token, portable)
    ssh-keygen -t ed25519-sk -O resident -O verify-required -O application=ssh:prod
    

    Options clés :

    • -O verify-required exige le PIN du token à chaque signature, en plus de la présence physique (touch). Sans cette option, seul le touch est requis : confort accru mais moindre garantie en cas de vol.
    • -O resident permet de récupérer la clé sur n'importe quel poste avec ssh-keygen -K. Pratique pour un workflow multi-machines, mais limite le nombre de slots du token (25 sur YubiKey 5 typiquement).
    • -O application=ssh:prod segmente l'usage par "scope" : la même clé physique peut héberger des slots distincts pour ssh:prod, ssh:lab, ssh:perso.

    #Récupération depuis le token

    cd ~/.ssh
    ssh-keygen -K        # Crée id_ed25519_sk_rk + id_ed25519_sk_rk.pub depuis les clés résidentes du token branché
    

    #Côté serveur

    Le format de ~/.ssh/authorized_keys est inchangé : la clé publique commence par [email protected]. Pour exiger côté serveur la vérification utilisateur (PIN), on ajoute l'option verify-required directement dans authorized_keys :

    verify-required sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29t... alice@laptop
    

    Combinée à PubkeyAuthOptions verify-required dans sshd_config, cette directive impose la vérification utilisateur même si la clé a été générée sans -O verify-required.

    #Touch ID sur macOS

    Sur macOS, secretive ou ssh-tpm-agent exposent le Secure Enclave comme un agent SSH. Sur Linux, tpm2-pkcs11 ou ssh-tpm-agent fournissent l'équivalent via un TPM 2.0. Ce sont des compléments aux clés FIDO2 matérielles, à privilégier pour les comptes nominatifs.

    #Migration progressive

    Pour un parc, la migration vers FIDO2 se fait par cohorte :

    1. Distribuer les tokens, former à ssh-keygen -t ed25519-sk
    2. Pousser les nouvelles clés publiques via la gestion de configuration (Ansible, Salt, Puppet) en mode additif
    3. Période de coexistence (30 jours) avec ancien et nouveau jeu de clés
    4. Retrait des anciennes clés ed25519 logicielles
    5. Activation finale de PubkeyAcceptedAlgorithms restreint à [email protected],[email protected]

    #4. PAM : pam_faillock, TOTP, OATH, Yubico

    PAM (Pluggable Authentication Modules) traite la deuxième couche d'authentification : verrouillage anti-bruteforce, OTP, contrôle d'accès horaire. Sur Debian/Ubuntu : /etc/pam.d/sshd. Sur RHEL : /etc/pam.d/sshd qui inclut system-auth et password-auth.

    #pam_faillock (anti-bruteforce local)

    pam_faillock (remplaçant moderne de pam_tally2) verrouille un compte après N échecs sur une fenêtre glissante. Configuration dans /etc/security/faillock.conf :

    deny = 5                    # Verrouille après 5 échecs
    fail_interval = 900         # Fenêtre de 15 minutes
    unlock_time = 1800          # Déverrouillage auto après 30 min
    even_deny_root = no         # Évite de se locker root
    silent                      # Ne révèle pas le nombre de tentatives restantes
    

    Activation dans /etc/pam.d/sshd (ou via authselect sur RHEL) :

    auth        required      pam_faillock.so preauth
    auth        sufficient    pam_unix.so nullok
    auth        [default=die] pam_faillock.so authfail
    account     required      pam_faillock.so
    

    Inspection : faillock --user alice affiche les tentatives. Reset : faillock --user alice --reset.

    #TOTP avec pam_google_authenticator

    apt install libpam-google-authenticator
    sudo -u alice google-authenticator -t -d -f -r 3 -R 30 -W
    

    Options : -t TOTP, -d désactive la réutilisation, -r 3 -R 30 rate-limite à 3 tentatives par 30s, -W autorise une fenêtre étendue de tolérance horaire.

    Dans /etc/pam.d/sshd, ajouter en tête :

    auth required pam_google_authenticator.so nullok
    

    Le flag nullok permet une migration progressive (utilisateurs sans TOTP encore configuré). Une fois la migration achevée, le retirer.

    #OATH HOTP/TOTP

    pam_oath est l'alternative open source plus standardisée (RFC 4226 / 6238). Configuration via /etc/users.oath :

    HOTP/T30 alice - 3132333435363738393031323334353637383930
    

    Dans /etc/pam.d/sshd :

    auth requisite pam_oath.so usersfile=/etc/users.oath window=10 digits=6
    

    #Yubico OTP avec pam_yubico

    Pour exploiter le mode OTP (long string générée par toucher la YubiKey, validée contre l'API YubiCloud ou un serveur YubiKey Validation Server self-hosted) :

    auth required pam_yubico.so id=16 key=LONGAPIKEY authfile=/etc/yubikey_mappings urllist=https://yubico.example.org/wsapi/2.0/verify
    

    /etc/yubikey_mappings contient les associations alice:ccccccaabbbb. Self-hosting du Yubico Validation Server : voir le projet yubico/yubikey-val sur GitHub (PHP, MariaDB).

    #AuthenticationMethods côté sshd

    Pour combiner clé publique (FIDO2) ET TOTP PAM :

    AuthenticationMethods publickey,keyboard-interactive:pam
    

    L'utilisateur s'authentifie d'abord par sa clé matérielle (touch + PIN), puis fournit son TOTP. Defense in depth.

    #5. auditd : règles SSH, rotation, forward Wazuh

    auditd capture les événements kernel via le sous-système audit. Pour SSH, on cible :

    • Modifications des fichiers de conf et clés
    • Appels exec de sshd
    • Événements de connexion/déconnexion (USER_LOGIN, USER_AUTH, CRED_ACQ)

    #Règles /etc/audit/rules.d/50-ssh.rules

    # Modification de la configuration SSH
    -w /etc/ssh/sshd_config -p wa -k ssh_config
    -w /etc/ssh/sshd_config.d/ -p wa -k ssh_config
    -w /etc/ssh/ssh_config -p wa -k ssh_config
    
    # Modification des clés d'hôte
    -w /etc/ssh/ssh_host_ed25519_key -p wa -k ssh_hostkey
    -w /etc/ssh/ssh_host_rsa_key -p wa -k ssh_hostkey
    
    # authorized_keys de tous les users (via /home et /root)
    -w /root/.ssh/ -p wa -k ssh_authkeys
    -w /home -p wa -k ssh_authkeys
    
    # PAM
    -w /etc/pam.d/sshd -p wa -k pam_ssh
    -w /etc/security/faillock.conf -p wa -k pam_ssh
    
    # Exec de sshd (utile pour repérer les sshd "shadow")
    -a always,exit -F arch=b64 -S execve -F path=/usr/sbin/sshd -k ssh_exec
    

    Recharger : augenrules --load && systemctl restart auditd.

    #Recherches utiles

    ausearch -k ssh_config -ts today
    ausearch -k ssh_authkeys -ts today --format text
    aureport --auth --summary -ts week
    

    #Rotation et rétention

    /etc/audit/auditd.conf :

    max_log_file = 200            # MB
    num_logs = 10
    max_log_file_action = ROTATE
    space_left = 500
    space_left_action = SYSLOG
    disk_full_action = HALT       # Sur systèmes critiques uniquement
    

    Pour des serveurs sensibles, disk_full_action = HALT est cohérent avec une politique "no log = no service" (alignée avec ANSSI ANSSI-PA-076). Pour des charges moins sensibles, SUSPEND suffit.

    #Forward vers Wazuh / SIEM

    Wazuh ingère nativement les logs auditd via son agent (<localfile><log_format>audit</log_format>). Pour un SIEM générique, auditd peut écrire en JSON via audisp-json (ou plugin audisp-syslog vers rsyslog → Loki / Elasticsearch / OpenSearch).

    Exemple de règle Wazuh pour détecter une modification non-attendue de authorized_keys :

    <rule id="100201" level="10">
      <if_sid>80700</if_sid>
      <field name="audit.key">ssh_authkeys</field>
      <description>Modification d'authorized_keys hors fenêtre de change</description>
    </rule>
    

    #6. fail2ban : jails sshd, sshd-ddos, action ipset

    fail2ban parse les logs (/var/log/auth.log ou journald) et bannit dynamiquement les IP fautives via iptables, nftables ou ipset.

    #Jail principale /etc/fail2ban/jail.d/sshd.local

    [DEFAULT]
    backend = systemd
    banaction = nftables-multiport
    banaction_allports = nftables-allports
    ignoreip = 127.0.0.1/8 ::1 10.42.0.0/24
    findtime = 10m
    bantime = 1h
    bantime.increment = true
    bantime.factor = 4
    bantime.maxtime = 14d
    
    [sshd]
    enabled = true
    port = 2222
    filter = sshd
    maxretry = 3
    findtime = 10m
    bantime = 2h
    
    [sshd-ddos]
    enabled = true
    port = 2222
    filter = sshd-ddos
    maxretry = 5
    findtime = 1m
    bantime = 24h
    

    bantime.increment = true avec factor = 4 augmente exponentiellement la durée de ban à chaque récidive : 2h, 8h, 32h, 5 jours, plafonné à 14 jours par maxtime. Une IP qui revient inlassablement finit donc bloquée sur le très long terme.

    #Action ipset (haute volumétrie)

    Sur un bastion exposé, iptables avec une règle par IP devient inefficace au-delà de quelques milliers d'entrées. ipset stocke les IP dans une structure de hash performante :

    banaction = nftables-allports[type=set, blocktype=drop]
    

    Ou en iptables :

    banaction = iptables-ipset-proto6-allports
    

    Avec ipset list f2b-sshd | wc -l pour visualiser la taille du set.

    #Whitelist via DNSBL / threat feed

    ignoreip peut référencer un fichier rechargé périodiquement, alimenté par un script qui agrège des whitelist internes (IP de bastions, monitoring, CI/CD).

    #Métriques

    fail2ban-client status sshd retourne le nombre d'IP bannies. À exporter vers Prometheus via fail2ban-prometheus-exporter pour graphes Grafana et alerting.

    #7. Bastion / jumphost : ProxyJump et SSH CA

    L'architecture cible élimine les accès SSH directs aux serveurs de production. Tous les flux passent par un bastion dédié, avec authentification certificat SSH (et non plus clés statiques).

    #ProxyJump côté client

    # ~/.ssh/config
    Host bastion-prod
        HostName bastion.prod.internal
        User alice
        Port 2222
        IdentityFile ~/.ssh/id_ed25519_sk_rk
        IdentitiesOnly yes
    
    Host *.prod.internal
        ProxyJump bastion-prod
        User alice
        IdentityFile ~/.ssh/id_ed25519_sk_rk
    

    ProxyJump (alias -J) crée un tunnel TCP via le bastion sans jamais y exposer la clé privée du client (contrairement à ProxyCommand historique avec agent forwarding).

    #SSH Certificate Authority

    OpenSSH supporte une vraie PKI SSH depuis 5.4 (2010). Le principe :

    1. Une CA SSH (paire de clés ed25519, conservée hors-ligne ou dans un HSM)
    2. La CA signe des certificats utilisateur de courte durée (8h typiquement)
    3. Les serveurs sont configurés avec TrustedUserCAKeys /etc/ssh/ca.pub et acceptent tout certificat valide signé par cette CA
    4. La CA signe également les clés d'hôte des serveurs, le client connaît la CA via ~/.ssh/known_hosts (@cert-authority *.prod.internal ssh-ed25519 AAAA...)

    #Génération de la CA

    ssh-keygen -t ed25519 -f /secure/ssh-ca-user -C "ssh-ca-user-2026"
    ssh-keygen -t ed25519 -f /secure/ssh-ca-host -C "ssh-ca-host-2026"
    

    Les clés privées de CA sont à protéger : HSM (pkcs11), Vault PKI Secret Engine, ou stockage offline avec procédure d'astreinte.

    #Signature d'un certificat user (8h)

    ssh-keygen -s /secure/ssh-ca-user \
        -I "alice-2026-05-03T1430" \
        -n alice,sre,prod-readonly \
        -V +8h \
        -O clear \
        -O permit-pty \
        -O source-address=10.42.0.0/24 \
        ~/.ssh/id_ed25519_sk_rk.pub
    
    • -I identité du certificat (loggée côté serveur)
    • -n principals : les comptes Unix sur lesquels ce certificat est valide
    • -V +8h validité de 8 heures
    • -O source-address restriction réseau

    Le certificat généré (id_ed25519_sk_rk-cert.pub) est posé à côté de la clé publique. SSH le présente automatiquement.

    #Côté serveur

    # /etc/ssh/sshd_config.d/00-hardening.conf
    TrustedUserCAKeys /etc/ssh/ca-user.pub
    HostCertificate /etc/ssh/ssh_host_ed25519_key-cert.pub
    RevokedKeys /etc/ssh/revoked-keys
    

    RevokedKeys est la KRL (Key Revocation List), générée par ssh-keygen -kf /etc/ssh/revoked-keys -u -s /secure/ssh-ca-user.pub après ajout d'identités révoquées.

    #Automatisation : Vault, Smallstep CA, Teleport

    En production, la signature manuelle est remplacée par :

    • HashiCorp Vault SSH Secret Engine : authentification de l'utilisateur sur Vault (OIDC / LDAP), Vault signe à la volée un certificat de courte durée
    • smallstep step-ca : open source, avec provisioner OIDC, ACME, JWK
    • Teleport : surcouche complète (audit session recording inclus)

    Tous trois exposent une CLI ou une API qui réduit la TTL effective des secrets longue durée à zéro.

    #Audit

    LogLevel VERBOSE côté serveur loggue :

    Accepted publickey for alice from 10.42.0.5 port 51234 ssh2: ED25519-CERT SHA256:... ID alice-2026-05-03T1430 (serial 0) CA ED25519 SHA256:...
    

    L'identité du certificat (alice-2026-05-03T1430) est unique et corrélable avec les logs d'émission de la CA. Tracabilité complète sans gestion individuelle des authorized_keys.

    #8. Ce qu'on supprime

    Le hardening passe autant par ce qu'on retire que par ce qu'on ajoute. Liste explicite des éléments à bannir :

    • PasswordAuthentication yes : zéro raison de le maintenir en 2026, tout poste utilisateur peut générer une paire de clés en 30 secondes
    • PermitRootLogin yes ou prohibit-password : jamais. Sudo nominatif, avec NOPASSWD limité aux commandes strictement nécessaires
    • X11Forwarding yes : surface d'attaque (CVE historiques sur le forwarding X11), inutile sauf cas industriel précis
    • Algorithmes faibles :
      • Ciphers CBC : aes128-cbc, aes256-cbc, 3des-cbc — vulnérables à des attaques de type Terrapin et oracle padding
      • RC4 : arcfour, arcfour128, arcfour256 — cassés
      • MAC SHA-1 : hmac-sha1, hmac-sha1-96 — collision possible
      • MAC non-ETM : préférer systématiquement les variantes *[email protected] (Encrypt-then-MAC)
    • KEX faibles : diffie-hellman-group1-sha1, diffie-hellman-group14-sha1, gss-* (sauf usage Kerberos explicite)
    • Host key algorithms : ssh-rsa (SHA-1), ssh-dss — interdire au profit de ssh-ed25519 et rsa-sha2-512
    • AllowAgentForwarding yes partout : à n'activer que sur les bastions, et idéalement remplacer par ProxyJump
    • PermitTunnel yes : sauf VPN SSH explicite
    • UseDNS yes : ajoute de la latence et des dépendances DNS au login, sans valeur sécurité réelle
    • Comptes inutilisés avec clés : auditer ~/.ssh/authorized_keys de tous les comptes (find /home /root -name authorized_keys -ls) et purger les clés orphelines

    Vérification post-modif :

    ssh -Q kex          # Liste les KEX supportés par le binaire client
    ssh -Q cipher       # Liste les ciphers supportés
    sshd -T | grep -E '^(kex|cipher|mac|host|pubkey)algorithms'   # Conf effective serveur
    

    #9. Snippet Ansible pour déployer la conf à un fleet

    Rôle Ansible minimaliste pour pousser la conf hardening sur un parc. Structure roles/ssh-hardening/ :

    # roles/ssh-hardening/tasks/main.yml
    ---
    - name: Vérifier la version OpenSSH minimale
      ansible.builtin.command: ssh -V
      register: ssh_version
      changed_when: false
      failed_when: false
    
    - name: Echec si OpenSSH < 9.6
      ansible.builtin.fail:
        msg: "OpenSSH {{ ssh_version.stderr }} trop ancien, 9.6+ requis"
      when: ssh_version.stderr is not search('OpenSSH_9\.([6-9]|1[0-9])')
    
    - name: Déposer la conf hardening
      ansible.builtin.template:
        src: 00-hardening.conf.j2
        dest: /etc/ssh/sshd_config.d/00-hardening.conf
        owner: root
        group: root
        mode: "0600"
        validate: "/usr/sbin/sshd -t -f %s"
      notify: reload sshd
    
    - name: Déposer la bannière légale
      ansible.builtin.copy:
        src: banner.txt
        dest: /etc/ssh/banner.txt
        owner: root
        group: root
        mode: "0644"
    
    - name: Déployer la CA SSH user trustée
      ansible.builtin.copy:
        src: "files/ca-user-{{ ssh_ca_generation }}.pub"
        dest: /etc/ssh/ca-user.pub
        owner: root
        group: root
        mode: "0644"
      notify: reload sshd
    
    - name: Pousser les règles auditd SSH
      ansible.builtin.copy:
        src: 50-ssh.rules
        dest: /etc/audit/rules.d/50-ssh.rules
        owner: root
        group: root
        mode: "0640"
      notify: reload auditd
    
    - name: Pousser la jail fail2ban sshd
      ansible.builtin.template:
        src: jail-sshd.local.j2
        dest: /etc/fail2ban/jail.d/sshd.local
        owner: root
        group: root
        mode: "0644"
      notify: reload fail2ban
    
    - name: faillock.conf
      ansible.builtin.template:
        src: faillock.conf.j2
        dest: /etc/security/faillock.conf
        owner: root
        group: root
        mode: "0644"
    
    - name: Configurer pam.d/sshd avec pam_faillock + pam_google_authenticator
      ansible.builtin.template:
        src: pam-sshd.j2
        dest: /etc/pam.d/sshd
        owner: root
        group: root
        mode: "0644"
        backup: true
    
    - name: Group ssh-users existe
      ansible.builtin.group:
        name: ssh-users
        state: present
    
    - name: Smoke test post-deploy
      ansible.builtin.command: sshd -T
      changed_when: false
      register: sshd_effective
    
    - name: Asserter que PasswordAuthentication est off
      ansible.builtin.assert:
        that:
          - "'passwordauthentication no' in sshd_effective.stdout"
          - "'permitrootlogin no' in sshd_effective.stdout"
          - "'kbdinteractiveauthentication no' in sshd_effective.stdout or 'authenticationmethods publickey,keyboard-interactive:pam' in sshd_effective.stdout"
    
    # roles/ssh-hardening/handlers/main.yml
    ---
    - name: reload sshd
      ansible.builtin.systemd:
        name: sshd
        state: reloaded
    
    - name: reload auditd
      ansible.builtin.command: augenrules --load
      changed_when: true
    
    - name: reload fail2ban
      ansible.builtin.systemd:
        name: fail2ban
        state: restarted
    

    Stratégie de déploiement :

    # playbook deploy-ssh-hardening.yml
    - hosts: linux_fleet
      serial: "10%"           # Par vagues de 10% du fleet
      max_fail_percentage: 0  # Stop dès le premier échec
      any_errors_fatal: true
      pre_tasks:
        - name: Vérifier qu'une session de secours existe
          ansible.builtin.assert:
            that: ansible_user is defined
      roles:
        - ssh-hardening
    

    L'option serial: "10%" couplée à max_fail_percentage: 0 bloque le déploiement à la première erreur, évitant un lockout massif. En complément, un job cron de healthcheck (ssh depuis un nœud bastion vers chaque cible, avec timeout 10s) permet une détection rapide.

    #10. Tester : ssh-audit, lynis, OpenVAS

    #ssh-audit

    L'outil de référence (jtesta/ssh-audit sur GitHub, Python). Usage :

    pip install ssh-audit
    ssh-audit -L                              # Mode listing des conf hardening de référence
    ssh-audit bastion.prod.internal:2222
    ssh-audit --policy=hardened_openssh_d2024_01 bastion.prod.internal:2222
    

    Un score [pass] sur tous les algorithmes et un grade A signalent une conf cohérente. Les warnings typiques portent sur la conservation de RSA pour rétro-compat, à arbitrer.

    #Lynis

    Audit système global (CISOFY). Section SSH spécifique :

    lynis audit system --tests-from-group ssh
    

    Lynis vérifie ~30 directives sshd_config et émet des recommandations alignées sur les benchmarks CIS.

    #OpenVAS / GVM

    Pour un scan vulnérabilité réseau, OpenVAS (Greenbone) couvre les CVE OpenSSH connues (regreSSHion, Terrapin, etc.) et signale les versions en retard. Lancement via la CLI :

    gvm-cli socket --xml '<create_target><name>fleet-prod</name><hosts>10.42.0.0/24</hosts></create_target>'
    

    #CIS Benchmark

    Pour une vérification industrielle, le CIS Distribution Independent Linux Benchmark v2.0 contient une section 5.x SSH avec ~40 contrôles. L'outil cis-cat-pro ou des rôles communautaires (dev-sec.ssh-hardening sur Ansible Galaxy) automatisent l'évaluation.

    #Test FIDO2 manuel

    ssh -v [email protected] 2>&1 | grep -E '(Authenticator|sk-)'
    

    Doit afficher :

    debug1: Server accepts key: ... ED25519-SK SHA256:...
    debug1: Authenticator requires user verification
    

    #Validation continue

    L'ensemble de ces outils gagne à être intégré en CI :

    # .gitlab-ci.yml ou GitHub Actions
    ssh-audit:
      stage: verify
      script:
        - ssh-audit --policy=hardened_openssh_d2024_01 ${TARGET}:${PORT}
      rules:
        - if: $CI_PIPELINE_SOURCE == "schedule"
    

    Un job hebdomadaire planifié contre l'ensemble de la fleet détecte rapidement toute régression de configuration (ex : un déploiement mal qualifié qui réintroduit un cipher CBC). Les résultats peuvent être exportés vers le SIEM via webhook ou agrégés dans Wazuh comme événements custom pour cartographier la dette de durcissement à l'échelle du parc.


    Cet article vous parle ? On accompagne PME, ESN et éditeurs SaaS dans leur conformité ISO 27001 / NIS2 — Lead Auditor certifié, tarifs publics, 100 % open source. Découvrir notre SOC managé open source →

    Cet article vous parle ?

    On accompagne PME, ESN et éditeurs SaaS dans leur conformité ISO 27001 / NIS2 — Lead Auditor certifié, tarifs publics, 100 % open source.

    Auteur : Équipe M-KIS