Certificado autofirmado en Ubuntu 24.04 con SAN
Crea un certificado TLS autofirmado que Chrome acepta en Ubuntu 24.04: comando openssl con SAN, configuración para nginx o Apache y confianza sin usar curl -k.
Qué va a crear
Un certificado TLS autofirmado que los navegadores y clientes modernos aceptan realmente, con subjectAltName correcto, permisos seguros para la clave, integración con nginx o Apache y el aspecto que casi todas las guías omiten: hacer que sus clientes confíen en él correctamente, en lugar de ignorar las advertencias y fijar curl -k en los scripts para siempre. Al final, una CA privada de cinco comandos para cuando un servicio interno se convierta en seis.
Primero, la decisión, porque un certificado autofirmado es la herramienta adecuada con mucha menos frecuencia de la que se utiliza. Si el servicio es accesible desde Internet público con un nombre DNS real, deje de leer y obtenga un certificado gratuito de Let's Encrypt con certbot en nginx o el equivalente para Apache. No cuesta nada, se renueva automáticamente y todos los navegadores del mundo ya confían en él. Un certificado autofirmado en un sitio público enseña a los usuarios a ignorar las advertencias de seguridad, un hábito peor que usar HTTP sin cifrado.
Un certificado autofirmado es adecuado cuando no interviene Internet público: un panel de administración vinculado a una dirección de túnel WireGuard en su VPS, un servidor de staging en una red privada, el tráfico entre servicios de varios backends, un dispositivo de laboratorio doméstico o el reemplazo del certificado provisional que Webmin genera para sí mismo en el puerto 10000. Let's Encrypt tampoco puede emitir certificados para 10.8.0.1 o git.internal.lan; ninguna CA pública incluirá una IP privada o un TLD inventado en un certificado. Para esos nombres, usted es la CA.
Todo lo siguiente se ejecuta en un sistema Ubuntu 24.04 recién instalado, que incluye OpenSSL 3.0.x (openssl version para confirmarlo). Nada de esto necesita acceso a Internet; todo funciona en un entorno aislado.
Por qué el comando antiguo de una sola línea genera certificados que Chrome rechaza
El comando que aparece en todos los tutoriales anteriores a 2017 es este:
# Do not run this — shown so you recognise it in old guides
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout selfsigned.key -out selfsigned.crtHace una serie de preguntas interactivas, coloca el nombre de host en el campo Common Name y genera un certificado sin la extensión subjectAltName. Ese certificado no puede utilizarse. Chrome dejó de leer Common Name en la versión 58, en abril de 2017. RFC 2818 ya había dejado obsoleta la coincidencia mediante CN en el año 2000. Firefox, Safari, curl y Python se comportan de la misma forma. Un certificado identifica su servidor mediante la extensión SAN o no lo identifica en absoluto. El navegador lo indica exactamente con estas palabras:
NET::ERR_CERT_COMMON_NAME_INVALID
This server could not prove that it is git.internal.lan; its security
certificate does not specify Subject Alternative Names.Ningún ajuste del almacén de confianza corrige ese error, porque el certificado realmente no identifica ningún nombre. Si ahora ve NET::ERR_CERT_COMMON_NAME_INVALID, su certificado no tiene SAN o contiene un SAN incorrecto, y debe generar uno nuevo. Por suerte, la corrección consiste en un solo comando.
Crear un certificado que los navegadores acepten: un solo comando
OpenSSL incorporó la opción -addext en 1.1.1. Por tanto, ya no necesita las configuraciones complejas que usaban las guías antiguas para inyectar un SAN. En Ubuntu 24.04:
sudo openssl req -x509 -newkey rsa:4096 -sha256 -days 730 -noenc \
-keyout /etc/ssl/private/git.internal.key \
-out /etc/ssl/certs/git.internal.crt \
-subj "/CN=git.internal.lan" \
-addext "subjectAltName=DNS:git.internal.lan,IP:10.8.0.1"Qué hace cada opción:
-x509genera directamente un certificado autofirmado en lugar de una solicitud de firma.-newkey rsa:4096genera una clave nueva en el mismo paso. RSA 4096 es compatible con los clientes antiguos. Si todos los clientes son modernos,-newkey ec -pkeyopt ec_paramgen_curve:P-256ocupa menos espacio y es más rápido.-noences la sintaxis de OpenSSL 3.x para la antigua-nodes: no establece una frase de contraseña para la clave. Ambas sintaxis funcionan. Una clave protegida con frase de contraseña hace que nginx se bloquee esperando una entrada en cada arranque. Para una clave de servidor, necesita esta opción.-days 730establece una validez de dos años. La sección sobre la expiración explica mejor ese número.-subjresponde las preguntas interactivas en la propia línea de comandos. El CN ahora es sólo informativo, pero establézcalo con el nombre principal. Algunas herramientas lo muestran.-addext "subjectAltName=..."es la opción esencial. Enumere todos los nombres y todas las IP que escribirán los clientes: entradasDNS:para nombres de host (se admiten comodines comoDNS:*.internal.lan) y entradasIP:para direcciones. Si alguien accederá ahttps://10.8.0.1, la entradaIP:10.8.0.1debe estar presente. Un SAN que sólo contenga un nombre DNS volverá a producirlesNET::ERR_CERT_COMMON_NAME_INVALID.
Confirme que el SAN se haya incluido antes de configurar cualquier otro componente:
openssl x509 -in /etc/ssl/certs/git.internal.crt -noout -ext subjectAltNameSalida correcta:
X509v3 Subject Alternative Name:
DNS:git.internal.lan, IP Address:10.8.0.1Si en su lugar muestra No extensions in certificate, el certificado no tiene SAN y los navegadores lo rechazarán. Genérelo de nuevo en lugar de continuar.
Bloquee la clave
Una clave privada que puede leer cualquier usuario del sistema no es privada. En Ubuntu, /etc/ssl/private ya tiene 710 root:ssl-cert, lo que evita accesos casuales, pero establezca explícitamente los permisos del propio archivo:
sudo chown root:root /etc/ssl/private/git.internal.key
sudo chmod 600 /etc/ssl/private/git.internal.keynginx y Apache leen los certificados como root antes de reducir privilegios, por lo que el modo 600 de root:root funciona con ellos. Si la clave pertenece a un servicio que se ejecuta con su propio usuario y carga la clave directamente, como una aplicación Node, Gitea o un demonio de Python, chown al usuario de ese servicio y mantenga el modo 600. Nunca haga lo siguiente: usar el modo 644, guardar una copia en un repositorio git o guardar una copia en /tmp.
Conéctelo a nginx
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name git.internal.lan;
ssl_certificate /etc/ssl/certs/git.internal.crt;
ssl_certificate_key /etc/ssl/private/git.internal.key;
location / {
proxy_pass http://127.0.0.1:3000;
}
}sudo nginx -t && sudo systemctl reload nginxnginx -t debe mostrar syntax is ok y test is successful antes de que la recarga haga cualquier cambio. Si muestra SSL_CTX_use_PrivateKey_file() failed ... key values mismatch, el certificado y la clave proceden de dos ejecuciones de generación diferentes; consulte la sección sobre modos de fallo.
Configúralo en Apache
sudo a2enmod ssl proxy proxy_httpssl por sí solo no es suficiente aquí: el vhost usa ProxyPass y, sin mod_proxy y mod_proxy_http, la prueba de configuración termina con Invalid command 'ProxyPass', perhaps misspelled or defined by a module not included in the server configuration. Guarda el vhost como /etc/apache2/sites-available/git-internal.conf:
<VirtualHost *:443>
ServerName git.internal.lan
SSLEngine on
SSLCertificateFile /etc/ssl/certs/git.internal.crt
SSLCertificateKeyFile /etc/ssl/private/git.internal.key
ProxyPass / http://127.0.0.1:3000/
ProxyPassReverse / http://127.0.0.1:3000/
</VirtualHost>sudo a2ensite git-internal
sudo apache2ctl configtest && sudo systemctl reload apache2configtest debería responder en Syntax OK. Ahora prueba desde una máquina cliente:
curl -v https://git.internal.lan/y obtendrás un error:
curl: (60) SSL certificate problem: self-signed certificateNo es un error del sistema. Es TLS funcionando: curl no conoce tu certificado y se niega a comunicarse con un servidor que no puede autenticar. La siguiente sección contiene la solución real, y no es lo que hace gran parte de Internet en este momento.
Haga que los clientes confíen en él y rechace estos antipatrones
Primero, las correcciones incorrectas, con su nombre real. curl -k (o --insecure) incluido directamente en un script, verify=False en Python requests y NODE_TLS_REJECT_UNAUTHORIZED=0 en Node no hacen que el certificado sea de confianza. Desactivan la verificación del certificado. Por tanto, el cliente se comunicará con cualquier servidor que presente cualquier certificado, incluido uno que un atacante haya introducido en la ruta. Se mantiene el coste de TLS y se pierde la autenticación, que era precisamente su objetivo. Además, estas opciones se propagan: se copian en una tarea de cron, después en un script de despliegue y luego en el código de producción, hasta que nadie recuerda qué conexiones debían ser temporales. Si un verify=False permanece después de la sesión de depuración que lo originó, el diseño es incorrecto.
La solución correcta consiste en indicar a cada sistema operativo cliente que este certificado es una raíz de confianza. En los clientes Ubuntu y Debian:
sudo cp git.internal.crt /usr/local/share/ca-certificates/git.internal.crt
sudo update-ca-certificatesLa línea relevante de la salida (a continuación aparece un bloque Running hooks in /etc/ca-certificates/update.d...):
Updating certificates in /etc/ssl/certs...
1 added, 0 removed; done.Estas líneas contienen dos problemas habituales. El archivo debe terminar en .crt. Una extensión .pem se ignora silenciosamente y se obtiene 0 added sin ningún mensaje de error. Además, el contenido debe estar en formato PEM. El archivo comienza con -----BEGIN CERTIFICATE-----. Convierta primero un binario DER con openssl x509 -inform der -in file.der -out file.crt. Añadir el propio certificado autofirmado como raíz funciona porque un certificado autofirmado es su propia raíz.
Después, curl, wget, git, apt y cualquier otro cliente que use OpenSSL con el almacén del sistema confiarán en el servidor sin ninguna opción adicional. Algunos clientes mantienen sus propios almacenes de confianza y requieren una configuración específica:
- Chrome/Chromium en Linux lee una base de datos NSS, no el almacén del sistema:
sudo apt install libnss3-toolsy despuéscertutil -d sql:$HOME/.pki/nssdb -A -t "C,," -n "git.internal" -i git.internal.crtpara cada usuario. - Firefox tiene su propio almacén: Settings → Privacy & Security → Certificates → Import, o cambie
security.enterprise_roots.enabledatrueenabout:configpara que lea el almacén del sistema. - Python requests incluye su propio paquete de CA (certifi) e ignora el almacén del sistema: pase
verify="/usr/local/share/ca-certificates/git.internal.crt"o exporteREQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt. - Node.js: exporte
NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/git.internal.crt.
En los clientes Windows, haga doble clic en .crt e instálelo en Trusted Root Certification Authorities. En macOS, añádalo al llavero System desde Keychain Access y márquelo como Always Trust.
Una raíz para muchos servicios: una CA privada
La confianza por certificado deja de escalar de inmediato: seis servicios por cuatro equipos cliente son veinticuatro instalaciones de confianza, y cada servicio nuevo añade más. La solución es una CA privada: los clientes confían en una raíz y usted firma con ella el certificado de cada servicio.
La opción sencilla es mkcert, que está disponible en los repositorios de Ubuntu 24.04 y gestiona los almacenes NSS (Chrome, Firefox) que update-ca-certificates no cubre:
sudo apt install -y mkcert libnss3-tools
mkcert -install
mkcert git.internal.lan "*.internal.lan" 10.8.0.1mkcert -install crea una raíz y la registra en todos los almacenes de confianza de ese equipo; el tercer comando genera git.internal.lan+2.pem y git.internal.lan+2-key.pem, listos para incluirlos en los fragmentos de configuración de nginx o Apache anteriores. Su diseño presupone un equipo de desarrollo: la clave raíz se guarda en el equipo donde se ejecutó -install. Por eso es perfecta para un portátil de desarrollo, pero no es adecuada para una flota de servidores.
Para los servidores, OpenSSL realiza todo el proceso de la CA en cinco comandos:
openssl genrsa -out lab-ca.key 4096
openssl req -x509 -new -key lab-ca.key -sha256 -days 3650 \
-out lab-ca.crt -subj "/CN=Lab Internal CA" \
-addext "basicConstraints=critical,CA:TRUE,pathlen:0" \
-addext "keyUsage=critical,keyCertSign,cRLSign"
openssl genrsa -out git.key 2048
openssl req -new -key git.key -out git.csr -subj "/CN=git.internal.lan" \
-addext "subjectAltName=DNS:git.internal.lan,IP:10.8.0.1"
openssl x509 -req -in git.csr -CA lab-ca.crt -CAkey lab-ca.key \
-CAcreateserial -days 730 -sha256 -copy_extensions copy -out git.crtEl problema está en el último comando: openssl x509 -req elimina de forma predeterminada todas las extensiones de la CSR, incluido el SAN que añadió cuidadosamente. -copy_extensions copy (una opción de OpenSSL 3.x, por lo que funciona en 24.04) las conserva; si la omite, el certificado firmado no tiene SAN y Chrome vuelve a mostrar NET::ERR_CERT_COMMON_NAME_INVALID. Verifíquelo con la misma comprobación openssl x509 -noout -ext subjectAltName de antes.
Distribuya lab-ca.crt a los clientes mediante los pasos anteriores para el almacén de confianza, una vez por equipo. Proteja lab-ca.key como la clave crítica que ahora es: establezca el modo 600 y, si es posible, guárdela en un equipo que no sea uno de los servidores para los que firma certificados, porque quien la tenga puede emitir un certificado para cualquier nombre en el que confíen sus clientes.
Caducidad y rotación
La duración de los certificados de las CA públicas se está reduciendo. El CA/Browser Forum limitó a 200 días los certificados de confianza pública emitidos a partir de marzo de 2026, frente a los 398 días anteriores. El límite será de 100 días en 2027 y de 47 días en marzo de 2029. Estas reglas sólo se aplican a las CA de confianza pública. No rigen para su CA privada, y los navegadores no las aplican a las raíces instaladas manualmente. Hay una restricción práctica: las plataformas de Apple rechazan cualquier certificado de servidor TLS válido durante más de 825 días, independientemente de quién lo haya emitido. Si se conectarán iPhones o Macs, mantenga los certificados finales en dos años o menos. -days 730 cumple ese límite en todos los casos. Una raíz de diez años con certificados finales de dos años es una estructura interna adecuada.
Los certificados de larga duración fallan de una sola forma: de manera silenciosa y simultánea, en una fecha que nadie recuerda haber elegido. Compruebe qué tiene:
openssl x509 -in /etc/ssl/certs/git.internal.crt -noout -enddateIncluya la renovación en un calendario real o haga que cron le avise con 30 días de antelación. openssl x509 -checkend 2592000 -in cert.crt devuelve un código distinto de cero cuando faltan como máximo esos segundos para la caducidad. Si ya usa Uptime Kuma para monitorizar el estado, sus monitores HTTPS avisan gratuitamente cuando se aproxima la caducidad del certificado.
La rotación con una CA privada es sencilla: vuelva a ejecutar los comandos de CSR y firma, sustituya los archivos y recargue el servidor web. La raíz no ha cambiado, por lo que ningún cliente lo percibe.
Modos de fallo y mensajes que verá
NET::ERR_CERT_AUTHORITY_INVALID, es el estado esperado antes de instalar la relación de confianza. No es un defecto del certificado. Si persiste después de instalar la raíz: en Linux, Chrome lee NSS en lugar del almacén del sistema (consulte el paso certutil); o el archivo copiado no terminaba en .crt y update-ca-certificates devolvió 0 added; o el servidor presenta un certificado distinto del certificado en el que estableció confianza. Compare las huellas digitales con openssl s_client -connect git.internal.lan:443 </dev/null 2>/dev/null | openssl x509 -noout -fingerprint -sha256.
NET::ERR_CERT_COMMON_NAME_INVALID, el certificado no tiene SAN o el SAN no incluye el nombre de la barra de direcciones. El caso clásico: el SAN incluye DNS:git.internal.lan, pero el usuario accedió a https://10.8.0.1. Los cambios en el almacén de confianza no pueden solucionar este problema. Emita de nuevo el certificado con la entrada que falta.
curl: (60) SSL certificate problem: self-signed certificate, curl no confía en el certificado. La variante self-signed certificate in certificate chain significa lo mismo para un certificado firmado por su CA privada. Solución puntual: curl --cacert lab-ca.crt https://.... Solución permanente: el almacén de confianza. No use -k.
unable to load certificate ... Expecting: TRUSTED CERTIFICATE (o Expecting: CERTIFICATE REQUEST, o no start line), hay una confusión entre archivos PEM. Proporcionó a OpenSSL un archivo del tipo incorrecto: una clave o una CSR donde esperaba un certificado, o un binario DER donde esperaba PEM. head -1 filename indica qué archivo tiene realmente. Un certificado comienza por -----BEGIN CERTIFICATE-----. Para DER, conviértalo con openssl x509 -inform der -in file.der -out file.crt.
nginx: [emerg] SSL_CTX_use_PrivateKey_file(...) failed (SSL: error ... key values mismatch), el certificado y la clave no corresponden entre sí, normalmente porque el comando de generación se ejecutó dos veces y los archivos se mezclaron. Confírmelo comparando openssl x509 -in git.internal.crt -noout -pubkey | sha256sum con openssl pkey -in git.internal.key -pubout | sha256sum. Si los hashes coinciden, forman un par válido. Si difieren, regenere ambos juntos.
FAQ
¿Por qué Chrome sigue mostrando «No seguro» después de crear un certificado autofirmado?
Si el error es NET::ERR_CERT_AUTHORITY_INVALID, el certificado es correcto; Chrome simplemente todavía no tiene motivos para confiar en él. Instálelo, o instale la raíz de su CA privada, en el almacén de confianza del cliente. Recuerde que, en Linux, Chrome usa la base de datos NSS mediante certutil, no el almacén del sistema. Si el error es NET::ERR_CERT_COMMON_NAME_INVALID, el certificado no tiene un Subject Alternative Name que coincida con la URL y debe volver a emitirse con -addext "subjectAltName=...".
¿Cómo hago que curl confíe en un certificado autofirmado sin usar -k?
Copie el certificado, en formato PEM y con la extensión .crt, en /usr/local/share/ca-certificates/ y ejecute sudo update-ca-certificates. La salida debe indicar 1 added. A partir de ese momento, curl lo valida como cualquier certificado público. Para una solicitud puntual sin modificar el sistema, curl --cacert /path/to/cert.crt valida sólo contra ese archivo. -k desactiva completamente la validación y no debe aparecer en los scripts.
¿Cuánto tiempo puede ser válido un certificado autofirmado?
Técnicamente, durante el tiempo que quiera. Los límites del CA/Browser Forum, 200 días actualmente y 47 para 2029, se aplican a las CA de confianza pública, no a la confianza privada. En la práctica, limite los certificados de servidor a 825 días, porque los dispositivos Apple rechazan los certificados más largos independientemente del emisor. Una raíz privada de diez años con certificados hoja de dos años (-days 730) es un valor predeterminado razonable. Programe la renovación, porque un certificado interno caducado puede dejar todo el servicio fuera de funcionamiento sin avisar, en una fecha que nadie recuerda.
¿Debo usar un certificado autofirmado o Let's Encrypt?
Si el servicio tiene un nombre DNS público y es accesible desde Internet, use siempre Let's Encrypt: es gratuito, automatizado y todos los clientes ya confían en él. Un certificado autofirmado, o una CA privada, se usa para lo que Let's Encrypt no puede emitir: IP privadas, nombres de host internos como .lan, redes aisladas y servicios ocultos deliberadamente detrás de una VPN. La decisión depende de la accesibilidad y del nombre, no de la solidez de la seguridad. La criptografía es idéntica.
¿Por qué se rechaza mi certificado incluso después de añadirlo a /usr/local/share/ca-certificates?
Compruebe tres cosas. El archivo debe terminar en .crt. Una extensión .pem se omite silenciosamente y update-ca-certificates informa 0 added. El contenido debe ser texto PEM que comience por -----BEGIN CERTIFICATE-----, no un archivo binario DER. Además, la aplicación debe usar realmente el almacén del sistema. Chrome en Linux, Firefox, Python requests, Node.js y Java mantienen cada uno un almacén de confianza privado y requieren añadir el certificado por separado.