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. curlyopenssl, 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.
Notalo ideal es mantener la CA en una máquina distinta del servidor web (o al menos fuera de sus rutas), porque quien tenga su clave privada puede emitir certificados válidos. En esta guía se crea en el directorio personal de tu usuario para simplificar.
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 campoCNen las solicitudes; será el identificador de cada cliente.client_cert: extensiones de los certificados de cliente.extendedKeyUsage = clientAuthimpide 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. Conoptionalse aceptan conexiones sin certificado y puedes decidir porlocationusando$ssl_client_verify, que valeSUCCESScuando 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
Importantela CRL caduca a los
default_crl_days(30 días). Cuando caduca, Nginx y Apache rechazan a todos los clientes. Regenera la CRL, cópiala y recarga el servidor antes de esa fecha, por ejemplo cada semana, y siempre justo después de revocar un certificado. Puedes ver la fecha límite conopenssl crl -in crl/ca.crl -noout -nextupdate.
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 dessl_client_certificate. Compruébalo conopenssl 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 conopenssl 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.