Ogni anno, migliaia di server vengono compromessi a causa di chiavi SSH mal gestite: chiavi private senza passphrase, algoritmi obsoleti come RSA a 1024 bit rimasti attivi per anni, agent forwarding abilitato su host non fidati. Il protocollo SSH è crittograficamente solido, ma la sua sicurezza dipende completamente da come si generano, distribuiscono e ruotano le chiavi. Questa guida ti insegna a usare ssh-keygen nel modo corretto, dalla scelta dell’algoritmo alla rotazione periodica, con 12 step pratici e tutti i comandi necessari per avere un setup SSH hardened in 30 minuti.

Perché la Gestione delle Chiavi SSH è Critica nel 2026

OpenSSH 10.0, rilasciato il 9 aprile 2025, ha introdotto il key exchange post-quantistico ibrido mlkem768x25519-sha256 come predefinito per le connessioni client. Questo segnala una svolta epocale nella sicurezza SSH: il protocollo si adatta alla minaccia dei computer quantistici, ma le vulnerabilità legate alla cattiva gestione delle chiavi restano umane, non tecnologiche.

Nel febbraio 2025, il MITRE ha registrato due vulnerabilità critiche in OpenSSH: CVE-2025-26465 affliggeva il client dalla versione 6.8p1 alla 9.9p1 e permetteva un attacco man-in-the-middle quando VerifyHostKeyDNS era attivo; CVE-2025-26466 abilitava un DoS pre-autenticazione sulle versioni dalla 9.5p1 alla 9.9p1. Il fix è arrivato con OpenSSH 9.9p2. La Singapore Cyber Security Agency ha raccomandato l’aggiornamento immediato a 9.9p2 e la disabilitazione di VerifyHostKeyDNS per chi non usa DNSSEC.

Secondo il rapporto Unit 42 del 2026, l’89% delle violazioni informatiche coinvolge credenziali compromesse, categoria in cui rientrano a pieno titolo le chiavi private SSH non protette o non ruotate. I pattern più comuni includono: chiavi senza passphrase trovate in repository Git pubblici, chiavi di ex dipendenti mai rimosse da authorized_keys, e agent forwarding usato su host intermedi compromessi per il movimento laterale. Tutti problemi risolvibili con i 12 step di questa guida.

Prerequisiti e Versioni Richieste

Prima di iniziare, verifica che l’ambiente soddisfi questi requisiti. Tutti i comandi sono stati testati su Ubuntu 24.04 LTS, ma funzionano su qualsiasi distribuzione Linux moderna e su macOS 14+.

  • OpenSSH 8.0 o superiore (preferibilmente 9.9p2 o 10.0 per le patch di sicurezza 2025). Verifica con ssh -V
  • Sistema operativo: Ubuntu 22.04+, Debian 12+, RHEL 9+, macOS 13+, o Windows 10 con OpenSSH nativo
  • Accesso root o sudo sul server per modificare /etc/ssh/sshd_config e gestire il servizio SSH
  • fail2ban 1.0+ (opzionale ma fortemente raccomandato per server esposti su Internet)
  • Python 3.8+ per lo script di audit delle chiavi autorizzate (Step 12)
  • Una connessione attiva al server remoto e accesso a un terminale locale
# Verifica la versione di OpenSSH installata localmente
ssh -V
# OpenSSH_9.9p2, OpenSSL 3.3.2 6 Feb 2024

# Verifica la versione del demone SSH sul server remoto
ssh utente@server "sshd -V 2>&1 | head -1"

# Aggiorna all'ultima versione disponibile su Ubuntu/Debian
sudo apt update && sudo apt install --only-upgrade openssh-client openssh-server -y

# Su RHEL/Rocky/AlmaLinux
sudo dnf update openssh-clients openssh-server -y

Confronto tra Tipi di Chiavi SSH: Ed25519, RSA, ECDSA

La scelta dell’algoritmo crittografico per la chiave SSH determina sicurezza, velocità e compatibilità. La tabella seguente riepiloga i quattro tipi principali con dati ufficiali basati sulle specifiche OpenSSH 2025:

Tipo di ChiaveDimensione (bit)Sicurezza equivalenteVelocitàCompatibilitàRaccomandazione 2026
Ed25519256~128 bitMolto veloceOpenSSH 6.5+ (2014)Scelta migliore: default da OpenSSH 9.5
RSA 40964096~150 bitLentaTutti i sistemiSolo per compatibilità legacy
ECDSA 256256~128 bitVeloceOpenSSH 5.7+ (2011)Accettabile, Ed25519 preferito
RSA 20482048~112 bitModerataTutti i sistemiMinimo accettabile, sconsigliato per nuovi setup
DSA1024 fisso<80 bitIrrilevanteDeprecatoRimuovere immediatamente

OpenSSH 9.5 (ottobre 2023) ha reso Ed25519 il tipo di chiave generato automaticamente da ssh-keygen senza l’opzione -t. Ed25519 usa la curva di Edwards a 25519 bit: produce chiavi di soli 68 byte (contro i 3.401 byte di RSA 4096), firma documenti in microsecondi, ed è resistente agli attacchi a canale laterale per design. Per sistemi che devono interoperare con apparecchiature di rete datate o server con OpenSSH precedente alla versione 6.5, usa RSA 4096 come fallback di compatibilità. Non usare mai RSA con meno di 2048 bit e rimuovi immediatamente qualsiasi chiave DSA presente.

Step 1-3: Generare una Chiave SSH con ssh-keygen

Step 1: Generare una Chiave Ed25519

Apri il terminale e usa il comando seguente per generare una coppia di chiavi Ed25519. L’opzione -a 100 aumenta il numero di round KDF (Key Derivation Function) per la protezione della passphrase: ogni tentativo di brute-force richiede 100 round di elaborazione invece dei 16 predefiniti, rallentando gli attacchi di circa 6 volte. Il commento con l’anno permette di identificare rapidamente le chiavi durante l’audit e pianificare la rotazione:

# Genera una coppia di chiavi Ed25519 con KDF rinforzato
ssh-keygen -t ed25519 -a 100 -C "[email protected]"

# Output atteso:
# Generating public/private ed25519 key pair.
# Enter file in which to save the key (/home/mario/.ssh/id_ed25519): [Invio per default]
# Enter passphrase (empty for no passphrase): [inserisci passphrase forte, min. 20 caratteri]
# Enter same passphrase again: [ripeti la passphrase]
# Your identification has been saved in /home/mario/.ssh/id_ed25519
# Your public key has been saved in /home/mario/.ssh/id_ed25519.pub
# The key fingerprint is:
# SHA256:xK8j2mNpQ7vR3bL9wE4cF6tY1sA5hD0iU8kJ2mNpQ7v [email protected]
# The key's randomart image is:
# +--[ED25519 256]--+
# |        .ooo.    |
# |       . o++ .   |
# |      . .=E +    |
# |       .+o+o .   |
# |      . S*+= .   |
# +----[SHA256]-----+

Il fingerprint SHA256 mostrato nell’output è il valore che dovresti annotare e conservare. Quando ti connetti per la prima volta a un server che presenta questo fingerprint come chiave host, puoi verificare out-of-band che la chiave sia autentica. Una passphrase di almeno 20 caratteri, con maiuscole, minuscole, numeri e simboli, è la tua ultima linea di difesa se il file della chiave privata viene rubato o copiato senza autorizzazione.

Step 2: Generare una Chiave RSA 4096 per Sistemi Legacy

Se devi connetterti a sistemi che non supportano Ed25519, come apparecchiature di rete con firmware datato o server con OpenSSH precedente alla versione 6.5, genera una chiave RSA 4096 separata. Usa un nome file distinto per tenerla separata dalla chiave principale Ed25519:

# Genera chiave RSA 4096 per compatibilità legacy
ssh-keygen -t rsa -b 4096 -a 100 -C "legacy-mario.rossi-2026" \
  -f ~/.ssh/id_rsa_legacy

# Verifica le chiavi generate e le loro dimensioni
ls -lh ~/.ssh/
# -rw------- 1 mario mario 3.4K giu 20 10:30 id_rsa_legacy
# -rw-r--r-- 1 mario mario  743 giu 20 10:30 id_rsa_legacy.pub
# -rw------- 1 mario mario  411 giu 20 10:30 id_ed25519
# -rw-r--r-- 1 mario mario  104 giu 20 10:30 id_ed25519.pub

# Mostra il fingerprint di entrambe le chiavi per confronto
ssh-keygen -l -f ~/.ssh/id_ed25519.pub
# 256 SHA256:xK8j... [email protected] (ED25519)
ssh-keygen -l -f ~/.ssh/id_rsa_legacy.pub
# 4096 SHA256:AbCd... legacy-mario.rossi-2026 (RSA)

Step 3: Impostare i Permessi Corretti

I permessi dei file SSH devono essere esatti. OpenSSH rifiuta le chiavi con permessi troppo permissivi e mostra l’errore WARNING: UNPROTECTED PRIVATE KEY FILE. Questo controllo è una misura di sicurezza: se altri utenti del sistema possono leggere la tua chiave privata, questa è compromessa a prescindere dalla robustezza crittografica:

# Imposta i permessi corretti (da eseguire dopo ogni nuova chiave)
chmod 700 ~/.ssh                      # Directory: solo il proprietario può accedere
chmod 600 ~/.ssh/id_ed25519           # Chiave privata: solo proprietario in lettura/scrittura
chmod 644 ~/.ssh/id_ed25519.pub       # Chiave pubblica: leggibile da tutti
chmod 600 ~/.ssh/authorized_keys      # Chiavi autorizzate: solo proprietario
chmod 600 ~/.ssh/config               # Config SSH locale: solo proprietario

# Verifica con ls -la
ls -la ~/.ssh/
# drwx------  2 mario mario 4096 giu 20 10:30 .
# -rw-------  1 mario mario  411 giu 20 10:30 id_ed25519
# -rw-r--r--  1 mario mario  104 giu 20 10:30 id_ed25519.pub
# -rw-------  1 mario mario  104 giu 20 10:30 authorized_keys

# Riparazione automatica dei permessi (utile dopo rsync o backup)
find ~/.ssh -type f -exec chmod 600 {} \;
find ~/.ssh -type d -exec chmod 700 {} \;
chmod 644 ~/.ssh/id_ed25519.pub ~/.ssh/*.pub 2>/dev/null || true

Step 4-5: Installare la Chiave Pubblica sul Server Remoto

Una volta generata la coppia di chiavi in locale, il passo successivo è installare la chiave pubblica sul server remoto. Solo il file .pub va copiato sul server: la chiave privata non deve mai lasciare il tuo dispositivo locale, non va mai trasmessa via email, chat, o copiata su server condivisi.

Step 4: Usare ssh-copy-id (Metodo Automatico)

Il comando ssh-copy-id è lo strumento ufficiale per questa operazione. Si connette al server usando il metodo di autenticazione disponibile (la password, per questa prima volta), crea la directory ~/.ssh con permessi 700 se non esiste, e aggiunge la chiave pubblica a ~/.ssh/authorized_keys con i permessi corretti. Evita automaticamente le chiavi duplicate:

# Copia la chiave pubblica sul server remoto (ultima volta con password)
ssh-copy-id -i ~/.ssh/id_ed25519.pub [email protected]

# Output atteso:
# /usr/bin/ssh-copy-id: INFO: Source of key(s) to be installed: "~/.ssh/id_ed25519.pub"
# /usr/bin/ssh-copy-id: INFO: attempting to log in with the new key(s)
# /usr/bin/ssh-copy-id: INFO: 1 key(s) remaining to be installed
# [email protected]'s password: [ultima volta che inserisci la password]
# Number of key(s) added: 1
# Now try logging into the machine, with: "ssh '[email protected]'"

# Testa immediatamente la connessione con la nuova chiave
ssh -i ~/.ssh/id_ed25519 [email protected]
# Successo: nessuna richiesta di password, solo passphrase della chiave (una volta)

# Su porta non standard (es. 2222)
ssh-copy-id -i ~/.ssh/id_ed25519.pub -p 2222 [email protected]

Step 5: Metodo Manuale se ssh-copy-id non è Disponibile

Su Windows o sistemi dove ssh-copy-id non è disponibile, puoi installare la chiave pubblica manualmente con questi comandi equivalenti che replicano esattamente la stessa operazione:

# Metodo manuale: Linux/macOS con pipe diretta (una sola riga)
cat ~/.ssh/id_ed25519.pub | ssh [email protected] \
  "mkdir -p ~/.ssh && chmod 700 ~/.ssh && \
   cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys && \
   echo 'Chiave aggiunta con successo'"

# Metodo manuale: PowerShell su Windows
$pubkey = Get-Content "$env:USERPROFILE\.ssh\id_ed25519.pub" -Raw
ssh [email protected] "mkdir -p ~/.ssh && chmod 700 ~/.ssh && echo '$pubkey' >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"

# Verifica che la chiave sia stata aggiunta correttamente
ssh [email protected] "cat ~/.ssh/authorized_keys"
# ssh-ed25519 AAAAC3NzaC1lZDI1NTE5... [email protected]

Step 6-7: Hardening di sshd_config per la Massima Sicurezza

La configurazione del demone SSH è il passaggio più critico di questa guida. Un sshd_config con i valori predefiniti espone il server a una superficie d’attacco evitabile: accesso root diretto, autenticazione con password, numero illimitato di tentativi di login. La configurazione hardened che segue rappresenta la baseline raccomandata per un server esposto su Internet nel 2026, basata sulle linee guida NIST SP 800-53 e sulle best practice della documentazione ufficiale OpenSSH.

Step 6: Applicare la Configurazione Hardened

# Backup obbligatorio prima di qualsiasi modifica
sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.backup.$(date +%Y%m%d)

# Verifica il backup
ls -lh /etc/ssh/sshd_config*
# -rw-r--r-- 1 root root 3.2K giu 20 10:00 /etc/ssh/sshd_config
# -rw-r--r-- 1 root root 3.2K giu 20 10:30 /etc/ssh/sshd_config.backup.20260620

# Apri la configurazione per le modifiche
sudo nano /etc/ssh/sshd_config
# /etc/ssh/sshd_config - Configurazione hardened 2026
# Backup: /etc/ssh/sshd_config.backup.YYYYMMDD

# === AUTENTICAZIONE ===
PermitRootLogin no                # Mai l'accesso root diretto
PasswordAuthentication no         # Solo chiavi, nessuna password
PubkeyAuthentication yes          # Autenticazione a chiave pubblica abilitata
KbdInteractiveAuthentication no   # Disabilita auth interattiva
PermitEmptyPasswords no           # Nessuna chiave senza passphrase accettata

# === LIMITI DI CONNESSIONE ===
MaxAuthTries 3                    # Massimo 3 tentativi per connessione
LoginGraceTime 20                 # 20 secondi per completare l'autenticazione
MaxSessions 5                     # Massimo 5 sessioni per connessione multiplexed
MaxStartups 10:30:60              # Limita connessioni parallele non autenticate

# === TUNNELING E FORWARDING ===
X11Forwarding no                  # Disabilita X11 forwarding
AllowTcpForwarding no             # Disabilita TCP forwarding
AllowAgentForwarding no           # Disabilita agent forwarding
PermitTunnel no                   # Disabilita tunnel tun/tap

# === SICUREZZA PROTOCOLLO ===
IgnoreRhosts yes                  # Ignora file .rhosts (protocollo obsoleto)
HostbasedAuthentication no        # Nessuna autenticazione basata su hostname
PermitUserEnvironment no          # Non accettare variabili d'ambiente dal client
StrictModes yes                   # Controlla permessi file chiavi prima di usarle

# === SESSIONI INATTIVE ===
ClientAliveInterval 300           # Ping ogni 5 minuti
ClientAliveCountMax 2             # Disconnetti dopo 2 ping senza risposta

# === LOGGING ===
LogLevel VERBOSE                  # Log dettagliato (include fingerprint chiavi usate)
SyslogFacility AUTH

# === PERFORMANCE ===
UseDNS no                         # Disabilita reverse DNS lookup (velocizza il login)

Step 7: Validare e Riavviare il Servizio SSH

Questo step richiede attenzione: un errore in sshd_config può bloccare completamente l’accesso al server. La sequenza corretta prevede di aprire una sessione di backup PRIMA di riavviare il demone:

# STEP 1: Valida la configurazione senza riavviare
sudo sshd -t
# Nessun output = configurazione valida
# In caso di errori (es: "Bad configuration option: PermitRooLogin"):
# correggi il typo e ripeti il test

# STEP 2: Apri una SECONDA finestra terminale con una connessione SSH attiva
# Questa sessione rimane aperta come "paracadute" durante il riavvio
# Non chiuderla finché non hai verificato che la nuova configurazione funziona

# STEP 3: Riavvia SSH solo dopo aver aperto la sessione di backup
sudo systemctl restart sshd

# Verifica lo stato del servizio
sudo systemctl status sshd
# Output atteso:
# Active: active (running) since 2026-06-20 10:35:00 UTC

# STEP 4: Dalla sessione di backup, apri una TERZA finestra e testa la connessione
ssh -i ~/.ssh/id_ed25519 [email protected]
# Successo: puoi chiudere la sessione di backup
# Fallimento: dalla sessione di backup ripristina:
#   sudo cp /etc/ssh/sshd_config.backup.20260620 /etc/ssh/sshd_config
#   sudo systemctl restart sshd

Step 8: Proteggere SSH con fail2ban

Anche con PasswordAuthentication no, il demone SSH gestisce connessioni da bot e scanner automatici che consumano risorse e riempiono i log. fail2ban analizza i log di autenticazione e blocca automaticamente gli IP che superano la soglia di tentativi falliti. Con i ban progressivi, ogni recidiva raddoppia la durata del blocco fino a una settimana. Questo è lo scudo fondamentale per qualsiasi server SSH esposto su Internet, come documentato dalla documentazione ufficiale di fail2ban.

# Installazione su Ubuntu/Debian
sudo apt update && sudo apt install fail2ban -y

# Su RHEL/Rocky/AlmaLinux
sudo dnf install fail2ban -y

# Crea la configurazione locale (non modificare jail.conf: viene sovrascritto dagli aggiornamenti)
sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
# /etc/fail2ban/jail.local - Sezione SSH con ban progressivi

[DEFAULT]
bantime   = 1h           # Primo ban: 1 ora
findtime  = 10m          # Finestra di osservazione: 10 minuti
maxretry  = 3            # Massimo 3 tentativi falliti

# Ban progressivo: ogni recidiva raddoppia il ban fino a 1 settimana
bantime.increment  = true
bantime.factor     = 2
bantime.maxtime    = 1w

[sshd]
enabled  = true
port     = ssh           # Cambia in "2222" se usi una porta non standard
filter   = sshd
logpath  = /var/log/auth.log    # Ubuntu/Debian
# logpath = /var/log/secure     # RHEL/Rocky: decommenta questa, commenta quella sopra
maxretry = 3
bantime  = 12h
backend  = systemd
# Avvia e abilita fail2ban al boot
sudo systemctl enable --now fail2ban

# Verifica lo stato della jail SSH
sudo fail2ban-client status sshd
# Status for the jail: sshd
# |- Filter
# |  |- Currently failed: 2
# |  |- Total failed: 47
# `- Actions
#    |- Currently banned: 3
#    `- Banned IP list: 185.220.101.47 45.95.169.212 94.102.49.193

# Sblocca manualmente il tuo IP se ti sei bannato accidentalmente
sudo fail2ban-client set sshd unbanip 192.168.1.50

# Controlla i log di fail2ban per analizzare i pattern di attacco
sudo journalctl -u fail2ban -n 50 --no-pager

Step 9-10: Ruotare le Chiavi SSH in Sicurezza

La rotazione delle chiavi SSH è il passaggio più trascurato nelle infrastrutture reali. Chiavi generate anni fa e mai sostituite sono un rischio concreto: un ex dipendente, un laptop rubato non segnalato, o una violazione non rilevata di un repository privato potrebbero aver esposto la chiave privata. La procedura corretta segue sempre il principio “aggiungi prima, rimuovi dopo” per non perdere mai l’accesso durante la transizione.

AmbienteFrequenza RotazioneTrigger Rotazione ImmediataNote
Produzione critica (finanza, PA, salute)Ogni 90 giorniDipendente che lascia, laptop rubato, sospetta compromissioneUsa SSH CA per scalabilità
Produzione standardOgni 6 mesiSegnalazione di compromissione, cambio teamRegistra ogni rotazione in un log
Staging e testOgni 12 mesiFine progetto, ex collaboratoriRimuovi chiavi dei collaboratori usciti
CI/CD (GitHub Actions, GitLab CI)Ogni 90 giorniSospetto leak nel codice sorgenteUsa chiavi dedicate, scope minimo
Sviluppo localeOgni 12-24 mesiCambio computer, furto, perditaUna chiave separata per progetto/host

Step 9: Generare la Nuova Chiave e Aggiungerla al Server

# 1. Genera la nuova chiave con anno corrente nel commento per tracciabilità
ANNO=$(date +%Y)
ssh-keygen -t ed25519 -a 100 -C "[email protected]${ANNO}" \
  -f ~/.ssh/id_ed25519_new

# 2. Aggiungi la NUOVA chiave al server PRIMA di rimuovere la vecchia
#    (non perdere mai l'accesso durante la transizione)
ssh-copy-id -i ~/.ssh/id_ed25519_new.pub [email protected]

# 3. Testa la connessione con la nuova chiave specificandola esplicitamente
ssh -i ~/.ssh/id_ed25519_new [email protected] "echo 'Nuova chiave: OK'"

# 4. Solo dopo aver verificato: rimuovi la vecchia chiave da authorized_keys
#    Questo esempio rimuove la riga contenente il commento dell'anno precedente
ssh -i ~/.ssh/id_ed25519_new [email protected] \
  "grep -v '[email protected]$((ANNO - 1))' ~/.ssh/authorized_keys \
   > /tmp/authorized_keys_new && \
   mv /tmp/authorized_keys_new ~/.ssh/authorized_keys && \
   chmod 600 ~/.ssh/authorized_keys && \
   echo 'Vecchia chiave rimossa'"

Step 10: Finalizzare la Rotazione e Aggiornare la Configurazione Locale

# 5. Sostituisci la chiave vecchia con quella nuova nel file system locale
mv ~/.ssh/id_ed25519_new ~/.ssh/id_ed25519
mv ~/.ssh/id_ed25519_new.pub ~/.ssh/id_ed25519.pub
chmod 600 ~/.ssh/id_ed25519
chmod 644 ~/.ssh/id_ed25519.pub

# 6. Aggiorna ~/.ssh/config per usare la nuova chiave (se hai configurazioni specifiche)
cat > ~/.ssh/config << 'EOF'
Host server-produzione
    HostName 192.168.1.100
    User mario
    IdentityFile ~/.ssh/id_ed25519
    IdentitiesOnly yes          # Usa solo la chiave specificata, non tutto l'agent
    AddKeysToAgent yes          # Aggiungi la chiave all'agent alla prima connessione

Host server-legacy
    HostName 10.0.0.50
    User admin
    IdentityFile ~/.ssh/id_rsa_legacy
    IdentitiesOnly yes

Host bastion
    HostName bastion.azienda.it
    User mario
    IdentityFile ~/.ssh/id_ed25519
    ControlMaster auto
    ControlPath ~/.ssh/sockets/%r@%h-%p
    ControlPersist 600
EOF
chmod 600 ~/.ssh/config

Step 11: SSH Certificate Authority per Ambienti Enterprise

Con più di 10 server o team numerosi, aggiornare manualmente authorized_keys su ogni macchina diventa ingestibile e soggetto a errori. Un nuovo collaboratore richiede l'aggiornamento di decine di file; quando lascia l'azienda, devi ricordare di rimuovere la sua chiave da tutti i server. La SSH Certificate Authority risolve entrambi i problemi: i server si fidano di una CA centrale e i certificati utente hanno una scadenza automatica.

# === SETUP SSH CERTIFICATE AUTHORITY ===
# Eseguire questi comandi su un sistema sicuro, idealmente offline o con accesso HSM

# 1. Crea la CA per la firma delle chiavi utente
sudo ssh-keygen -t ed25519 -f /etc/ssh/ca_user -C "SSH User CA 2026" -a 100
# /etc/ssh/ca_user     -> chiave privata CA (custodire offline o in HSM)
# /etc/ssh/ca_user.pub -> chiave pubblica CA (da distribuire ai server)

# 2. Configura ogni server per fidarsi della CA utenti
sudo install -m 644 /etc/ssh/ca_user.pub /etc/ssh/trusted-user-ca-keys.pem
echo "TrustedUserCAKeys /etc/ssh/trusted-user-ca-keys.pem" | sudo tee -a /etc/ssh/sshd_config
sudo systemctl reload sshd

# 3. Firma la chiave pubblica di un utente (validità 52 settimane, scade automaticamente)
sudo ssh-keygen -s /etc/ssh/ca_user \
  -I "mario-laptop-2026" \
  -n mario \
  -V +52w \
  ~/.ssh/id_ed25519.pub
# Risultato: ~/.ssh/id_ed25519-cert.pub

# 4. Verifica il certificato firmato
ssh-keygen -L -f ~/.ssh/id_ed25519-cert.pub
# Output:
#   Type: [email protected] user certificate
#   Public key: ED25519-CERT SHA256:xK8j...
#   Signing CA: ED25519 SHA256:yR9m... (using ssh-ed25519)
#   Key ID: "mario-laptop-2026"
#   Valid: from 2026-06-20T10:00:00 to 2027-06-19T10:00:00
#   Principals: mario

# 5. La connessione funziona senza aggiungere nulla a authorized_keys
ssh [email protected]
# Il server verifica il certificato contro la CA pubblica

# 6. Per revocare: il certificato scade automaticamente dopo 52 settimane
#    Per revoca immediata: non rinnovare il certificato alla scadenza

Step 12: Audit delle Chiavi SSH Autorizzate

L'ultimo step riguarda la visibilità: sapere esattamente quali chiavi sono autorizzate su ogni server. Uno script di audit periodico rivela chiavi orfane (senza commento identificativo), chiavi con algoritmi obsoleti, o chiavi di utenti non più attivi. Esegui questo audit almeno ogni trimestre e integra i risultati in un processo di remediation automatica per le criticità trovate. La man page di ssh-keygen documenta tutte le opzioni disponibili per l'analisi delle chiavi.

#!/usr/bin/env python3
"""
Audit SSH authorized_keys su server remoti.
Uso: python3 ssh_audit.py
"""

import subprocess
import sys

# Lista dei server da auditare
SERVERS = [
    "[email protected]",
    "[email protected]",
    "[email protected]",
]

# Tipi di chiave obsoleti o da monitorare
OBSOLETE_TYPES = {"ssh-dss"}          # DSA: deprecato, rimuovi subito
LEGACY_TYPES   = {"ssh-rsa"}          # RSA: accettabile solo se 4096 bit

def audit_server(server: str) -> None:
    print(f"\n{'='*60}")
    print(f"Audit: {server}")
    print('='*60)

    try:
        result = subprocess.run(
            ["ssh", "-o", "BatchMode=yes", "-o", "ConnectTimeout=10",
             server, "cat ~/.ssh/authorized_keys 2>/dev/null || echo ''"],
            capture_output=True, text=True, timeout=15
        )
    except subprocess.TimeoutExpired:
        print(f"  [ERRORE] Timeout connessione")
        return

    if result.returncode != 0:
        print(f"  [ERRORE] Connessione fallita: {result.stderr.strip()}")
        return

    lines = [l.strip() for l in result.stdout.split("\n")
             if l.strip() and not l.startswith("#")]

    if not lines:
        print("  [INFO] Nessuna chiave in authorized_keys")
        return

    for i, line in enumerate(lines, 1):
        parts = line.split()
        key_type = parts[0] if parts else "?"
        comment  = parts[-1] if len(parts) > 2 else ""

        issues = []
        if key_type in OBSOLETE_TYPES:
            issues.append("TIPO OBSOLETO - rimuovere immediatamente")
        if key_type in LEGACY_TYPES:
            issues.append("Tipo legacy - verificare dimensione chiave")
        if not comment or "@" not in comment:
            issues.append("Nessun identificativo utente nel commento")

        stato = "[CRITICO]" if any("immediatamente" in i for i in issues) \
                else "[ATTENZIONE]" if issues else "[OK]"

        print(f"  Chiave {i}: {key_type} | {comment or 'N/A'} {stato}")
        for issue in issues:
            print(f"    -> {issue}")

if __name__ == "__main__":
    for s in SERVERS:
        audit_server(s)
    print("\n[Fine audit] Risolvi tutte le criticità [CRITICO] entro 24 ore.")

# Esempio output:
# ============================================================
# Audit: [email protected]
# ============================================================
#   Chiave 1: ssh-ed25519 | [email protected] [OK]
#   Chiave 2: ssh-dss | N/A [CRITICO]
#     -> TIPO OBSOLETO - rimuovere immediatamente
#     -> Nessun identificativo utente nel commento

6 Errori Critici nella Gestione delle Chiavi SSH

Questi sono i 6 errori più frequenti nella gestione delle chiavi SSH, basati sui pattern di vulnerabilità documentati da OpenSSH e dai principali vendor di sicurezza nel 2025-2026. Ognuno di questi errori ha causato violazioni reali nelle infrastrutture enterprise:

  1. Chiave privata senza passphrase. Una chiave privata senza passphrase equivale a una password in chiaro su disco. Se il laptop viene rubato, se un malware legge i file home, o se il file finisce in un backup non cifrato, l'attaccante ha accesso immediato a tutti i server con quella chiave. La soluzione è semplice: usa sempre -a 100 e una passphrase di almeno 20 caratteri. Usa ssh-agent per non doverla reinserire a ogni connessione.
  2. Agent forwarding abilitato su host non fidati. Con AllowAgentForwarding yes nel server e ForwardAgent yes nel client, il server remoto può usare il tuo ssh-agent per autenticarsi su altri server. Un server intermedio compromesso può sfruttare il socket dell'agent per muoversi lateralmente su tutta la rete. CVE-2025-26465 ha amplificato questo rischio. Mantieni sempre AllowAgentForwarding no in sshd_config e ForwardAgent no come default in ~/.ssh/config.
  3. Chiavi DSA o RSA sotto i 2048 bit ancora attive. OpenSSH ha deprecato DSA e le chiavi RSA inferiori a 2048 bit. Queste chiavi offrono meno di 80 bit di sicurezza. Identifica le chiavi obsolete con ssh-keygen -l -f ~/.ssh/authorized_keys e rimuovile subito.
  4. CVE-2025-26465: VerifyHostKeyDNS abilitato senza DNSSEC. Se hai VerifyHostKeyDNS yes nella tua configurazione SSH client su OpenSSH dalla versione 6.8p1 alla 9.9p1, sei vulnerabile a un attacco MitM documentato nel febbraio 2025. Soluzione immediata: aggiorna a OpenSSH 9.9p2 e disabilita VerifyHostKeyDNS se non usi DNSSEC. Verifica la versione con ssh -V.
  5. Chiavi orfane in authorized_keys. Le chiavi di ex dipendenti, collaboratori temporanei, o servizi dismessi rimangono attive per mesi o anni in authorized_keys. Ogni chiave non rimossa è una porta aperta potenziale. Pianifica una revisione trimestrale con lo script di audit del Step 12 e rimuovi sistematicamente le chiavi non più necessarie.
  6. PermitRootLogin yes. L'accesso diretto come root via SSH espone l'account più privilegiato agli attacchi di forza bruta e ai credential stuffing automatizzati. Imposta sempre PermitRootLogin no e usa sudo dopo l'autenticazione con un account normale. Se i processi di automazione richiedono privilegi root, usa un account di servizio dedicato con sudo specifico e scope minimo.

Troubleshooting: 10 Problemi SSH Comuni e le Soluzioni

La diagnostica SSH richiede di analizzare sia il lato client che quello server. Il comando ssh -vvv è lo strumento principale: mostra ogni step del processo di autenticazione, da quale chiave viene offerta a perché il server la rifiuta. Usa sudo journalctl -fu sshd sul server per i log in tempo reale.

Errore / SintomoCausa ProbabileSoluzione
Permission denied (publickey)Chiave non in authorized_keys, permessi errati, o chiave sbagliatachmod 700 ~/.ssh && chmod 600 ~/.ssh/authorized_keys + ssh -vvv per debug
WARNING: UNPROTECTED PRIVATE KEY FILEPermessi della chiave privata troppo permissivichmod 600 ~/.ssh/id_ed25519
Host key verification failedL'host ha cambiato chiave (aggiornamento) o MitM sospettossh-keygen -R hostname e verifica il fingerprint del server out-of-band
Too many authentication failuresssh-agent offre troppe chiavi, il server raggiunge MaxAuthTriesAggiungi IdentitiesOnly yes e IdentityFile specifico in ~/.ssh/config
Connection refusedSSH non in ascolto, porta errata, o firewall bloccantesudo systemctl status sshd + sudo ufw allow ssh
IP bannato da fail2banTroppi tentativi falliti dal proprio IPsudo fail2ban-client set sshd unbanip TUO_IP
Bad owner or permissions on ~/.ssh/configIl file config ha permessi troppo apertichmod 600 ~/.ssh/config && chown $USER ~/.ssh/config
sign_and_send_pubkey: signing failedChiave corrotta o passphrase errataVerifica con ssh-keygen -l -f ~/.ssh/id_ed25519; rigenera se corrotta
Login lento (10-30 secondi)Reverse DNS lookup lento da parte del serverAggiungi UseDNS no in sshd_config e riavvia SSH
Server unexpectedly closed network connectionsshd crashato, MaxSessions raggiunto, o ban attivosudo journalctl -u sshd -n 50 --no-pager per analizzare i log
# Diagnostica avanzata: ogni step del handshake SSH
ssh -vvv [email protected] 2>&1 | grep -E "(Offering|accepts|fail|debug1: Authen)"

# Passi chiave nell'output:
# "Offering public key: /home/mario/.ssh/id_ed25519 ED25519 SHA256:..."
#   -> Il client sta offrendo questa chiave al server

# "Server accepts key: /home/mario/.ssh/id_ed25519 ED25519 SHA256:..."
#   -> Il server ha accettato: autenticazione riuscita

# "Authentications that can continue: publickey" senza "accepts key"
#   -> La chiave non e' in authorized_keys del server

# "Permissions ... are too open" nel log del server
#   -> Permessi errati su authorized_keys o sulla directory .ssh

# Controlla i log del server in tempo reale durante il tentativo di connessione
sudo journalctl -fu sshd --no-pager
# Cerca: "Accepted publickey for mario" o "Failed publickey for mario"

Tecniche Avanzate: Multiplexing, Jump Host e FIDO2

Una volta padroneggiata la gestione base delle chiavi, queste tre tecniche avanzate migliorano sicurezza e produttività nelle infrastrutture complesse senza compromettere il modello di sicurezza stabilito nei passi precedenti.

SSH Multiplexing condivide una singola connessione TCP tra più sessioni verso lo stesso host. Riduce il tempo di connessione da 1-2 secondi a meno di 100ms per le sessioni successive e diminuisce il numero di autenticazioni necessarie. Aggiunge queste righe al tuo ~/.ssh/config:

# Multiplexing: aggiunto globalmente a ~/.ssh/config
Host *
    ControlMaster auto
    ControlPath ~/.ssh/sockets/%r@%h-%p
    ControlPersist 600         # Mantieni la connessione 10 minuti dopo la chiusura

# Crea la directory per i socket di controllo
mkdir -p ~/.ssh/sockets && chmod 700 ~/.ssh/sockets

# Jump Host: accesso a server interno via bastion, senza agent forwarding
ssh -J [email protected] [email protected]

# In ~/.ssh/config (soluzione permanente)
Host server-interno
    HostName 10.0.0.100
    User mario
    ProxyJump bastion.azienda.it
    IdentityFile ~/.ssh/id_ed25519
    ForwardAgent no            # MAI agent forwarding attraverso il bastion

# Chiave SSH hardware su YubiKey (OpenSSH 8.2+, richiede YubiKey 5+)
ssh-keygen -t ed25519-sk -O resident -O application=ssh:azienda \
  -C "yubikey-mario.rossi-2026"
# La chiave privata non lascia mai il token hardware fisico
# Installazione identica a una chiave normale: ssh-copy-id installa la .pub

Progetto Completo: Script di Setup SSH Sicuro

Lo script seguente automatizza l'intero processo per un nuovo server: genera la chiave Ed25519 se non esiste, la installa sul server, applica il hardening a sshd_config in modo non distruttivo (non sovrascrive la configurazione intera, modifica solo i parametri specificati), e fornisce un report finale. Eseguilo una volta per ogni nuovo server da mettere in produzione:

#!/bin/bash
# ssh-secure-setup.sh - Setup SSH sicuro automatizzato
# Uso: ./ssh-secure-setup.sh UTENTE SERVER_IP [PORTA]
# Esempio: ./ssh-secure-setup.sh mario 192.168.1.100
# Porta non standard: ./ssh-secure-setup.sh mario 192.168.1.100 2222

set -euo pipefail

UTENTE="${1:?Specificare utente, es: mario}"
SERVER="${2:?Specificare IP server, es: 192.168.1.100}"
PORTA="${3:-22}"
CHIAVE="$HOME/.ssh/id_ed25519"
ANNO=$(date +%Y)
EMAIL="${USER}@$(hostname -f 2>/dev/null | cut -d. -f2- || echo 'local')"

GREEN='\033[0;32m'; YELLOW='\033[1;33m'; RED='\033[0;31m'; NC='\033[0m'
ok()   { echo -e "${GREEN}[OK]${NC} $*"; }
warn() { echo -e "${YELLOW}[!]${NC} $*"; }
err()  { echo -e "${RED}[ERRORE]${NC} $*" >&2; exit 1; }

echo "=== SSH Secure Setup: ${UTENTE}@${SERVER}:${PORTA} ==="

# Genera chiave Ed25519 se non esiste
if [[ ! -f "$CHIAVE" ]]; then
    ok "Generazione chiave Ed25519 (KDF a 100 round)..."
    ssh-keygen -t ed25519 -a 100 -C "${EMAIL}-${ANNO}" -f "$CHIAVE"
    chmod 600 "$CHIAVE" && chmod 644 "${CHIAVE}.pub"
fi
ok "Chiave: $CHIAVE"
ok "Fingerprint: $(ssh-keygen -l -f "$CHIAVE" 2>/dev/null)"

# Installa la chiave sul server
ok "Installazione chiave sul server..."
ssh-copy-id -i "${CHIAVE}.pub" -p "$PORTA" "${UTENTE}@${SERVER}" || \
  err "ssh-copy-id fallito. Verifica la password o la connettività."

# Testa la connessione con la nuova chiave
ssh -i "$CHIAVE" -p "$PORTA" -o BatchMode=yes -o ConnectTimeout=10 \
    "${UTENTE}@${SERVER}" "echo 'Connessione con chiave: OK'" || \
  err "Test connessione fallito. Debug: ssh -vvv -i $CHIAVE -p $PORTA ${UTENTE}@${SERVER}"

# Hardening remoto di sshd_config
ok "Applicazione hardening sshd_config..."
ssh -i "$CHIAVE" -p "$PORTA" "${UTENTE}@${SERVER}" "bash -s" << 'REMOTE'
set -e
CONFIG="/etc/ssh/sshd_config"
BACKUP="${CONFIG}.bak.$(date +%Y%m%d)"
sudo cp "$CONFIG" "$BACKUP" 2>/dev/null || true

set_param() {
    local key="$1" val="$2"
    if sudo grep -qE "^[[:space:]]*#?[[:space:]]*${key}[[:space:]]" "$CONFIG" 2>/dev/null; then
        sudo sed -i "s|^[[:space:]]*#\?[[:space:]]*${key}[[:space:]].*|${key} ${val}|" "$CONFIG"
    else
        echo "${key} ${val}" | sudo tee -a "$CONFIG" > /dev/null
    fi
}

set_param PermitRootLogin no
set_param PasswordAuthentication no
set_param MaxAuthTries 3
set_param LoginGraceTime 20
set_param X11Forwarding no
set_param AllowTcpForwarding no
set_param AllowAgentForwarding no
set_param ClientAliveInterval 300
set_param ClientAliveCountMax 2
set_param UseDNS no
set_param LogLevel VERBOSE

if sudo sshd -t; then
    sudo systemctl restart sshd
    echo "sshd_config hardened: OK"
else
    sudo cp "$BACKUP" "$CONFIG"
    echo "ERRORE: configurazione non valida, backup ripristinato"
    exit 1
fi
REMOTE

ok "Setup completato per ${UTENTE}@${SERVER}:${PORTA}"
warn "Testa la connessione in una nuova finestra PRIMA di chiudere questa sessione"
warn "Prossima rotazione chiave: $(date -d '+6 months' '+%Y-%m-%d' 2>/dev/null || date -v+6m '+%Y-%m-%d')"

Copertura Correlata

Approfondimenti su shattered.io

FAQ: Domande Frequenti su ssh-keygen e Chiavi SSH

Qual è il tipo di chiave SSH più sicuro nel 2026?

Ed25519 è il tipo raccomandato per tutti i nuovi deployment. Offre sicurezza equivalente a circa 128 bit con chiavi da soli 256 bit, firma e verifica più veloci rispetto a RSA e ECDSA, e produce chiavi di soli 68 byte contro i 3.401 di RSA 4096. OpenSSH lo ha reso il tipo predefinito dalla versione 9.5. RSA 4096 resta accettabile solo per compatibilità con sistemi legacy che non supportano Ed25519. DSA è deprecato e disabilitato nelle versioni moderne di OpenSSH.

Devo usare una passphrase per la chiave SSH?

Sempre, per le chiavi personali e di produzione. La passphrase cifra la chiave privata con AES-256 e la protegge se il file viene rubato. Usa ssh-agent per non reinserire la passphrase a ogni connessione: la inserisci una volta all'inizio della sessione e l'agent la gestisce in modo sicuro in memoria. L'unica eccezione accettabile sono le chiavi per processi automatizzati (CI/CD senza interazione umana), dove puoi valutare l'SSH Certificate Authority con certificati a breve scadenza come alternativa più sicura.

Ogni quanto devo ruotare le chiavi SSH?

Per ambienti di produzione standard, ogni 6 mesi è la frequenza minima. Per ambienti critici (finanza, pubblica amministrazione, sanità) la rotazione trimestrale è best practice. Avvia sempre una rotazione immediata in questi casi: dipendente che lascia l'azienda, furto o smarrimento di un dispositivo, vulnerability disclosure che coinvolge il tuo setup SSH, o sospetta compromissione di qualsiasi sistema che aveva accesso alla chiave privata.

Cosa fa l'opzione -a in ssh-keygen?

L'opzione -a specifica il numero di round della funzione KDF (Key Derivation Function) usata per derivare la chiave di cifratura dalla passphrase. Con il valore predefinito (16-32 round), il brute-force della passphrase è relativamente veloce. Con -a 100, ogni tentativo richiede 100 round di elaborazione, rallentando gli attacchi di circa 6 volte rispetto al default. Per chiavi ad alta sicurezza considera valori tra 64 e 256 round, bilanciando sicurezza con il tempo accettabile per sbloccare la chiave a ogni utilizzo.

Come posso verificare quale chiave SSH sta usando il client?

Usa il flag -v (o -vvv per il massimo dettaglio): ssh -v mario@server. Nell'output trovi la riga "Offering public key: /home/mario/.ssh/id_ed25519 ED25519 SHA256:..." che mostra quale chiave viene offerta, e "Server accepts key:..." se viene accettata. Per vedere tutte le chiavi nell'agent: ssh-add -l. Per aggiungere una chiave all'agent con timeout automatico di 8 ore: ssh-add -t 8h ~/.ssh/id_ed25519.

SSH Certificate Authority vs authorized_keys: quale scegliere?

Con meno di 5 server e un team piccolo, il sistema tradizionale con authorized_keys è sufficiente. Oltre questa soglia, la SSH Certificate Authority è più sicura e gestibile: non serve toccare ogni server per aggiungere o revocare utenti, i certificati scadono automaticamente, e l'audit è centralizzato. Il costo è la complessità iniziale del setup e la necessità assoluta di proteggere la chiave privata della CA come il segreto aziendale più critico.

ssh-keygen funziona su Windows nel 2026?

Sì. OpenSSH è incluso nativamente in Windows 10 dalla versione 1809 e in Windows 11. Puoi usare ssh-keygen, ssh-copy-id e ssh-agent da Prompt dei comandi o PowerShell. Le chiavi vengono salvate in C:\Users\NomeUtente\.ssh\. Per usare l'agent su Windows, abilita il servizio "OpenSSH Authentication Agent" in Servizi di sistema o con PowerShell come amministratore: Set-Service ssh-agent -StartupType Automatic; Start-Service ssh-agent. Consulta la documentazione OpenSSH di Ubuntu per ulteriori dettagli sull'installazione cross-platform.

Come funziona ssh-agent e quando usarlo?

ssh-agent è un demone che mantiene in memoria le chiavi private decifrate per la durata della sessione. Quando SSH necessita di autenticarsi, interroga l'agent invece di richiedere la passphrase all'utente. Avvio: eval "$(ssh-agent -s)". Aggiunta chiave: ssh-add -t 8h ~/.ssh/id_ed25519 (il flag -t 8h imposta un timeout automatico di 8 ore, dopo il quale l'agent rimuove la chiave dalla memoria). Su macOS il Portachiavi integra l'agent automaticamente e la passphrase viene salvata in modo sicuro nel keychain.