Volver al índice
root@vps-donweb:~/docs/ssl$

SSL con Let's Encrypt

herramienta Certbot
emisor Let's Encrypt
categoría Servidor web
¿QUÉ ES? Let's Encrypt y Certbot

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.

Tu dominioejemplo.com
Certbotsolicita cert
Let's Encryptverifica dominio
Certificadoemitido y activo
Nginxsirve HTTPS

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.

MÉTODOS Cómo Let's Encrypt verifica que controlás el dominio

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.

ARCHIVOS Qué genera Certbot

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.

INSTALAR Certbot en Ubuntu
# 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ó.

REQUISITOS Antes de pedir el certificado

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.

OBTENER Certificado para un dominio con Nginx

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.

OBTENER Certificado con desafío standalone

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.

LISTAR Ver certificados instalados
# 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 Revocar y borrar certificados
# 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.

NGINX Config completa HTTPS generada por Certbot

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;
}
MÚLTIPLES DOMINIOS Cada dominio con su propio certificado

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.

AGREGAR DOMINIO Expandir un certificado existente

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.

AUTO Renovación automática con systemd

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.

MANUAL Renovar y probar la renovación
# 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.

HOOKS Ejecutar acciones antes y después de renovar

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.

ALERTA Notificación por email antes del vencimiento

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 -
WILDCARD Certificado para todos los subdominios

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.

CLOUDFLARE Wildcard automático con API de Cloudflare

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.

NGINX Usar el wildcard en Nginx
# 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;
    }
}
TLS Configuración SSL/TLS robusta para Nginx

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;
HSTS PRELOAD Registrar el dominio en HSTS Preload

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.

VERIFICAR Comandos de diagnóstico SSL
# 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
ERRORES Problemas comunes y soluciones
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;
TESTING Herramientas externas para auditar SSL

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.