Volver al Blog
José Manuel Requena Plens

Dominando Mutual TLS (mTLS) con Nginx: Una Guía Completa

Implementa Mutual TLS (mTLS) con Nginx — crea una CA, genera certificados de cliente, configura revocación CRL y OCSP, y habilita Zero Trust.

Imagen de portada de Dominando Mutual TLS (mTLS) con Nginx: Una Guía Completa

Mutual TLS (mTLS) representa el estándar de referencia para asegurar servicios web privados. A diferencia de las contraseñas, las API keys o incluso el 2FA, mTLS requiere que el cliente (tu navegador/dispositivo) presente un certificado criptográfico firmado por tu propia Certificate Authority (CA) — un secreto que no puede ser robado mediante phishing, adivinado o forzado por fuerza bruta.

Si el cliente no tiene el certificado, Nginx rechaza la conexión antes de que la aplicación siquiera se cargue. Esto es perfecto para asegurar paneles de administración privados, interfaces NAS o herramientas internas.

En esta guía completa, te guiaré a través de la implementación de mTLS en Nginx, cubriendo todo desde la creación de una certificate authority hasta temas avanzados como revocación OCSP vs CRL, arquitectura Zero Trust y ajuste de rendimiento. Esta guía incorpora las mejores prácticas de Smallstep, SSL.com y la documentación oficial de Nginx.


¿Por qué mTLS? El enfoque Zero Trust

En el panorama de seguridad actual, el modelo basado en perímetro (“confía en todo lo que está dentro del firewall”) está obsoleto. Zero Trust asume que los atacantes podrían estar ya dentro de tu red y requiere verificación para cada solicitud de acceso.

mTLS es una piedra angular de la arquitectura Zero Trust porque proporciona autenticación mutua: tanto el servidor como el cliente verifican la identidad del otro mediante certificados criptográficos.

mTLS vs Otros Métodos de Autenticación
Método¿Phishing posible?Almacenamiento de credencialesAutomatizaciónZero Trust
Usuario/ContraseñaBase de datos del servidorFácilNo
API KeysSí (si se exponen)Servidor + clienteFácilNo
Contraseña + 2FAParcialmenteServidor + autenticadorDifícilParcial
mTLSNoHardware/keychainMedio

Por qué mTLS no puede ser víctima de phishing

A diferencia de las contraseñas, los certificados de cliente:

  1. Nunca se transmiten — solo se envía una firma que demuestra la posesión
  2. Están vinculados al hardware — almacenados en keychains/TPMs seguros
  3. Requieren acceso a la clave privada — la clave privada nunca sale del dispositivo
  4. Se validan criptográficamente — falsificarlos es computacionalmente inviable

TLS Unidireccional vs Mutual TLS

Entender la diferencia es fundamental:

TLS Unidireccional vs Mutual TLS
AspectoTLS Unidireccional (HTTPS Estándar)Mutual TLS (mTLS)
El servidor se autentica anteCliente (navegador)Cliente (navegador)
El cliente se autentica anteNo se verificaEl servidor verifica el certificado del cliente
Requisito de certificadoSolo el servidorTanto servidor como cliente
Caso de usoSitios web públicosAPIs, paneles de administración, IoT, B2B

El modelo HTTPS estándar

En el HTTPS regular, solo el servidor presenta un certificado.

El cliente (navegador) lo verifica, pero el servidor no tiene forma de verificar quién es el cliente — solo sabe que la conexión está cifrada.

La mejora de mTLS

mTLS añade un segundo paso: después de verificar el servidor, el servidor solicita un certificado al cliente.

Solo los clientes que presentan un certificado válido firmado por una CA de confianza pueden continuar.


El flujo del handshake mTLS

Entender el handshake ayuda con la depuración.

Aquí está el flujo completo:

Servidor NginxClienteServidor NginxClienteConexión TCP Establecida"Envíame TU certificado"Pipeline de VerificaciónComienza la Sesión CifradaConexión Terminadaalt[Todas las Comprobaciones Pasan][Alguna Comprobación Falla]ClientHello (versión TLS, cipher suites)ServerHello + Certificado del ServidorCertificateRequestCertificado del ClienteCertificateVerify (prueba de firma)Finished1. Validar cadena de firma CA2. Comprobar contra CRL/OCSP3. Verificar CN/SAN si es necesarioFinished + 200 OK400 Bad Request / 403 Forbidden
Flujo del Handshake mTLS en Nginx

Pasos clave explicados

  1. ClientHello: El cliente inicia TLS, proponiendo cipher suites
  2. ServerHello: El servidor responde con su certificado
  3. CertificateRequest: El servidor solicita al cliente que demuestre su identidad
  4. Certificado del Cliente: El cliente envía su certificado
  5. CertificateVerify: El cliente demuestra que posee la clave privada (sin revelarla)
  6. Verificación: El servidor valida toda la cadena

Requisitos previos y configuración de directorios

Antes de comenzar, asegúrate de tener:

  • Un servidor Linux (Debian/Ubuntu/CentOS/RHEL) con Nginx instalado
  • OpenSSL (versión mínima 1.1.1, compruébalo con openssl version, recomendado 3.5.0+)
  • Acceso root o sudo

Crea tu directorio PKI

Una estructura de directorios bien organizada es esencial para gestionar certificados:

# Create the main directory structure sudo mkdir -p /etc/nginx/pki/{ca,certs,crl,private,newcerts} cd /etc/nginx/pki # Secure the private directory sudo chmod 700 private # Initialize OpenSSL CA database files sudo touch index.txt echo 1000 | sudo tee serial > /dev/null echo 1000 | sudo tee crlnumber > /dev/null

Creando tu certificate authority

Una CA es la raíz de confianza en tu PKI.

Todos los certificados de cliente deben estar firmados por esta CA para que Nginx los acepte.

Decisiones de diseño de la CA

Antes de generar, decide sobre estos parámetros:

Opciones de Configuración de la CA
ParámetroRecomendaciónJustificación
Tamaño de ClaveRSA de 4096 bits o ECDSA P-384Mayor margen de seguridad contra ataques clásicos
Validez10 años para la CAMás largo que cualquier certificado de cliente
Algoritmo HashSHA-256 como mínimoSHA-1 está obsoleto
ExtensionesCA:TRUE, keyUsage:keyCertSign,cRLSignNecesario para firmar certificados y CRLs

Crea la configuración de OpenSSL

Crea un archivo de configuración completo de OpenSSL:

/​etc/​nginx/​pki/​openssl.cnf
# =============================================================
# OpenSSL Configuration for mTLS Certificate Authority
# =============================================================

[ ca ]
default_ca = CA_default

[ CA_default ]
# Directory structure
dir               = /etc/nginx/pki
database          = $dir/index.txt
new_certs_dir     = $dir/newcerts
certificate       = $dir/ca/ca.crt
serial            = $dir/serial
private_key       = $dir/private/ca.key
crlnumber         = $dir/crlnumber
crl               = $dir/crl/ca.crl

# Certificate defaults
default_days      = 365          # Client certs valid 1 year
default_crl_days  = 30           # CRL valid 30 days
default_md        = sha256       # Use SHA-256
preserve          = no
policy            = policy_loose
copy_extensions   = copy         # Copy SANs from CSR
# SECURITY WARNING: copy_extensions=copy copies ALL extensions from the CSR.
# This is safe only when you trust the CSR generator. A malicious CSR could
# include basicConstraints: CA:TRUE to create a subordinate CA.
# Use copy_extensions=none or explicitly filter extensions for untrusted CSRs.

[ policy_loose ]
countryName             = optional
stateOrProvinceName     = optional
localityName            = optional
organizationName        = optional
organizationalUnitName  = optional
commonName              = supplied
emailAddress            = optional

# =============================================================
# CA Certificate Request Settings
# =============================================================
[ req ]
default_bits        = 4096
distinguished_name  = req_distinguished_name
string_mask         = utf8only
default_md          = sha256
x509_extensions     = v3_ca
prompt              = no

[ req_distinguished_name ]
C                   = ES
ST                  = Valencia
L                   = Valencia
O                   = JMRP-IO-LAB
OU                  = Infrastructure Security
CN                  = JMRP-IO-LAB Root CA

# =============================================================
# Certificate Extensions
# =============================================================

# For the CA certificate itself
[ v3_ca ]
subjectKeyIdentifier    = hash
authorityKeyIdentifier  = keyid:always,issuer
basicConstraints        = critical, CA:TRUE
keyUsage                = critical, digitalSignature, cRLSign, keyCertSign

# For client certificates
[ v3_client ]
subjectKeyIdentifier    = hash
authorityKeyIdentifier  = keyid,issuer
basicConstraints        = critical, CA:FALSE
keyUsage                = critical, nonRepudiation, digitalSignature, keyEncipherment
extendedKeyUsage        = clientAuth

Desglose de la configuración

Analicemos el archivo openssl.cnf para entender qué controla cada sección.

1. Configuración global de la CA ([ ca ] y [ CA_default ])

Esta sección indica a OpenSSL dónde encontrar los archivos y cómo firmar los certificados.

Configuración por Defecto de la CA
DirectivaValor/TipoDescripción
dirRuta (String)Directorio base de tu PKI (p. ej., /etc/nginx/pki).
databaseRuta de archivoArchivo de texto (index.txt) que registra todos los certificados emitidos y revocados.
new_certs_dirRuta de directorioDonde se archivan copias de cada certificado emitido por número de serie.
certificateRuta de archivoEl certificado público de la CA utilizado para firmar otros.
private_keyRuta de archivoLa clave privada de la CA. Debe estar protegida.
default_daysEntero (Días)Período de validez por defecto para los certificados emitidos (p. ej., 365).
policyNombre de secciónQué sección de política aplicar para la coincidencia de campos DN (p. ej., policy_loose).
copy_extensionscopy | noneCrucial: Copia los SANs (Subject Alternative Names) del CSR al certificado final.

2. Políticas de validación ([ policy_loose ])

Controla qué campos en la Solicitud de Firma de Certificado (CSR) deben coincidir con los propios campos de la CA.

Reglas de Política
CampoReglaSignificado
countryNameoptionalEl cliente no necesita especificar un país.
commonNamesuppliedObligatorio. El cliente debe proporcionar un Common Name (utilizado para identificación).
organizationNameoptionalPuede dejarse vacío.

3. Configuración de solicitudes ([ req ])

Valores por defecto utilizados al ejecutar openssl req para generar claves o CSRs.

Valores por Defecto de Solicitudes
DirectivaValorDescripción
default_bitsEntero (p. ej., 4096)Tamaño de clave por defecto si no se especifica.
distinguished_nameNombre de secciónApunta a la sección que define los valores DN por defecto ([ req_distinguished_name ]).
x509_extensionsNombre de secciónExtensiones a añadir al crear un certificado raíz autofirmado (p. ej., v3_ca).

4. Extensiones de certificado ([ v3_* ])

Estas secciones definen las “capacidades” de los certificados que emites. Este es el núcleo de seguridad.

Perfiles de Extensiones
Perfil/DirectivaValorEfecto
[ v3_ca ] - Para la CA Raíz
basicConstraintscritical, CA:TRUEIdentidad: Marca este certificado como una Certificate Authority que puede firmar otros.
keyUsagekeyCertSign, cRLSignPermisos: Autorizado para firmar certificados y CRLs.
[ v3_client ] - Para Dispositivos de Usuario
basicConstraintscritical, CA:FALSERestricción: Este certificado no puede utilizarse para firmar otros certificados.
extendedKeyUsageclientAuthPropósito: Válido únicamente para autenticar un cliente ante un servidor (mTLS).

Genera la clave y el certificado de la CA

cd /etc/nginx/pki # Generate CA private key (4096-bit RSA) sudo openssl genrsa -out private/ca.key 4096 # Secure the private key sudo chmod 400 private/ca.key # Generate the self-signed CA certificate (valid 10 years) sudo openssl req -new -x509 -days 3650 \ -key private/ca.key \ -out ca/ca.crt \ -config openssl.cnf \ -extensions v3_ca

Verifica tu certificado de CA

openssl x509 -in ca/ca.crt -text -noout | head -30

Certificate: Data: Version: 3 (0x2) Serial Number: 1f:23:94:bf:00:2d:11:f7:c5:14:ac:c6:24:a9:d1:ff:b3:4e:cc:3a Signature Algorithm: sha256WithRSAEncryption Issuer: C=ES, ST=Valencia, L=Valencia, O=JMRP-IO-LAB, OU=Infrastructure Security, CN=JMRP-IO-LAB Root CA Validity Not Before: Jan 13 18:21:21 2026 GMT Not After : Jan 11 18:21:21 2036 GMT Subject: C=ES, ST=Valencia, L=Valencia, O=JMRP-IO-LAB, OU=Infrastructure Security, CN=JMRP-IO-LAB Root CA Subject Public Key Info: Public Key Algorithm: rsaEncryption Public-Key: (4096 bit) X509v3 extensions: X509v3 Subject Key Identifier: … X509v3 Basic Constraints: critical CA:TRUE X509v3 Key Usage: critical Digital Signature, Certificate Sign, CRL Sign


Generando certificados de cliente

Cada cliente (dispositivo, usuario o servicio) necesita su propio certificado firmado por tu CA.

SAN vs CN: ¿cuál usar?

Subject Alternative Name (SAN) vs Common Name (CN)
AspectoCommon Name (CN)Subject Alternative Name (SAN)
EstándarLegacy (obsoleto para certificados de servidor)Moderno, conforme con RFC 6125
Múltiples identidadesNoSí (DNS, email, URI)
Ideal paraIdentificación simple de clienteServicios, clientes multi-identidad

Para la identificación simple de usuarios (como estamos haciendo), CN es suficiente. Para autenticación servicio-a-servicio, usa SANs.

Genera un certificado de cliente

cd /etc/nginx/pki # Set device/user name CLIENT_NAME="iphone-jmrp" # Generate client private key sudo openssl genrsa -out private/${CLIENT_NAME}.key 2048 # Create Certificate Signing Request (CSR) sudo openssl req -new \ -key private/${CLIENT_NAME}.key \ -out certs/${CLIENT_NAME}.csr \ -subj "/CN=${CLIENT_NAME}" # Sign with your CA sudo openssl ca -config openssl.cnf \ -extensions v3_client \ -batch \ -in certs/${CLIENT_NAME}.csr \ -out certs/${CLIENT_NAME}.crt

Using configuration from openssl.cnf Check that the request matches the signature Signature ok The Subject’s Distinguished Name is as follows commonName :ASN.1 12:‘iphone-jmrp’ Certificate is to be certified until Jan 13 18:21:36 2027 GMT (365 days)

Write out database with 1 new entries Database updated

PKCS12 vs PEM: formatos de exportación

Comparación de Formatos de Certificado
FormatoContieneIdeal ParaProtegido por contraseña
PEM (.crt, .key)Archivos de texto separadosServidores Linux, scriptingOpcional
PKCS12 (.p12, .pfx)Paquete binario todo-en-unoNavegadores, móvil, WindowsObligatorio

Crea el paquete PKCS12 para clientes

# Bundle certificate, key, and CA into .p12 file sudo openssl pkcs12 -export \ -out certs/${CLIENT_NAME}.p12 \ -inkey private/${CLIENT_NAME}.key \ -in certs/${CLIENT_NAME}.crt \ -certfile ca/ca.crt \ -name "${CLIENT_NAME}" # You will be prompted for an export password # This protects the .p12 file during transfer

Configurando Nginx para mTLS

Ahora configura Nginx para requerir y validar certificados de cliente.

Entendiendo las opciones de ssl_verify_client

La directiva ssl_verify_client controla cómo Nginx gestiona los certificados de cliente:

Opciones de ssl_verify_client
ValorComportamientoCaso de Uso
onRequiere certificado válido; rechaza sin élControl de acceso estricto (recomendado)
optionalAcepta si es válido; continúa sin élAutenticación híbrida, verificación opcional
optional_no_caAcepta cualquier certificado, sin validación de CASolo desarrollo/pruebas
offNo se requiere certificado de clienteHTTPS estándar

Configuración completa de Nginx

/​etc/​nginx/​sites-available/​secure.example.com.conf
server {
    listen 443 ssl;
    listen [::]:443 ssl;
    http2 on;
    server_name secure.example.com;
    
    # =================================================
    # Server TLS Configuration
    # =================================================
    ssl_certificate     /etc/ssl/certs/secure.example.com.crt;
    ssl_certificate_key /etc/ssl/private/secure.example.com.key;
    
    # =================================================
    # Client Certificate (mTLS) Configuration
    # =================================================
    
    # Path to your Certificate Authority
    ssl_client_certificate /etc/nginx/pki/ca/ca.crt;
    
    # Require valid client certificate
    ssl_verify_client on;
    
    # Verify up to 2 levels in the certificate chain
    ssl_verify_depth 2;
    
    # Optional: Certificate Revocation List
    # ssl_crl /etc/nginx/pki/crl/ca.crl;
    
    # =================================================
    # TLS Protocol Configuration
    # =================================================
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305;
    ssl_prefer_server_ciphers on;
    
    # Performance: SSL Session Caching
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 1d;
    ssl_session_tickets off;
    
    # =================================================
    # Logging (Include Client DN for Debugging)
    # =================================================
    access_log /var/log/nginx/secure.access.log combined;
    error_log /var/log/nginx/secure.error.log warn;
    
    # =================================================
    # Application
    # =================================================
    location / {
        # First, clear any client-provided values to prevent spoofing
        proxy_set_header X-Client-DN "";
        proxy_set_header X-Client-Verify "";
        proxy_set_header X-Client-Fingerprint "";
        
        # Then set authenticated values from Nginx's SSL module
        # Backend should only trust these when requests come directly from Nginx
        proxy_set_header X-Client-DN $ssl_client_s_dn;
        proxy_set_header X-Client-Verify $ssl_client_verify;
        proxy_set_header X-Client-Fingerprint $ssl_client_fingerprint;
        
        proxy_pass http://127.0.0.1:8080;
    }
}

Prueba y recarga

# Test configuration syntax sudo nginx -t # If successful, reload sudo systemctl reload nginx

Ahora, verifiquemos las conexiones usando curl.

1. Acceso Denegado (Sin Certificado):

Mostrar salida de curl (400 Bad Request)
> GET / HTTP/1.1
> Host: secure.example.com
> User-Agent: curl/8.14.1
> Accept: */*
> 
< HTTP/1.1 400 Bad Request
< Server: nginx
< Content-Type: text/html
< Content-Length: 230
< Connection: close
< 
<html>
<head><title>400 No required SSL certificate was sent</title></head>
<body>
<center><h1>400 Bad Request</h1></center>
<center>No required SSL certificate was sent</center>
<hr><center>nginx</center>
</body>
</html>

2. Acceso Concedido (Con Certificado Válido):

Mostrar salida de curl (200 OK)
* TLSv1.3 (IN), TLS handshake, Request CERT (13):
{ [210 bytes data]
* TLSv1.3 (OUT), TLS handshake, Certificate (11):
} [812 bytes data]
* TLSv1.3 (OUT), TLS handshake, CERT verify (15):
} [264 bytes data]
* TLSv1.3 (IN), TLS handshake, Finished (20):
{ [52 bytes data]
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384
* Server certificate:
*  subject: CN=secure.example.com
*  issuer: CN=secure.example.com
> GET / HTTP/1.1
> Host: secure.example.com
> User-Agent: curl/8.14.1
> 
< HTTP/1.1 200 OK
< Server: nginx
< Content-Type: text/plain
mTLS Authentication Successful! Client DN: CN=iphone-jmrp

Configuración de protocolo TLS y cipher suites

Una configuración TLS adecuada es fundamental para la seguridad.

Esto es lo que hace cada ajuste:

Selección de versión de protocolo

Versiones del Protocolo TLS
VersiónEstadoSeguridad
TLS 1.0ObsoletoVulnerable (BEAST, POODLE)
TLS 1.1ObsoletoCipher suites débiles, sin AEAD
TLS 1.2SoportadoSeguro con cipher suites adecuados
TLS 1.3RecomendadoMejor seguridad, handshake más rápido

Configuración recomendada

Ajustes de Seguridad TLS
# Only allow TLS 1.2 and 1.3
ssl_protocols TLSv1.2 TLSv1.3;

# Strong cipher suites (ECDHE for forward secrecy, GCM for AEAD)
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305;

# Server chooses the cipher (prevents downgrade attacks)
ssl_prefer_server_ciphers on;

# Session resumption for performance
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;

# Disable session tickets (more secure, less memory)
ssl_session_tickets off;

# Diffie-Hellman parameters for non-ECDHE (if needed)
# Generate with: openssl dhparam -out /etc/nginx/dhparam.pem 4096
ssl_dhparam /etc/nginx/dhparam.pem;

Control de acceso avanzado

Por defecto, cualquier cliente con un certificado válido firmado por tu CA puede acceder al sitio.

Puedes añadir controles granulares usando variables de Nginx.

Variables mTLS útiles

Nginx expone estas variables para la información del certificado de cliente:

Variables mTLS de Nginx
VariableContenidoEjemplo
$ssl_client_verifyResultado de la verificaciónSUCCESS, FAILED:reason
$ssl_client_s_dnDN del sujetoCN=iphone-jmrp
$ssl_client_i_dnDN del emisorCN=JMRP-IO-LAB Root CA
$ssl_client_fingerprintFingerprint SHA1AB:CD:EF:12:34:…
$ssl_client_serialNúmero de serie del certificado1000

Autorización basada en CN con map

Usa la directiva map para crear una lista de permitidos:

/​etc/​nginx/​conf.d/​mtls-access.conf
# Place this OUTSIDE the server block (in nginx.conf or included file)

# Map client DN to access permission
map $ssl_client_s_dn $mtls_access_allowed {
    default 0;
    
    # Allowed certificates by CN
    "CN=iphone-jmrp"      1;
    "CN=macbook-jmrp"     1;
    "CN=ipad-home"        1;
    
    # Pattern matching with regex
    ~"CN=admin-.*"        1;    # All admin-* certificates
}

Luego en tu bloque server:

Control de Acceso en el Bloque Server
server {
    # ... SSL configuration ...
    
    location / {
        # Verify certificate is valid (handled by ssl_verify_client on)
        # Then check our custom authorization
        if ($mtls_access_allowed = 0) {
            return 403 "Valid certificate, but not authorized for this resource.";
        }
        
        proxy_pass http://backend;
    }
}

Caso de uso avanzado: bypass de rate limit

Un beneficio de mTLS que a menudo se pasa por alto es la capacidad de confiar en clientes autenticados a nivel de red.

Por ejemplo, podrías querer aplicar protección estricta contra DDoS y Rate Limiting al internet público, pero permitir que tus propios dispositivos (autenticados mediante mTLS) tengan acceso ilimitado.

Puedes lograr esto en Nginx usando una directiva map para incluir en la lista blanca a los clientes verificados.

Ejemplo de configuración

/​etc/​nginx/​nginx.conf
http {
    # ...
    
    # 1. Map the verification status to a rate-limit key
    map $ssl_client_verify $limit_req_whitelist {
        "SUCCESS" "";                  # If verified, key is empty (no limit)
        default   $binary_remote_addr; # Otherwise, limit by IP
    }

    # 2. Define the zone using the map variable
    # If $limit_req_whitelist is empty, the request is not counted!
    limit_req_zone $limit_req_whitelist zone=req_limit_per_ip:10m rate=5r/s;

    server {
        # IMPORTANT: To allow unauthenticated clients to reach this logic,
        # you must set 'ssl_verify_client optional' instead of 'on'.
        # If set to 'on', Nginx rejects the handshake before the rate limiter runs.
        ssl_verify_client optional;

        # 3. Apply the limit
        limit_req zone=req_limit_per_ip burst=10 nodelay;
        
        # ...
    }
}

Revocación de certificados: CRL vs OCSP

¿Qué sucede cuando un dispositivo se pierde o se ve comprometido?

Necesitas revocar su certificado. Hay dos métodos:

Comparación CRL vs OCSP
AspectoCRL (Certificate Revocation List)OCSP (Online Certificate Status)
Cómo funcionaEl servidor descarga la lista completa de certificados revocadosEl servidor consulta al respondedor para un solo certificado
InmediatezCon retardo (depende del intervalo de actualización del CRL)Casi en tiempo real
Ancho de bandaMayor (descarga la lista completa)Menor (consulta individual)
Directiva Nginxssl_crlssl_stapling (para el certificado del servidor)
ComplejidadSimple (archivo en disco)Requiere un servicio respondedor OCSP

Método 1: CRL (recomendado para despliegues pequeños)

Paso 1: revocar un certificado

cd /etc/nginx/pki # Revoke the certificate sudo openssl ca -config openssl.cnf \ -revoke certs/lost-device.crt

Using configuration from openssl.cnf Revoking Certificate 1000. Database updated

Paso 2: generar el archivo CRL

# Generate the CRL sudo openssl ca -config openssl.cnf \ -gencrl -out crl/ca.crl # Verify the CRL openssl crl -in crl/ca.crl -text -noout

Paso 3: configurar Nginx

Añadir al bloque server
# Enable CRL checking
ssl_crl /etc/nginx/pki/crl/ca.crl;
sudo nginx -t && sudo systemctl reload nginx

Verificar la revocación

Acceder al sitio con el certificado revocado debería fallar ahora:

Mostrar salida de curl (Revocado - 400 Bad Request)
< HTTP/1.1 400 Bad Request
< Server: nginx
< Content-Type: text/html
< Content-Length: 208
< Connection: close
< 
<html>
<head><title>400 The SSL certificate error</title></head>
<body>
<center><h1>400 Bad Request</h1></center>
<center>The SSL certificate error</center>
<hr><center>nginx</center>
</body>
</html>

Método 2: OCSP (para despliegues más grandes)

Para empresas con muchos certificados, OCSP es más eficiente.

Esto requiere ejecutar un respondedor OCSP (fuera del alcance de esta guía, pero herramientas como step-ca lo proporcionan).


Gestión del ciclo de vida de los certificados

Un ciclo de vida completo incluye creación, distribución, monitorización, renovación y revocación.

El ciclo de vida del certificado

No

Generar Clave y CSR

Firmar con la CA

Exportar a PKCS12

Distribución Segura

Instalar en Dispositivo

Monitorizar Expiración

¿Expirado?

Renovar

¿Comprometido?

Revocar y Actualizar CRL

Flujo del Ciclo de Vida del Certificado

Scripts de automatización

Gestionar certificados manualmente es propenso a errores.

Aquí tienes dos scripts esenciales para automatizar el ciclo de vida.

1. Generador de certificados de cliente

Este script automatiza todo el proceso de creación

: generando la clave, el CSR, firmándolo y exportando el paquete PKCS#12.

/​usr/​local/​bin/​mtls-add-client.sh
#!/bin/bash
set -e

# Configuration
PKI_DIR="/etc/nginx/pki"
DAYS_VALID=365
CLIENT_NAME=$1
EXPORT_PASS=$2

if [ -z "$CLIENT_NAME" ]; then
    echo "Usage: $0 <client-name> [export-password]"
    exit 1
fi

# Validate CLIENT_NAME: only allow alphanumerics, dots, underscores, hyphens
# Reject path traversal sequences, leading dots, slashes, and other unsafe chars
if ! echo "$CLIENT_NAME" | grep -qE '^[A-Za-z0-9._-]+$'; then
    echo "❌ Error: CLIENT_NAME contains invalid characters."
    echo "   Only alphanumerics, dots, underscores, and hyphens are allowed."
    exit 1
fi

# Additional check to prevent path traversal patterns
if echo "$CLIENT_NAME" | grep -qE '(\.\.|^\.)|/'; then
    echo "❌ Error: CLIENT_NAME contains path traversal sequences or slashes."
    exit 1
fi

if [ -z "$EXPORT_PASS" ]; then
    echo "No password provided. Generating a random one..."
    EXPORT_PASS=$(openssl rand -base64 12)
    echo "Generated Password: $EXPORT_PASS"
fi

cd $PKI_DIR

echo "1. Generating private key for $CLIENT_NAME..."
openssl genrsa -out private/${CLIENT_NAME}.key 2048

echo "2. Creating CSR..."
openssl req -new -key private/${CLIENT_NAME}.key \
    -out certs/${CLIENT_NAME}.csr \
    -subj "/CN=${CLIENT_NAME}"

echo "3. Signing certificate..."
openssl ca -config openssl.cnf \
    -extensions v3_client \
    -days $DAYS_VALID \
    -batch \
    -in certs/${CLIENT_NAME}.csr \
    -out certs/${CLIENT_NAME}.crt

echo "4. Exporting to PKCS12 (.p12)..."
# Use stdin for password to avoid exposure in process list
echo "$EXPORT_PASS" | openssl pkcs12 -export \
    -out certs/${CLIENT_NAME}.p12 \
    -inkey private/${CLIENT_NAME}.key \
    -in certs/${CLIENT_NAME}.crt \
    -certfile ca/ca.crt \
    -name "${CLIENT_NAME}" \
    -passout stdin

echo "----------------------------------------"
echo "✅ Certificate created successfully!"
echo "Files:"
echo " - Key: $PKI_DIR/private/${CLIENT_NAME}.key"
echo " - Cert: $PKI_DIR/certs/${CLIENT_NAME}.crt"
echo " - Bundle: $PKI_DIR/certs/${CLIENT_NAME}.p12"
echo "----------------------------------------"
echo "⚠️  Export Password: $EXPORT_PASS"
echo "Transfer the .p12 file securely to the client device."
# Usage example: Generate certificate for a new device sudo /usr/local/bin/mtls-add-client.sh my-iphone securepassword

2. Script de auto-renovación

Este script comprueba los certificados próximos a expirar

, los revoca, refirma el CSR original y genera un nuevo paquete .p12.

/​usr/​local/​bin/​mtls-renew.sh
#!/bin/bash
# mTLS Certificate Renewal Script
set -e

PKI_DIR="/etc/nginx/pki"
DAYS_BEFORE_EXPIRY=30
RENEW_DAYS=365
RENEWED=false

echo "Checking for expiring certificates in $PKI_DIR/certs..."

while read -r cert; do
    # Extract expiration date
    end_date=$(openssl x509 -enddate -noout -in "$cert" | cut -d= -f2)
    end_epoch=$(date -d "$end_date" +%s)
    now_epoch=$(date +%s)
    days_left=$(( (end_epoch - now_epoch) / 86400 ))
    
    # Extract Common Name robustly (handles "CN=Name", "CN = Name", and trailing DN fields)
    CN=$(openssl x509 -subject -noout -nameopt RFC2253 -in "$cert" | \
         sed -n 's/.*CN=\([^,/]*\).*/\1/p' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
    
    if [ "$days_left" -lt "$DAYS_BEFORE_EXPIRY" ]; then
        echo "--------------------------------------------------"
        echo "⚠️  Certificate for \"$CN\" expires in $days_left days."
        echo "   (File: $cert)"
        echo "   Renewing now..."
        
        # 1. Archive old cert
        cp "$cert" "$cert.old.$(date +%F)"
        
        # 2. Re-sign the existing CSR
        # We assume CSR exists in certs/ directory as $CN.csr
        CSR="$PKI_DIR/certs/$CN.csr"
        if [ ! -f "$CSR" ]; then
            echo "   ❌ CSR not found at $CSR. Cannot renew automatically."
            continue
        fi
        
        # Revoke old cert (to clear DB index and maintain CRL)
        openssl ca -config $PKI_DIR/openssl.cnf \
            -revoke "$cert"
        
        # Regenerate CRL immediately after revocation
        openssl ca -config $PKI_DIR/openssl.cnf \
            -gencrl -out $PKI_DIR/crl/ca.crl
        
        # Re-sign
        openssl ca -config $PKI_DIR/openssl.cnf \
            -extensions v3_client \
            -days $RENEW_DAYS \
            -batch \
            -in "$CSR" \
            -out "$cert"
            
        echo "   ✅ Certificate re-signed. New validity: $RENEW_DAYS days."
        echo "   ✅ CRL regenerated."
        
        # 3. Generate new P12
        NEW_PASS=$(openssl rand -base64 12)
        P12="$PKI_DIR/certs/$CN.p12"
        KEY="$PKI_DIR/private/$CN.key"
        
        if [ -f "$KEY" ]; then
            # Use stdin for password to avoid exposure in process list
            echo "$NEW_PASS" | openssl pkcs12 -export \
                -out "$P12" \
                -inkey "$KEY" \
                -in "$cert" \
                -certfile $PKI_DIR/ca/ca.crt \
                -name "$CN" \
                -passout stdin
                
            echo "   📦 New PKCS#12 bundle created: $P12"
            echo "   🔑 New Export Password: $NEW_PASS"
        else
            echo "   ⚠️ Private key not found. Skipped P12 generation."
        fi
        
        # Mark that we renewed at least one certificate
        RENEWED=true
        echo "--------------------------------------------------"
    else
        echo "✅ $CN: $days_left days remaining."
    fi
done < <(find "$PKI_DIR/certs" -name "*.crt" -type f)

# Reload Nginx if any certificates were renewed
if [ "$RENEWED" = "true" ]; then
    echo ""
    echo "========================================"
    echo "🔄 Reloading Nginx to apply updated CRL..."
    echo "========================================"
    
    # Test configuration first
    if nginx -t 2>&1; then
        systemctl reload nginx
        echo "✅ Nginx reloaded successfully."
    else
        echo "❌ Nginx configuration test failed. Skipping reload."
        exit 1
    fi
fi

Programa esto en crontab para que se ejecute diariamente:

crontab -e
0 3 * * * /usr/local/bin/mtls-renew.sh >> /var/log/mtls-renew.log 2>&1

Instalando certificados en los clientes

Transfiere el archivo .p12 de forma segura

(AirDrop, USB, nube cifrada) e instálalo en cada dispositivo.

  1. Transfiere el archivo .p12 mediante AirDrop o guárdalo en la app Archivos
  2. Toca el archivo en Archivos
  3. Ve a AjustesPerfil Descargado (aparece en la parte superior)
  4. Toca Instalar e introduce el código de tu dispositivo
  5. Introduce la contraseña de exportación del certificado
  6. El certificado ya está instalado. Safari lo usará automáticamente al visitar el sitio con mTLS
  1. Haz doble clic en el archivo .p12
  2. Se abre Acceso a Llaveros. Introduce la contraseña de exportación
  3. Elige el llavero inicio de sesión (no Sistema)
  4. Haz clic en Añadir
  5. Reinicia tu navegador
  6. Al visitar el sitio, macOS te pregunta “Selecciona un Certificado”
  1. Haz doble clic en el archivo .p12
  2. Se abre el Asistente de Importación de Certificados
  3. Selecciona Usuario Actual (no Equipo Local)
  4. Introduce la contraseña
  5. Selecciona “Seleccionar automáticamente el almacén de certificados”
  6. Haz clic en Finalizar
  7. Reinicia tu navegador

Edge y Chrome presentarán ahora el certificado cuando sea necesario.

  1. Guarda el .p12 en el almacenamiento del dispositivo
  2. Ve a AjustesSeguridadCifrado y credenciales
  3. Toca Instalar un certificadoCertificado de usuario de VPN y aplicación
  4. Navega hasta el archivo .p12
  5. Introduce la contraseña y asigna un nombre
  6. El certificado queda instalado para Chrome y otras aplicaciones

Para acceso por línea de comandos/scripting usando archivos PEM:

# Using curl with client certificate
curl --cert /path/to/client.crt \
     --key /path/to/client.key \
     --cacert /path/to/ca.crt \
     https://secure.example.com

# Or with PKCS12
curl --cert-type P12 \
     --cert /path/to/client.p12:password \
     https://secure.example.com

Guía de solución de problemas

Errores comunes y soluciones

Diagnóstico de Errores mTLS
ErrorCausaSolución
400 No required SSL certificate was sentEl navegador no envió un certificadoComprueba que el certificado está instalado; reinicia el navegador; prueba en modo incógnito
400 The SSL certificate errorLa validación del certificado fallóVerifica que ssl_client_certificate apunta a la CA correcta
403 ForbiddenCertificado válido pero no autorizadoComprueba el CRL o las reglas map; examina el error_log
Bucle de selección de certificadoEl SO sigue preguntandoConfía en la CA en el keychain; reinicia el navegador
ssl_client_verify: FAILEDLa validación de la cadena fallóAsegúrate de que la cadena completa está en ssl_client_certificate

Comandos de depuración

Verificar el certificado contra la CA

# Verify a client certificate is valid against your CA openssl verify -CAfile /etc/nginx/pki/ca/ca.crt \ /etc/nginx/pki/certs/client.crt

Probar la conexión mTLS con curl

# Test mTLS connection curl -v --cert client.crt --key client.key \ --cacert ca.crt https://secure.example.com # If using PKCS12 curl -v --cert-type P12 --cert client.p12:password \ https://secure.example.com

Comprobar los logs de error de Nginx

# Watch error log in real-time tail -f /var/log/nginx/error.log | grep ssl

2026/01/12 12:00:00 [info] 1234#5678: *1 client certificate verification failed: certificate revoked, client: 192.168.1.100, server: secure.example.com


Mejores prácticas de seguridad

Gestión de claves

Lista de verificación

  1. Usa claves RSA de 4096 bits para la CA (2048 como mínimo para clientes)
  2. Habilita CRL/OCSP para revocar certificados comprometidos
  3. Validez corta de certificados de cliente (1 año máximo, más corta para alta seguridad)
  4. Solo TLS 1.2+, con cipher suites robustos
  5. Almacenamiento en caché de sesiones para rendimiento
  6. Monitoriza la expiración y automatiza la renovación
  7. CAs separadas para diferentes niveles de confianza si es necesario
  8. Registra los DNs de los clientes para trazabilidad de auditoría
  9. Rotación periódica de certificados de cliente

Profundización

Documentación Oficial

Tutoriales y Guías

Herramientas

  • step-ca — Certificate authority moderna
  • cfssl — Kit de herramientas PKI de Cloudflare
  • easy-rsa — Scripts simples de gestión de CA