Con la autenticación por certificado de cliente, también llamada TLS mutuo (mTLS), el servidor no solo presenta su certificado: exige que el cliente presente uno firmado por una autoridad de certificación (CA) de confianza. Quien no tenga un certificado válido no llega ni a enviar una petición HTTP. En esta guía crearás una CA privada con OpenSSL en Ubuntu 24.04, emitirás certificados de cliente, protegerás un sitio de Nginx (y verás el equivalente en Apache) y revocarás un certificado con una lista de revocación (CRL).

Es una buena opción para paneles de administración, APIs internas o comunicación entre servicios, donde los clientes son conocidos y pocos.

Requisitos previos

  • Un servidor con Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath, con un usuario no root con privilegios sudo.
  • Nginx sirviendo tu dominio por HTTPS con un certificado de servidor válido, por ejemplo de Let's Encrypt en /etc/letsencrypt/live/your_domain/. Si usas Apache, consulta el paso 5.
  • curl y openssl, que vienen instalados en Ubuntu.

En los ejemplos, your_domain es el dominio del sitio que vas a proteger.

El certificado del servidor y la CA de clientes son independientes: el navegador sigue validando el servidor con el certificado público de Let's Encrypt, y Nginx valida a los clientes con tu CA privada.

Paso 1: Crear la estructura de la CA

openssl ca lleva un registro de los certificados emitidos y revocados en un archivo índice, lo que es necesario para generar la CRL más adelante. Crea los directorios y archivos de control:

mkdir -p ~/client-ca/{certs,crl,csr,newcerts,private}
cd ~/client-ca
chmod 700 private
touch index.txt
echo 1000 > serial
echo 1000 > crlnumber

Crea el archivo de configuración de la CA:

nano ~/client-ca/openssl.cnf
[ ca ]
default_ca = client_ca

[ client_ca ]
dir              = .
database         = $dir/index.txt
serial           = $dir/serial
crlnumber        = $dir/crlnumber
new_certs_dir    = $dir/newcerts
certificate      = $dir/certs/ca.crt
private_key      = $dir/private/ca.key
default_md       = sha256
default_days     = 365
default_crl_days = 30
unique_subject   = no
policy           = policy_client
x509_extensions  = client_cert
copy_extensions  = none

[ policy_client ]
commonName = supplied

[ client_cert ]
basicConstraints       = CA:FALSE
keyUsage               = critical, digitalSignature
extendedKeyUsage       = clientAuth
subjectKeyIdentifier   = hash
authorityKeyIdentifier = keyid,issuer

[ req ]
distinguished_name = req_dn
x509_extensions    = v3_ca
prompt             = no

[ req_dn ]
CN = Example Client CA

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

Qué define cada sección:

  • client_ca: dónde están los archivos de la CA, validez por defecto de los certificados (365 días) y de la CRL (30 días). dir = . significa que los comandos se ejecutan desde ~/client-ca.
  • policy_client: solo exige el campo CN en las solicitudes; será el identificador de cada cliente.
  • client_cert: extensiones de los certificados de cliente. extendedKeyUsage = clientAuth impide usarlos como certificado de servidor.
  • v3_ca: extensiones del certificado raíz de la CA.

Paso 2: Generar la clave y el certificado de la CA

Genera la clave privada de la CA con curva P-256, cifrada con una contraseña que te pedirá dos veces. Guárdala en un gestor de contraseñas, la necesitarás cada vez que emitas o revoques un certificado:

cd ~/client-ca
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -aes256 -out private/ca.key
chmod 400 private/ca.key

Crea el certificado autofirmado de la CA, válido durante 10 años:

openssl req -config openssl.cnf -new -x509 -key private/ca.key -days 3650 -out certs/ca.crt

Comprueba el sujeto y las extensiones:

openssl x509 -in certs/ca.crt -noout -subject -ext basicConstraints,keyUsage
subject=CN = Example Client CA
X509v3 Basic Constraints: critical
    CA:TRUE
X509v3 Key Usage: critical
    Certificate Sign, CRL Sign

Paso 3: Emitir un certificado de cliente

Cada cliente debería generar su propia clave y enviar solo la solicitud de firma (CSR). Aquí lo haces todo en el mismo equipo para el ejemplo. Genera la clave y la CSR del cliente client1:

cd ~/client-ca
openssl req -new -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes \
  -keyout private/client1.key -subj "/CN=client1" -out csr/client1.csr

Firma la CSR con la CA. Te pedirá la contraseña de la CA y dos confirmaciones:

openssl ca -config openssl.cnf -in csr/client1.csr -out certs/client1.crt
Using configuration from openssl.cnf
Enter pass phrase for ./private/ca.key:
Check that the request matches the signature
Signature ok
The Subject's Distinguished Name is as follows
commonName            :ASN.1 12:'client1'
Certificate is to be certified until Sep 25 10:00:00 2027 GMT (365 days)
Sign the certificate? [y/n]:y


1 out of 1 certificate requests certified, commit? [y/n]y
Write out database with 1 new entries
Database updated

Verifica que el certificado está firmado por tu CA:

openssl verify -CAfile certs/ca.crt certs/client1.crt
certs/client1.crt: OK

Para importar el certificado en un navegador o en el llavero del sistema, empaquétalo con su clave en un archivo PKCS#12. Te pedirá una contraseña de exportación que el usuario tendrá que introducir al importarlo:

openssl pkcs12 -export -in certs/client1.crt -inkey private/client1.key \
  -certfile certs/ca.crt -name client1 -out certs/client1.p12

Si algún sistema antiguo rechaza el archivo al importarlo, vuelve a generarlo añadiendo -legacy, que usa los algoritmos de cifrado de OpenSSL 1.1.

Paso 4: Exigir certificado de cliente en Nginx

Copia el certificado de la CA (nunca su clave) a un directorio de Nginx:

sudo mkdir -p /etc/nginx/client-ca
sudo cp ~/client-ca/certs/ca.crt /etc/nginx/client-ca/ca.crt

Abre la configuración del sitio:

sudo nano /etc/nginx/sites-available/your_domain

Añade las directivas de verificación al bloque server HTTPS. En este ejemplo el sitio responde con el nombre del cliente para que puedas ver que funciona; en un caso real, reemplaza el location por tu aplicación:

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name your_domain;

    ssl_certificate     /etc/letsencrypt/live/your_domain/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/your_domain/privkey.pem;

    # Autenticación de clientes
    ssl_client_certificate /etc/nginx/client-ca/ca.crt;
    ssl_verify_client on;
    ssl_verify_depth 1;

    location / {
        default_type text/plain;
        return 200 "Hola, $ssl_client_s_dn\n";
    }
}
  • ssl_client_certificate: CA con la que se validan los clientes. Nginx también envía su nombre al cliente para que el navegador sepa qué certificado ofrecer.
  • ssl_verify_client on: rechaza cualquier conexión sin un certificado válido. Con optional se aceptan conexiones sin certificado y puedes decidir por location usando $ssl_client_verify, que vale SUCCESS cuando el cliente se ha autenticado.
  • ssl_verify_depth 1: el certificado del cliente está firmado directamente por la CA raíz.

Si el sitio hace de proxy hacia una aplicación, pásale la identidad del cliente en cabeceras:

    location / {
        proxy_set_header X-SSL-Client-Verify $ssl_client_verify;
        proxy_set_header X-SSL-Client-DN     $ssl_client_s_dn;
        proxy_pass http://127.0.0.1:8080;
    }

La aplicación debe escuchar solo en 127.0.0.1 para que nadie pueda saltarse Nginx y falsificar esas cabeceras.

Comprueba y recarga Nginx:

sudo nginx -t
sudo systemctl reload nginx

Prueba sin certificado:

curl -s https://your_domain | grep title
<head><title>400 No required SSL certificate was sent</title></head>

Y con el certificado de cliente:

cd ~/client-ca
curl --cert certs/client1.crt --key private/client1.key https://your_domain
Hola, CN=client1

Paso 5 (alternativa): Exigir certificado de cliente en Apache

En Apache la configuración equivalente va dentro del VirtualHost HTTPS. Copia el certificado de la CA:

sudo mkdir -p /etc/apache2/client-ca
sudo cp ~/client-ca/certs/ca.crt /etc/apache2/client-ca/ca.crt

Edita el sitio:

sudo nano /etc/apache2/sites-available/your_domain-le-ssl.conf
<VirtualHost *:443>
    ServerName your_domain

    SSLEngine on
    SSLCertificateFile    /etc/letsencrypt/live/your_domain/fullchain.pem
    SSLCertificateKeyFile /etc/letsencrypt/live/your_domain/privkey.pem

    SSLCACertificateFile /etc/apache2/client-ca/ca.crt
    SSLVerifyClient require
    SSLVerifyDepth 1

    # Pasar la identidad del cliente a la aplicación
    RequestHeader set X-SSL-Client-DN "%{SSL_CLIENT_S_DN}s"

    # ... resto de la configuración del sitio
</VirtualHost>

Para proteger solo una ruta, deja SSLVerifyClient none a nivel de VirtualHost y pon SSLVerifyClient require dentro de un bloque <Location /admin>. Apache renegociará la conexión al entrar en esa ruta (esto no funciona con HTTP/2 ni con TLS 1.3 en todos los navegadores, por lo que es más fiable proteger el VirtualHost completo).

Activa el módulo headers, comprueba y recarga:

sudo a2enmod headers
sudo apache2ctl configtest
sudo systemctl reload apache2

Las pruebas con curl son las mismas que en Nginx. Sin certificado, la conexión TLS falla con un error de handshake en lugar de una página 400.

Paso 6: Revocar un certificado con una CRL

Si un portátil se pierde o un empleado se marcha, revoca su certificado. Crea primero un segundo certificado para poder comprobar que el resto siguen funcionando:

cd ~/client-ca
openssl req -new -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes \
  -keyout private/client2.key -subj "/CN=client2" -out csr/client2.csr
openssl ca -config openssl.cnf -in csr/client2.csr -out certs/client2.crt

Revoca client1:

openssl ca -config openssl.cnf -revoke certs/client1.crt
Revoking Certificate 1000.
Database updated

Genera la CRL, que es la lista firmada de certificados revocados:

openssl ca -config openssl.cnf -gencrl -out crl/ca.crl
openssl crl -in crl/ca.crl -noout -text | grep -A1 'Serial Number'
    Serial Number: 1000
        Revocation Date: Sep 25 10:30:00 2026 GMT

Copia la CRL al servidor web y añádela a la configuración de Nginx, dentro del mismo bloque server:

sudo cp ~/client-ca/crl/ca.crl /etc/nginx/client-ca/ca.crl
    ssl_crl /etc/nginx/client-ca/ca.crl;

En Apache, las directivas equivalentes son:

    SSLCARevocationFile /etc/apache2/client-ca/ca.crl
    SSLCARevocationCheck leaf

Recarga el servidor web y prueba los dos certificados:

sudo nginx -t && sudo systemctl reload nginx
curl -s --cert certs/client1.crt --key private/client1.key https://your_domain | grep title
curl -s --cert certs/client2.crt --key private/client2.key https://your_domain
<head><title>400 The SSL certificate error</title></head>
Hola, CN=client2

Solución de problemas

Nginx registra el motivo exacto del rechazo con nivel info, que no se guarda con la configuración por defecto de Ubuntu. Para diagnosticar, añade temporalmente esta línea al bloque server y recarga Nginx:

    error_log /var/log/nginx/your_domain_error.log info;

Después revisa el log:

sudo tail -n 20 /var/log/nginx/your_domain_error.log

Mensajes habituales:

  • client SSL certificate verify error: (10:certificate has expired): el certificado del cliente ha caducado. Emite uno nuevo.
  • (20:unable to get local issuer certificate): el certificado no lo firmó la CA de ssl_client_certificate. Compruébalo con openssl verify -CAfile certs/ca.crt certificado.crt.
  • (23:certificate revoked): el certificado está en la CRL; es el comportamiento esperado.
  • (12:CRL has expired): la CRL ha caducado y bloquea a todos los clientes. Regenérala con openssl ca -config openssl.cnf -gencrl -out crl/ca.crl, cópiala y recarga.

Si el navegador no ofrece ningún certificado, comprueba que el .p12 está importado en el almacén personal del usuario y que el certificado incluye TLS Web Client Authentication en openssl x509 -in certs/client1.crt -noout -ext extendedKeyUsage.

Conclusión

Has creado una CA privada con OpenSSL, emitido certificados de cliente, configurado Nginx (o Apache) para exigirlos y revocado un certificado con una CRL. Solo los dispositivos con un certificado de tu CA pueden acceder al sitio. Como siguientes pasos, mueve la clave de la CA a una máquina aislada, automatiza la regeneración de la CRL con un temporizador de systemd y, si gestionas muchos clientes, valora una CA con emisión automatizada como step-ca.