Let's Encrypt es una autoridad certificadora (CA) gratuita y automática que emite certificados SSL/TLS. Certbot es la herramienta oficial que interactúa con Let's Encrypt para obtener, instalar y renovar los certificados automáticamente.
Los certificados de Let's Encrypt duran 90 días. Certbot configura una tarea automática para renovarlos antes de que expiren — en la práctica nunca vencen.
HTTP-01 (el más común)
Certbot pone un archivo temporal en /.well-known/acme-challenge/ de tu sitio. Let's Encrypt lo visita por HTTP para confirmar que controlás el servidor. Requiere que el puerto 80 esté abierto y el dominio apuntando al VPS.
DNS-01 (para wildcard)
Certbot crea un registro TXT en el DNS de tu dominio. Let's Encrypt lo verifica. Permite obtener certificados wildcard (*.ejemplo.com) y funciona aunque el puerto 80 esté cerrado. Requiere acceso a la API del proveedor DNS.
Por cada dominio, Certbot crea estos archivos en /etc/letsencrypt/live/ejemplo.com/
/etc/letsencrypt/live/ejemplo.com/ ├── cert.pem # certificado del dominio (solo este) ├── chain.pem # certificados intermedios de Let's Encrypt ├── fullchain.pem # cert.pem + chain.pem (usar este en Nginx) └── privkey.pem # clave privada (nunca compartir)
En Nginx siempre usar fullchain.pem como certificado, no cert.pem. El fullchain incluye la cadena completa que los navegadores necesitan para validar.
# Actualizar repos apt update # Instalar Certbot y el plugin de Nginx apt install certbot python3-certbot-nginx # Verificar instalación certbot --version # Para el desafío DNS (necesario para wildcards) apt install python3-certbot-dns-cloudflare # si usás Cloudflare apt install python3-certbot-dns-digitalocean # si usás DigitalOcean DNS
El plugin python3-certbot-nginx permite que Certbot modifique la configuración de Nginx automáticamente. Conveniente pero revisá siempre lo que cambió.
Let's Encrypt verifica que el dominio apunta al servidor antes de emitir. Verificá esto primero:
# El dominio debe resolver a la IP del VPS dig ejemplo.com dig www.ejemplo.com nslookup ejemplo.com # El puerto 80 debe estar abierto y Nginx corriendo curl -I http://ejemplo.com # Verificar que Nginx está activo systemctl status nginx # El firewall debe permitir puerto 80 y 443 ufw allow 80 ufw allow 443
Si el dominio no resuelve a la IP del VPS, Certbot fallará. Los cambios DNS pueden tardar hasta 48hs en propagarse, aunque generalmente son minutos.
El modo más simple — Certbot detecta la config de Nginx y modifica todo automáticamente.
# Certbot configura Nginx automáticamente certbot --nginx -d ejemplo.com -d www.ejemplo.com # Solo obtener el certificado, sin tocar Nginx certbot certonly --nginx -d ejemplo.com -d www.ejemplo.com # Para un subdominio certbot --nginx -d app.ejemplo.com # Múltiples subdominios en un solo certificado certbot --nginx \ -d ejemplo.com \ -d www.ejemplo.com \ -d app.ejemplo.com \ -d api.ejemplo.com
Certbot te pregunta tu email (para alertas de vencimiento) y si querés redirigir HTTP → HTTPS. Respondé que sí a la redirección.
Cuando Nginx no está instalado o hay que detenerlo temporalmente. Certbot levanta su propio servidor HTTP en el puerto 80.
# Detener Nginx antes systemctl stop nginx # Obtener certificado con servidor standalone certbot certonly --standalone -d ejemplo.com -d www.ejemplo.com # Volver a levantar Nginx systemctl start nginx
Útil durante la configuración inicial o para dominios que aún no tienen virtual host configurado en Nginx.
# Ver todos los certificados gestionados por Certbot certbot certificates # Output de ejemplo: # Found the following certs: # Certificate Name: ejemplo.com # Domains: ejemplo.com www.ejemplo.com # Expiry Date: 2024-12-01 (VALID: 89 days) # Certificate Path: /etc/letsencrypt/live/ejemplo.com/fullchain.pem # Private Key Path: /etc/letsencrypt/live/ejemplo.com/privkey.pem # Ver fecha de vencimiento de un certificado openssl x509 -enddate -noout \ -in /etc/letsencrypt/live/ejemplo.com/fullchain.pem # Ver todos los detalles del certificado openssl x509 -text -noout \ -in /etc/letsencrypt/live/ejemplo.com/fullchain.pem # Ver qué dominios cubre el certificado openssl x509 -noout -ext subjectAltName \ -in /etc/letsencrypt/live/ejemplo.com/fullchain.pem
# Eliminar un certificado (sin revocar) certbot delete --cert-name ejemplo.com # Revocar y eliminar (si la clave fue comprometida) certbot revoke --cert-path \ /etc/letsencrypt/live/ejemplo.com/fullchain.pem certbot delete --cert-name ejemplo.com
Solo revocás un certificado si la clave privada fue comprometida. En todos los demás casos alcanza con no renovarlo y eliminarlo.
Esto es lo que Certbot agrega a tu config de Nginx automáticamente. Útil entenderlo para modificarlo después.
# Bloque HTTP → redirige todo a HTTPS server { listen 80; listen [::]:80; server_name ejemplo.com www.ejemplo.com; # Certbot necesita este location para renovar location /.well-known/acme-challenge/ { root /var/www/html; } # Todo lo demás → HTTPS location / { return 301 https://$host$request_uri; } } # Bloque HTTPS principal server { listen 443 ssl http2; listen [::]:443 ssl http2; server_name ejemplo.com www.ejemplo.com; root /var/www/ejemplo.com/public; index index.html index.php; # Certificados de Let's Encrypt ssl_certificate /etc/letsencrypt/live/ejemplo.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/ejemplo.com/privkey.pem; # Parámetros SSL recomendados por Certbot include /etc/letsencrypt/options-ssl-nginx.conf; ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; location / { try_files $uri $uri/ =404; } access_log /var/log/nginx/ejemplo.com.access.log; error_log /var/log/nginx/ejemplo.com.error.log; }
Lo más ordenado es un certificado por dominio/subdominio principal, cada uno con su bloque server en Nginx.
# Obtener cert para el dominio principal certbot --nginx -d ejemplo.com -d www.ejemplo.com # Obtener cert para subdominio de app certbot --nginx -d app.ejemplo.com # Obtener cert para subdominio de API certbot --nginx -d api.ejemplo.com # Resultado: 3 certificados independientes certbot certificates # → ejemplo.com (cubre ejemplo.com, www.ejemplo.com) # → app.ejemplo.com (cubre app.ejemplo.com) # → api.ejemplo.com (cubre api.ejemplo.com)
Certificados independientes por subdominio son más fáciles de gestionar y revocar individualmente si algo sale mal.
Si ya tenés un certificado y querés que cubra un dominio adicional, usás --expand.
# Agregar nuevo dominio al certificado existente certbot --nginx --expand \ -d ejemplo.com \ -d www.ejemplo.com \ -d nuevo.ejemplo.com # Importante: listar TODOS los dominios del cert, # no solo el nuevo — los que no listés se perderán
Con --expand hay que incluir todos los dominios del certificado original más el nuevo. Si omitís alguno se pierde del certificado.
Certbot instala automáticamente un timer de systemd que intenta renovar los certificados dos veces por día. Solo renueva los que vencen en menos de 30 días.
# Ver el timer de renovación automática systemctl list-timers | grep certbot systemctl status certbot.timer # Ver el servicio de renovación systemctl status certbot.service # Ver cuándo se ejecutó por última vez journalctl -u certbot.service # Si no está habilitado, habilitarlo systemctl enable certbot.timer systemctl start certbot.timer
Con el timer de systemd activo no necesitás hacer nada más. Los certificados se renuevan solos antes de vencer.
# Simulación de renovación (no hace cambios reales) certbot renew --dry-run # Renovar todos los certificados próximos a vencer certbot renew # Forzar renovación aunque no esté próximo a vencer certbot renew --force-renewal # Renovar solo un dominio específico certbot renew --cert-name ejemplo.com # Renovar y recargar Nginx después certbot renew --post-hook "systemctl reload nginx"
Ejecutá --dry-run una vez después de instalar para confirmar que la renovación automática va a funcionar correctamente.
Los hooks permiten ejecutar comandos automáticamente durante el proceso de renovación. Se definen en /etc/letsencrypt/renewal-hooks/
# Estructura de hooks disponibles ls /etc/letsencrypt/renewal-hooks/ # → pre/ (antes de renovar) # → deploy/ (después de renovar exitosamente) # → post/ (siempre al final, éxito o no) # Ejemplo: recargar Nginx tras renovación exitosa # /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
#!/bin/bash # Recargar Nginx después de renovar el certificado systemctl reload nginx echo "Nginx recargado: $(date)" >> /var/log/certbot-deploy.log
# Dar permisos de ejecución chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
Los scripts en deploy/ solo se ejecutan cuando el certificado se renueva exitosamente — no en intentos fallidos ni cuando no hay nada que renovar.
Certbot ya envía emails de alerta al vencimiento si registraste un email al obtener el certificado. Podés verificarlo y configurar alertas adicionales.
# Ver el email registrado cat /etc/letsencrypt/accounts/*/meta.json | python3 -m json.tool # Actualizar email de contacto certbot update_account --email nuevo@email.com # Script para alertar si un cert vence en menos de 14 días # Agregar a cron para ejecutar diariamente
#!/bin/bash
# /usr/local/bin/check-ssl-expiry.sh
DOMAIN="ejemplo.com"
DAYS_WARN=14
EXPIRY=$(openssl s_client -connect $DOMAIN:443 -servername $DOMAIN \
2>/dev/null | openssl x509 -noout -enddate | cut -d= -f2)
EXPIRY_EPOCH=$(date -d "$EXPIRY" +%s)
NOW_EPOCH=$(date +%s)
DAYS_LEFT=$(( (EXPIRY_EPOCH - NOW_EPOCH) / 86400 ))
if [ $DAYS_LEFT -lt $DAYS_WARN ]; then
echo "SSL de $DOMAIN vence en $DAYS_LEFT días ($EXPIRY)" \
| mail -s "⚠ SSL próximo a vencer" tu@email.com
fi
chmod +x /usr/local/bin/check-ssl-expiry.sh
echo "0 8 * * * /usr/local/bin/check-ssl-expiry.sh" | crontab -
Un certificado wildcard *.ejemplo.com cubre cualquier subdominio: app.ejemplo.com, api.ejemplo.com, staging.ejemplo.com, etc. Requiere el desafío DNS-01 — no funciona con HTTP-01.
# Obtener certificado wildcard (desafío DNS manual) certbot certonly --manual --preferred-challenges dns \ -d ejemplo.com -d *.ejemplo.com # Certbot te pedirá crear un registro TXT en tu DNS: # _acme-challenge.ejemplo.com → "valor_que_te_da_certbot" # Verificar que el TXT propagó antes de continuar dig TXT _acme-challenge.ejemplo.com nslookup -type=TXT _acme-challenge.ejemplo.com
El certificado wildcard no cubre el dominio raíz (ejemplo.com) — por eso hay que pedirlo explícitamente con -d ejemplo.com -d *.ejemplo.com.
Si tu DNS está en Cloudflare, el plugin automatiza la creación del registro TXT y la renovación funciona sin intervención manual.
# Instalar plugin de Cloudflare apt install python3-certbot-dns-cloudflare # Crear archivo de credenciales mkdir -p /etc/letsencrypt/cloudflare nano /etc/letsencrypt/cloudflare/credentials.ini
# /etc/letsencrypt/cloudflare/credentials.ini # Opción A: API Token (recomendado — más seguro) dns_cloudflare_api_token = tu_api_token_aqui # Opción B: Global API Key (acceso total, menos recomendado) # dns_cloudflare_email = tu@email.com # dns_cloudflare_api_key = tu_global_key_aqui
# Permisos restrictivos (solo root puede leer) chmod 600 /etc/letsencrypt/cloudflare/credentials.ini # Obtener certificado wildcard automáticamente certbot certonly \ --dns-cloudflare \ --dns-cloudflare-credentials /etc/letsencrypt/cloudflare/credentials.ini \ -d ejemplo.com \ -d *.ejemplo.com
Con este método la renovación automática funciona sin intervención — Certbot actualiza el registro TXT de Cloudflare solo.
# El mismo certificado para todos los subdominios server { listen 443 ssl http2; server_name app.ejemplo.com; ssl_certificate /etc/letsencrypt/live/ejemplo.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/ejemplo.com/privkey.pem; include /etc/letsencrypt/options-ssl-nginx.conf; ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; root /var/www/app.ejemplo.com/public; location / { try_files $uri $uri/ =404; } } # Otro subdominio, mismo certificado wildcard server { listen 443 ssl http2; server_name api.ejemplo.com; ssl_certificate /etc/letsencrypt/live/ejemplo.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/ejemplo.com/privkey.pem; include /etc/letsencrypt/options-ssl-nginx.conf; ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; location / { proxy_pass http://127.0.0.1:4000; } }
Más allá del certificado, la configuración de los protocolos y cifrados determina la seguridad real de la conexión. Crear un snippet reutilizable en /etc/nginx/snippets/ssl-params.conf
# /etc/nginx/snippets/ssl-params.conf # Solo TLS 1.2 y 1.3 (deshabilitar SSL 3.0, TLS 1.0, TLS 1.1) ssl_protocols TLSv1.2 TLSv1.3; # Preferir cifrados del servidor sobre los del cliente ssl_prefer_server_ciphers on; # Suite de cifrados seguros ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256; # Parámetros Diffie-Hellman (generar una vez) ssl_dhparam /etc/nginx/dhparam.pem; # Caché de sesiones SSL (reduce handshakes repetidos) ssl_session_cache shared:SSL:10m; ssl_session_timeout 1d; ssl_session_tickets off; # OCSP Stapling (verificación de revocación más rápida) ssl_stapling on; ssl_stapling_verify on; resolver 8.8.8.8 8.8.4.4 valid=300s; resolver_timeout 5s; # HSTS — forzar HTTPS por 1 año add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
# Generar parámetros DH (ejecutar una sola vez, tarda unos minutos) openssl dhparam -out /etc/nginx/dhparam.pem 2048 # Incluir el snippet en cada server block HTTPS include snippets/ssl-params.conf;
Los navegadores incluyen una lista de dominios que siempre deben ir por HTTPS — incluso en la primera visita, antes de recibir el header HSTS. Para registrarse:
# Requisitos para preload: # 1. Servir HTTPS en el dominio raíz # 2. Redirigir HTTP → HTTPS # 3. Cubrir todos los subdominios con HTTPS # 4. Header HSTS con max-age mínimo de 31536000 (1 año) # con includeSubDomains y preload add_header Strict-Transport-Security \ "max-age=31536000; includeSubDomains; preload" always;
Una vez en la lista preload es muy difícil salir. No activar preload a menos que todos los subdominios soporten HTTPS permanentemente.
# Ver certificado que está sirviendo el servidor openssl s_client -connect ejemplo.com:443 -servername ejemplo.com # Ver solo la fecha de vencimiento echo | openssl s_client -connect ejemplo.com:443 2>/dev/null \ | openssl x509 -noout -enddate # Ver todos los detalles del certificado remoto echo | openssl s_client -connect ejemplo.com:443 2>/dev/null \ | openssl x509 -noout -text # Verificar que la cadena de certificados es correcta openssl verify -CAfile /etc/letsencrypt/live/ejemplo.com/chain.pem \ /etc/letsencrypt/live/ejemplo.com/cert.pem # Ver qué protocolos y cifrados acepta el servidor nmap --script ssl-enum-ciphers -p 443 ejemplo.com # Verificar HSTS curl -I https://ejemplo.com | grep -i strict # Verificar redirección HTTP → HTTPS curl -I http://ejemplo.com
PROBLEMA: "too many certificates already issued" → Let's Encrypt limita a 5 certs por dominio por semana. → Usar --staging para probar sin gastar el límite: certbot --nginx --staging -d ejemplo.com PROBLEMA: Certbot falla con "Connection refused" → El puerto 80 no está abierto o Nginx no responde. ufw allow 80 systemctl status nginx curl -I http://ejemplo.com PROBLEMA: "DNS problem: NXDOMAIN" → El dominio no resuelve a la IP del VPS. dig ejemplo.com # verificar registro A PROBLEMA: Certificado correcto pero navegador muestra error → Probablemente se usa cert.pem en vez de fullchain.pem. → Cambiar en nginx: ssl_certificate fullchain.pem PROBLEMA: Renovación automática falla certbot renew --dry-run # ver el error exacto journalctl -u certbot.service # ver logs del timer PROBLEMA: "OCSP stapling" errors en nginx error.log → Agregar resolver en el server block: resolver 8.8.8.8 8.8.4.4 valid=300s;
Estas herramientas online analizan la configuración SSL del servidor y dan una nota con recomendaciones detalladas.
# SSL Labs — el más completo (da nota A/B/C/F) https://www.ssllabs.com/ssltest/analyze.html?d=ejemplo.com # SecurityHeaders.com — analiza headers HTTP de seguridad https://securityheaders.com/?q=ejemplo.com # SSL Checker — verificación rápida de cadena de certs https://www.sslchecker.com/sslchecker # Mozilla SSL Configuration Generator # Genera config recomendada para Nginx según perfil https://ssl-config.mozilla.org/
Con la configuración de esta guía deberías obtener A o A+ en SSL Labs. El objetivo mínimo para producción es A.