Renovar SSL automáticamente con Certbot y cron en Linux
Aprende a renovar SSL automáticamente con Certbot y cron en Linux: timer systemd, hook de recarga de Nginx, monitoreo y troubleshooting de producción.
Los certificados emitidos por Let’s Encrypt tienen una validez de 90 días — una decisión intencional del proyecto para forzar la automatización y reducir la ventana de exposición en caso de compromiso. La renovación manual cada 60-80 días es insostenible en producción: basta un viaje, una guardia sobrecargada o un servidor olvidado para que TLS expire y el sitio empiece a devolver ERR_CERT_DATE_INVALID en el navegador de los clientes.
Este tutorial es para sysadmins y desarrolladores que ya tienen Certbot instalado y al menos un certificado emitido, y quieren garantizar que la renovación se ejecute sin intervención, recargue el servidor web sin downtime y avise si algo se rompe. Vamos a cubrir los dos enfoques — cron clásico y systemd timer — elegir cuándo usar cada uno, configurar deploy hooks para recargar Nginx/Apache y armar un monitoreo básico.
Tiempo estimado de ejecución: 15 a 25 minutos, dependiendo de cuántos dominios gestiones y si vas a configurar alertas personalizadas.
Requisitos previos
Ubuntu 22.04 LTS o superior (o Debian 11+, Rocky 9+), Certbot 2.0+ instalado, al menos un certificado ya emitido en /etc/letsencrypt/live/, acceso sudo y Nginx o Apache activo sirviendo el dominio. El puerto 80 debe estar abierto en el firewall para el desafío HTTP-01 (o DNS configurado para DNS-01).
Confirma la versión de Certbot y la presencia de al menos un certificado antes de continuar:
certbot --version
sudo certbot certificates
La salida de certbot certificates debe listar cada certificado con nombre, dominios cubiertos y fecha de expiración. Si no aparece nada, emite un primer certificado antes de configurar la automatización — esta documentación asume que la etapa de emisión inicial ya fue realizada.
/etc/letsencrypt/live/ /etc/letsencrypt/renewal-hooks/ /etc/letsencrypt/renewal/ /var/log/letsencrypt/ Verificando si el timer systemd ya está activo
En distros modernas el paquete certbot instala automáticamente una unit systemd que se encarga de la renovación — antes de tocar cron, confirma si ya está corriendo. La mayoría de las instalaciones Ubuntu 22.04+ no necesita ningún cron manual.
Lista los timers activos y busca certbot:
systemctl list-timers --all | grep -i certbotSi aparece una línea como certbot.timer ... loaded active waiting, el timer ya está programado. La próxima ejecución viene en la columna NEXT. Salta a la sección de deploy hook si es ese tu caso.
Inspecciona la configuración del timer para entender la frecuencia:
systemctl cat certbot.timerLa unit por defecto se dispara dos veces al día (00:00 y 12:00) con RandomizedDelaySec=43200 — es decir, un retraso aleatorio de hasta 12 horas. Esto distribuye las renovaciones de todos los usuarios de Let’s Encrypt en el mundo, evitando concentración de carga.
Verifica el servicio que el timer dispara:
systemctl cat certbot.serviceEl ExecStart típico es /usr/bin/certbot -q renew — el -q (quiet) suprime la salida en casos de “nothing to renew”, evitando ruido en el log. Cuando ocurre una renovación, la salida completa va al journal.
Configurando la renovación vía cron tradicional
Si estás en una distro antigua, en un servidor sin systemd o simplemente prefieres cron, usa este enfoque. En distros modernas, deshabilita el timer antes para evitar dos ejecuciones concurrentes.
Deshabilita el timer systemd si vas a usar cron:
sudo systemctl disable --now certbot.timerEste comando detiene el timer y elimina el symlink que lo activa en el boot. Puedes reactivarlo en cualquier momento con enable --now.
Edita el crontab de root y agrega la entrada de renovación:
sudo crontab -eAgrega:
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
0 3,15 * * * /usr/bin/certbot renew --quietEsta línea se ejecuta a las 03:00 y a las 15:00 todos los días. Certbot solo renueva certificados con menos de 30 días restantes — ejecutarlo dos veces al día garantiza un segundo intento si el primero falla por inestabilidad temporal. El PATH explícito es obligatorio: cron hereda un entorno mínimo y los comandos sin ruta absoluta fallan silenciosamente.
Confirma que el cron fue registrado:
sudo crontab -lLa línea debe aparecer. Cron lee el archivo automáticamente — no es necesario reiniciar ningún servicio.
Evita programar renovaciones en horarios redondos como 00:00 o 12:00 sin jitter — miles de servidores hacen lo mismo y la API de Let’s Encrypt puede aplicar rate-limit. Usa horarios impares (03:17, 15:43) o agrega sleep $((RANDOM \% 3600)) antes del certbot para aleatorizar.
Configurando deploy hook para recarga sin downtime
Renovar el certificado no basta — Nginx o Apache siguen usando el cert antiguo cargado en memoria hasta recibir la señal para recargar. Los deploy hooks resuelven esto elegantemente.
Crea el hook de recarga de Nginx:
sudo mkdir -p /etc/letsencrypt/renewal-hooks/deploy
sudo tee /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh > /dev/null <<'EOF'
#!/bin/bash
systemctl reload nginx
EOF
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.shCualquier script ejecutable en /etc/letsencrypt/renewal-hooks/deploy/ se ejecuta después de una renovación exitosa — y solo después. A diferencia del --post-hook, que se ejecuta siempre, el deploy hook se omite si ningún certificado fue efectivamente renovado, evitando recargas innecesarias de Nginx.
Para Apache, usa el equivalente:
sudo tee /etc/letsencrypt/renewal-hooks/deploy/reload-apache.sh > /dev/null <<'EOF'
#!/bin/bash
systemctl reload apache2
EOF
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-apache.shsystemctl reload (no restart) recarga la configuración sin cortar conexiones activas — los workers existentes terminan sus requests mientras los nuevos workers ya usan el cert renovado.
Prueba el pipeline completo en modo dry-run:
sudo certbot renew --dry-runEl --dry-run simula una renovación contra el servidor staging de Let’s Encrypt — no consume cuota, no reemplaza el certificado real, pero ejecuta todos los hooks. Revisa la salida: al final debe aparecer Congratulations, all simulated renewals succeeded y el hook debe haberse ejecutado.
Si ejecutas Nginx y HAProxy en el mismo servidor y cada uno usa certificados diferentes, configura el hook dentro del archivo de renewal específico en /etc/letsencrypt/renewal/dominio.conf en la sección [renewalparams] con renew_hook = systemctl reload haproxy. Los hooks en el directorio global se ejecutan para todo cert renovado.
Monitoreando la renovación
Automatización sin observabilidad es una trampa — descubrir que el cert expiró por el error del navegador es el peor escenario posible. Arma un check activo que avise antes.
Crea un script de verificación de expiración:
sudo tee /usr/local/bin/check-ssl-expiry.sh > /dev/null <<'EOF'
#!/bin/bash
THRESHOLD_DAYS=20
ALERT_EMAIL="[email protected]"
for cert in /etc/letsencrypt/live/*/cert.pem; do
domain=$(basename "$(dirname "$cert")")
end_date=$(openssl x509 -enddate -noout -in "$cert" | cut -d= -f2)
end_epoch=$(date -d "$end_date" +%s)
now_epoch=$(date +%s)
days_left=$(( (end_epoch - now_epoch) / 86400 ))
if [ "$days_left" -lt "$THRESHOLD_DAYS" ]; then
echo "ALERTA: $domain expira en $days_left días" | \
mail -s "SSL expirando: $domain" "$ALERT_EMAIL"
fi
done
EOF
sudo chmod +x /usr/local/bin/check-ssl-expiry.shPrograma el script para que se ejecute diariamente vía cron o systemd timer. Si un certificado entra en la zona de menos de 20 días, recibes un email — significa que la renovación automática falló y necesita investigación manual.
Programa el check diario:
echo "30 8 * * * /usr/local/bin/check-ssl-expiry.sh" | sudo tee -a /etc/crontabSe ejecuta cada mañana a las 08:30. Combínalo con integración de alertas (Slack webhook, Discord, o tu sistema de métricas) sustituyendo la llamada mail por curl al webhook.
Verificación final
Ejecuta los comandos siguientes para confirmar que toda la configuración está activa:
sudo certbot renew --dry-run
systemctl list-timers --all | grep certbot
ls -la /etc/letsencrypt/renewal-hooks/deploy/
sudo /usr/local/bin/check-ssl-expiry.sh
El dry-run debe pasar sin errores. El timer (si optaste por systemd) debe estar activo. Los hooks deben ser ejecutables. El script de verificación no debe emitir alerta — si lo hace, el cert está en ventana crítica y necesitas investigar los logs en /var/log/letsencrypt/letsencrypt.log.
Resolución de problemas
”Failed authorization procedure” en el renew
Causa común: puerto 80 bloqueado por firewall o redirigido a 443 sin excepción para la ruta /.well-known/acme-challenge/. Certbot necesita servir el desafío HTTP-01 en esa ruta. Confirma con curl http://tu-dominio/.well-known/acme-challenge/test — debe devolver 404 de tu servidor, no error de conexión. Si fuerzas redirect global a HTTPS, agrega una excepción para esa ruta en Nginx.
”Hook command exited with code 1”
El script del hook devolvió error. Ejecútalo manualmente para depurarlo: sudo bash /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh. Errores típicos: nginx config inválida (nginx -t primero), servicio no corriendo o permisos incorrectos en el script.
El timer aparece activo pero nunca renueva
Verifica la hora del servidor con timedatectl — si el reloj está incorrecto, el timer se dispara pero certbot rechaza la operación. Sincroniza con sudo systemctl restart systemd-timesyncd. Otro caso: el cert aún no está en la ventana de 30 días antes de expirar; certbot lo ignora silenciosamente. Fuerza con --force-renewal solo si confirmaste otras causas.
Próximos pasos
Con la renovación automatizada y monitoreada, considera estos próximos pasos para aumentar la robustez de tu setup TLS:
- Configurar OCSP stapling en Nginx — reduce la latencia de validación del cert en el navegador y disminuye la carga en los servidores OCSP de Let’s Encrypt.
- Evaluar wildcard certificates con desafío DNS-01 — útil si gestionas muchos subdominios; requiere un plugin DNS específico de tu proveedor.
- Implementar HSTS preload después de confirmar la estabilidad de la renovación — difícil de revertir, así que actívalo solo cuando la automatización haya corrido por al menos 2 ciclos completos.
- Centralizar los logs de renovación en un sistema de observabilidad — útil en flotas de servidores donde verificar cada máquina individualmente no escala.
Si estás llevando esto a producción, una VPS Hostini ya viene con Ubuntu LTS actualizado, SSH endurecido por defecto y tráfico con protección DDoS — base sólida para automatizaciones de TLS sin sorpresas en el kernel o en la red.
Preguntas frecuentes
¿Por qué mi cron de renovación no está ejecutándose aunque esté configurado?
Cron exige PATH explícito — el entorno de cron no hereda el PATH del shell interactivo. Agrega `PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin` al inicio del crontab y usa rutas absolutas para certbot (`/usr/bin/certbot`). Verifica también `/var/log/syslog | grep CRON` para confirmar la ejecución.
¿Puedo forzar la renovación antes de los 30 días para probar?
Usa `certbot renew --dry-run` para simular sin consumir cuota de Let's Encrypt. Para forzar una renovación real, ejecuta `certbot renew --force-renewal` — pero cuidado: Let's Encrypt limita a 5 certificados duplicados por semana y las renovaciones forzadas cuentan para el límite.
¿Cuál es la diferencia entre cron tradicional y systemd timer para Certbot?
El paquete certbot moderno (Ubuntu 22.04+, Debian 12+) ya instala un systemd timer (`certbot.timer`) que se ejecuta 2 veces al día con jitter aleatorio. Cron tradicional sigue funcionando pero el timer tiene ventajas: es persistente (se ejecuta en el boot si perdió la ventana), randomización anti-thundering-herd y logs centralizados en journalctl.
¿Cómo hago que Nginx o Apache recarguen sin downtime tras la renovación?
Usa el deploy hook: `certbot renew --deploy-hook 'systemctl reload nginx'`. El hook solo se ejecuta cuando un certificado fue efectivamente renovado, evitando recargas innecesarias. Reload (no restart) recarga la configuración sin cortar conexiones activas — cero downtime.
El renew falló con error de rate limit. ¿Cuánto tiempo hasta que pueda intentar de nuevo?
Let's Encrypt aplica varios límites: 50 certificados por dominio raíz por semana, 5 duplicados por semana, 5 fallos por hora por cuenta+hostname. La ventana es deslizante — espera hasta que la request más antigua salga de la ventana. Usa staging (`--staging`) para probar sin afectar la cuota de producción.
¿Cómo me notifican si la renovación falla silenciosamente?
Configura el email de contacto en Certbot (`--email`) — Let's Encrypt envía aviso 20, 10 y 1 día antes de la expiración. Para monitoreo activo, agrega un script que verifique `openssl x509 -enddate -noout -in /etc/letsencrypt/live/dominio/cert.pem` y dispare una alerta si restan menos de 20 días.