--- title: "Cómo configurar mTLS con certificados de cliente en Nginx" description: "Crea una CA privada con OpenSSL, emite certificados de cliente y exige autenticación mutua TLS (mTLS) en Nginx o Apache en Ubuntu 24.04, con revocación por CRL." category: "SSL/TLS y Certificados" language: es source: https://cubepath.com/es/docs/ssl-tls-y-certificados/configuracion-de-autenticacion-de-certificado-de-cliente updated: 2026-09-25 --- # Cómo configurar mTLS con certificados de cliente en Nginx 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. > Nota: lo 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: ```bash 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: ```bash nano ~/client-ca/openssl.cnf ``` ```ini [ 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: ```bash 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: ```bash openssl req -config openssl.cnf -new -x509 -key private/ca.key -days 3650 -out certs/ca.crt ``` Comprueba el sujeto y las extensiones: ```bash openssl x509 -in certs/ca.crt -noout -subject -ext basicConstraints,keyUsage ``` ```output 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`: ```bash 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: ```bash openssl ca -config openssl.cnf -in csr/client1.csr -out certs/client1.crt ``` ```output 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: ```bash openssl verify -CAfile certs/ca.crt certs/client1.crt ``` ```output 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: ```bash 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: ```bash 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: ```bash 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: ```nginx 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: ```nginx 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: ```bash sudo nginx -t sudo systemctl reload nginx ``` Prueba sin certificado: ```bash curl -s https://your_domain | grep title ``` ```output 400 No required SSL certificate was sent ``` Y con el certificado de cliente: ```bash cd ~/client-ca curl --cert certs/client1.crt --key private/client1.key https://your_domain ``` ```output 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: ```bash sudo mkdir -p /etc/apache2/client-ca sudo cp ~/client-ca/certs/ca.crt /etc/apache2/client-ca/ca.crt ``` Edita el sitio: ```bash sudo nano /etc/apache2/sites-available/your_domain-le-ssl.conf ``` ```apache 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 ``` Para proteger solo una ruta, deja `SSLVerifyClient none` a nivel de `VirtualHost` y pon `SSLVerifyClient require` dentro de un bloque ``. 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: ```bash 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: ```bash 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`: ```bash openssl ca -config openssl.cnf -revoke certs/client1.crt ``` ```output Revoking Certificate 1000. Database updated ``` Genera la CRL, que es la lista firmada de certificados revocados: ```bash openssl ca -config openssl.cnf -gencrl -out crl/ca.crl ``` ```bash openssl crl -in crl/ca.crl -noout -text | grep -A1 'Serial Number' ``` ```output 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`: ```bash sudo cp ~/client-ca/crl/ca.crl /etc/nginx/client-ca/ca.crl ``` ```nginx ssl_crl /etc/nginx/client-ca/ca.crl; ``` En Apache, las directivas equivalentes son: ```apache SSLCARevocationFile /etc/apache2/client-ca/ca.crl SSLCARevocationCheck leaf ``` Recarga el servidor web y prueba los dos certificados: ```bash sudo nginx -t && sudo systemctl reload nginx ``` ```bash 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 ``` ```output 400 The SSL certificate error Hola, CN=client2 ``` > Importante: la 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 con `openssl 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: ```nginx error_log /var/log/nginx/your_domain_error.log info; ``` Después revisa el log: ```bash 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.