Configurar SSL/TLS de Let's Encrypt para Nginx en Debian 12 y Ubuntu 24.04

12 min de lectura·Matthieu·nginxssltlslets-encryptcertbothttpssecurity|

Obtener y renovar automáticamente certificados TLS gratuitos con Certbot para Nginx en Debian 12 o Ubuntu 24.04. Cubre configuración DNS, instalación de Certbot, redirección HTTP a HTTPS, endurecimiento TLS, HTTP/2, HSTS y la discontinuación de OCSP.

Este tutorial te guía a través de la obtención de un certificado TLS gratuito de Let's Encrypt usando Certbot, la configuración de Nginx para HTTPS y la activación de la renovación automática.

Si todavía no has instalado Nginx, comienza con Instalar Nginx en Debian 12 y Ubuntu 24.04 desde el repositorio oficial. Para una visión general de la administración de Nginx en un VPS, consulta Administración de Nginx en un VPS.

¿Qué necesitas antes de solicitar un certificado?

Antes de que Certbot pueda emitir un certificado, tu dominio debe apuntar a la dirección IP de tu servidor y Nginx debe estar ejecutándose con un server block para ese dominio. Let's Encrypt valida la propiedad del dominio enviando una solicitud HTTP a tu servidor. Si el DNS no resuelve a tu VPS o Nginx no está escuchando, la verificación falla.

Necesitas:

Configurar los registros DNS

Crea un registro A en tu proveedor de DNS:

Tipo Nombre Valor TTL
A example.com 203.0.113.10 300
AAAA example.com 2001:db8::1 300

Reemplaza las direcciones IP con las direcciones reales de tu servidor. Configura el TTL bajo (300 segundos) durante la configuración para que los cambios se propaguen rápido. Puedes aumentarlo después.

Verificar la resolución DNS

Espera unos minutos después de crear los registros, luego verifica desde tu máquina local (no desde el servidor):

dig +short example.com A
dig +short example.com AAAA

Deberías ver las direcciones IP de tu servidor en la salida. Si no ves nada o ves una IP diferente, el registro aún no se ha propagado. Espera e inténtalo de nuevo.

Verifica que Nginx responde en el puerto 80 desde tu máquina local:

curl -I http://example.com

Deberías obtener una respuesta HTTP/1.1 200 OK con Server: nginx. Si la conexión expira, revisa las reglas de tu firewall.

¿Cómo instalar Certbot en Debian 12 y Ubuntu 24.04?

Instala Certbot y su plugin para Nginx desde el repositorio de paquetes de tu distribución usando apt. El plugin de Nginx permite que Certbot modifique automáticamente tus server blocks para habilitar TLS.

sudo apt update
sudo apt install certbot python3-certbot-nginx -y

Verifica la instalación:

certbot --version

En Debian 12, esto instala Certbot 2.1.0. En Ubuntu 24.04, obtienes Certbot 2.9.0. Ambas versiones funcionan para todo lo que cubre este tutorial.

Dato importante: si instalaste Nginx desde el repositorio oficial de nginx.org (como se recomienda en Instalar Nginx en Debian 12 y Ubuntu 24.04 desde el repositorio oficial), el plugin de Certbot para Nginx funciona sin configuración adicional. Detecta los server blocks en /etc/nginx/conf.d/ y /etc/nginx/sites-enabled/.

¿Cómo obtener un certificado de Let's Encrypt para Nginx?

Ejecuta certbot --nginx con tu nombre de dominio. Certbot contacta a Let's Encrypt, demuestra que controlas el dominio mediante un desafío HTTP-01, obtiene el certificado y edita tu server block de Nginx para usarlo. Todo el proceso toma unos 30 segundos.

sudo certbot --nginx -d example.com -d www.example.com

Certbot te pedirá tu dirección de correo (para recordatorios de renovación) y la aceptación de los Términos de Servicio. Después:

  1. Coloca un archivo de desafío HTTP-01 en tu web root
  2. Pide a Let's Encrypt que lo verifique
  3. Descarga el certificado firmado
  4. Modifica tu server block de Nginx para agregar las directivas TLS
  5. Recarga Nginx

Verifica que el certificado fue emitido:

sudo ls -la /etc/letsencrypt/live/example.com/

Deberías ver:

lrwxrwxrwx 1 root root  ... cert.pem -> ../../archive/example.com/cert1.pem
lrwxrwxrwx 1 root root  ... chain.pem -> ../../archive/example.com/chain1.pem
lrwxrwxrwx 1 root root  ... fullchain.pem -> ../../archive/example.com/fullchain1.pem
lrwxrwxrwx 1 root root  ... privkey.pem -> ../../archive/example.com/privkey1.pem

Estos son enlaces simbólicos. fullchain.pem es tu certificado más la cadena de CA intermedia. privkey.pem es tu clave privada.

Comprueba que Nginx está ejecutándose con la nueva configuración:

sudo nginx -t && sudo systemctl status nginx

nginx -t prueba la sintaxis de la configuración. Si muestra test is successful, la configuración es válida.

¿Qué cambia Certbot en tu configuración de Nginx?

Certbot agrega varias líneas a tu server block. Esto es lo que inserta (las líneas marcadas con # managed by Certbot):

server {
    server_name example.com www.example.com;

    listen 443 ssl;
    listen [::]:443 ssl;

    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; # managed by Certbot
    include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot

    # ... tus location blocks existentes ...
}

El archivo options-ssl-nginx.conf contiene la configuración TLS predeterminada de Certbot. La reemplazaremos con configuraciones más estrictas en la sección de endurecimiento a continuación.

Certbot también crea un segundo server block para redirigir HTTP a HTTPS. Mejoraremos esa redirección en la siguiente sección.

Puedes ver exactamente qué cambió comparando tu configuración:

sudo diff /etc/nginx/conf.d/example.com.conf /etc/nginx/conf.d/example.com.conf.bak 2>/dev/null || echo "No backup found. Certbot modifies in place."

¿Cómo redirigir HTTP a HTTPS en Nginx?

Todo el tráfico HTTP debe redirigirse a HTTPS con una redirección 301 (permanente). Certbot puede agregar esto automáticamente, pero su valor predeterminado usa una sentencia if dentro del server block existente. Eso es un anti-patrón en Nginx. Un server block dedicado es más limpio y confiable.

Reemplaza la redirección de Certbot con un server block separado. Edita tu archivo de configuración (la ruta depende de tu configuración; normalmente /etc/nginx/conf.d/example.com.conf):

# HTTP -> HTTPS redirect (separate server block, not an if-statement)
server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;
    return 301 https://$host$request_uri;
}

Esto va en el mismo archivo que tu server block HTTPS, o en un archivo separado. Asegúrate de eliminar cualquier bloque de redirección generado por Certbot para evitar duplicados.

Prueba y recarga:

sudo nginx -t
sudo systemctl reload nginx

Verifica la redirección desde tu máquina local:

curl -I http://example.com

Salida esperada:

HTTP/1.1 301 Moved Permanently
Location: https://example.com/

¿Cómo endurecer la configuración TLS para un servidor de producción?

La configuración TLS predeterminada de Certbot (options-ssl-nginx.conf) es intencionalmente conservadora. Para un servidor de producción, necesitas configuraciones más estrictas. Seguiremos el perfil Intermediate de Mozilla del SSL Configuration Generator, que equilibra seguridad con compatibilidad de clientes hasta Firefox 27, Chrome 31 y Android 4.4.2.

Crea un archivo de snippet (fragmento de configuración) que puedas incluir con include desde cada server block:

sudo nano /etc/nginx/snippets/tls-params.conf

Agrega lo siguiente:

# TLS protocol versions, TLS 1.2 and 1.3 only
# TLS 1.0 and 1.1 are deprecated (RFC 8996)
ssl_protocols TLSv1.2 TLSv1.3;

# Ciphers, Mozilla Intermediate profile (January 2026)
# Source: https://ssl-config.mozilla.org/
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:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305;
ssl_prefer_server_ciphers off;

# DH parameters, 2048-bit, RFC 7919 ffdhe2048
ssl_dhparam /etc/nginx/dhparam.pem;

# Session settings
ssl_session_timeout 1d;
ssl_session_cache shared:TLS:10m;
ssl_session_tickets off;

# HSTS, tell browsers to always use HTTPS (2 years)
# Only add includeSubDomains if ALL subdomains use HTTPS
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;

# Hide Nginx version in error pages and headers
server_tokens off;

Genera el archivo de parámetros DH (esto toma unos segundos):

sudo openssl dhparam -out /etc/nginx/dhparam.pem 2048

Verifica que el archivo fue creado:

sudo ls -la /etc/nginx/dhparam.pem

Ahora actualiza tu server block HTTPS para usar estas configuraciones en lugar de los valores predeterminados de Certbot. Elimina la línea include /etc/letsencrypt/options-ssl-nginx.conf; y la línea ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;. Reemplázalas con:

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name example.com www.example.com;

    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    # TLS hardening (replaces Certbot defaults)
    include snippets/tls-params.conf;

    # ... your location blocks ...
}

Prueba y recarga:

sudo nginx -t
sudo systemctl reload nginx

¿Qué versiones de TLS y cifrados deberías usar?

Mozilla publica tres perfiles TLS. Esta es la comparación:

Perfil Protocolos Cliente compatible más antiguo Caso de uso
Modern Solo TLS 1.3 Firefox 63, Chrome 70, Android 10 Servicios donde todos los clientes son recientes
Intermediate TLS 1.2 + 1.3 Firefox 27, Chrome 31, Android 4.4 Servidores web de propósito general
Old TLS 1.0 + 1.1 + 1.2 + 1.3 Firefox 1, Chrome 1, IE 8 Solo sistemas heredados

Usa Intermediate a menos que tengas una razón específica para no hacerlo. Cubre más del 99.9% de los navegadores actuales mientras excluye protocolos débiles. TLS 1.0 y 1.1 fueron formalmente deprecados por RFC 8996 en marzo de 2021.

La lista de cifrados en nuestro snippet usa solo cifrados AEAD (GCM y ChaCha20-Poly1305). ssl_prefer_server_ciphers off permite que el cliente elija su cifrado preferido. Esta es la recomendación de Mozilla porque los clientes modernos toman mejores decisiones que una preferencia estática del servidor.

¿Let's Encrypt todavía soporta OCSP stapling?

No. Let's Encrypt cerró su servicio OCSP el 6 de agosto de 2025. Los certificados emitidos después de mayo de 2025 no contienen URL de OCSP responder. El estado de revocación ahora se publica exclusivamente a través de Certificate Revocation Lists (CRL, listas de revocación de certificados). Si solo usas certificados de Let's Encrypt, elimina cualquier directiva ssl_stapling de tu configuración de Nginx.

Esta es la línea de tiempo:

  1. Diciembre 2024. Let's Encrypt anunció el plan de finalizar OCSP.
  2. 30 de enero de 2025. Los certificados que solicitaban la extensión OCSP Must-Staple comenzaron a fallar.
  3. 7 de mayo de 2025. Los nuevos certificados dejaron de incluir URLs de OCSP responder. Se agregaron URLs de CRL en su lugar.
  4. 6 de agosto de 2025. El servicio OCSP se cerró por completo. Todos los certificados emitidos anteriormente con URLs de OCSP ya han expirado.

Si ves ssl_stapling on; o ssl_stapling_verify on; en alguna guía o fragmento de configuración, es un consejo desactualizado para usuarios de Let's Encrypt. Estas directivas son inofensivas (Nginx las ignora silenciosamente cuando no hay URL de OCSP responder), pero agregan desorden innecesario. Elimínalas.

Si usas certificados de otra CA que todavía proporciona OCSP, esas directivas siguen siendo válidas para esos certificados.

¿Por qué Let's Encrypt dejó de usar OCSP? Dos razones. OCSP es un riesgo de privacidad: cada vez que un navegador verificaba la revocación del certificado vía OCSP, la CA sabía qué sitio se visitaba y desde qué IP. En su punto máximo, el servicio OCSP de Let's Encrypt manejaba 340 mil millones de solicitudes por mes. Cambiar a CRL elimina esa filtración de privacidad. Los CRL son descargados en bloque por los navegadores, por lo que no se envía ninguna solicitud por visita a la CA.

¿Cómo habilitar HTTP/2 con Nginx?

HTTP/2 multiplexa solicitudes sobre una sola conexión, reduciendo la latencia. Si instalaste Nginx desde el repositorio oficial de nginx.org (versión 1.25.1 o posterior), habilita HTTP/2 con la directiva http2 dentro de tu server block.

Agrega http2 on; dentro de tu server block HTTPS:

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    http2 on;

    server_name example.com www.example.com;

    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    include snippets/tls-params.conf;

    # ... your location blocks ...
}

La directiva http2 on; reemplazó la sintaxis deprecada listen 443 ssl http2; en Nginx 1.25.1. La sintaxis antigua todavía funciona pero produce advertencias de deprecación en el log de errores. Si estás ejecutando el Nginx empaquetado por la distribución (1.22 en Debian 12, 1.24 en Ubuntu 24.04), usa la sintaxis antigua listen 443 ssl http2; en su lugar.

Prueba y recarga:

sudo nginx -t
sudo systemctl reload nginx

Verifica que HTTP/2 está activo:

curl -I --http2 -s https://example.com | head -1

Salida esperada:

HTTP/2 200

Si ves HTTP/1.1 en su lugar, verifica que http2 on; está dentro del server block correcto y que tu versión de Nginx lo soporta.

¿Cómo funciona la renovación automática de certificados?

Los certificados de Let's Encrypt expiran después de 90 días. Certbot instala un temporizador de systemd (certbot.timer) que verifica dos veces al día si algún certificado está a menos de 30 días de expirar. Si es así, lo renueva automáticamente. No necesitas configurar un cron job.

Verifica que el temporizador está activo:

systemctl status certbot.timer

Deberías ver Active: active (waiting) y una línea mostrando la próxima hora de activación.

Consulta cuándo ocurrirá la próxima renovación:

systemctl list-timers certbot.timer

Esto muestra los tiempos NEXT y LAST de ejecución.

Probar la renovación sin renovar realmente

Ejecuta un dry-run (simulación) para verificar que el proceso de renovación funciona de principio a fin:

sudo certbot renew --dry-run

Esto contacta al servidor de staging de Let's Encrypt y simula una renovación completa. Si muestra Congratulations, all simulated renewals succeeded, tu configuración es correcta.

Configurar un deploy hook para recargar Nginx

Cuando Certbot renueva un certificado, Nginx necesita recargarse para usar los nuevos archivos. Configura un deploy hook que se ejecute solo tras una renovación exitosa:

sudo mkdir -p /etc/letsencrypt/renewal-hooks/deploy
sudo nano /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh

Agrega:

#!/bin/bash
/usr/bin/systemctl reload nginx

Hazlo ejecutable:

sudo chmod 700 /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh

Verifica los permisos:

ls -la /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh

La salida esperada muestra -rwx------ (solo root puede leer y ejecutar).

El directorio deploy ejecuta scripts solo después de una renovación exitosa, no en cada activación del temporizador. Esto evita recargas innecesarias.

¿Cómo verificar que tu configuración TLS es correcta?

Después de completar todos los pasos anteriores, ejecuta estos comandos de verificación. Cada uno comprueba un aspecto diferente de tu configuración.

Comprobar la cadena de certificados con OpenSSL

Desde tu máquina local:

openssl s_client -connect example.com:443 -servername example.com < /dev/null 2>/dev/null | openssl x509 -noout -dates -subject -issuer

Salida esperada:

notBefore=Mar 19 00:00:00 2026 GMT
notAfter=Jun 17 00:00:00 2026 GMT
subject=CN = example.com
issuer=C = US, O = Let's Encrypt, CN = R12

Dato importante: la fecha notAfter debería ser aproximadamente 90 días después de la emisión. El emisor debería ser Let's Encrypt. Los intermediarios RSA actuales son R12 y R13. Para certificados ECDSA, busca E7 o E8.

Comprobar la versión TLS y el cifrado en uso

openssl s_client -connect example.com:443 -servername example.com < /dev/null 2>/dev/null | grep -E "Protocol|Cipher"

Esperado:

    Protocol  : TLSv1.3
    Cipher    : TLS_AES_256_GCM_SHA384

Si ves TLSv1.2, también está bien. Depende de qué versión prefiera tu OpenSSL local.

Comprobar las cabeceras HTTPS

curl -I https://example.com

Busca:

HTTP/2 200
strict-transport-security: max-age=63072000; includeSubDomains

Si falta strict-transport-security, verifica la línea add_header en tu snippet TLS. El parámetro always asegura que la cabecera se envíe incluso en respuestas de error.

Resumen de comandos de verificación

Comando Qué comprueba Qué buscar
dig +short example.com Resolución DNS La IP de tu servidor
curl -I http://example.com Redirección HTTP 301 con Location: https://
curl -I https://example.com HTTPS + cabeceras HTTP/2 200, cabecera HSTS
openssl s_client -connect ... Cadena de certificados, versión TLS Emisor Let's Encrypt, TLSv1.2 o 1.3
certbot renew --dry-run Proceso de renovación all simulated renewals succeeded
systemctl status certbot.timer Temporizador de renovación automática active (waiting)

Probar con SSL Labs

Para una auditoría externa detallada, envía tu dominio a SSL Labs Server Test. Con la configuración de esta guía, deberías obtener una puntuación A o A+. La calificación A+ requiere HSTS, que habilitamos en el snippet TLS.

Referencia completa de configuración de Nginx

Aquí está el server block completo con todas las configuraciones de este tutorial combinadas:

# HTTP -> HTTPS redirect
server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;
    return 301 https://$host$request_uri;
}

# HTTPS server block
server {
    listen 443 ssl;
    listen [::]:443 ssl;
    http2 on;

    server_name example.com www.example.com;

    # Let's Encrypt certificate
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    # TLS hardening
    include snippets/tls-params.conf;

    root /var/www/example.com/html;
    index index.html;

    location / {
        try_files $uri $uri/ =404;
    }

    # Deny access to hidden files
    location ~ /\. {
        deny all;
    }
}

Y el snippet TLS en /etc/nginx/snippets/tls-params.conf:

ssl_protocols TLSv1.2 TLSv1.3;
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:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305;
ssl_prefer_server_ciphers off;
ssl_dhparam /etc/nginx/dhparam.pem;

ssl_session_timeout 1d;
ssl_session_cache shared:TLS:10m;
ssl_session_tickets off;

add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
server_tokens off;

Para cabeceras de seguridad adicionales como Content-Security-Policy, X-Frame-Options y Permissions-Policy, consulta Hardening de seguridad de Nginx en Ubuntu y Debian.

¿Algo salió mal?

Certbot dice "Could not automatically find a matching server block" Certbot busca una directiva server_name que coincida con tu dominio -d. Asegúrate de que tu archivo de server block esté en /etc/nginx/conf.d/ o /etc/nginx/sites-enabled/ y contenga server_name example.com;.

Certbot dice "Connection refused" o "Challenge failed" El puerto 80 debe estar abierto. Revisa tu firewall:

sudo ufw status          # if using UFW
sudo nft list ruleset    # if using nftables

También verifica que Nginx está escuchando en el puerto 80:

sudo ss -tlnp | grep ':80'

"SSL: error" en el log de errores de Nginx después de editar la configuración TLS Probablemente tienes un error de sintaxis en la cadena de cifrados o una ruta de archivo faltante. Comprueba:

sudo nginx -t
sudo journalctl -u nginx -n 20 --no-pager

El navegador muestra "NET::ERR_CERT_DATE_INVALID" El certificado puede haber expirado. Comprueba la fecha de expiración:

sudo certbot certificates

Si ha expirado, fuerza una renovación:

sudo certbot renew --force-renewal

certbot renew --dry-run falla Causa habitual: el DNS del dominio ya no apunta a este servidor, o el puerto 80 está bloqueado. Certbot necesita acceso HTTP-01 para la renovación.

HTTP/2 no funciona Comprueba tu versión de Nginx: nginx -v. Si es anterior a 1.25.1, usa listen 443 ssl http2; en lugar de la directiva separada http2 on;. Si es 1.25.1 o posterior, asegúrate de que http2 on; esté dentro del bloque server correcto (no en http ni en location).