Un servidor HTTPS no solo debe enviar su propio certificado, también los certificados intermedios que lo enlazan con una autoridad raíz de confianza. Cuando falta un intermedio o el orden es incorrecto, el sitio puede funcionar en Chrome y fallar en curl, en aplicaciones Java o Python, en móviles Android y en integraciones de terceros. En esta guía aprenderás a diagnosticar la cadena que sirve tu servidor con OpenSSL, localizar el problema y corregir la configuración de Nginx o Apache en Ubuntu 24.04.
Requisitos previos
Para seguir esta guía necesitas:
- Un servidor con Ubuntu 24.04 LTS con Nginx o Apache sirviendo HTTPS, por ejemplo un VPS de CubePath, y un usuario con privilegios
sudo. - Los archivos del certificado en formato PEM: el certificado del dominio, los intermedios que te entregó la autoridad certificadora (CA) y la clave privada.
- OpenSSL y curl, que vienen instalados por defecto en Ubuntu.
Sustituye your_domain por tu dominio en todos los comandos.
Cómo funciona una cadena de certificados
Una cadena válida tiene tres niveles:
| Posición | Certificado | ¿Lo envía el servidor? |
|---|---|---|
| 0 | Certificado del dominio (hoja), emitido para your_domain | Sí, siempre el primero |
| 1..n | Uno o varios intermedios de la CA | Sí, a continuación y en orden |
| Raíz | Autofirmado, ya incluido en el almacén del sistema o navegador | No es necesario |
Cada certificado debe estar firmado por el siguiente: el issuer (emisor) del certificado 0 es el subject (sujeto) del certificado 1, y así hasta llegar a una raíz que el cliente ya conoce.
Los navegadores de escritorio suelen ocultar una cadena incompleta porque descargan el intermedio que falta a partir de la URL que trae el propio certificado o lo tienen en caché. La mayoría de clientes de línea de comandos y librerías no lo hacen, y por eso el error aparece "solo en algunos clientes".
Paso 1: Reproducir el error
Empieza comprobando qué ve un cliente que no completa cadenas por su cuenta. curl es suficiente:
curl -sSI https://your_domain
Con una cadena incompleta, el resultado es:
curl: (60) SSL certificate problem: unable to get local issuer certificate
OpenSSL da más detalle. La opción -servername envía el nombre por SNI, imprescindible si el servidor aloja varios dominios:
openssl s_client -connect your_domain:443 -servername your_domain </dev/null 2>/dev/null | grep 'Verify return code'
Estos son los códigos más habituales:
| Código | Significado habitual |
|---|---|
0 (ok) | La cadena es válida |
20 (unable to get local issuer certificate) | Falta un intermedio, o la raíz no está en el almacén del cliente |
21 (unable to verify the first certificate) | El servidor solo envía el certificado hoja, sin intermedios |
10 (certificate has expired) | Algún certificado de la cadena ha caducado |
18 (self-signed certificate) / 19 (self-signed certificate in certificate chain) | Certificado autofirmado o raíz desconocida para el cliente |
Paso 2: Ver la cadena que envía el servidor
Muestra todos los certificados que entrega el servidor con -showcerts:
openssl s_client -connect your_domain:443 -servername your_domain -showcerts </dev/null 2>/dev/null | sed -n '/^Certificate chain/,/^---/p' | grep -E '^ *[0-9]+ s:|^ *i:'
Una cadena correcta de Let's Encrypt tiene este aspecto: el emisor de la posición 0 coincide con el sujeto de la posición 1.
0 s:CN = your_domain
i:C = US, O = Let's Encrypt, CN = R11
1 s:C = US, O = Let's Encrypt, CN = R11
i:C = US, O = Internet Security Research Group, CN = ISRG Root X1
Fíjate en estos síntomas:
- Solo aparece la posición 0: el servidor no envía intermedios. Es el caso más frecuente.
- El emisor de 0 no coincide con el sujeto de 1: intermedio equivocado (de otra CA o de otra generación) o certificados en orden incorrecto.
- La posición 0 no es tu dominio: el orden del archivo está invertido, o el servidor responde con otro
serveroVirtualHostpor defecto.
Paso 3: Revisar los archivos del servidor
Localiza qué archivos usa tu servidor web:
sudo grep -rn 'ssl_certificate' /etc/nginx/sites-enabled/
sudo grep -rn 'SSLCertificate' /etc/apache2/sites-enabled/
Cuenta cuántos certificados contiene el archivo configurado. Para un certificado de CA pública debería haber al menos dos:
sudo grep -c 'BEGIN CERTIFICATE' /etc/ssl/your_domain/fullchain.pem
Lista el sujeto y el emisor de cada certificado del archivo, en el orden en que aparecen:
sudo openssl crl2pkcs7 -nocrl -certfile /etc/ssl/your_domain/fullchain.pem | openssl pkcs7 -print_certs -noout
subject=CN = your_domain
issuer=C = US, O = Let's Encrypt, CN = R11
subject=C = US, O = Let's Encrypt, CN = R11
issuer=C = US, O = Internet Security Research Group, CN = ISRG Root X1
Comprueba también que la clave privada corresponde al certificado. Los dos hashes deben ser idénticos:
sudo openssl x509 -noout -pubkey -in /etc/ssl/your_domain/fullchain.pem | sha256sum
sudo openssl pkey -pubout -in /etc/ssl/your_domain/privkey.pem | sha256sum
Si no coinciden, Nginx se niega a arrancar con key values mismatch y Apache con AH02565: Certificate and private key ... do not match. En ese caso, busca la clave con la que generaste la petición de firma (CSR).
Paso 4: Recuperar el intermedio que falta
Lo más fiable es descargar el paquete de intermedios desde el panel de tu CA. Si no lo tienes, el propio certificado indica dónde obtener su emisor en la extensión Authority Information Access:
openssl x509 -in /etc/ssl/your_domain/cert.pem -noout -ext authorityInfoAccess
Authority Information Access:
CA Issuers - URI:http://r11.i.lencr.org/
Descarga el intermedio y conviértelo a PEM. Normalmente se publica en formato DER:
curl -fsSL -o /tmp/intermediate.der http://r11.i.lencr.org/
openssl x509 -inform DER -in /tmp/intermediate.der -out /tmp/intermediate.pem
openssl x509 -in /tmp/intermediate.pem -noout -subject
Algunas CA publican el intermedio como PKCS#7 (extensión .p7c). En ese caso, extráelo con openssl pkcs7 -inform DER -in archivo.p7c -print_certs -out /tmp/intermediate.pem.
Verifica sin conexión que el certificado del dominio se valida con ese intermedio y el almacén de raíces del sistema:
openssl verify -untrusted /tmp/intermediate.pem /etc/ssl/your_domain/cert.pem
/etc/ssl/your_domain/cert.pem: OK
Paso 5: Construir el archivo fullchain
Concatena primero el certificado del dominio y después los intermedios, de la hoja hacia la raíz. No incluyas la raíz: no aporta nada, aumenta el tamaño del handshake y algunos analizadores lo marcan como aviso.
sudo sh -c 'cat /etc/ssl/your_domain/cert.pem /tmp/intermediate.pem > /etc/ssl/your_domain/fullchain.pem'
Si tu CA entrega un archivo ca-bundle con varios intermedios, revisa su orden con el comando crl2pkcs7 del paso 3 antes de concatenarlo: debe ir del intermedio más cercano a tu certificado hacia la raíz.
Notasi usas Certbot no construyas la cadena a mano. Apunta siempre a
/etc/letsencrypt/live/your_domain/fullchain.pem, nunca acert.pem, y Certbot mantendrá la cadena correcta en cada renovación.
Paso 6: Configurar el servidor web
Nginx
Nginx espera la cadena completa en ssl_certificate. No existe una directiva separada para los intermedios:
server {
listen 443 ssl;
server_name your_domain;
ssl_certificate /etc/ssl/your_domain/fullchain.pem;
ssl_certificate_key /etc/ssl/your_domain/privkey.pem;
}
Comprueba y recarga:
sudo nginx -t && sudo systemctl reload nginx
Apache
Desde Apache 2.4.8, SSLCertificateFile admite la cadena completa y SSLCertificateChainFile está obsoleta. Ubuntu 24.04 incluye Apache 2.4.58:
<VirtualHost *:443>
ServerName your_domain
SSLEngine on
SSLCertificateFile /etc/ssl/your_domain/fullchain.pem
SSLCertificateKeyFile /etc/ssl/your_domain/privkey.pem
</VirtualHost>
Comprueba y recarga:
sudo apachectl configtest && sudo systemctl reload apache2
Paso 7: Verificar la corrección
Repite las comprobaciones del paso 1. Ambas deben pasar:
openssl s_client -connect your_domain:443 -servername your_domain </dev/null 2>/dev/null | grep 'Verify return code'
curl -sSI https://your_domain | head -n 1
Verify return code: 0 (ok)
HTTP/2 200
Para una comprobación externa, analiza el dominio en SSL Labs (https://www.ssllabs.com/ssltest/): en la sección Certification Paths no debe aparecer Extra download ni Incomplete.
Solución de problemas
El servidor sigue enviando la cadena antigua. Nginx y Apache solo leen los certificados al arrancar o recargar. Confirma que has recargado el servicio correcto y que no hay otro bloque server o VirtualHost para el mismo nombre con rutas distintas (sudo nginx -T | grep -n ssl_certificate).
Error 20 solo en un servidor cliente concreto. Si la cadena es correcta desde otros equipos, el cliente tiene un almacén de raíces desactualizado. En Debian y Ubuntu, actualízalo con sudo apt update && sudo apt install --only-upgrade ca-certificates. Las aplicaciones Java usan su propio almacén (cacerts), que se actualiza con el JDK.
Error 10 (certificado caducado) con un certificado del dominio vigente. Algún intermedio del archivo ha caducado, normalmente porque se reutilizó un paquete antiguo tras renovar. Revisa las fechas de cada uno con openssl x509 -noout -subject -enddate y sustitúyelo por el intermedio actual de la CA.
El certificado hoja no aparece en la posición 0. El archivo está en orden inverso. Nginx en ese caso suele fallar con key values mismatch, porque compara la clave con el primer certificado del archivo. Reordénalo como en el paso 5.
Conclusión
Has aprendido a leer la cadena que entrega un servidor con openssl s_client, a identificar intermedios ausentes o desordenados, a recuperar el intermedio desde la extensión AIA y a configurar Nginx y Apache con un fullchain correcto. Como siguientes pasos, automatiza la emisión y renovación con Certbot para no volver a gestionar cadenas a mano, y añade una comprobación periódica de Verify return code a tu sistema de monitorización.