Los servicios internos (paneles de administración, APIs entre microservicios, bases de datos en red privada) también deben cifrar su tráfico, pero muchas veces no tienen un nombre público con el que obtener un certificado de Let's Encrypt. La alternativa correcta no es un certificado autofirmado distinto para cada servicio, sino una autoridad certificadora (CA) privada: instalas su certificado raíz una vez en tus equipos y todos los certificados que firme serán de confianza. En esta guía crearás una CA privada con OpenSSL en Ubuntu 24.04, emitirás un certificado de servidor y otro de cliente, y configurarás Nginx y los clientes para usarlos.

Requisitos previos

Para seguir esta guía necesitas:

  • Un servidor con Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath, y un usuario no root con privilegios sudo. OpenSSL 3 viene instalado por defecto.
  • Un nombre interno para el servicio (en esta guía, app.internal.example) y la IP privada por la que se accede (10.0.0.10). Sustitúyelos por los tuyos.
  • Nginx instalado (sudo apt install nginx) si quieres seguir el paso 5.

Paso 1: Preparar el directorio de la CA

Crea un directorio para la CA con permisos restringidos, de modo que solo tu usuario pueda leer las claves:

mkdir -p ~/internal-ca
chmod 700 ~/internal-ca
cd ~/internal-ca

Todos los comandos siguientes se ejecutan desde este directorio.

Paso 2: Crear la CA raíz

Genera la clave privada de la CA. Esta guía usa claves de curva elíptica P-256, que son pequeñas, rápidas y compatibles con cualquier cliente actual:

openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out ca.key
chmod 600 ca.key

Para que el certificado raíz tenga exactamente las extensiones de una CA, define su configuración en un archivo:

nano ca.cnf
[req]
distinguished_name = dn
prompt = no
x509_extensions = v3_ca

[dn]
O = Example Corp
CN = Example Corp Internal Root CA

[v3_ca]
basicConstraints = critical, CA:TRUE, pathlen:0
keyUsage = critical, keyCertSign, cRLSign
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always

pathlen:0 impide que esta CA firme otras CA intermedias, y keyUsage limita su uso a firmar certificados y listas de revocación.

Crea el certificado raíz autofirmado con una validez de 10 años:

openssl req -x509 -new -config ca.cnf -key ca.key -sha256 -days 3650 -out ca.crt

Comprueba el resultado:

openssl x509 -in ca.crt -noout -subject -dates -ext basicConstraints,keyUsage
subject=O = Example Corp, CN = Example Corp Internal Root CA
notBefore=Sep 25 10:00:00 2026 GMT
notAfter=Sep 22 10:00:00 2036 GMT
X509v3 Basic Constraints: critical
    CA:TRUE, pathlen:0
X509v3 Key Usage: critical
    Certificate Sign, CRL Sign

Paso 3: Emitir un certificado de servidor

Los clientes modernos ignoran el campo CN y validan el nombre solo contra la extensión Subject Alternative Name (SAN). Un certificado sin SAN falla en Chrome, curl, Go o Python aunque el CN sea correcto, así que la incluirás siempre.

Genera la clave del servidor y una petición de firma (CSR):

openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out app.key
chmod 600 app.key
openssl req -new -key app.key -subj "/CN=app.internal.example" -out app.csr

Crea el archivo de extensiones con todos los nombres y direcciones IP por los que se accederá al servicio:

nano app.ext
basicConstraints = CA:FALSE
keyUsage = critical, digitalSignature
extendedKeyUsage = serverAuth
subjectAltName = DNS:app.internal.example, IP:10.0.0.10
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid, issuer

Firma la petición con la CA. La validez de 397 días evita problemas con clientes de Apple, que rechazan certificados de servidor con validez excesiva:

openssl x509 -req -in app.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
  -days 397 -sha256 -extfile app.ext -out app.crt

-CAcreateserial crea el archivo ca.srl, que guarda el siguiente número de serie. Consérvalo junto a la CA para que cada certificado tenga un número distinto.

Verifica que el certificado está firmado por tu CA y contiene los nombres correctos:

openssl verify -CAfile ca.crt app.crt
openssl x509 -in app.crt -noout -subject -issuer -ext subjectAltName
app.crt: OK
subject=CN = app.internal.example
issuer=O = Example Corp, CN = Example Corp Internal Root CA
X509v3 Subject Alternative Name:
    DNS:app.internal.example, IP Address:10.0.0.10

Paso 4: Hacer que el sistema confíe en la CA

En cada equipo que vaya a conectarse a tus servicios, instala el certificado raíz (nunca la clave) en el almacén del sistema. En Ubuntu y Debian el archivo debe tener extensión .crt:

sudo cp ca.crt /usr/local/share/ca-certificates/example-internal-ca.crt
sudo update-ca-certificates
Updating certificates in /etc/ssl/certs...
1 added, 0 removed; done.

En Rocky Linux y otras distribuciones de la familia RHEL, cópialo a /etc/pki/ca-trust/source/anchors/ y ejecuta sudo update-ca-trust.

Ten en cuenta que no todo el software usa el almacén del sistema:

  • Python con requests usa su propio paquete certifi. Indícale el almacén del sistema con la variable REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt.
  • Node.js necesita NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/example-internal-ca.crt.
  • Java usa el almacén cacerts del JDK. Impórtalo con sudo keytool -importcert -cacerts -alias example-internal-ca -file ca.crt (la contraseña por defecto es changeit).
  • Firefox mantiene su propio almacén; importa el certificado desde Ajustes > Privacidad y seguridad > Ver certificados > Autoridades.

Paso 5: Usar el certificado en Nginx

Copia el certificado y la clave del servidor a una ubicación del sistema con permisos adecuados. Si el servicio está en otra máquina, cópialos allí con scp:

sudo mkdir -p /etc/ssl/internal
sudo cp app.crt /etc/ssl/internal/app.crt
sudo cp app.key /etc/ssl/internal/app.key
sudo chmod 600 /etc/ssl/internal/app.key

Crea el sitio:

sudo nano /etc/nginx/sites-available/app.internal.example
server {
    listen 443 ssl;
    server_name app.internal.example;

    ssl_certificate     /etc/ssl/internal/app.crt;
    ssl_certificate_key /etc/ssl/internal/app.key;
    ssl_protocols       TLSv1.2 TLSv1.3;

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host $host;
    }
}

Como la CA emite directamente el certificado del servidor, no hay intermedios que añadir. Activa el sitio y recarga:

sudo ln -s /etc/nginx/sites-available/app.internal.example /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

Desde un equipo que confía en la CA (paso 4) y resuelve el nombre, comprueba la conexión sin -k ni --insecure:

curl -sSI https://app.internal.example
HTTP/1.1 200 OK
Server: nginx

Si el nombre aún no está en tu DNS interno, puedes probar forzando la resolución: curl -sSI --resolve app.internal.example:443:10.0.0.10 https://app.internal.example.

Paso 6: Autenticar clientes con certificados (mTLS)

La misma CA puede emitir certificados de cliente para que solo los equipos o servicios autorizados puedan conectarse. La diferencia está en el uso extendido clientAuth. En el directorio de la CA:

openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out client.key
chmod 600 client.key
openssl req -new -key client.key -subj "/CN=backup-agent" -out client.csr
printf 'basicConstraints = CA:FALSE\nkeyUsage = critical, digitalSignature\nextendedKeyUsage = clientAuth\n' > client.ext
openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
  -days 397 -sha256 -extfile client.ext -out client.crt
openssl verify -CAfile ca.crt -purpose sslclient client.crt
client.crt: OK

En el servidor, copia ca.crt a /etc/ssl/internal/ca.crt y añade estas dos directivas al bloque server de Nginx:

    ssl_client_certificate /etc/ssl/internal/ca.crt;
    ssl_verify_client      on;

Recarga Nginx con sudo nginx -t && sudo systemctl reload nginx. Ahora una petición sin certificado recibe 400 No required SSL certificate was sent, y una con él funciona:

curl -sSI --cert client.crt --key client.key https://app.internal.example
HTTP/1.1 200 OK

Renovar certificados

Antes de que caduque un certificado, repite el paso 3 (o el 6) reutilizando el mismo archivo .ext: genera una CSR nueva, fírmala y sustituye los archivos en el servidor. Después recarga el servicio. Para ver cuándo caduca un certificado en uso:

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

El certificado raíz dura 10 años. Anota su fecha de caducidad y planifica su sustitución con tiempo, ya que implica redistribuir el nuevo ca.crt a todos los clientes.

Solución de problemas

curl: (60) SSL certificate problem: unable to get local issuer certificate. El cliente no confía en la CA. Repite el paso 4 en ese equipo y comprueba que el archivo en /usr/local/share/ca-certificates/ termina en .crt.

no alternative certificate subject name matches target host name. Te conectas con un nombre o IP que no está en la SAN. Revisa openssl x509 -in app.crt -noout -ext subjectAltName y vuelve a emitir el certificado con el nombre que falta.

key values mismatch al recargar Nginx. El certificado y la clave no corresponden. Compara openssl x509 -in app.crt -noout -pubkey | sha256sum con openssl pkey -in app.key -pubout | sha256sum: deben coincidir.

Conclusión

Has creado una CA privada con OpenSSL, emitido certificados de servidor y cliente con las extensiones correctas, distribuido la raíz al almacén del sistema y protegido un servicio interno con TLS y autenticación mutua en Nginx. Si el número de servicios crece, considera una CA con emisión automática por ACME, como step-ca, para renovar sin intervención manual. También puedes aplicar el mismo esquema de mTLS a otros servicios internos como PostgreSQL o tus APIs entre microservicios.