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.

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.
| Método | ¿Phishing posible? | Almacenamiento de credenciales | Automatización | Zero Trust |
|---|---|---|---|---|
| Usuario/Contraseña | Sí | Base de datos del servidor | Fácil | No |
| API Keys | Sí (si se exponen) | Servidor + cliente | Fácil | No |
| Contraseña + 2FA | Parcialmente | Servidor + autenticador | Difícil | Parcial |
| mTLS | No | Hardware/keychain | Medio | Sí |
Por qué mTLS no puede ser víctima de phishing
A diferencia de las contraseñas, los certificados de cliente:
- Nunca se transmiten — solo se envía una firma que demuestra la posesión
- Están vinculados al hardware — almacenados en keychains/TPMs seguros
- Requieren acceso a la clave privada — la clave privada nunca sale del dispositivo
- Se validan criptográficamente — falsificarlos es computacionalmente inviable
TLS Unidireccional vs Mutual TLS
Entender la diferencia es fundamental:
| Aspecto | TLS Unidireccional (HTTPS Estándar) | Mutual TLS (mTLS) |
|---|---|---|
| El servidor se autentica ante | Cliente (navegador) | Cliente (navegador) |
| El cliente se autentica ante | No se verifica | El servidor verifica el certificado del cliente |
| Requisito de certificado | Solo el servidor | Tanto servidor como cliente |
| Caso de uso | Sitios web públicos | APIs, 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:
Pasos clave explicados
- ClientHello: El cliente inicia TLS, proponiendo cipher suites
- ServerHello: El servidor responde con su certificado
- CertificateRequest: El servidor solicita al cliente que demuestre su identidad
- Certificado del Cliente: El cliente envía su certificado
- CertificateVerify: El cliente demuestra que posee la clave privada (sin revelarla)
- 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:
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:
| Parámetro | Recomendación | Justificación |
|---|---|---|
| Tamaño de Clave | RSA de 4096 bits o ECDSA P-384 | Mayor margen de seguridad contra ataques clásicos |
| Validez | 10 años para la CA | Más largo que cualquier certificado de cliente |
| Algoritmo Hash | SHA-256 como mínimo | SHA-1 está obsoleto |
| Extensiones | CA:TRUE, keyUsage:keyCertSign,cRLSign | Necesario para firmar certificados y CRLs |
Crea la configuración de OpenSSL
Crea un archivo de configuración completo de OpenSSL:
# =============================================================
# 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 = clientAuthDesglose 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.
| Directiva | Valor/Tipo | Descripción |
|---|---|---|
dir | Ruta (String) | Directorio base de tu PKI (p. ej., /etc/nginx/pki). |
database | Ruta de archivo | Archivo de texto (index.txt) que registra todos los certificados emitidos y revocados. |
new_certs_dir | Ruta de directorio | Donde se archivan copias de cada certificado emitido por número de serie. |
certificate | Ruta de archivo | El certificado público de la CA utilizado para firmar otros. |
private_key | Ruta de archivo | La clave privada de la CA. Debe estar protegida. |
default_days | Entero (Días) | Período de validez por defecto para los certificados emitidos (p. ej., 365). |
policy | Nombre de sección | Qué sección de política aplicar para la coincidencia de campos DN (p. ej., policy_loose). |
copy_extensions | copy | none | Crucial: 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.
| Campo | Regla | Significado |
|---|---|---|
countryName | optional | El cliente no necesita especificar un país. |
commonName | supplied | Obligatorio. El cliente debe proporcionar un Common Name (utilizado para identificación). |
organizationName | optional | Puede dejarse vacío. |
3. Configuración de solicitudes ([ req ])
Valores por defecto utilizados al ejecutar openssl req para generar claves o CSRs.
| Directiva | Valor | Descripción |
|---|---|---|
default_bits | Entero (p. ej., 4096) | Tamaño de clave por defecto si no se especifica. |
distinguished_name | Nombre de sección | Apunta a la sección que define los valores DN por defecto ([ req_distinguished_name ]). |
x509_extensions | Nombre de sección | Extensiones 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.
| Perfil/Directiva | Valor | Efecto |
|---|---|---|
| [ v3_ca ] - Para la CA Raíz | ||
basicConstraints | critical, CA:TRUE | Identidad: Marca este certificado como una Certificate Authority que puede firmar otros. |
keyUsage | keyCertSign, cRLSign | Permisos: Autorizado para firmar certificados y CRLs. |
| [ v3_client ] - Para Dispositivos de Usuario | ||
basicConstraints | critical, CA:FALSE | Restricción: Este certificado no puede utilizarse para firmar otros certificados. |
extendedKeyUsage | clientAuth | Propósito: Válido únicamente para autenticar un cliente ante un servidor (mTLS). |
Genera la clave y el certificado de la CA
Verifica tu certificado de CA
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?
| Aspecto | Common Name (CN) | Subject Alternative Name (SAN) |
|---|---|---|
| Estándar | Legacy (obsoleto para certificados de servidor) | Moderno, conforme con RFC 6125 |
| Múltiples identidades | No | Sí (DNS, email, URI) |
| Ideal para | Identificación simple de cliente | Servicios, 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
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
| Formato | Contiene | Ideal Para | Protegido por contraseña |
|---|---|---|---|
| PEM (.crt, .key) | Archivos de texto separados | Servidores Linux, scripting | Opcional |
| PKCS12 (.p12, .pfx) | Paquete binario todo-en-uno | Navegadores, móvil, Windows | Obligatorio |
Crea el paquete PKCS12 para clientes
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:
| Valor | Comportamiento | Caso de Uso |
|---|---|---|
on | Requiere certificado válido; rechaza sin él | Control de acceso estricto (recomendado) |
optional | Acepta si es válido; continúa sin él | Autenticación híbrida, verificación opcional |
optional_no_ca | Acepta cualquier certificado, sin validación de CA | Solo desarrollo/pruebas |
off | No se requiere certificado de cliente | HTTPS estándar |
Configuración completa de Nginx
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
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-jmrpConfiguració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
| Versión | Estado | Seguridad |
|---|---|---|
| TLS 1.0 | Obsoleto | Vulnerable (BEAST, POODLE) |
| TLS 1.1 | Obsoleto | Cipher suites débiles, sin AEAD |
| TLS 1.2 | Soportado | Seguro con cipher suites adecuados |
| TLS 1.3 | Recomendado | Mejor seguridad, handshake más rápido |
Configuración recomendada
# 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:
| Variable | Contenido | Ejemplo |
|---|---|---|
$ssl_client_verify | Resultado de la verificación | SUCCESS, FAILED:reason |
$ssl_client_s_dn | DN del sujeto | CN=iphone-jmrp |
$ssl_client_i_dn | DN del emisor | CN=JMRP-IO-LAB Root CA |
$ssl_client_fingerprint | Fingerprint SHA1 | AB:CD:EF:12:34:… |
$ssl_client_serial | Número de serie del certificado | 1000 |
Autorización basada en CN con map
Usa la directiva map para crear una lista de permitidos:
# 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:
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
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:
| Aspecto | CRL (Certificate Revocation List) | OCSP (Online Certificate Status) |
|---|---|---|
| Cómo funciona | El servidor descarga la lista completa de certificados revocados | El servidor consulta al respondedor para un solo certificado |
| Inmediatez | Con retardo (depende del intervalo de actualización del CRL) | Casi en tiempo real |
| Ancho de banda | Mayor (descarga la lista completa) | Menor (consulta individual) |
| Directiva Nginx | ssl_crl | ssl_stapling (para el certificado del servidor) |
| Complejidad | Simple (archivo en disco) | Requiere un servicio respondedor OCSP |
Método 1: CRL (recomendado para despliegues pequeños)
Paso 1: revocar un certificado
Using configuration from openssl.cnf Revoking Certificate 1000. Database updated
Paso 2: generar el archivo CRL
Paso 3: configurar Nginx
# Enable CRL checking
ssl_crl /etc/nginx/pki/crl/ca.crl;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
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."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
fiPrograma esto en crontab para que se ejecute diariamente:
0 3 * * * /usr/local/bin/mtls-renew.sh >> /var/log/mtls-renew.log 2>&1Instalando certificados en los clientes
Transfiere el archivo .p12 de forma segura
(AirDrop, USB, nube cifrada) e instálalo en cada dispositivo.
- Transfiere el archivo
.p12mediante AirDrop o guárdalo en la app Archivos - Toca el archivo en Archivos
- Ve a Ajustes → Perfil Descargado (aparece en la parte superior)
- Toca Instalar e introduce el código de tu dispositivo
- Introduce la contraseña de exportación del certificado
- El certificado ya está instalado. Safari lo usará automáticamente al visitar el sitio con mTLS
- Haz doble clic en el archivo
.p12 - Se abre Acceso a Llaveros. Introduce la contraseña de exportación
- Elige el llavero inicio de sesión (no Sistema)
- Haz clic en Añadir
- Reinicia tu navegador
- Al visitar el sitio, macOS te pregunta “Selecciona un Certificado”
- Haz doble clic en el archivo
.p12 - Se abre el Asistente de Importación de Certificados
- Selecciona Usuario Actual (no Equipo Local)
- Introduce la contraseña
- Selecciona “Seleccionar automáticamente el almacén de certificados”
- Haz clic en Finalizar
- Reinicia tu navegador
Edge y Chrome presentarán ahora el certificado cuando sea necesario.
- Guarda el
.p12en el almacenamiento del dispositivo - Ve a Ajustes → Seguridad → Cifrado y credenciales
- Toca Instalar un certificado → Certificado de usuario de VPN y aplicación
- Navega hasta el archivo
.p12 - Introduce la contraseña y asigna un nombre
- 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.comGuía de solución de problemas
Errores comunes y soluciones
| Error | Causa | Solución |
|---|---|---|
400 No required SSL certificate was sent | El navegador no envió un certificado | Comprueba que el certificado está instalado; reinicia el navegador; prueba en modo incógnito |
400 The SSL certificate error | La validación del certificado falló | Verifica que ssl_client_certificate apunta a la CA correcta |
403 Forbidden | Certificado válido pero no autorizado | Comprueba el CRL o las reglas map; examina el error_log |
| Bucle de selección de certificado | El SO sigue preguntando | Confía en la CA en el keychain; reinicia el navegador |
ssl_client_verify: FAILED | La 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
Probar la conexión mTLS con curl
Comprobar los logs de error de Nginx
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
- Usa claves RSA de 4096 bits para la CA (2048 como mínimo para clientes)
- Habilita CRL/OCSP para revocar certificados comprometidos
- Validez corta de certificados de cliente (1 año máximo, más corta para alta seguridad)
- Solo TLS 1.2+, con cipher suites robustos
- Almacenamiento en caché de sesiones para rendimiento
- Monitoriza la expiración y automatiza la renovación
- CAs separadas para diferentes niveles de confianza si es necesario
- Registra los DNs de los clientes para trazabilidad de auditoría
- Rotación periódica de certificados de cliente
Profundización
Documentación Oficial
- Nginx ngx_http_ssl_module — Todas las directivas SSL/TLS
- Documentación de CA de OpenSSL — Comandos de la certificate authority
Tutoriales y Guías
- Smallstep Hello mTLS — Tutorial interactivo de mTLS
- Guía de mTLS de SSL.com — Autenticación de IoT y usuarios
- Cloudflare: ¿Qué es mTLS? — Resumen conceptual