
Il problema: quando 199 giorni diventano un incubo
Era una mattina come tante quando ho ricevuto l’email che ha cambiato tutto:
“Gentile Cliente, ti informiamo di un cambiamento importante che riguarda la validità dei certificati SSL: a partire dal 12 marzo 2026, i certificati avranno una validità tecnica di 199 giorni dalla data di emissione.”
Per chi gestisce un paio di siti personali, questo significa poco. Per chi, come me, gestisce una ventina di certificati SSL distribuiti su una trentina di macchine di diversi clienti, significa una cosa sola: il lavoro manuale sta per raddoppiare.
E non finisce qui. Il calendario del CA/Browser Forum è impietoso:
- Marzo 2026: validità 199 giorni (circa 2 rinnovi/anno)
- Marzo 2027: validità 100 giorni (circa 4 rinnovi/anno)
- Marzo 2029: validità 47 giorni (circa 8 rinnovi/anno)
Ho fatto due conti: con 20 certificati e 8 rinnovi all’anno, parliamo di 160 operazioni manuali ogni anno. Tra conversioni di formato, upload su server diversi, configurazioni Nginx, Apache, firewall… un delirio.
L’idea: togliermi di mezzo
Il mio obiettivo era chiaro: non voglio più essere il collo di bottiglia.
Ogni volta che un certificato scade, non deve dipendere dalla mia disponibilità. I colleghi devono poter caricare i certificati. I server devono poterseli andare a prendere da soli. Io devo poter dormire la notte.
La soluzione? Un Certificate Hub centralizzato:
- Una macchina Linux dove caricare i certificati
- Conversione automatica in tutti i formati necessari
- I server client pescano i certificati in autonomia
- Log di tutto quello che succede
L’architettura
Dopo alcune riflessioni, ho definito questa architettura:
┌─────────────────────────────────────────────────────────────────┐
│ CERTIFICATE HUB │
├─────────────────────────────────────────────────────────────────┤
│ │
│ INGRESSO (upload) │ USCITA (distribuzione) │
│ ───────────────── │ ────────────────────── │
│ • Solo rete interna/VPN │ • HTTPS esposto │
│ • SFTP con FileZilla │ • Auth: IP whitelist │
│ • Utente dedicato │ • Fallback: user+password │
│ │ • Log ogni download │
│ │
│ /incoming/ → [conversione] → /ready/ │
│ │
└─────────────────────────────────────────────────────────────────┘
Le scelte architetturali e il perché
Perché SFTP e non FTP?
FTP trasmette le credenziali in chiaro. SFTP usa SSH, è cifrato, e non richiede di aprire porte aggiuntive se SSH è già attivo. Inoltre FileZilla lo supporta nativamente, quindi per i colleghi non cambia nulla.
Perché un utente condiviso?
Gestisco un team piccolo. Creare utenti separati aggiungerebbe complessità senza benefici reali. L’utente certmanager è dedicato solo a questa funzione e vive in una “gabbia” (chroot) che gli impedisce di vedere il resto del sistema.
Perché la separazione incoming/ready?
incoming: dove si caricano i file grezzi (può essere un disastro)ready: dove finiscono i file elaborati, puliti, pronti all’uso
I colleghi toccano solo incoming. I server client leggono solo da ready. Nessuno può fare danni.
Perché convertire automaticamente in tutti i formati?
Gestisco ambienti misti:
- Nginx: vuole
fullchain.pem+ chiave separata - Apache: vuole certificato, catena e chiave separati
- Firewall/Windows: vogliono
.pfx
Generare tutto automaticamente significa che ogni server trova già il formato che gli serve.
L’ambiente di test
Per questo progetto ho usato:
- 3 VM VMware Workstation con Ubuntu
- 1 VM = Certificate Hub
- 2 VM = Server di test (simulano i client)
La VM del Certificate Hub è una Ubuntu Server minimale. Non serve interfaccia grafica.
Implementazione passo-passo
Fase 1: Preparazione del sistema
# Aggiorna il sistema
sudo apt update && sudo apt upgrade -y
# Installa i pacchetti necessari
sudo apt install -y nginx openssl apache2-utils inotify-tools
Cosa installiamo e perché:
nginx: servirà per la distribuzione HTTPS (fase successiva)openssl: per le conversioni dei certificatiapache2-utils: per creare le password di autenticazioneinotify-tools: per monitorare le cartelle in tempo reale (opzionale)
Fase 2: Creazione dell’utente dedicato
# Crea l'utente certmanager
sudo useradd -m -s /bin/bash certmanager
sudo passwd certmanager # Scegli una password robusta!
Fase 3: Struttura delle cartelle
# Crea la struttura
sudo mkdir -p /srv/certs/{incoming,ready,archive,config,scripts}
# Imposta i permessi base
sudo chown root:root /srv/certs
sudo chmod 755 /srv/certs
# incoming: scrivibile da certmanager
sudo chown certmanager:certmanager /srv/certs/incoming
sudo chmod 755 /srv/certs/incoming
La struttura finale:
/srv/certs/
├── incoming/ ← Upload via SFTP (scrivibile)
├── ready/ ← File elaborati (sola lettura)
├── archive/ ← Storico versioni
├── config/ ← Configurazioni
└── scripts/ ← Script di automazione
Fase 4: Configurazione SFTP con chroot
Questa è la parte cruciale per la sicurezza. Configuriamo SSH per “ingabbiare” l’utente certmanager.
# Backup della configurazione SSH
sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.backup
# Modifica la configurazione
sudo nano /etc/ssh/sshd_config
Aggiungi in fondo al file:
# Chroot per certmanager - accesso solo a /srv/certs
Match User certmanager
ChrootDirectory /srv/certs
ForceCommand internal-sftp
AllowTcpForwarding no
X11Forwarding no
Cosa fa questa configurazione:
ChrootDirectory: l’utente vede/srv/certscome se fosse la root/ForceCommand internal-sftp: può usare SOLO SFTP, niente shellAllowTcpForwarding no: non può creare tunnelX11Forwarding no: non può inoltrare sessioni grafiche
# Riavvia SSH (su Ubuntu il servizio si chiama ssh, non sshd!)
sudo systemctl restart ssh
Fase 5: Test con FileZilla
Ora verifica che tutto funzioni:
- Apri FileZilla
- Connessione rapida:
- Host:
sftp://IP_DELLA_VM - Utente:
certmanager - Password: quella che hai scelto
- Porta:
22
Dovresti vedere solo le cartelle incoming, ready, archive, ecc. Nessun accesso al resto del sistema.
Fase 6: Lo script di conversione automatica
Questo è il cuore del sistema. Lo script:
- Scansiona
/incoming/cercando nuovi certificati - Estrae il CN (Common Name) dal certificato
- Genera tutti i formati necessari
- Li posiziona in
/ready/con una struttura ordinata - Logga tutto
sudo nano /srv/certs/scripts/convert-certs.sh
#!/bin/bash
# === CONFIGURAZIONE ===
INCOMING_DIR="/srv/certs/incoming"
READY_DIR="/srv/certs/ready"
ARCHIVE_DIR="/srv/certs/archive"
LOG_FILE="/var/log/certs/conversion.log"
PFX_PASSWORD="12345678"
# === FUNZIONI ===
log() {
echo "$(date '+%Y-%m-%d %H:%M:%S') | $1" >> "$LOG_FILE"
echo "$(date '+%Y-%m-%d %H:%M:%S') | $1"
}
log_warning() {
echo "$(date '+%Y-%m-%d %H:%M:%S') | WARNING: $1" >> "$LOG_FILE"
echo "$(date '+%Y-%m-%d %H:%M:%S') | WARNING: $1"
}
# Estrae CN dal certificato
get_cn_from_cert() {
openssl x509 -in "$1" -noout -subject 2>/dev/null | sed -n 's/.*CN *= *\([^,]*\).*/\1/p'
}
# Estrae data scadenza dal certificato
get_expiry_from_cert() {
openssl x509 -in "$1" -noout -enddate 2>/dev/null | cut -d= -f2
}
# Converte * in star per nomi file
sanitize_domain() {
echo "$1" | sed 's/\*/star/g'
}
# Crea cartella log se non esiste
mkdir -p "$(dirname "$LOG_FILE")"
log "=========================================="
log "=== Avvio scansione certificati ==="
log "=========================================="
# Scansiona ogni cartella in incoming
for CLIENT_FOLDER in "$INCOMING_DIR"/*/; do
[ -d "$CLIENT_FOLDER" ] || continue
FOLDER_NAME=$(basename "$CLIENT_FOLDER")
# Salta cartelle che iniziano con . o _
[[ "$FOLDER_NAME" == .* ]] || [[ "$FOLDER_NAME" == _* ]] && continue
# Estrai cliente e dominio dal nome cartella (formato: cliente_dominio)
if [[ "$FOLDER_NAME" == *"_"* ]]; then
CLIENTE="${FOLDER_NAME%%_*}"
DOMINIO_CARTELLA="${FOLDER_NAME#*_}"
else
log_warning "$FOLDER_NAME - formato non valido (usa cliente_dominio). Saltato."
continue
fi
# Cerca il file certificato
CRT_FILE=$(find "$CLIENT_FOLDER" -maxdepth 1 -name "*.crt" -type f 2>/dev/null | head -1)
KEY_FILE=$(find "$CLIENT_FOLDER" -maxdepth 1 -name "*.key" -type f 2>/dev/null | head -1)
CHAIN_FILE=$(find "$CLIENT_FOLDER" -maxdepth 1 \( -name "chain.pem" -o -name "*chain*.pem" -o -name "ca-bundle.crt" -o -name "*intermediate*" \) -type f 2>/dev/null | head -1)
# Se non c'è .crt, salta
if [ -z "$CRT_FILE" ]; then
continue
fi
# Estrai CN dal certificato
CN_RAW=$(get_cn_from_cert "$CRT_FILE")
if [ -z "$CN_RAW" ]; then
log_warning "$FOLDER_NAME - impossibile leggere CN dal certificato. Saltato."
continue
fi
# Sanitizza il CN per i nomi file (sostituisce * con star)
CN_SAFE=$(sanitize_domain "$CN_RAW")
# Estrai data scadenza
EXPIRY_DATE=$(get_expiry_from_cert "$CRT_FILE")
log "Trovato: $CLIENTE / $CN_RAW"
# Verifica corrispondenza nome cartella vs CN
DOMINIO_CARTELLA_NORMALIZED=$(echo "$DOMINIO_CARTELLA" | tr '[:upper:]' '[:lower:]')
CN_SAFE_NORMALIZED=$(echo "$CN_SAFE" | tr '[:upper:]' '[:lower:]')
if [ "$DOMINIO_CARTELLA_NORMALIZED" != "$CN_SAFE_NORMALIZED" ]; then
log_warning "Nome cartella ($DOMINIO_CARTELLA) non corrisponde al certificato ($CN_RAW)"
log_warning "I file verranno generati come: $CN_SAFE.*"
fi
# Crea cartella destinazione usando il CN reale
DEST_DIR="$READY_DIR/$CLIENTE/$CN_SAFE"
mkdir -p "$DEST_DIR"
# Copia i file con nome basato sul CN
cp "$CRT_FILE" "$DEST_DIR/$CN_SAFE.crt"
log " -> $CN_SAFE.crt copiato"
if [ -n "$KEY_FILE" ]; then
cp "$KEY_FILE" "$DEST_DIR/$CN_SAFE.key"
log " -> $CN_SAFE.key copiato"
elif [ -f "$DEST_DIR/$CN_SAFE.key" ]; then
log " -> $CN_SAFE.key già presente (riutilizzata)"
else
log_warning "Nessuna chiave privata trovata per $CN_RAW"
fi
if [ -n "$CHAIN_FILE" ]; then
cp "$CHAIN_FILE" "$DEST_DIR/$CN_SAFE.chain.pem"
log " -> $CN_SAFE.chain.pem copiato"
fi
# === GENERA FORMATI ===
# Fullchain per Nginx (cert + chain)
if [ -f "$DEST_DIR/$CN_SAFE.chain.pem" ]; then
cat "$DEST_DIR/$CN_SAFE.crt" "$DEST_DIR/$CN_SAFE.chain.pem" > "$DEST_DIR/$CN_SAFE.fullchain.pem"
else
cp "$DEST_DIR/$CN_SAFE.crt" "$DEST_DIR/$CN_SAFE.fullchain.pem"
fi
log " -> $CN_SAFE.fullchain.pem generato (Nginx)"
# PFX per firewall/Windows (se abbiamo la key)
if [ -f "$DEST_DIR/$CN_SAFE.key" ]; then
if [ -f "$DEST_DIR/$CN_SAFE.chain.pem" ]; then
openssl pkcs12 -export \
-out "$DEST_DIR/$CN_SAFE.pfx" \
-inkey "$DEST_DIR/$CN_SAFE.key" \
-in "$DEST_DIR/$CN_SAFE.crt" \
-certfile "$DEST_DIR/$CN_SAFE.chain.pem" \
-password pass:"$PFX_PASSWORD" 2>/dev/null
else
openssl pkcs12 -export \
-out "$DEST_DIR/$CN_SAFE.pfx" \
-inkey "$DEST_DIR/$CN_SAFE.key" \
-in "$DEST_DIR/$CN_SAFE.crt" \
-password pass:"$PFX_PASSWORD" 2>/dev/null
fi
log " -> $CN_SAFE.pfx generato (Firewall/Windows)"
fi
# Genera INFO.txt
cat > "$DEST_DIR/INFO.txt" << EOF
==========================================
CERTIFICATO: $CN_RAW
==========================================
Cliente: $CLIENTE
Dominio: $CN_RAW
Scadenza: $EXPIRY_DATE
Generato: $(date '+%Y-%m-%d %H:%M:%S')
FILE DISPONIBILI:
$CN_SAFE.crt - Certificato (Apache)
$CN_SAFE.key - Chiave privata
$CN_SAFE.chain.pem - Catena intermedi (se presente)
$CN_SAFE.fullchain.pem - Cert + Chain (Nginx)
$CN_SAFE.pfx - Bundle (Firewall/Windows)
Password PFX: $PFX_PASSWORD
URL DOWNLOAD (quando attivo HTTPS):
/ready/$CLIENTE/$CN_SAFE/
==========================================
EOF
log " -> INFO.txt generato"
# Imposta permessi
chown -R root:certmanager "$READY_DIR/$CLIENTE"
find "$READY_DIR/$CLIENTE" -type d -exec chmod 750 {} \;
find "$READY_DIR/$CLIENTE" -type f -exec chmod 640 {} \;
# Archivia con data
ARCHIVE_DEST="$ARCHIVE_DIR/$CLIENTE/$CN_SAFE/$(date '+%Y%m%d_%H%M%S')"
mkdir -p "$ARCHIVE_DEST"
cp "$DEST_DIR"/* "$ARCHIVE_DEST/" 2>/dev/null
log " -> Archiviato in $ARCHIVE_DEST"
log "OK: $CLIENTE / $CN_RAW completato"
log "------------------------------------------"
done
log "=== Scansione completata ==="
log ""
# Rendi eseguibile lo script
sudo chmod +x /srv/certs/scripts/convert-certs.sh
# Crea la cartella per i log
sudo mkdir -p /var/log/certs
Fase 7: Automazione con cron
# Modifica il crontab di root
sudo crontab -e
Aggiungi questa riga:
*/5 * * * * /srv/certs/scripts/convert-certs.sh > /dev/null 2>&1
Ora lo script gira automaticamente ogni 5 minuti.
Note sulla gestione dei certificati wildcard
I certificati wildcard (es. *.example.com) presentano un problema: l’asterisco non è un carattere valido nei nomi file.
Convenzione adottata:
- Nel nome cartella: usa
staral posto di* - Esempio:
cliente_star.example.com
Lo script rileva automaticamente il CN reale dal certificato, quindi anche se il collega sbaglia il nome della cartella, i file vengono generati con il nome corretto. Un warning viene scritto nel log per segnalare la discrepanza.
Cosa abbiamo ottenuto (Parte 1)
| Componente | Stato |
|---|---|
| VM Ubuntu Certificate Hub | ✅ |
| Utente dedicato con chroot SFTP | ✅ |
| Accesso FileZilla funzionante | ✅ |
| Conversione automatica multi-formato | ✅ |
| Nomi file basati sul CN del certificato | ✅ |
| Warning per discrepanze nei nomi | ✅ |
| File INFO.txt con riepilogo | ✅ |
| Archiviazione storica | ✅ |
| Cron ogni 5 minuti | ✅ |
Prossimi passi (Parte 2)
Nella seconda parte dell’articolo implementeremo:
- Nginx HTTPS per la distribuzione ai server client
- Autenticazione mista: IP whitelist + basic auth fallback
- Log di ogni download
- Script curl da installare sui server client
- Test end-to-end completo
Considerazioni finali
Questo progetto nasce da un’esigenza concreta: non impazzire quando i certificati passeranno a 47 giorni di validità. La soluzione non è rivoluzionaria, ma è pratica.
L’obiettivo non era creare il sistema perfetto, ma un sistema che:
- Funzioni in modo affidabile
- Sia gestibile anche da colleghi meno tecnici
- Mi tolga dal percorso critico
Con questa prima fase, abbiamo costruito le fondamenta. I colleghi possono già caricare certificati via FileZilla, e il sistema genera automaticamente tutti i formati necessari.
La vera magia arriverà con la seconda parte, quando i server potranno pescare i certificati in autonomia.
Questo articolo è parte di una serie. La Parte 2 coprirà la distribuzione automatica ai server client.
