5 step per mettere in sicurezza un server Linux
Guida pratica all'hardening di un server Ubuntu: patch di sicurezza, privilegi minimi, chiavi SSH, accessi per gruppo, IPv6, UFW e porte Docker.
Partiamo da una verità poco spettacolare: non esiste un comando capace di rendere Linux invulnerabile. Esistono però alcune precauzioni che, se applicate bene, riducono parecchio i rischi più comuni.
In questa guida ne vediamo cinque, pensate soprattutto per un piccolo server Ubuntu o per un homelab esposto in rete. Non sarà la checklist definitiva della sicurezza, ma una base concreta da cui partire senza limitarsi a copiare comandi alla cieca.
Ambito e limiti
I comandi sono pensati per Ubuntu Server e sistemi Debian-based che usano APT, systemd, OpenSSH e UFW. Su altre distribuzioni cambiano nomi, percorsi e servizi. Qui ci occupiamo del sistema di base, non della sicurezza delle applicazioni, della cifratura del disco, dei backup, di AppArmor o SELinux, del monitoraggio e della sicurezza fisica. In cloud entrano in gioco anche firewall del provider, security group e console di emergenza.
Una piccola regola prima di cominciare: crea uno snapshot o un backup della configurazione, assicurati di avere una console alternativa e mantieni aperta la sessione SSH corrente. La chiuderemo soltanto dopo avere provato l’accesso da un secondo terminale.
Negli esempi userò alice come utente e server.example come server: sostituiscili con i tuoi valori.
Step 1. Aggiornare senza confondere patch e cambio versione
Tenere il server aggiornato sembra la parte più semplice, ma i comandi di APT non sono tutti intercambiabili. Prima di lanciarli distinguiamo tre operazioni con effetti molto diversi.
Patch: restare nella release corrente
Cominciamo dalla manutenzione ordinaria. apt update aggiorna soltanto l’elenco dei pacchetti disponibili, quindi da solo non installa nulla. Per vedere e applicare gli aggiornamenti della release in uso:
1
2
3
sudo apt update
apt list --upgradable
sudo apt upgrade
Su Ubuntu le patch di sicurezza arrivano normalmente come correzioni compatibili con la stessa release, spesso tramite backport. In altre parole: correggere una vulnerabilità non significa passare automaticamente a una nuova versione di Ubuntu.
full-upgrade: quando cambiano le dipendenze
full-upgrade non è semplicemente il pulsante “aggiorna di più”. Serve quando APT deve gestire anche cambi di dipendenze, installazioni o rimozioni di pacchetti:
1
sudo apt full-upgrade
Qui conviene fermarsi un secondo prima di premere Invio: leggi sempre l’elenco dei pacchetti che verranno rimossi. Su un server importante fallo durante una finestra di manutenzione e con un backup che sai davvero ripristinare.
Release upgrade: passare a una nuova Ubuntu
Qui il salto è più grande. Passare, per esempio, da Ubuntu 22.04 LTS a 24.04 LTS è un vero release upgrade:
1
sudo do-release-upgrade
Non è manutenzione ordinaria e non va improvvisato durante una pausa caffè. Prima servono compatibilità delle applicazioni verificata, spazio libero, backup, accesso di emergenza e un piano per tornare indietro. Anche la release corrente deve essere completamente aggiornata.
Automatizzare le patch di sicurezza
Se il server resta acceso a lungo, affidarsi alla memoria non è una grande strategia. Su Ubuntu Server unattended-upgrades è normalmente già installato e configurato; puoi verificarlo o abilitarlo così:
1
2
sudo apt install unattended-upgrades
sudo dpkg-reconfigure --priority=low unattended-upgrades
Non basta installarlo: controlliamo che il timer esista, facciamo una simulazione e guardiamo i log.
1
2
3
systemctl status apt-daily-upgrade.timer --no-pager
sudo unattended-upgrade --dry-run --debug
sudo tail -n 50 /var/log/unattended-upgrades/unattended-upgrades.log
I repository di terze parti non vengono inclusi automaticamente per impostazione predefinita. Occhio anche ai riavvii richiesti: installare una patch del kernel senza riavviare può lasciare in esecuzione proprio il kernel precedente.
Step 2. Creare un utente e capire quali privilegi ha
Lavorare sempre come root è comodo, ma ogni errore arriva senza freni. Creiamo quindi un utente nominativo per le attività quotidiane:
1
2
sudo adduser alice
id alice
Creare l’utente è la parte facile. Quella interessante è capire quali poteri gli stiamo dando. Senza deleghe amministrative e gruppi privilegiati sarà più limitato; aggiungendolo a sudo, invece, potrà diventare amministratore usando la propria password:
1
sudo usermod -aG sudo alice
Adesso verifichiamolo davvero, senza dare per scontato che l’assenza di errori significhi configurazione corretta:
1
2
3
id alice
getent group sudo
sudo -l -U alice
Non fermarti al gruppo
sudo:id alicemostra tutti i gruppi supplementari. Per esempio, l’appartenenza al gruppodockerconcede privilegi equivalenti a root tramite il daemon Docker. Rimuovi ogni appartenenza non necessaria e considera privilegiati anche i socket o gli strumenti che permettono di controllare VM e container.
I nuovi gruppi diventano effettivi in una nuova sessione. Apriamone una e guardiamo i privilegi dal punto di vista di alice:
1
2
3
4
su - alice
id
sudo -l
exit
Se l’account deve soltanto eseguire un’applicazione o un servizio, probabilmente sudo non gli serve affatto. La regola è semplice: concedere ciò che serve, non ciò che un giorno potrebbe tornare comodo.
Un gruppo dedicato per chi può entrare via SSH
Un account può esistere sul sistema senza dover entrare da remoto. Un gruppo dedicato ci permette di separare bene le due cose:
1
2
3
sudo groupadd --system ssh-login
sudo usermod -aG ssh-login alice
getent group ssh-login
Nel prossimo step useremo AllowGroups ssh-login. Prima, però, aggiungi al gruppo tutti gli amministratori, gli account di automazione e gli utenti che devono continuare ad accedere via SSH. Dimenticarne uno qui significa lasciarlo fuori dopo.
Step 3. Usare le chiavi SSH senza perdere quella privata
Con le chiavi SSH il server non deve verificare una password riutilizzabile a ogni login. C’è però una regola da ricordare: la chiave pubblica può viaggiare, quella privata resta con te.
Client e server hanno responsabilità diverse
- La coppia di chiavi si genera sul dispositivo client, come il laptop, non sul server.
- La chiave privata non deve mai essere copiata sul server.
- La chiave privata dovrebbe essere protetta da una passphrase robusta.
- Il server riceve soltanto la chiave pubblica, cioè il contenuto del file
.pub.- È opportuno conservare una copia di recupero cifrata e separata dal dispositivo principale.
Su Linux, macOS o PowerShell con OpenSSH installato, genera la coppia sul client:
1
ssh-keygen -t ed25519 -a 64 -C "alice@laptop"
Puoi accettare il percorso proposto oppure sceglierne uno riconoscibile. Non saltare la passphrase: protegge la chiave privata se il file viene copiato o rubato. Alla fine troverai due file:
~/.ssh/id_ed25519: chiave privata, da non condividere;~/.ssh/id_ed25519.pub: chiave pubblica, destinata al server.
Su Windows gli stessi file si trovano normalmente in %USERPROFILE%\.ssh.
Sul server va soltanto la chiave pubblica
Qui è facile cadere in una piccola trappola. Da Linux o macOS, ssh-copy-id aggiunge la chiave pubblica senza sostituire quelle già presenti:
1
ssh-copy-id -i ~/.ssh/id_ed25519.pub alice@server.example
Da PowerShell, dove ssh-copy-id spesso non è disponibile:
1
2
Get-Content "$env:USERPROFILE\.ssh\id_ed25519.pub" |
ssh alice@server.example "umask 077; mkdir -p ~/.ssh; cat >> ~/.ssh/authorized_keys"
Il dettaglio importante è cat >>, che aggiunge la chiave. Copiare direttamente il file .pub con scp verso authorized_keys può invece sovrascrivere le chiavi già presenti e trasformare una scorciatoia in un problema.
Sul server controlla i permessi e le impronte delle chiavi autorizzate:
1
2
3
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys
ssh-keygen -lf ~/.ssh/authorized_keys
Prima di toccare l’autenticazione con password, apri un secondo terminale e prova una connessione che accetti soltanto la chiave pubblica:
1
ssh -o PreferredAuthentications=publickey -o PasswordAuthentication=no alice@server.example
Meglio usare una chiave diversa per ogni dispositivo: se perdi il laptop potrai revocare una sola riga da authorized_keys, senza rifare tutto. Conserva inoltre una copia di recupero della chiave privata in un archivio cifrato e offline, ma non mettere la passphrase nello stesso posto.
Avanzato: chiave hardware FIDO2
Una chiave hardware compatibile con OpenSSH custodisce il segreto crittografico nel dispositivo, dal quale non può essere esportato. Con una chiave residente il token conserva anche il riferimento necessario per ricostruire i file locali su un altro client; aggiungendo
verify-required, ogni firma richiede la verifica tramite PIN.
1 ssh-keygen -t ed25519-sk -O resident -O verify-required -C "alice@security-key"Il tocco fisico resta normalmente richiesto. Non tutti i dispositivi supportano
ed25519-sk; alcuni richiedonoecdsa-sk. Le credenziali residenti possono essere recuperate sul client conssh-keygen -K, mentre sul server continua ad arrivare soltanto la chiave pubblica. Registra anche una seconda chiave di recupero: perdere o rompere l’unico token non deve significare perdere il server.
Step 4. Rendere SSH più robusto senza chiudersi fuori
Qui arriviamo al passaggio più delicato, quello in cui è facile chiudersi fuori dal proprio server. Niente panico: teniamo aperta la sessione corrente, controlliamo ogni modifica e proveremo il nuovo accesso da un secondo terminale.
Su Ubuntu è più ordinato aggiungere uno snippet dedicato invece di modificare direttamente /etc/ssh/sshd_config. Prima controlla che il file principale contenga la direttiva Include:
1
2
grep -n '^Include' /etc/ssh/sshd_config
sudoedit /etc/ssh/sshd_config.d/00-cybertome-hardening.conf
Inserisci:
1
2
3
4
5
PermitRootLogin no
PubkeyAuthentication yes
PasswordAuthentication no
KbdInteractiveAuthentication no
AllowGroups ssh-login
Nella configurazione standard non serve ridefinire AuthorizedKeysFile: OpenSSH cerca già .ssh/authorized_keys. Attenzione però a TOTP, PAM, SSO e agli altri sistemi basati su autenticazione keyboard-interactive: in quei casi non disabilitarla senza adattare la configurazione.
Prima di applicare qualsiasi cosa, chiediamo direttamente a OpenSSH se la sintassi è valida e quali valori userà davvero:
1
2
sudo sshd -t
sudo sshd -T | grep -E '^(port|addressfamily|permitrootlogin|pubkeyauthentication|passwordauthentication|kbdinteractiveauthentication|allowgroups)[[:space:]]'
Solo se il test non restituisce errori possiamo ricaricare il servizio:
1
sudo systemctl reload ssh.service
La vecchia sessione resta ancora aperta. Prova nuovamente l’accesso da un secondo terminale e, se qualcosa non torna, guarda subito i log:
1
sudo journalctl -u ssh.service --since "10 minutes ago"
La porta SSH non è una misura di sicurezza
La porta 22 non è una vulnerabilità e non c’è nulla di segreto nel suo numero. Spostare SSH può ridurre il rumore nei log e qualche tentativo automatico opportunistico, ma una scansione trova comunque il servizio. Non sostituisce chiavi robuste, utenti limitati, patch e firewall.
Su Linux, per impostazione predefinita, le porte inferiori a 1024 sono privilegiate: per occuparle servono root o la capability CAP_NET_BIND_SERVICE. La porta 2222 è quindi valida per OpenSSH, ma non è privilegiata e, quando è libera, può essere occupata anche da un processo locale senza tali privilegi. La soglia effettiva si può verificare con:
1
sysctl net.ipv4.ip_unprivileged_port_start
In questa guida manteniamo quindi la porta 22: è privilegiata, non entra in conflitto con un altro servizio assegnato e semplifica firewall, client e configurazioni cloud. Se hai un motivo operativo per cambiarla, non scegliere una porta bassa a caso: verifica che non sia assegnata o già in uso, aprila prima nei firewall e provala da una nuova sessione prima di rimuovere la vecchia regola.
IPv6 non sparisce se lo ignoriamo
OpenSSH usa normalmente AddressFamily any, quindi può ascoltare sia su IPv4 sia su IPv6. Forzare AddressFamily inet non rende il server magicamente più sicuro: rischia soltanto di nascondere una configurazione di rete lasciata a metà.
Verifica se il server possiede indirizzi IPv6 globali e su quali socket ascolta SSH:
1
2
ip -6 address show scope global
sudo ss -lntp 'sport = :22'
Se hai configurato una porta diversa, sostituisci 22 nel filtro.
Se IPv6 è attivo, applica anche a IPv6 le stesse regole firewall e di hardening. Disabilitalo globalmente soltanto quando sei certo che non venga utilizzato da servizi, container, VPN o infrastruttura del provider.
Step 5. Alzare il firewall senza lasciare porte dimenticate
È il momento di alzare il ponte levatoio, ma prima assicuriamoci di non lasciare noi stessi fuori dal castello. Cominciamo dai servizi realmente in ascolto:
1
sudo ss -lntup
Non aprire una porta soltanto perché il numero ti sembra familiare. Associa ogni socket al suo processo e chiediti da quali reti quel servizio debba essere davvero raggiungibile.
Installa UFW e imposta le policy di base:
1
2
3
4
sudo apt install ufw
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw app list
Prima di abilitare UFW, lasciamo aperto il cancello che stiamo usando: SSH. Con la porta standard e il profilo installato da OpenSSH:
1
sudo ufw limit OpenSSH
limit mette un freno semplice ai nuovi tentativi. È utile contro parte del rumore automatico, ma non sostituisce l’autenticazione a chiave né un sistema di rilevamento più completo.
Apri HTTP e HTTPS soltanto se il server deve davvero pubblicare un servizio web:
1
2
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
Per un servizio destinato esclusivamente alla LAN, limita la sorgente:
1
sudo ufw allow from 192.168.1.0/24 to any port 8080 proto tcp
Infine abilita il firewall e controlla le regole numerate:
1
2
3
sudo ufw enable
sudo ufw status verbose
sudo ufw status numbered
Adesso prova da un altro dispositivo, sia dalla LAN sia dall’esterno quando ha senso. Una regola perfetta sulla carta ma verificata dal posto sbagliato può regalare soltanto una falsa sensazione di sicurezza.
UFW deve proteggere anche IPv6
Controlla che UFW gestisca IPv6 e che le regole attese compaiano anche con il suffisso (v6):
1
2
grep '^IPV6=' /etc/default/ufw
sudo ufw status verbose
Con IPv6 attivo non fare affidamento sul NAT del router come barriera implicita: verifica firewall host, firewall del provider e raggiungibilità reale da una rete esterna IPv6.
Attenzione: UFW e porte pubblicate da Docker
Questo è un dettaglio facile da perdere: Docker crea proprie regole firewall e NAT. Una porta pubblicata con
-p 8080:80viene normalmente associata a tutti gli indirizzi del server e il traffico può essere deviato prima delle catene usate da UFW. Di conseguenza, una policyufw denypuò non proteggere una porta pubblicata da Docker.Non pubblicare porte che servono solo tra container. Quando un servizio deve essere raggiunto soltanto da un reverse proxy sullo stesso host, vincolalo esplicitamente al loopback:
1 2 ports: - "127.0.0.1:8080:80"Controlla sempre con
docker psesudo ss -lntup, poi prova la porta da un’altra macchina su IPv4 e IPv6. Con il backend iptables, le restrizioni avanzate vanno progettate nella catenaDOCKER-USER; con il backend nftables il modello è diverso. Non disabilitare alla cieca la gestioneiptables/ip6tablesdi Docker, perché può rompere il networking dei container. Mantieni Docker aggiornato e consulta la documentazione della versione installata.
Dove siamo arrivati
A questo punto il server non è invulnerabile, e diffiderei di chi promette il contrario. Abbiamo però ridotto diversi rischi comuni in modo concreto:
- patch di sicurezza applicate e verificabili;
- accesso amministrativo nominativo, senza login diretto come root;
- SSH con chiavi protette e utenti ammessi tramite gruppo;
- autenticazione con password disabilitata soltanto dopo il collaudo;
- porte host e Docker inventariate, filtrate e testate anche su IPv6.
Il lavoro non finisce qui. Backup con prove di ripristino, aggiornamento delle applicazioni e delle immagini container, gestione dei segreti, log centralizzati, alert e revisione periodica degli accessi fanno parte della stessa storia. L’hardening non è una cerimonia da fare una volta sola: ogni tanto bisogna tornare a vedere se quelle difese stanno ancora facendo il loro mestiere.
Riferimenti ufficiali
- Ubuntu Security: aggiornamenti di sicurezza
- Ubuntu Server: aggiornamenti automatici
- Ubuntu Server: gestione degli utenti
- Ubuntu Server: configurare OpenSSH
- OpenSSH: sshd_config(5)
- OpenSSH: ssh-keygen(1) e chiavi FIDO2
- Linux Kernel: soglia delle porte non privilegiate
- Ubuntu Server: firewall e UFW
- Docker: packet filtering, firewall e UFW
- Docker: pubblicazione delle porte
- Docker: implicazioni del gruppo docker