SSD Nodes Learn
Guías Matt ConnorPor Matt Connor · Actualizado 2026-07-24

crear certificados autofirmados en Ubuntu

Guía para generar certificados TLS con SAN en Ubuntu 24.04 que Chrome acepte. Incluye comandos openssl y configuración en nginx o Apache sin usar curl -k.

Lo que vas a construir

Un certificado TLS autofirmado que los navegadores y clientes modernos acepten correctamente —con el subjectAltName adecuado, permisos de clave válidos y configurado en nginx o Apache—. Además, incluiremos la parte que casi todas las guías omiten: lograr que tus clientes confíen en él correctamente, en lugar de ignorar las advertencias y hardcodear curl -k en tus scripts permanentemente. Al final, obtendrás 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 mucho menos veces de lo que se suele usar. Si el servicio es accesible desde internet mediante un nombre DNS real, deja de leer y obtén un certificado gratuito de Let's Encrypt con certbot en nginx o el equivalente para Apache. No tiene coste, se renueva solo y todos los navegadores del mundo ya confían en él. Un certificado autofirmado en un sitio público entrena a tus usuarios para ignorar las advertencias de seguridad, lo cual es un hábito peor que usar HTTP simple.

El uso de certificados autofirmados es correcto cuando no hay internet público de por medio: un panel de administración vinculado a una dirección de túnel WireGuard en tu VPS, un servidor de staging en una red privada, tráfico entre servicios de backend, un dispositivo de home-lab, o para reemplazar el certificado temporal que Webmin genera para sí mismo en el puerto 10000. Let's Encrypt no 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, tú eres la CA.

Todo lo siguiente se ejecuta en una instancia limpia de Ubuntu 24.04 con OpenSSL 3.0.x (usa openssl version para confirmar). Nada de esto requiere acceso a internet; todo funciona en entornos air-gapped.

Por qué el comando de una sola línea anterior genera certificados rechazados por Chrome

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.crt

Este comando realiza una serie de preguntas interactivas, inserta el hostname en el campo Common Name y genera un certificado sin la extensión subjectAltName. Ese certificado no es válido. Chrome dejó de leer el Common Name en la versión 58, en abril de 2017 —el RFC 2818 ya había deprecado la coincidencia por CN en el año 2000— y Firefox, Safari, curl y Python funcionan de la misma manera. Un certificado identifica al servidor mediante la extensión SAN o no lo identifica. El navegador muestra exactamente este mensaje:

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.

Modificar el almacén de confianza (trust-store) no soluciona este error, porque el certificado no contiene ningún nombre. Si estás viendo NET::ERR_CERT_COMMON_NAME_INVALID en este momento, tu certificado no tiene SAN (o tiene el incorrecto) y necesitas generar uno nuevo. Afortunadamente, la solución es un solo comando.

Generar un certificado aceptado por los navegadores: un solo comando

OpenSSL añadió el flag -addext en la versión 1.1.1. Esto elimina la necesidad de usar archivos de configuración complejos para insertar un SAN, como hacían las guías antiguas. 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"

Función de cada flag:

  • -x509 genera un certificado autofirmado directamente en lugar de una solicitud de firma.
  • -newkey rsa:4096 genera una clave nueva en el mismo paso. RSA 4096 es compatible con clientes antiguos; si todos los clientes son modernos, -newkey ec -pkeyopt ec_paramgen_curve:P-256 es más pequeño y rápido.
  • -noenc es la sintaxis de OpenSSL 3.x para el antiguo -nodes: sin contraseña en la clave. Ambas sintaxis funcionan. Una clave con contraseña hace que nginx se detenga esperando entrada en cada reinicio; para una clave de servidor, debe evitarse esto.
  • -days 730 — dos años; más detalles sobre este número en la sección de expiración.
  • -subj responde a las preguntas interactivas de forma automática. El CN es solo estético ahora, pero asígnale el nombre principal; algunas herramientas lo muestran.
  • -addext "subjectAltName=..." es el flag fundamental. Liste todos los nombres y todas las IP que los clientes usarán: entradas DNS: para hostnames (los comodines como DNS:*.internal.lan son válidos), entradas IP: para direcciones. Si alguien accederá mediante https://10.8.0.1, la entrada IP:10.8.0.1 debe estar presente; un SAN que solo contenga DNS causará NET::ERR_CERT_COMMON_NAME_INVALID nuevamente.

Confirme que el SAN se aplicó correctamente antes de realizar cualquier configuración:

openssl x509 -in /etc/ssl/certs/git.internal.crt -noout -ext subjectAltName

Salida correcta:

X509v3 Subject Alternative Name:
    DNS:git.internal.lan, IP Address:10.8.0.1

Si en su lugar aparece No extensions in certificate, el certificado no tiene SAN y los navegadores lo rechazarán. Es mejor regenerarlo que continuar con el error.

Proteja la clave

Una clave privada que cualquier usuario del sistema pueda leer no es una clave privada. En Ubuntu, /etc/ssl/private ya tiene 710 root:ssl-cert, lo que evita el acceso no autorizado, pero configure el archivo de forma explícita:

sudo chown root:root /etc/ssl/private/git.internal.key
sudo chmod 600 /etc/ssl/private/git.internal.key

Tanto nginx como Apache leen los certificados como root antes de reducir sus privilegios, por lo que el modo root:root 600 funciona para ellos. Si la clave es para un servicio que se ejecuta con su propio usuario y carga la clave por sí mismo —una aplicación Node, Gitea o un daemon de Python— cámbiela a chown para ese usuario del servicio, manteniendo el modo 600. Lo que nunca debe hacer: usar el modo 644, guardar una copia en un repositorio git o una copia en /tmp.

Integración con 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 nginx

nginx -t debe mostrar syntax is ok y test is successful antes de que el reload surta efecto. Si muestra SSL_CTX_use_PrivateKey_file() failed ... key values mismatch, el certificado y la clave pertenecen a dos ejecuciones de generación distintas; consulte la sección de modos de fallo.

Integración con Apache

sudo a2enmod ssl proxy proxy_http

ssl no es suficiente en este caso: el vhost siguiente utiliza ProxyPass, y sin mod_proxy y mod_proxy_http la prueba de configuración falla con Invalid command 'ProxyPass', perhaps misspelled or defined by a module not included in the server configuration. Guarde 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 apache2

configtest debería responder a Syntax OK. Ahora realice la prueba desde una máquina cliente:

curl -v https://git.internal.lan/

y recibirá un error:

curl: (60) SSL certificate problem: self-signed certificate

Esto no es un error del sistema. Es el funcionamiento de TLS: curl no reconoce su certificado y rechaza la conexión con un servidor que no puede autenticar. La siguiente sección contiene la solución real, la cual no es la que utiliza la mitad de internet en este momento.

Lograr que los clientes confíen en el certificado — y los anti-patrones que debe rechazar

Las soluciones incorrectas primero, identificadas por su nombre. curl -k (o --insecure) integrado en un script, verify=False en peticiones de Python, NODE_TLS_REJECT_UNAUTHORIZED=0 en Node; ninguna de estas opciones hace que su certificado sea confiable. Estas opciones desactivan la verificación del certificado, lo que significa que el cliente aceptará cualquier servidor que presente cualquier certificado, incluido uno colocado por un atacante en la ruta. Mantiene la carga de procesamiento de TLS pero pierde la autenticación, que era su propósito. Peor aún, estos flags se propagan: se pegan en un cron job, luego en un script de despliegue y después en el código de producción, hasta que nadie recuerda qué conexiones debían ser temporales. Si un verify=False sobrevive más allá de la sesión de depuración que lo originó, el diseño es incorrecto.

La solución correcta es configurar cada sistema operativo cliente para que este certificado sea una raíz de confianza. En clientes Ubuntu y Debian:

sudo cp git.internal.crt /usr/local/share/ca-certificates/git.internal.crt
sudo update-ca-certificates

La línea relevante en la salida (le sigue un bloque Running hooks in /etc/ca-certificates/update.d...):

Updating certificates in /etc/ssl/certs...
1 added, 0 removed; done.

Hay dos errores comunes en esas líneas. El archivo debe terminar en .crt; una extensión .pem se ignora silenciosamente y obtendrá 0 added sin mensaje de error. Además, el contenido debe ser PEM; el archivo comienza con -----BEGIN CERTIFICATE-----; convierta primero un binario DER con openssl x509 -inform der -in file.der -out file.crt. Agregar el certificado autofirmado como raíz funciona porque un certificado autofirmado es su propia raíz.

Después de eso, curl, wget, git, apt y cualquier otra herramienta que use OpenSSL contra el bundle del sistema confiarán en el servidor sin necesidad de flags. Algunos clientes utilizan sus propios almacenes de confianza y requieren un tratamiento individual:

  • Chrome/Chromium en Linux lee una base de datos NSS, no el almacén del sistema: sudo apt install libnss3-tools, y luego certutil -d sql:$HOME/.pki/nssdb -A -t "C,," -n "git.internal" -i git.internal.crt por usuario.
  • Firefox tiene su propio almacén: Settings → Privacy & Security → Certificates → Import, o cambie security.enterprise_roots.enabled a true en about:config para que lea el almacén del sistema.
  • Python requests incluye su propio bundle de CA (certifi) e ignora el almacén del sistema: pase verify="/usr/local/share/ca-certificates/git.internal.crt" o exporte REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt.
  • Node.js: exporte NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/git.internal.crt.

En clientes Windows, haga doble clic en el .crt e instálelo en Trusted Root Certification Authorities; en macOS, agréguelo al System keychain en Keychain Access y márquelo como Always Trust.

Una raíz para múltiples servicios: una CA privada pequeña

La confianza por certificado individual no escala: seis servicios por cuatro máquinas cliente resultan en veinticuatro instalaciones de confianza, y cada nuevo servicio aumenta la carga. La solución es una CA privada: los clientes confían en una raíz y usted firma el certificado de cada servicio con ella.

La opción sencilla es mkcert, disponible en los repositorios de Ubuntu 24.04, que 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.1

mkcert -install crea una raíz y la registra en todos los almacenes de confianza de esa máquina; el tercer comando genera git.internal.lan+2.pem y git.internal.lan+2-key.pem, listos para usar en los fragmentos de nginx o Apache anteriores. Su diseño asume una máquina de desarrollo: la clave raíz reside en el equipo donde se ejecutó -install; por tanto, es ideal para un portátil de desarrollo pero no es adecuada para un grupo de servidores.

Para servidores, OpenSSL estándar realiza toda 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.crt

El error ocurre en el último comando: openssl x509 -req elimina todas las extensiones del CSR por defecto, incluyendo el SAN que añadió manualmente. -copy_extensions copy (una opción de OpenSSL 3.x, compatible con 24.04) las mantiene; si se omite, el certificado firmado no tendrá SAN y Chrome mostrará nuevamente NET::ERR_CERT_COMMON_NAME_INVALID. Verifique con la misma comprobación openssl x509 -noout -ext subjectAltName que anteriormente.

Distribuya lab-ca.crt a los clientes mediante los pasos de almacén de confianza mencionados arriba; solo es necesario una vez por máquina. Proteja lab-ca.key como un activo crítico: use modo 600 y, preferiblemente, manténgalo en un equipo que no sea uno de los servidores que firma, ya que quien lo posea puede emitir un certificado para cualquier nombre que sus clientes acepten.

Expiración y rotación

La duración de los certificados de CA públicas está disminuyendo. El CA/Browser Forum limitó los certificados nuevos de confianza pública a 200 días en marzo de 2026 (antes eran 398), bajando a 100 días en 2027 y a 47 días para marzo de 2029. Sin embargo, estas reglas solo afectan a las CA de confianza pública. Su CA privada no está sujeta a ellas y los navegadores no las aplican a las raíces instaladas manualmente. Existe una restricción real: las plataformas de Apple rechazan cualquier certificado de servidor TLS con una validez superior a 825 días, independientemente del emisor. Si necesita compatibilidad con iPhones o Macs, mantenga los certificados leaf en dos años o menos. -days 730 cumple con ese requisito en todos los casos; una raíz de diez años con certificados leaf de dos años es una configuración interna adecuada.

Los certificados de larga duración fallan de una sola manera: de forma silenciosa y simultánea, en una fecha que nadie recuerda haber elegido. Verifique sus certificados actuales:

openssl x509 -in /etc/ssl/certs/git.internal.crt -noout -enddate

Agende la renovación en un calendario real o configure un cron para recibir avisos con 30 días de antelación; openssl x509 -checkend 2592000 -in cert.crt devuelve un código de salida distinto de cero cuando la expiración ocurre dentro de ese número de segundos. Si ya utiliza Uptime Kuma para monitoreo de estado, sus monitores HTTPS detectan la expiración próxima de certificados sin costo adicional.

La rotación con una CA privada es un proceso sencillo: ejecute nuevamente los comandos de CSR y firma, reemplace los archivos y recargue el servidor web. La raíz no ha cambiado, por lo que ningún cliente detectará cambios.

Modos de fallo y los mensajes que verá

NET::ERR_CERT_AUTHORITY_INVALID — es el estado esperado antes de instalar la 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 (ver el paso certutil); o el archivo copiado no terminaba en .crt y update-ca-certificates indicó 0 added; o el servidor presenta un certificado distinto al que usted confió; 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 cubre el nombre en la barra de direcciones. El caso clásico: el SAN lista DNS:git.internal.lan pero el usuario navegó a https://10.8.0.1. Los cambios en el almacén de confianza no corrigen esto; reemita el certificado con la entrada faltante.

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 temporal: curl --cacert lab-ca.crt https://...; solución permanente: el almacén de confianza. No es -k.

unable to load certificate ... Expecting: TRUSTED CERTIFICATE (o Expecting: CERTIFICATE REQUEST, o no start line) — confusión de formato PEM. Proporcionó a OpenSSL el tipo de archivo incorrecto: una clave o un CSR donde se esperaba un certificado, o un binario DER donde se esperaba un PEM. head -1 filename le indica qué tiene realmente; un certificado comienza con -----BEGIN CERTIFICATE-----. Para DER, convierta 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 coinciden, generalmente porque el comando de generación se ejecutó dos veces y los archivos se mezclaron. Confirme con openssl x509 -in git.internal.crt -noout -pubkey | sha256sum frente a openssl pkey -in git.internal.key -pubout | sha256sum; los hashes coincidentes indican que es un par válido. Si difieren, regenere ambos archivos juntos.

FAQ

¿Por qué Chrome sigue mostrando "Not secure" después de crear un certificado autofirmado?

Si el error es NET::ERR_CERT_AUTHORITY_INVALID, el certificado es correcto; Chrome simplemente no tiene motivos para confiar en él todavía. Instale el certificado (o su CA raíz privada) en el almacén de confianza del cliente. Tenga en cuenta que en Linux, Chrome utiliza la base de datos NSS mediante certutil en lugar del 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 reemitirse con -addext "subjectAltName=...".

¿Cómo hago que curl confíe en un certificado autofirmado sin usar -k?

Copie el certificado (formato PEM, 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 verificará como cualquier certificado público. Para una solicitud única sin modificar el sistema, curl --cacert /path/to/cert.crt verifica únicamente contra ese archivo; -k desactiva la verificación por completo y no debe usarse en ningún script.

¿Cuánto tiempo puede ser válido un certificado autofirmado?

Técnicamente puede durar lo que desee. Los límites del CA/Browser Forum (actualmente 200 días, 47 para 2029) obligan a las CAs de confianza pública, no a las de confianza privada. En la práctica, limite los certificados del servidor a 825 días, ya que los dispositivos Apple rechazan cualquier certificado con un periodo mayor independientemente del emisor. Una raíz privada de diez años con certificados finales de dos años (-days 730) es una configuración estándar segura; simplemente programe la renovación, ya que un certificado interno caducado detiene todos los servicios sin previo aviso 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 ya es de confianza para todos los clientes. Los certificados autofirmados (o una CA privada) se usan para lo que Let's Encrypt no puede emitir: IPs privadas, nombres de host internos como .lan, redes aisladas (air-gapped) y servicios ocultos deliberadamente tras una VPN. La decisión depende de la accesibilidad y el nombre, no de la robustez criptográfica; la criptografía es idéntica.

¿Por qué se rechaza mi certificado incluso después de añadirlo a /usr/local/share/ca-certificates?

Verifique tres puntos. El archivo debe terminar en .crt; una extensión .pem se ignora silenciosamente y update-ca-certificates reportará 0 added. El contenido debe ser texto PEM que comience con -----BEGIN CERTIFICATE-----, no binario DER. Además, la aplicación debe utilizar el almacén del sistema; Chrome en Linux, Firefox, Python requests, Node.js y Java mantienen su propio almacén de confianza y requieren que el certificado se añada por separado.

#openssl#tls#self-signed#ubuntu#security