SSD Nodes Learn Hosting plans →
Guías Matt ConnorPor Matt Connor

Instalar Certbot para nginx en Ubuntu 24.04

Instala Certbot con apt o snap en Ubuntu 24.04, configura nginx y evita errores de renovación como el timeout del puerto 80 con Let's Encrypt.

Instalar Certbot: apt o snap

En Ubuntu 24.04, sudo apt install certbot python3-certbot-nginx proporciona un Certbot funcional que emite certificados de Let's Encrypt reales y reconocidos públicamente. La documentación de upstream de Certbot recomienda usar snap; la diferencia es limitada: snap sigue las versiones de upstream, mientras que el paquete del archivo sigue la versión incluida con la LTS y recibe correcciones de seguridad.

Elija una opción. Tener dos copias de Certbot significa tener dos temporizadores de renovación dirigidos al mismo árbol de /etc/letsencrypt. La copia que olvidó es la que terminará causando problemas inesperados.

La opción con apt:

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

Esto instala /usr/bin/certbot, el complemento de nginx, un par certbot.service + certbot.timer y una entrada /etc/cron.d/certbot que no hace nada bajo systemd.

La opción con snap:

sudo apt remove certbot python3-certbot-nginx
sudo snap install core && sudo snap refresh core
sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/bin/certbot

snap incluye su propio temporizador, snap.certbot.renew.timer. Elimine el paquete de apt antes de instalar snap.

Después, ambas instalaciones funcionan de la misma forma, salvo por el momento exacto en que deciden renovar los certificados (se explica en la sección Renovación). Certbot 2.x usa de forma predeterminada claves ECDSA (P-256). Use --key-type rsa sólo para un cliente que no admita ECDSA. Todo el estado se almacena en /etc/letsencrypt: archive/ contiene los archivos reales de clave y certificado, live/ apunta mediante enlaces simbólicos a los archivos actuales, renewal/ contiene un archivo de configuración por certificado y accounts/ contiene la clave de su cuenta de ACME.

Qué hace realmente HTTP-01 y por qué el puerto 80 no es opcional

El desafío HTTP-01 es una devolución de llamada. Solicita a Let's Encrypt un certificado que cubra example.com; Let's Encrypt resuelve el nombre en el DNS público, abre una conexión al puerto 80 de la dirección que encuentra y solicita http://example.com/.well-known/acme-challenge/<token>. Su servidor responde con el contenido exacto del token que Certbot acaba de escribir en disco. Ese es todo el mecanismo. De ahí se derivan tres consecuencias que explican la mayoría de las emisiones fallidas.

  • El puerto 80 debe ser accesible desde Internet pública, no sólo desde su portátil. Una regla de ufw, un grupo de seguridad del proveedor cloud o un firewall de la consola de la VPS que abra sólo 443 impide la emisión y todas las renovaciones posteriores.
  • El DNS ya debe apuntar a este servidor. El servidor de validación realiza su propia resolución desde el exterior; sus entradas /etc/hosts y la caché del navegador no le sirven.
  • Si publica un registro AAAA, primero se intenta IPv6. Let's Encrypt reintenta mediante IPv4 sólo cuando la conexión IPv6 agota el tiempo de espera. Una conexión IPv6 rechazada no se reintenta, y un registro AAAA obsoleto que apunte a un host que acepta la conexión y sirve otra cosa provoca un fallo definitivo.

Se permiten las redirecciones: la validación sigue una redirección HTTP a HTTPS y no le importa que el certificado del otro extremo falte, haya caducado o sea autofirmado. Lo que no hará es iniciar la conexión en un puerto distinto del 80. Certbot no implementa TLS-ALPN-01, por lo que «usar sólo 443» no es una alternativa.

Elegir un autenticador: --nginx, --webroot, --standalone

--nginx es la opción predeterminada adecuada cuando nginx ya está en ejecución y ya sirve el dominio. Certbot analiza la configuración, inserta una ubicación temporal para el desafío, recarga nginx, valida el dominio y después escribe las directivas TLS en el bloque de servidor. No hay tiempo de inactividad.

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

Para usarlo mediante un script en un sistema recién instalado:

sudo certbot --nginx \
  -d example.com -d www.example.com \
  --agree-tos -m ops@example.com --no-eff-email \
  --redirect --non-interactive

--webroot es adecuada cuando no quiere que Certbot modifique la configuración de nginx. Puede generar esa configuración a partir de una plantilla, mantenerla en git o distribuirla con Ansible. Certbot sólo escribe el archivo del desafío en un directorio que ya está sirviendo.

sudo certbot certonly --webroot -w /var/www/example.com \
  -d example.com -d www.example.com \
  --deploy-hook "systemctl reload nginx"

--standalone es adecuada cuando ningún servicio escucha en el puerto 80: por ejemplo, un servidor de correo, una API que sólo usa 443 o un script de primera ejecución que se ejecuta antes de que exista nginx. Certbot enlaza el puerto 80 durante unos segundos. Si nginx está en ejecución, el proceso falla. Deténgalo durante la ejecución:

sudo certbot certonly --standalone -d mail.example.com \
  --pre-hook "systemctl stop nginx" \
  --post-hook "systemctl start nginx"

Esos hooks se registran en la configuración de renovación del certificado, por lo que la misma detención y el mismo arranque se realizan automáticamente durante la renovación.

Un bloque de servidor que funciona antes y después de que exista el certificado

El problema del huevo y la gallina es el siguiente: nginx no se inicia si ssl_certificate apunta a un archivo que no existe, y Certbot no puede validar el dominio mientras nginx está detenido. Ponga primero el sitio en funcionamiento en el puerto 80.

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

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

    location ^~ /.well-known/acme-challenge/ {
        root /var/www/example.com;
        default_type "text/plain";
        try_files $uri =404;
    }

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

Ejecute sudo nginx -t && sudo systemctl reload nginx, confirme desde fuera del servidor que curl -I http://example.com/ responde y, después, emita el certificado. A continuación:

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

    location ^~ /.well-known/acme-challenge/ {
        root /var/www/example.com;
        default_type "text/plain";
    }

    location / {
        return 301 https://$host$request_uri;
    }
}

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;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

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

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

El prefijo ACME más largo ya tiene prioridad sobre location /, por lo que return 301 nunca recibe una solicitud de desafío. El modificador ^~ resulta útil en otro caso: cuando ese prefijo coincide, nginx omite por completo las ubicaciones con expresiones regulares, de modo que una regla añadida posteriormente, como location ~ /\. { deny all; }, no puede bloquear el desafío. Mantener esa ubicación en el puerto 80 permite que las renovaciones sigan funcionando después de que el resto del sitio pase a usar exclusivamente HTTPS.

Los dos bloques anteriores sirven archivos desde el disco. Si nginx actúa en cambio como frontend de una aplicación, location / se convierte en un bloque proxy_pass y el bloque de servidor de proxy inverso, línea por línea cubre las cabeceras que necesita la aplicación, mientras que la ubicación ACME y las directivas TLS permanecen exactamente igual.

La sintaxis de HTTP/2 depende de la versión de nginx. Ubuntu 24.04 incluye nginx 1.24, que requiere la sintaxis en línea, listen 443 ssl http2;, y no se inicia si se proporciona la directiva independiente http2 on;. Debian 13 incluye nginx 1.26, que requiere http2 on; y sólo muestra una advertencia porque la forma en línea está obsoleta. Compruebe primero nginx -v.

Apunte nginx a live/, nunca a archive/. Los enlaces simbólicos live/ se vuelven a apuntar durante cada renovación; una ruta fija dentro de archive/ le deja usando un certificado que caduca sin que lo sustituya la renovación.

Los comodines requieren DNS-01, y DNS-01 requiere un plugin

Un certificado comodín (*.example.com) no se puede validar mediante HTTP-01 porque no existe un nombre de host único desde el que obtener un archivo. DNS-01 es la única opción: demuestra que controla el dominio publicando un registro TXT _acme-challenge.example.com. Certbot necesita credenciales de API del proveedor de DNS para hacerlo sin intervención, y para eso existen los plugins del proveedor. La guía completa para certificados comodín explica el funcionamiento de los registros TXT y el problema de renovación del modo manual; a continuación se muestra la versión breve para Cloudflare.

sudo snap set certbot trust-plugin-with-root=ok
sudo snap install certbot-dns-cloudflare

En la ruta de apt se usa sudo apt install python3-certbot-dns-cloudflare en su lugar. Las credenciales se guardan en un archivo accesible sólo por root:

# /root/.secrets/cloudflare.ini
# then: sudo chmod 600 /root/.secrets/cloudflare.ini
dns_cloudflare_api_token = your_scoped_token_here

Limite el token a los permisos de edición de DNS en esa única zona. Es una clave para su DNS; trátela como tal.

sudo certbot certonly \
  --dns-cloudflare \
  --dns-cloudflare-credentials /root/.secrets/cloudflare.ini \
  -d example.com -d '*.example.com'

Entrecomille el comodín para que el shell no lo expanda. DNS-01 también resuelve lo que HTTP-01 no puede: certificados para hosts sin el puerto público 80, un servicio interno, un equipo accesible sólo mediante una VPN WireGuard autohospedada en un VPS o un panel de administración en una interfaz privada.

Renovación: los 90 días, el temporizador y el deploy hook

Los certificados de Let's Encrypt son válidos durante 90 días. El paquete apt (Certbot 2.9.0) los renueva cuando quedan menos de 30 días. El snap sigue la versión actual de Certbot, que desde la versión 4.1 también solicita a Let's Encrypt un momento de renovación recomendado mediante ACME Renewal Info (ARI) y puede renovar antes; en un certificado de Let's Encrypt, esto suele ocurrir aproximadamente cuando han transcurrido dos tercios de su vigencia. En ambos casos se obtiene una ventana aproximada de 30 días en la que una renovación defectuosa es un problema corregible y no una interrupción del servicio. A partir del próximo año, las renovaciones serán más frecuentes: el perfil predeterminado de Let's Encrypt pasará a emitir certificados de 64 días el 10 de febrero de 2027 y certificados de 45 días el 16 de febrero de 2028. Let's Encrypt ya no envía correos de advertencia sobre vencimientos. Nadie le avisará directamente, así que ahora debe encargarse usted de la monitorización.

Compruebe el temporizador que instaló su sistema:

systemctl list-timers 'certbot*' 'snap.certbot*'
sudo certbot certificates

certbot renew recorre todas las configuraciones de /etc/letsencrypt/renewal/, omite las que todavía no deben renovarse y renueva las demás con las opciones guardadas en esa configuración. Son las mismas opciones de la ejecución original, salvo que las haya cambiado posteriormente con certbot reconfigure. Por eso es importante la primera ejecución: es la que queda registrada.

Renovar el archivo del disco no cambia nada por sí solo. nginx sigue sirviendo el certificado antiguo desde la memoria hasta que recibe la orden de recargarlo. Configure un deploy hook una sola vez:

sudo tee /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh >/dev/null <<'EOF'
#!/bin/sh
set -e
nginx -t && systemctl reload nginx
EOF
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh

Cualquier ejecutable de renewal-hooks/deploy/ se ejecuta después de cada renovación correcta. La opción --deploy-hook hace lo mismo para un certificado concreto y guarda renew_hook = ... en su configuración de renovación. certbot --nginx recarga el servicio automáticamente; las configuraciones --webroot y --standalone no lo hacen. La ausencia de un hook explica exactamente por qué un sitio sirve un certificado caducado mientras certbot certificates informa correctamente de que hay uno nuevo. Cualquier otro componente que lea el certificado al iniciarse necesita el mismo hook. Una aplicación en contenedores, como una instalación de Nextcloud VPS con Docker, TLS y copias de seguridad, también necesita que aquí se configure su propio paso de reinicio o recarga.

Pruebas de renovación reales

sudo certbot renew --dry-run

Esto ejecuta el desafío completo contra el entorno de staging de Let's Encrypt: la misma ruta de código, el mismo firewall y el mismo DNS. No consume los límites de tasa de producción, porque staging tiene sus propios límites, mucho más amplios. Tampoco guarda certificados en disco. Si hoy pasa la prueba, la renovación desatendida dentro de 60 días también pasará, siempre que no cambie nada en el servidor.

Una ejecución de prueba no demuestra que se ejecute el hook de recarga. Ese comportamiento varía según la versión de Certbot. Pruebe esa parte manualmente: ejecute directamente el script del hook, confirme que systemctl reload nginx se ejecuta correctamente y compruebe sudo grep renew_hook /etc/letsencrypt/renewal/example.com.conf.

Los errores que encontrará realmente

Could not bind to IPv4 or IPv6., --standalone mientras nginx ya ocupa el puerto 80. Use --nginx o --webroot, o detenga nginx durante la ejecución. Confirme qué proceso lo ocupa con sudo ss -lntp | grep ':80'.

Timeout during connect (likely firewall problem), Let's Encrypt no pudo acceder al puerto 80. Compruebe la conectividad desde fuera hacia dentro: sudo ufw status (ábralo con sudo ufw allow 'Nginx Full'), después el firewall del propio proveedor de VPS y, por último, DNS. Pruebe desde un sistema que no sea su servidor: curl -sSv http://example.com/.well-known/acme-challenge/test. Un registro AAAA obsoleto produce el mismo mensaje.

unauthorized :: Invalid response from http://example.com/.well-known/acme-challenge/xyz: 404, el puerto 80 es accesible, pero el token no se está sirviendo. La petición llegó a otro bloque de servidor (compruebe cuál gestiona default_server), o el directorio pasado a -w no es el que sirve nginx. Cree un archivo en /var/www/example.com/.well-known/acme-challenge/test y solicítelo desde el exterior; si devuelve 404, el certificado nunca fue el problema.

DNS problem: NXDOMAIN looking up A for example.com, el nombre no se resuelve públicamente. Puede deberse a registros nuevos que aún no se han propagado o a un registro de una zona que su registrador no está sirviendo.

too many certificates (5) already issued ..., se alcanzó un límite de frecuencia. Es el límite que más suele alcanzarse al depurar en un bucle. El mensaje indica el límite y termina con un intervalo de retry after. Let's Encrypt limita a cinco por semana los certificados duplicados con exactamente el mismo conjunto de nombres y no ofrece una excepción para ese límite, por lo que sólo el paso del tiempo lo desbloquea. Por separado, permite 50 certificados nuevos por dominio registrado y semana; ese límite se puede aumentar solicitando una excepción, una solicitud para exactamente el mismo conjunto de nombres que un certificado existente cuenta como renovación y está exenta de este límite, y las renovaciones coordinadas mediante ARI están exentas de todos los límites. Depure contra staging con --dry-run.

nginx: [emerg] cannot load certificate "/etc/letsencrypt/live/example.com/fullchain.pem": No such file or directory, nginx está configurado para un certificado que nunca se emitió o que se eliminó con certbot delete. Comente el bloque de servidor TLS, inicie nginx, emita el certificado y restaure el bloque.

open() "/etc/letsencrypt/options-ssl-nginx.conf" failed, ese archivo se incluye en el paquete del plugin de nginx. En un sistema certonly sin python3-certbot-nginx, añada el plugin o sustituya la línea include por sus propios ajustes de ssl_protocols y ssl_ciphers.

Administración a escala

Un certificado puede incluir hasta 100 nombres, y un único certbot --nginx -d a.example.com -d b.example.com ... resulta tentador, hasta que un registro DNS obsoleto falla la validación y hace que todos los demás nombres de ese certificado dejen de funcionar. Los certificados independientes por sitio fallan de forma aislada, que es lo que se necesita en un servidor que aloja más de un par de servicios. Cuando ya hay varios sitios, una puerta de entrada compatible con ACME resulta útil: un proxy inverso Traefik que ejecuta varias aplicaciones con Docker Compose solicita y renueva los certificados por sí mismo, y Certbot deja de ser necesario. Elegir qué proxy colocar en esa puerta de entrada es una decisión independiente, y comparar Nginx con Caddy y Traefik depende principalmente de cuánto trabajo de certificados y configuración por aplicación se quiera delegar al proxy.

Haga una copia de seguridad completa de /etc/letsencrypt, con sudo tar -czf letsencrypt-$(date +%F).tar.gz -C /etc letsencrypt y los enlaces simbólicos intactos. Ese árbol contiene accounts/, la clave de cuenta de ACME, que no se puede regenerar de forma idéntica. Migrar a un VPS nuevo se reduce entonces a: sincronizar el árbol con -a, instalar Certbot, actualizar el destino de DNS y ejecutar certbot renew --dry-run antes de cambiar el tráfico.

Si reconstruye el servidor o cambia a una nueva versión LTS, el temporizador de renovación no se migra automáticamente. Después de cualquier migración, restauración de una instantánea o actualización de la distribución, ejecute systemctl list-timers 'certbot*' y una --dry-run. Omitir este paso puede dejar un sitio fuera de servicio 89 días después, a las 3am, con un certificado que todos daban por renovado automáticamente.

Las rutas HTTP-01 anteriores suponen una máquina bajo su control, con una IP pública y el puerto 80 abierto a Internet; en otras palabras, un VPS. DNS-01 no necesita ninguna de esas condiciones. El procedimiento es el mismo en cualquiera de los dos casos.

Los mismos pasos para el certificado se aplican con Apache en lugar de nginx, y cuando no es posible usar un certificado público, un certificado autofirmado en Ubuntu sirve para los servicios internos.

FAQ

¿Necesito abrir el puerto 80 si mi sitio sólo ofrece HTTPS?

Sí, para el desafío HTTP-01. Let's Encrypt siempre inicia la solicitud de validación en el puerto 80 y Certbot no implementa TLS-ALPN-01. Por tanto, un firewall que sólo abre el puerto 443 bloquea tanto la primera emisión como todas las renovaciones desatendidas posteriores. Una redirección del puerto 80 a HTTPS es válida; la validación la sigue. La única forma de omitir por completo el puerto 80 es usar DNS-01, ya sea mediante un plugin del proveedor o mediante --manual --preferred-challenges dns de Certbot. En este último caso, debe añadir usted mismo el registro TXT y la renovación deja de ser automática, a menos que proporcione un script --manual-auth-hook.

¿apt o snap? ¿Cuál debo instalar para Certbot con nginx en Ubuntu 24.04?

Use apt. sudo apt install certbot python3-certbot-nginx le proporciona Certbot 2.9.0 en Ubuntu 24.04, una versión suficientemente actual para todo lo que se explica en esta guía. También recibe parches de seguridad mediante unattended-upgrades y no requiere snapd. Elija snap sólo si necesita de inmediato la versión más reciente o un plugin DNS distribuido exclusivamente como snap. En cualquier caso, elija exactamente una opción: dos instalaciones crean dos temporizadores de renovación apuntando al mismo árbol /etc/letsencrypt, y la instalación olvidada es la que termina causando problemas.

¿Puede Certbot emitir un certificado comodín para nginx?

Sólo mediante DNS-01. Un comodín como *.example.com no tiene un nombre de host único desde el que se pueda obtener un archivo de desafío. Por tanto, --nginx, --webroot y --standalone quedan descartados. Instale el plugin de su proveedor DNS, coloque un token de API con permisos limitados en un archivo de credenciales accesible sólo para root y ejecute certbot certonly --dns-cloudflare -d example.com -d '*.example.com'. Encierre el comodín entre comillas para evitar que el shell lo interprete mediante expansión de patrones.

¿Por qué nginx sigue sirviendo el certificado antiguo después de una renovación correcta?

nginx mantiene el certificado en memoria y no detecta el archivo nuevo en disco hasta que se recarga. certbot --nginx realiza la recarga automáticamente, pero las ejecuciones de --webroot y --standalone no lo hacen. Por tanto, una renovación puede completarse correctamente mientras el navegador sigue viendo un certificado próximo a caducar. Coloque un script ejecutable en /etc/letsencrypt/renewal-hooks/deploy/ que ejecute nginx -t && systemctl reload nginx. Se ejecutará después de cada renovación correcta.

¿certbot renew --dry-run demuestra que la renovación funcionará?

En gran medida. Ejecuta el desafío real contra el entorno de staging, con el mismo firewall, el mismo DNS y la misma ruta de código, sin afectar a los límites de solicitudes de producción ni guardar certificados en disco. Por tanto, una prueba correcta indica que la parte de red funciona. Sin embargo, no demuestra de forma fiable que se ejecute el hook de despliegue. Compruébelo por separado: ejecute manualmente el script del hook y revise sudo grep renew_hook /etc/letsencrypt/renewal/example.com.conf.