Una server blade nera in un data center buio, racchiusa in uno scudo esagonale color smeraldo e luminoso, con particelle di luce verde in arrivo che si infrangono contro la barriera
Guida alla sicurezza

Blindare un nuovo VPS nella prima ora

Un indirizzo IPv4 pubblico viene sondato entro pochi minuti dal suo primo pacchetto, da macchine che non hanno mai sentito parlare di te e mai lo faranno. È una buona notizia: quasi tutto ciò che prende di mira un nuovo server è generico, e un'ora dedicata basta a fermarne quasi la totalità. La stessa ora, se eseguita nell'ordine sbagliato, ti chiude fuori da una macchina in cui nessuno può accedere al posto tuo.

Ti consegniamo una shell root circa 47 secondi dopo la conferma del pagamento, e poi ci fermiamo, deliberatamente. Non installiamo alcun agente, non gestiamo il tuo firewall, e non conserviamo una copia delle tue credenziali — una chiave che deteniamo noi è una chiave che potremmo essere costretti a consegnare, ed è un principio che attraversa l'intera piattaforma. La conseguenza è semplice e va detta chiaramente: la sicurezza del tuo server è lo stato in cui lo lasci durante la prima ora.

Quello che segue è quell'ora, nell'ordine in cui la eseguiamo noi stessi, su Debian 13 e Ubuntu 24.04. Due impostazioni fanno gran parte del lavoro. Il resto di questa pagina esiste per via di tre cose che sembrano corrette, superano il controllo finale di qualsiasi tutorial, e sono silenziosamente sbagliate: una configurazione SSH drop-in che viene sovrascritta senza avviso, un'impostazione di porta che uno sshd attivato tramite socket ignora, e un runtime per container che pubblica le porte sotto il tuo firewall. Ognuna di queste ha già colto in fallo qualcuno che aveva fatto tutto il resto correttamente.

Chi sta davvero bussando alla porta

Osserva journalctl -u ssh su un server online da un'ora e la prima volta il volume sarà allarmante. Non dovrebbe esserlo. Quello che stai guardando è il rumore di fondo di internet: una manciata di operazioni di scansione, alcune accademiche, alcune commerciali, alcune criminali, che enumerano continuamente l'intero spazio IPv4 e passano i risultati a bot che indovinano credenziali. Il tuo indirizzo è stato raggiunto perché esiste, in ordine numerico, e verrà raggiunto di nuovo tra poche ore qualunque cosa tu faccia.

Questo cambia in modo utile la natura del problema. Non ti stai difendendo da un avversario che ti ha scelto e che si adatterà; ti stai difendendo da uno script con un repertorio fisso — root, admin, ubuntu, test, git, oracle, postgres, e le ventimila password comparse nei breach dump. Non ha pazienza, non ha creatività e non ha interesse per una macchina che non risponde al primo tentativo. Disattivare l'autenticazione via password non rallenta questo attaccante. Lo elimina completamente dal tavolo da gioco.

Vale la pena conoscere due dettagli. Primo, IPv6 è drammaticamente più silenzioso, perché un /64 non può essere spazzolato per intero — ma nel momento in cui il tuo record AAAA diventa pubblico, o l'indirizzo della tua macchina compare in un header di posta o in un log di certificate transparency, il silenzio finisce. Non trattare mai IPv6 come un nascondiglio; trattalo come un pagliaio più piccolo. Secondo, un indirizzo IPv4 ha un passato. Ha avuto un inquilino prima di te, e se quell'inquilino ha gestito male un server di posta o ha ospitato qualcosa che è finito in una lista nera, ne erediti la reputazione finché non si esaurisce. Se la posta ti interessa, controlla l'indirizzo sulle solite blocklist prima di costruirci sopra — è un controllo di cinque minuti che risparmia due settimane di debug sulla deliverability.

Le due impostazioni che fanno il novanta per cento del lavoro

Quasi ogni compromissione reale di un piccolo server nasce in uno di due punti: una password che si poteva indovinare, o un servizio in ascolto che non avrebbe dovuto esserlo. Le correzioni corrispondenti sono PasswordAuthentication no e un firewall con policy predefinita drop. Non sono affascinanti, richiedono insieme un quarto d'ora, e valgono più di tutti gli altri controlli di questa pagina messi insieme.

Il motivo è strutturale, non statistico. Entrambe sono chiuse per default — falliscono dalla parte della sicurezza. Uno sshd solo a chiavi non può essere forzato con il brute-force per quanti tentativi arrivino, perché nel codice non esiste alcun percorso che accetti una password. Un firewall default-deny protegge servizi che non hai ancora installato, incluso il database che aggiungerai tra tre mesi e dimenticherai di vincolare a localhost. Tutto il resto in una checklist di hardening è un elenco di cose negative: una lista di elementi specifici da disattivare, completa solo quanto lo è la lista stessa.

Quindi se leggi una sola sezione e chiudi la scheda, leggi questa, esegui i passaggi da 2 a 5 qui sotto, e considera l'ora ben spesa. Il resto è genuinamente utile e genuinamente secondario.

Dove vive oggi la configurazione SSH, e la trappola che nasconde

Su Debian 13 e Ubuntu 24.04, /etc/ssh/sshd_config si apre con una riga Include /etc/ssh/sshd_config.d/*.conf, e le immagini cloud includono un file in quella directory — di solito 50-cloud-init.conf — che imposta già PasswordAuthentication. Modificare il file principale e aggiungere le proprie direttive in fondo sembra naturale e produce una configurazione che non fa quello che dichiara.

La regola è l'unica cosa di sshd che sorprende quasi tutti: per ogni parola chiave, vince il primo valore ottenuto. È l'opposto di quasi ogni altro sistema di merge delle configurazioni che hai usato. L'Include si trova vicino all'inizio del file principale, quindi un drop-in viene letto prima del corpo di sshd_config — e tra i drop-in, decide l'ordine alfabetico. Un file chiamato 10-hardening.conf batte 50-cloud-init.conf. Un file chiamato 60-hardening.conf perde contro di esso, silenziosamente, e non lo scoprirai da un riavvio che segnala successo.

Per questo non ci si deve mai fidare del file appena scritto. Chiedi al demone cosa ha effettivamente risolto:

sshd -t                 # syntax check — silence means valid
sshd -T | grep -Ei '^(permitrootlogin|passwordauthentication|pubkeyauthentication|kbdinteractive|allowusers|port) '

sshd -T stampa la configurazione effettiva dopo che ogni include, override e valore predefinito è stato risolto. Se quell'output dice passwordauthentication yes, allora l'autenticazione via password è attiva, indipendentemente da ciò che dichiara qualsiasi file su disco. Nient'altro conta come verifica.

La seconda trappola appartiene alla stessa famiglia. Ubuntu 24.04 avvia sshd tramite attivazione socket: ssh.socket possiede la porta in ascolto, e Port in sshd_config viene ignorato del tutto. Verifica con systemctl is-enabled ssh.socket; se è abilitato e vuoi una porta diversa, cambiala con systemctl edit ssh.socket e un override ListenStream= — non in sshd_config, dove l'impostazione sembrerà corretta e non farà nulla.

Spostarsi dalla porta 22: teatro, ma teatro economico

Siamo onesti su questo punto, perché internet non lo è. Spostare sshd sulla porta 2222 o 47000 non ferma alcun attaccante competente. Una scansione TCP completa di un host richiede secondi, il servizio si annuncia da solo nel banner, e chiunque abbia deciso di attaccare proprio te lo scoprirà prima di finire il caffè.

Quello che fa davvero è tagliare il tuo log di autenticazione di circa il novantacinque per cento, e questo ha un valore reale: è la differenza tra log che scorri velocemente e log che leggi davvero. Un segnale che riesci a vedere batte un segnale che hai sepolto. Se monitori anche solo qualcosa, un auth.log silenzioso è il modo più economico in assoluto per rendere visibile un'anomalia.

I costi sono piccoli ma reali, ed è per questo che lo definiamo facoltativo e non consigliato. Dimenticherai la porta quando tornerai tra otto mesi. Una porta non standard sopra 1024 può, in linea di principio, essere reclamata da un processo senza privilegi se sshd smette di essere in ascolto. I firewall in uscita restrittivi sulle reti dei client bloccano le porte insolite, quindi ogni tanto non riuscirai a raggiungere il tuo stesso server da un ufficio o da un hotel. E su un sistema attivato tramite socket devi cambiarla nel posto giusto, come nella sezione precedente. Fallo se i log silenziosi contano per te. Non farlo e poi sentirti sicuro, e non farlo mai al posto di solo a chiavi.

Default-deny, e il ruleset che usiamo davvero

Un firewall che elenca cosa bloccare è uno schedario. Un firewall che elenca cosa permettere è un controllo di sicurezza. La distinzione è tutta la partita, perché solo il secondo copre il servizio che installerai il mese prossimo, la porta di debug che hai aperto un venerdì, e il container che ha deciso di esporsi da solo.

Sulla nostra piattaforma hai due livelli, e sono indipendenti. Il filtro edge è opzionale, configurato per singolo server nel pannello e applicato sull'hypervisor, così il traffico bloccato non raggiunge mai il tuo guest — utile per le regole di livello 4 che vuoi applicate prima che il tuo kernel gli dedichi un ciclo, e per impedire che un guest compromesso resti raggiungibile mentre ci lavori. Il firewall del guest è interamente tuo: nftables, iptables, pf, qualunque cosa includa la tua immagine. Non lo tocchiamo mai. Usali entrambi se la macchina conta davvero; usa sempre almeno il secondo.

Due cose nel ruleset del passaggio 5 meritano una spiegazione, perché la maggior parte dei ruleset copiati e incollati le sbaglia. Non droppare tutto l'ICMP. Sembra ordinato ma rompe la path-MTU discovery, il che produce la peggior classe di bug che esista: le richieste piccole funzionano, le risposte grandi restano in sospeso, e nei tuoi log non c'è alcun riferimento al firewall. Accetta almeno destination-unreachable, time-exceeded e parameter-problem. Non filtrare ICMPv6 per tipo a meno che tu non conosca l'elenco. IPv6 si basa su ICMPv6 per il neighbour discovery e i router advertisement; bloccalo in modo generico e la tua connettività IPv6 muore in un modo che sembra un problema di routing. Accettare tutto l'ICMPv6 su un singolo host è un compromesso ragionevole, ed è esattamente ciò che fa il ruleset qui sotto.

Un avvertimento prima di eseguire flush ruleset: se Docker è installato, quella riga rimuove le regole NAT e filter che Docker ha scritto, e il networking dei container si ferma finché systemctl restart docker non le rimette a posto. Carica prima il tuo ruleset, riavvia Docker subito dopo, e leggi la sezione successiva prima di pubblicare anche una sola porta di un container.

Le porte che non sapevi fossero aperte

Chiedi alla macchina su cosa è in ascolto. Non cosa pensi di aver installato — cosa è effettivamente bound in questo momento:

ss -tulpn | grep -vE '127\.0\.0\.1|\[::1\]'

Tutto ciò che sopravvive a questo filtro è raggiungibile da un luogo diverso dalla macchina stessa. Su un'immagine standard le scoperte più comuni sono rpcbind sulla 111 (nulla di ciò che usi ne ha bisogno), un listener exim4 residuo dell'installazione di base, e uno stub di systemd-resolved innocuo su loopback ma un resolver aperto se non lo è. Quelli pericolosi arrivano dopo, con il software che installi di proposito: PostgreSQL su 0.0.0.0:5432 perché un tutorial diceva di modificare listen_addresses, Redis senza password perché Redis non ha avuto una password di default per gran parte della sua vita, un nodo Elasticsearch, un exporter di Prometheus, un notebook Jupyter. Ognuno di questi è stato il primo passo di una violazione reale, e non solo una volta.

Ed ecco la trappola che coglie in fallo anche le persone attente. Docker non chiede il permesso al tuo firewall. Quando scrivi -p 5432:5432, Docker inserisce regole DNAT nella chain PREROUTING della tabella nat, che il kernel valuta prima che il pacchetto arrivi mai alla tua chain input; il traffico viene poi inoltrato al container. La tua policy input default-deny non viene consultata, ufw status mostra la porta come chiusa, e il database è sulla internet pubblica. Questo è un comportamento documentato di Docker, è così da un decennio, e ha esposto un numero enorme di database.

La correzione è una singola stringa, ed è l'abitudine che vale la pena costruire:

# wrong — reachable from the entire internet, whatever your firewall says
docker run -p 5432:5432 postgres

# right — bound to loopback, reachable only through an SSH tunnel or a proxy
docker run -p 127.0.0.1:5432:5432 postgres

Infine, cerca un parere esterno. Una scansione delle porte dal server ti dice cosa pensa il kernel; una scansione da altrove ti dice cosa vede il mondo, che è l'unico numero che conta davvero. Esegui nmap -Pn -p- <your-ip> dal tuo laptop, non dalla macchina, e confronta il risultato con la lista che ti aspettavi.

Aggiornamenti automatici: attivali, e fallo sul serio

La finestra che porta alla compromissione dei piccoli server non è lo zero-day. Sono le quattro settimane tra la pubblicazione di una CVE con un proof of concept funzionante e la prossima volta che ti capiterà di collegarti. Gli strumenti d'attacco assorbono un nuovo bug remoto nel giro di giorni; una macchina che aggiorni “quando ci arrivi” resta esposta per l'intero intervallo, e ogni operatore onesto sa quanto sia realmente lungo quell'intervallo.

L'obiezione agli aggiornamenti automatici è la paura di rompere qualcosa, e merita di essere presa sul serio — per poi essere circoscritta. Limita l'automazione al pacchetto di sicurezza della tua distribuzione, dove i maintainer effettuano il backport delle correzioni nella versione già pacchettizzata invece di distribuire nuove release upstream. Un aggiornamento di sicurezza Debian per openssl è una build corretta della stessa versione che già usi; il rischio che cambi il comportamento è molto minore del rischio di lasciare aperto per un mese un buco raggiungibile da remoto. Gli aggiornamenti di funzionalità restano manuali, dove è giusto che stiano.

La parte che tutti saltano è il riavvio. Una libssl corretta su disco non serve a nulla per il processo che ne aveva mappato la vecchia versione all'avvio, e un aggiornamento del kernel non serve a nulla finché non ci riavvii dentro. O accetti un riavvio non presidiato in una finestra che scegli tu, oppure installi needrestart e lo lasci dire quali servizi girano ancora contro librerie cancellate — ma fai almeno una delle due cose. “Gli aggiornamenti automatici sono attivi” mentre uptime segna 340 giorni è un'illusione rassicurante, ed è quella che vediamo più spesso.

fail2ban, CrowdSec, o niente del tutto

fail2ban legge i tuoi log, nota i fallimenti ripetuti da un indirizzo e lo bandisce per un po'. CrowdSec fa lo stesso e condivide i verdetti su una rete di partecipanti, così puoi bloccare un indirizzo che si è comportato male altrove prima ancora che raggiunga te. Sono entrambi software validi. Nessuno dei due fa, su un server SSH solo a chiavi, ciò che la maggior parte delle persone crede.

Una volta che PasswordAuthentication è no, un brute-force su SSH non può avere successo. Non “è improbabile che riesca” — semplicemente non esiste un percorso di codice che lo permetta. Bandire un indirizzo dopo cinque fallimenti quindi non previene nulla; riduce il volume dei log e una quantità trascurabile di CPU. È un beneficio reale, solo che non è un beneficio di sicurezza, e il compromesso non è gratuito: una jail scritta male che osserva il log sbagliato ha chiuso fuori dai propri server più amministratori di quanti attaccanti abbia mai bloccato.

Il punto in cui questi strumenti si guadagnano davvero il loro posto è un livello più su, sui servizi che esponi realmente: un form di login, un pannello admin di WordPress, un'API con rate limit, un server di posta con SMTP AUTH. Quelli accettano davvero password, sono davvero attaccabili via brute-force, e una lista di ban è esattamente il controllo giusto. Il nostro verdetto quindi è ristretto e specifico. Salta la jail su SSH. Metti CrowdSec o fail2ban davanti a ciò che ha un campo password. E qualunque tu scelga, metti prima in whitelist il tuo indirizzo di gestione — ignoreip esiste apposta per la sera che altrimenti ricorderesti per il motivo sbagliato.

Fare in modo che la macchina ti avvisi quando cambia

La prevenzione è ciò che puoi fare in un'ora. Il rilevamento è ciò che ti dice che quell'ora non è bastata. Non deve essere elaborato, e su un singolo server tre accorgimenti economici coprono gran parte del terreno.

Conserva i log. Su molte immagini il journal vive in /run e svanisce al riavvio, il che significa che la traccia di un incidente scompare proprio nel riavvio che lo segue. Creare /var/log/journal è una correzione di una riga ed è la cosa a maggior valore in questa sezione.

Fatti avvisare sui login. Una riga in /etc/ssh/sshrc che lancia logger a ogni inizio sessione non costa nulla e ti dà una traccia pulita e grep-abile, separata dal chiacchiericcio di sshd stesso. Un'avvertenza che coglie in fallo molti: sshd esegue ~/.ssh/rc al posto di /etc/ssh/sshrc quando l'utente ne ha uno, quindi il file di sistema viene saltato proprio per l'account che più probabilmente ha un dotfile personalizzato. Se ti serve un hook che non possa essere oscurato in questo modo, usa pam_exec invece.

Sappi com'era il filesystem il primo giorno. AIDE registra gli hash dei tuoi binari, librerie e file di unit e segnala cosa è cambiato da allora. Il problema è fondamentale e di solito viene ignorato: un database conservato sulla stessa macchina che sta sorvegliando può essere rigenerato da chiunque l'abbia compromessa, e da quel momento il controllo segnalerà per sempre “nessuna modifica”. Copia il database fuori dalla macchina, o quantomeno registrane l'hash altrove, e lo strumento vale i suoi venti minuti. Lasciato lì dov'è, è solo una coperta di Linus.

Vale la pena enunciare il principio generale a sé stante: il log di cui non puoi fidarti è quello sulla macchina compromessa. Qualunque cosa su cui intendi davvero fare affidamento — un journal, un database di integrità, un backup — dovrebbe avere una copia da qualche parte che lo stesso attaccante non controlla. Un secondo piccolo server in un'altra giurisdizione è una risposta legittima a questo, e ne basta uno da cinque dollari.

Cosa questo non fa

Tutto quanto sopra è lavoro perimetrale, e vale la pena essere precisi sul suo confine, così non scambierai una porta blindata per una cassaforte.

Non protegge i dati su una macchina in esecuzione. Un kernel blindato e un firewall chiuso sono irrilevanti per chiunque abbia accesso all'hypervisor, e per il contenuto della RAM mentre la macchina è accesa. Se la tua preoccupazione è il disco quando il server è spento, sequestrato o dismesso, quella è la cifratura del disco, ed è una procedura diversa con una modalità di guasto diversa — vedi cifrare un disco VPS con LUKS.

Non ti rende anonimo. Un server può essere blindato alla perfezione e comunque rivelare chi lo possiede, tramite un record WHOIS, una chiave SSH riutilizzata, un tag di analytics, un client SSH che si connette da casa senza alcun salto intermedio, o un certificato che collega due identità. Pagare in Monero e poi accedere dal proprio indirizzo personale vanifica il pagamento. Questa modalità di fallimento ha una pagina tutta sua: gli errori che ti deanonimizzano.

Non sistema la tua applicazione. Una SQL injection, un endpoint admin non autenticato o una dipendenza con una backdoor non si curano affatto delle tue impostazioni sshd. La maggior parte delle compromissioni di server ben gestiti arriva attraverso la porta che hai aperto deliberatamente, di proposito, verso un servizio che hai scritto o installato tu.

Non è un backup. Ransomware, un rm -rf con una variabile che era vuota, e un aggiornamento fallito finiscono tutti allo stesso modo. Fai una copia, mettila da un'altra parte, e ripristinala una volta prima di averne davvero bisogno — la stessa disciplina che rende sopravvivibile un trasloco verso un altro host rende sopravvivibile anche un brutto martedì.

Niente di tutto questo è un argomento contro quell'ora. È un argomento per sapere esattamente cosa quell'ora ha comprato.

  1. Fai il deploy con una chiave, mai con una password

    Genera la keypair sulla tua macchina e incolla la metà pubblica nel form di deploy; l'immagine la installerà e non avrai mai una password di root da perdere. Se fai il deploy da un'immagine contrassegnata cloud-init, puoi invece passare l'intera configurazione della prima ora come user-data — fino a 64 KiB — e la macchina si avvia già blindata.

    # on your laptop, not on the server
    ssh-keygen -t ed25519 -a 100 -C "$(whoami)@laptop-cvh"
    cat ~/.ssh/id_ed25519.pub        # paste this into the deploy form

    Usa ed25519 a meno che qualcosa nella tua toolchain non lo rifiuti, nel qual caso RSA a 4096 bit va benissimo. Proteggi la chiave privata con una passphrase e caricala in un agent; un file di chiave senza passphrase su un laptop è una password scritta sul monitor. Lo stesso set di chiavi viene iniettato nella modalità di ripristino, ed è ciò che rende recuperabile il passaggio 4.

  2. Apri la seconda sessione prima di toccare qualsiasi cosa

    Questo non è facoltativo e non è paranoia. Da qui in poi modificherai il demone attraverso cui sei connesso e il firewall che ti permette di raggiungerlo. Tieni una sessione aperta e inattiva come corda di sicurezza verso la macchina, e fai ogni modifica in un'altra. Se una modifica è sbagliata, la sessione aperta è ancora autenticata e può annullarla; una nuova connessione verrebbe rifiutata.

    # terminal A — the rope. Log in and leave it alone.
    ssh -i ~/.ssh/id_ed25519 root@<server-ip>
    
    # terminal B — where every command below runs.
    ssh -i ~/.ssh/id_ed25519 root@<server-ip>

    Testa ogni modifica aprendo una terza connessione, mai riconnettendo quella su cui stai lavorando. Se la terza connessione fallisce, hai comunque due shell funzionanti e un problema, non una crisi.

  3. Crea l'account che userai davvero

    Root via SSH è comodo per esattamente un'ora. Dopodiché, un account nominale con sudo ti dà una traccia di controllo, ti protegge da un comando digitato male che gira con privilegi completi, e ti permette di disabilitare un account compromesso senza disabilitare la macchina.

    adduser --disabled-password --gecos "" deploy
    install -d -m 0700 -o deploy -g deploy /home/deploy/.ssh
    cp /root/.ssh/authorized_keys /home/deploy/.ssh/authorized_keys
    chown deploy:deploy /home/deploy/.ssh/authorized_keys
    chmod 0600 /home/deploy/.ssh/authorized_keys
    usermod -aG sudo deploy
    
    # a long random password, stored in your manager, used only by sudo
    passwd deploy

    Imposta quella password. Un account sudo la cui password non è mai stata impostata non può usare sudo, e scoprirlo dopo aver disabilitato il login di root è il modo classico per restare chiusi fuori con ogni file perfettamente a posto. Verifica prima di proseguire: da un nuovo terminale, ssh deploy@<server-ip>, poi sudo -v. Devono funzionare entrambi.

  4. Scrivi il drop-in SSH, poi chiedi al demone cosa ha letto

    Un drop-in numerato che si ordina in testa batte qualunque cosa abbia incluso l'immagine cloud, secondo la regola d'ordinamento vista sopra. Scrivilo, valida la sintassi, rileggi la configurazione effettiva, e solo dopo ricarica.

    cat > /etc/ssh/sshd_config.d/10-hardening.conf <<'EOF'
    PermitRootLogin no
    PasswordAuthentication no
    KbdInteractiveAuthentication no
    PubkeyAuthentication yes
    AuthenticationMethods publickey
    PermitEmptyPasswords no
    AllowUsers deploy
    MaxAuthTries 3
    LoginGraceTime 20
    X11Forwarding no
    AllowAgentForwarding no
    AllowTcpForwarding no      # remove this line if you use ssh -L / -D tunnels
    ClientAliveInterval 300
    ClientAliveCountMax 2
    EOF
    chmod 0644 /etc/ssh/sshd_config.d/10-hardening.conf
    
    sshd -t                                    # silence means the syntax is valid
    sshd -T | grep -Ei '^(permitrootlogin|passwordauthentication|allowusers) '
    systemctl reload ssh

    Leggi l'output di sshd -T prima di ricaricare, non dopo. Deve dire permitrootlogin no e passwordauthentication no. Se non lo fa, il tuo file viene sovrascritto da uno che si ordina prima — esegui ls /etc/ssh/sshd_config.d/ e rinomina il tuo con un numero più basso. Ora apri un terzo terminale e accedi come deploy. Solo quando funziona dovresti chiudere la corda di sicurezza.

  5. Carica un firewall default-deny

    nftables è incluso in entrambe le distribuzioni e sostituisce il ruleset di iptables con un unico file leggibile. Adatta le porte accettate ai servizi che usi realmente — l'elenco qui sotto presuppone SSH e un server web, e nient'altro.

    apt install -y nftables
    cat > /etc/nftables.conf <<'EOF'
    #!/usr/sbin/nft -f
    flush ruleset
    
    table inet filter {
      chain input {
        type filter hook input priority filter; policy drop;
    
        ct state established,related accept
        ct state invalid drop
        iif lo accept
    
        # ICMP must live: dropping it blackholes path-MTU discovery.
        ip protocol icmp icmp type { echo-request, echo-reply, destination-unreachable, time-exceeded, parameter-problem } accept
        ip6 nexthdr icmpv6 accept
    
        tcp dport 22 ct state new accept
        tcp dport { 80, 443 } ct state new accept
    
        limit rate 5/minute burst 5 packets log prefix "nft-drop: " level info
      }
      chain forward { type filter hook forward priority filter; policy drop; }
      chain output  { type filter hook output  priority filter; policy accept; }
    }
    EOF
    
    nft -c -f /etc/nftables.conf              # check before you commit to it
    systemctl enable --now nftables
    nft list ruleset | head -40

    Se Docker è installato, flush ruleset rimuove anche le regole di Docker: esegui poi systemctl restart docker e conferma che i tuoi container rispondano ancora. Poi, dal tuo laptop, nmap -Pn -p- <server-ip> e verifica che l'elenco delle porte aperte corrisponda a quelle che hai appena permesso — né più, né meno.

  6. Attiva gli aggiornamenti di sicurezza non presidiati

    Solo il pacchetto di sicurezza, con una finestra di riavvio che hai scelto tu invece di una che continuerai a rimandare. Scegli un'ora tranquilla per i tuoi utenti e non allo scoccare dell'ora esatta, così non entri in collisione con il cron di tutti gli altri.

    apt install -y unattended-upgrades needrestart
    cat > /etc/apt/apt.conf.d/51-cvh-auto <<'EOF'
    // Debian. On Ubuntu, replace the two Origins-Pattern lines with:
    //   "${distro_id}:${distro_codename}-security";
    Unattended-Upgrade::Origins-Pattern {
      "origin=Debian,codename=${distro_codename}-security,label=Debian-Security";
      "origin=Debian,codename=${distro_codename},label=Debian-Security";
    };
    Unattended-Upgrade::Automatic-Reboot "true";
    Unattended-Upgrade::Automatic-Reboot-WithUsers "false";
    Unattended-Upgrade::Automatic-Reboot-Time "04:17";
    Unattended-Upgrade::Remove-Unused-Kernel-Packages "true";
    APT::Periodic::Update-Package-Lists "1";
    APT::Periodic::Unattended-Upgrade "1";
    EOF
    
    unattended-upgrade --dry-run --debug | tail -25
    systemctl status unattended-upgrades --no-pager

    Se un riavvio non presidiato è davvero inaccettabile, impostalo su "false" e lascia che needrestart -b segnali quali servizi girano ancora contro librerie cancellate e se è installato un kernel più recente — poi agisci di conseguenza. Ciò che non è accettabile è non fare nessuna delle due cose.

  7. Chiudi ciò che è in ascolto, e verifica dall'esterno

    Elenca cosa è associato a qualcosa di diverso da loopback, rimuovi ciò che non usi, e conferma il risultato da una macchina diversa da questa.

    ss -tulpn | grep -vE '127\.0\.0\.1|\[::1\]'
    
    # the usual leftovers on a base image
    systemctl disable --now rpcbind.socket rpcbind 2>/dev/null
    apt purge -y exim4-base exim4-config exim4-daemon-light 2>/dev/null
    apt autoremove --purge -y
    
    # what each running unit is allowed to reach
    systemd-analyze security --no-pager | head -20

    Per tutto ciò che deve continuare a girare ma non ha bisogno di pubblico — un database, una cache, un pannello admin, un exporter di metriche — vincolalo a 127.0.0.1 nella sua stessa configurazione e raggiungilo tramite un tunnel SSH: ssh -L 5432:127.0.0.1:5432 deploy@<server-ip>. È strettamente meglio che aprire una porta e sperare che l'autenticazione del servizio stesso sia solida, ed è lo schema a cui ricorrere per default.

  8. Registra la baseline e arma gli allarmi

    Dieci minuti che ripagano solo il giorno in cui qualcosa va storto — che è esattamente il giorno in cui non riuscirai a ricostruire nulla a memoria.

    # persistent journal, so a reboot stops erasing the evidence
    mkdir -p /var/log/journal
    systemd-tmpfiles --create --prefix /var/log/journal
    systemctl restart systemd-journald
    
    # a clean, greppable line per SSH session
    cat > /etc/ssh/sshrc <<'EOF'
    logger -t ssh-login "user=$USER from=${SSH_CONNECTION%% *} tty=${SSH_TTY:-none}"
    EOF
    
    # file-integrity baseline — then get the database off the machine
    apt install -y aide
    aideinit
    mv /var/lib/aide/aide.db.new /var/lib/aide/aide.db
    sha256sum /var/lib/aide/aide.db     # copy this hash somewhere else
    
    last -n 20 ; lastb -n 20            # who got in, and who tried

    Concludi annotando, fuori dal server, quattro cose: il fingerprint della chiave host SSH ottenuto con ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub, l'hash di AIDE qui sopra, le porte che hai aperto deliberatamente, e dove risiede la chiave privata. Quella nota è ciò che trasforma una brutta mattinata in una checklist invece che in un'indagine.

Confronto

I controlli, classificati per ciò che ti offrono davvero

Ogni controllo della prima ora messo a confronto con le due domande che decidono se merita il suo posto: cosa blocca davvero, e cosa non blocca — qualunque cosa lascino intendere i tutorial.
ControlloCosa bloccaCosa non bloccaCostoVerdetto
SSH solo a chiavi (PasswordAuthentication no)Ogni bot che indovina credenziali su internet, in modo permanente e per costruzioneChiunque sia in possesso della tua chiave privata, o un difetto in sshd stesso5 minuti, una tantumNon negoziabile
Firewall default-deny (nftables)Servizi che avevi dimenticato fossero in ascolto, e ogni servizio che installerai per sbaglio in futuroAttacchi contro le porte che hai aperto deliberatamente10 minuti, una tantumNon negoziabile
Aggiornamenti di sicurezza non presidiatiLa finestra degli n-day — le settimane tra un exploit pubblico e il tuo prossimo accessoGli zero-day, e tutto ciò che richiede un riavvio che non pianifichi mai5 minuti più una finestra di riavvioNon negoziabile
Utente nominale con sudo invece di rootComandi digitati male eseguiti con privilegi completi, e processi che girano come root senza motivoUn'escalation di privilegi locale nelle mani di chi è già dentro3 minutiNe vale la pena
Baseline di integrità dei file (AIDE)Modifiche silenziose a binari di sistema, librerie e file di unitAssolutamente nulla, se il database resta sulla macchina che sorveglia20 minuti più uno storage esterno alla macchinaNe vale la pena se la macchina conta davvero
Spostare SSH dalla porta 22Circa il 95% del volume del tuo log di autenticazioneChiunque esegua una scansione delle porte, cioè chiunque conti davvero2 minuti, più la sera in cui dimenticherai la portaFacoltativo, per log silenziosi
fail2ban / CrowdSec su sshdI recidivi, e la CPU che sprecano su di teNulla, una volta disattivata l'autenticazione via password10 minuti, più un rischio reale di restare chiusi fuoriSalta su SSH; usalo sulla tua app
FAQ

Domande che meritano risposta

Quanto tempo dopo il deploy iniziano le scansioni?

Tipicamente pochi minuti. Gli scanner che coprono tutta internet perlustrano continuamente l'intero spazio IPv4, quindi un indirizzo appena assegnato viene raggiunto al passaggio successivo indipendentemente da cosa ci giri sopra. Dai per scontato di essere sondato dal momento in cui la macchina risponde al suo primo pacchetto — ed è per questo che l'hardening avviene nella prima ora e non nel primo weekend.

Mi serve ancora fail2ban se il login via password è disattivato?

Su SSH, no. Con PasswordAuthentication no non esiste alcun percorso di codice che un brute-force possa vincere, quindi bandire dopo cinque fallimenti non previene nulla — riduce il rumore nei log, il che vale qualcosa ma non è sicurezza. Il posto in cui questi strumenti sono davvero utili è davanti a tutto ciò che accetta realmente una password: un form di login web, un pannello admin, SMTP AUTH. Se ne installi uno, metti prima in whitelist il tuo indirizzo.

ufw, firewalld o nftables?

Va bene qualunque dei tre, purché la policy predefinita sia drop e tu capisca cosa hai messo in produzione. ufw è il più amichevole e sotto il cofano scrive regole nftables sulle attuali Debian e Ubuntu. nftables puro è un unico file leggibile che puoi confrontare con diff e mettere sotto version control, ed è per questo che lo usiamo noi. La scelta conta molto meno della policy predefinita — e nessuno dei tre cambia il fatto che Docker pubblica le porte al di sotto di tutti loro.

Conviene spostare SSH dalla porta 22?

Solo per avere log più silenziosi. Non ferma alcun attaccante competente — una scansione completa richiede secondi e il banner identifica il servizio. Rimuove però gran parte del rumore dal tuo log di autenticazione, il che rende visibili le anomalie reali. Trattalo come igiene dei log, mai come un controllo, e verifica se il tuo sshd è attivato tramite socket prima di cambiarla: su Ubuntu 24.04 la direttiva Port in sshd_config viene ignorata e la porta appartiene a ssh.socket.

Gli aggiornamenti automatici mi romperanno il sito alle quattro del mattino?

Limitati al pacchetto di sicurezza, molto raramente. Quei pacchetti sono correzioni retroportate sulla versione che già usi, non nuove release upstream. Il rischio realistico è il riavvio, non la patch — quindi scegli tu la finestra, imposta Automatic-Reboot-WithUsers su false così attende finché qualcuno è collegato, e se il servizio davvero non può riavviarsi senza presidio, esegui needrestart -b a intervalli regolari e agisci su ciò che segnala. L'unica posizione indifendibile sono gli aggiornamenti automatici con un uptime misurato in anni.

Mi sono chiuso fuori da solo. Quali sono le mie opzioni?

Avvia il server in modalità di ripristino dal pannello. Avvia un ambiente Alpine in RAM con le stesse chiavi SSH del tuo account e il tuo disco smontato su /dev/vda, così puoi montarlo, correggere il drop-in di sshd o il file del firewall, smontarlo e riavviare. L'avvio in modalità di ripristino di per sé non tocca nulla sul disco. Il fingerprint della chiave host del rescue è diverso dal tuo normale — è previsto, non è un'intercettazione.

Conviene usare Lynis o uno script di hardening CIS al posto di questa guida?

Come controllo di verifica sì; come sostituto no. Lynis è un secondo parere genuinamente utile e troverà cose che questa pagina non menziona. Gli script di remediation CIS automatizzati sono una proposta diversa: applicano centinaia di modifiche pensate per una flotta di workstation aziendali, molte delle quali rompono un server in modi difficili da rintracciare settimane dopo. Fai prima l'ora a mano, così capisci cosa sta facendo la tua macchina, poi esegui uno strumento di audit e leggine i risultati uno alla volta.

L'hardening rende anonimo il mio server?

No, e confondere le due cose è un errore comune e costoso. L'hardening controlla chi può entrare; l'anonimato controlla chi può sapere che è tuo. Un server bloccato alla perfezione lascia comunque trapelare la proprietà tramite un record WHOIS, una chiave SSH riutilizzata, un tag di analytics o un login da amministratore dal tuo indirizzo di casa. Pagare in Monero e poi collegarti direttamente dalla tua connessione personale vanifica completamente il pagamento — questo è trattato in gli errori che ti deanonimizzano.

Deploy your offshore server.

Scegli una regione. Scegli un piano. Incolla una chiave. Paga. I prossimi 47 secondi sono a nostro carico.