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

> Una entrada de jmrp.io, publicada como documento propio. Índice: https://jmrp.io/llms-full.txt

Canonical: https://jmrp.io/es/blog/001-secure-nginx-client-certificates/
Language: es
Alternate: https://jmrp.io/blog/001-secure-nginx-client-certificates/index.md
License: https://creativecommons.org/licenses/by/4.0/
Type: TechArticle
Published: 2025-12-16
Updated: 2026-09-02
Instructions re-tested: 2026-08-22 · nginx 1.31.3
Author: José Manuel Requena Plens
Summary: Implementa Mutual TLS (mTLS) con Nginx — crea una CA, genera certificados de cliente, configura revocación CRL y OCSP, y habilita Zero Trust.
Tags: Nginx, Security, Cryptography
Topics: Mutual authentication (Q6944186), Transport Layer Security (Q206494), Nginx (Q306144), Public-key infrastructure (Q658476), OpenSSL (Q1052790), Zero trust architecture (Q104852562)
Build-Date: 2026-09-06

Preguntas que responde:

**¿Qué es Mutual TLS (mTLS)?**

Mutual TLS requiere que el cliente presente un certificado criptográfico firmado por tu propia Certificate Authority, de modo que tanto el servidor como el cliente verifican la identidad del otro. Si el cliente no tiene un certificado válido, Nginx rechaza la conexión antes de que la aplicación siquiera se cargue.

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

Los certificados de cliente nunca se transmiten (solo se envía una firma que demuestra la posesión), están vinculados al hardware en keychains/TPMs seguros y requieren acceso a la clave privada, que nunca sale del dispositivo. Aunque un atacante robe tu contraseña e intercepte tu código 2FA, seguiría sin poder acceder al recurso sin el archivo de certificado en un dispositivo autorizado.

**¿Qué controla la directiva ssl_verify_client en Nginx?**

Controla cómo Nginx gestiona los certificados de cliente: on requiere un certificado válido y rechaza las conexiones sin él, optional acepta un certificado válido pero continúa sin él, optional_no_ca acepta cualquier certificado sin validación de CA (solo desarrollo) y off no requiere certificado de cliente. Nunca uses optional_no_ca en producción, ya que anula el propósito de mTLS.

**¿Cómo revoco un certificado de cliente perdido o comprometido?**

Usa openssl ca -revoke para revocar el certificado, luego regenera el CRL con openssl ca -gencrl y apunta Nginx a él con ssl_crl. Debes regenerar el CRL y recargar Nginx cada vez que revoques un certificado.

**¿Cuál es la diferencia entre la revocación CRL y OCSP?**

Con CRL el servidor descarga la lista completa de certificados revocados (simple, archivo en disco, pero con retardo y mayor ancho de banda), mientras que OCSP consulta a un respondedor por un solo certificado (casi en tiempo real y menor ancho de banda, pero requiere ejecutar un servicio respondedor OCSP). CRL se recomienda para despliegues pequeños; OCSP es más eficiente para los más grandes.

**¿Qué tamaños de clave debo usar para la CA y los certificados de cliente?**

Usa RSA de 4096 bits (o ECDSA P-384) para la CA, con un mínimo de 2048 bits para los clientes. La CA debería ser válida durante unos 10 años, mientras que los certificados de cliente deberían tener una validez corta de un año como máximo.


Pasos (Asegurar Nginx con certificados de cliente mutual TLS (mTLS)):
1. Preparar el directorio PKI
2. Crear la certificate authority
3. Generar certificados de cliente
4. Exportar un paquete PKCS12
5. Configurar Nginx para mTLS
6. Añadir revocación de certificados (CRL)
7. Instalar certificados en los dispositivos cliente

---

**[Mutual TLS (mTLS)](https://www.cloudflare.com/learning/access-management/what-is-mutual-tls/)** 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](https://smallstep.com/hello-mtls), [SSL.com](https://www.ssl.com/article/authenticating-users-and-iot-devices-with-mutual-tls/) y la [documentación oficial de Nginx](https://nginx.org/en/docs/http/ngx_http_ssl_module.html).

## TL;DR — mTLS en Nginx de un vistazo

- **mTLS no puede ser víctima de phishing** porque el cliente debe presentar un certificado firmado por tu propia CA; la clave privada nunca sale del dispositivo ni se transmite, así que Nginx rechaza las conexiones sin un certificado válido **antes** de que la aplicación se cargue.
- **Crea tu propia PKI**: genera una CA **RSA de 4096 bits** válida 10 años y firma certificados de cliente por dispositivo (mínimo 2048 bits, validez de 1 año), distribuyéndolos como paquetes **PKCS12 (.p12)** protegidos por contraseña.
- **Actívalo en Nginx** con `ssl_client_certificate` (tu CA), `ssl_verify_client on` y `ssl_verify_depth` — o `optional` más un `map` para exigirlo solo en rutas críticas (ver mi despliegue más abajo); nunca uses `optional_no_ca` en producción. Restringe a **TLS 1.2/1.3** con cipher suites ECDHE fuertes.
- **Añade control granular** con un `map` sobre `$ssl_client_s_dn` para listas de permitidos por CN, y usa `$ssl_client_verify` para que los clientes autenticados **eviten el rate limiting**.
- **Revoca certificados comprometidos** mediante **CRL** (`openssl ca -revoke` + `-gencrl`, luego `ssl_crl`) para despliegues pequeños u **OCSP** para los más grandes — regenera el CRL y recarga Nginx cada vez. Automatiza la creación y renovación con scripts.

---

## ¿Por qué Zero Trust se apoya en mTLS y no en contraseñas?

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](https://www.cloudflare.com/learning/security/glossary/what-is-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 credenciales | Automatización | Zero Trust |
| --- | --- | --- | --- | --- |
| **Usuario/Contraseña** | Sí [mal] | Base de datos del servidor | Fácil | No [mal] |
| **API Keys** | Sí (si se exponen) [mal] | Servidor + cliente | Fácil | No [mal] |
| **Contraseña + 2FA** | Parcialmente [cuidado] | Servidor + autenticador | Difícil | Parcial [cuidado] |
| **mTLS** | No [bien] | Hardware/keychain | Medio | Sí [bien] |

### 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

**Idea clave**

**Punto Clave:** Con mTLS, incluso si un atacante roba tu contraseña E intercepta tu código 2FA, seguiría sin poder acceder al recurso protegido sin el archivo de certificado instalado en un dispositivo autorizado.

---

## Cómo lo ejecuto en mi propia infraestructura

Nada de lo que sigue es teoría: este blog se sirve desde el mismo servidor donde mTLS protege mis servicios sensibles desde enero de 2025. Mi CA privada ("JMRP DNS CA", emitida con una validez de 10 años) firma los certificados de cliente que dan acceso a cuatro servicios: el panel de Grafana, la galería de fotos (Immich), el NAS y la propia interfaz de administración de Nginx.

Un detalle que ningún tutorial me contó y que solo aprendí desplegándolo: en producción **no** uso `ssl_verify_client on` a secas. Uso `optional` combinado con un `map` y enforcement selectivo por *location*:

```nginx
map $ssl_client_verify $client_verified {
    "SUCCESS" 1;
    default   0;
}

# En las rutas críticas (p. ej. la API de subida de Immich):
location = /api/assets {
    if ($client_verified = 0) {
        return 403 "Invalid certificate";
    }
    # ...
}
```

¿Por qué? Con `on` a nivel de servidor, cualquier cliente sin certificado recibe un error TLS críptico antes de llegar siquiera a Nginx — imposible de depurar para un usuario no técnico y sin posibilidad de degradar a otra capa de autenticación. Con `optional` + `map`, el handshake siempre se completa y soy yo quien decide, ruta a ruta, qué exige certificado (las APIs de escritura devuelven un 403 explícito) y qué puede caer a una segunda capa. Esa segunda capa existe: las peticiones desde mi LAN pasan por un allowlist de red independiente, de modo que el certificado nunca es el único control — defensa en profundidad, no una llave maestra única.

En cuanto a la emisión: mantengo un flujo con OpenSSL por dispositivo (un CSR, una config y una ficha de metadatos por cliente), con `ssl_verify_depth 2` en el servidor. Hoy hay tres certificados activos, cada uno ligado a un dispositivo y una persona concretos — cuando un dispositivo se retira, su certificado se retira con él.

Una confesión honesta: **no publico CRL ni OCSP**. Con tres certificados en dispositivos que controlo físicamente, revocar a esta escala significa reemitir desde la CA — un compromiso razonable aquí que sería inaceptable con decenas de clientes. Si tu flota crece, `ssl_crl` deja de ser opcional.

## ¿Qué diferencia hay entre TLS unidireccional y mutual TLS?

Entender la diferencia es fundamental:

**TLS Unidireccional vs Mutual TLS**

| 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 [mal] | El servidor verifica el certificado del cliente [bien] |
| **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.

---

## ¿Qué ocurre durante un handshake mTLS?

Entender el handshake ayuda con la depuración.

Aquí está el flujo completo:

**Flujo del Handshake mTLS en Nginx**

```mermaid
sequenceDiagram
    participant C as Cliente
    participant N as Servidor Nginx
    
    Note over C,N: Conexión TCP Establecida
    
    C->>N: ClientHello (versión TLS, cipher suites)
    N->>C: ServerHello + Certificado del Servidor
    N->>C: CertificateRequest
    Note right of N: "Envíame TU certificado"
    
    C->>N: Certificado del Cliente
    C->>N: CertificateVerify (prueba de firma)
    C->>N: Finished
    
    Note over N: Pipeline de Verificación
    N->>N: 1. Validar cadena de firma CA
    N->>N: 2. Comprobar contra CRL/OCSP
    N->>N: 3. Verificar CN/SAN si es necesario
    
    alt Todas las Comprobaciones Pasan
        N->>C: Finished + 200 OK
        Note over C,N: Comienza la Sesión Cifrada
    else Alguna Comprobación Falla
        N->>C: 400 Bad Request / 403 Forbidden
        Note over C,N: Conexión Terminada
    end
```

### 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](https://docs.openssl.org/) (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:

```bash
# 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
```

**Información**

**Estructura de Directorios Explicada:**

- `ca/` — Certificados de la Certificate Authority
- `certs/` — Certificados de cliente y servidor emitidos
- `private/` — Claves privadas (¡chmod 700!)
- `crl/` — Listas de Revocación de Certificados (CRL)
- `newcerts/` — Archivo de certificados emitidos por OpenSSL

---

## Crear tu certificate authority

Una CA es la raíz de confianza en tu PKI. Los certificados que emite siguen el perfil [X.509 v3](https://datatracker.ietf.org/doc/html/rfc5280) (RFC 5280), el mismo estándar sobre el que se asienta la PKI pública de la web.

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á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:

**Fichero: `/etc/nginx/pki/openssl.cnf`**

```ini
# =============================================================
# 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**

| 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.

**Reglas de Política**

| 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.

**Valores por Defecto de Solicitudes**

| 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.

**Perfiles de Extensiones**

| 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

```bash
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

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

**Salida — Detalles del Certificado de la CA**

```text
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
```

---

## Generar 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)**

| 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 [mal] | Sí (DNS, email, URI) [bien] |
| **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

```bash
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
```

**Salida de la Firma del Certificado**

```text
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 o PEM: qué formato de exportación necesitas?

**Comparación de Formatos de Certificado**

| 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

```bash
# 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
```

**Advertencia**

**Seguridad:** Elige una contraseña de exportación fuerte. Esta contraseña protege el certificado durante la transferencia a los dispositivos cliente. Transmítela por separado del archivo .p12 (p. ej., mediante un mensaje cifrado o en persona).

---

## Configurar Nginx para mTLS

Ahora configura Nginx para requerir y validar certificados de cliente.

### ¿Qué opción de ssl_verify_client deberías usar?

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

**Opciones de ssl_verify_client**

| 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 |

**Advertencia**

**¡Nunca uses `optional_no_ca` en producción!** Acepta _cualquier_ certificado sin validar la cadena de CA, anulando el propósito de mTLS.

### Configuración completa de Nginx

**Fichero: `/etc/nginx/sites-available/secure.example.com.conf`**

```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;
    }
}
```

**Advertencia**

**Advertencia sobre Límites de Confianza:** Los headers `X-Client-DN`, `X-Client-Verify` y `X-Client-Fingerprint` pueden ser falsificados por clientes maliciosos si los proxies intermedios no los limpian. Asegúrate de que:

- Cualquier load balancer o reverse proxy delante de Nginx elimine estos headers antes de reenviar
- Las aplicaciones backend solo confíen en estos headers cuando las solicitudes provienen directamente de Nginx (valida la IP de origen o usa mTLS entre Nginx y el backend)
- Considera usar una interfaz de red interna exclusiva para la conexión entre Nginx y el backend

### Prueba y recarga

```bash
# Test configuration syntax
sudo nginx -t

# If successful, reload
sudo systemctl reload nginx
```

Ahora, verifiquemos las conexiones usando `curl`.

**1. Acceso Denegado (Sin Certificado):**

**Salida — curl -v https://secure.example.com**

```text
> 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):**

**Salida — curl -v --cert client.crt --key client.key https://secure.example.com**

```text
* 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

En una configuración moderna solo caben TLS 1.2 y TLS 1.3. [TLS 1.3](https://datatracker.ietf.org/doc/html/rfc8446) (RFC 8446) es el estándar actual, con un handshake más rápido y conjuntos de cifrado exclusivamente AEAD:

**Versiones del Protocolo TLS**

| Versión | Estado | Seguridad |
| --- | --- | --- |
| TLS 1.0 | Obsoleto [mal] | Vulnerable (BEAST, POODLE) |
| TLS 1.1 | Obsoleto [mal] | Cipher suites débiles, sin AEAD |
| TLS 1.2 | Soportado [bien] | Seguro con cipher suites adecuados |
| TLS 1.3 | Recomendado [bien] | Mejor seguridad, handshake más rápido |

### Configuración recomendada

**Ajustes de Seguridad TLS**

```nginx
# 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;
```

**Consejo**

**Consejo de Rendimiento:** El almacenamiento en caché de sesiones SSL reduce significativamente la sobrecarga del handshake. Una caché de 10 MB puede almacenar ~40.000 sesiones. Esto es especialmente importante para mTLS donde los handshakes son más costosos.

---

## ¿Cómo limitar a qué puede acceder cada certificado de cliente?

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**

| 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:

**Fichero: `/etc/nginx/conf.d/mtls-access.conf`**

```nginx
# 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**

```nginx
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

**Fichero: `/etc/nginx/nginx.conf`**

```nginx
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;
        
        # ...
    }
}
```

**Idea clave**

**Cómo funciona:** La directiva `limit_req_zone` de Nginx ignora cualquier solicitud donde la variable clave está vacía. Al mapear `SUCCESS` a una cadena vacía, los clientes autenticados con mTLS evitan completamente el rate limiter, mientras que el resto se rastrea por su dirección IP.

---

## 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**

| 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

```bash
cd /etc/nginx/pki

# Revoke the certificate
sudo openssl ca -config openssl.cnf \
    -revoke certs/lost-device.crt
```

**Salida de la Revocación**

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

#### Paso 2: generar el archivo CRL

```bash
# 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**

```nginx
# Enable CRL checking
ssl_crl /etc/nginx/pki/crl/ca.crl;
```

```bash
sudo nginx -t && sudo systemctl reload nginx
```

**Advertencia**

**Importante:** Debes regenerar el CRL y recargar Nginx **cada vez** que revoques un certificado. Considera automatizar esto con un cron job.

### Verificar la revocación

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

**Salida — curl -v --cert revoked.crt ...**

```text
< 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](https://smallstep.com/docs/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

**Flujo del Ciclo de Vida del Certificado**

```mermaid
flowchart LR
    A[Generar Clave y CSR] --> B[Firmar con la CA]
    B --> C[Exportar a PKCS12]
    C --> D[Distribución Segura]
    D --> E[Instalar en Dispositivo]
    E --> F[Monitorizar Expiración]
    F --> G{¿Expirado?}
    G -->|No| F
    G -->|Sí| H[Renovar]
    H --> B
    E --> I{¿Comprometido?}
    I -->|Sí| J[Revocar y Actualizar CRL]
    J --> F
```

## 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.

**Fichero: `/usr/local/bin/mtls-add-client.sh`**

```bash
#!/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."
```

```bash
# 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`.

**Fichero: `/usr/local/bin/mtls-renew.sh`**

```bash
#!/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
```

**Advertencia**

**Nota de Plataforma:** Este script usa `date -d` de GNU, que es específico de Linux. En macOS/BSD, necesitarás modificar el cálculo de `end_epoch` usando `date -j -f` o utilizar una herramienta multiplataforma como el módulo `datetime` de Python.

Programa esto en crontab para que se ejecute diariamente:

**crontab -e**

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

---

## Instalar certificados en los clientes

Transfiere el archivo `.p12` de forma segura

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

**Opción 1/5 — iOS (iPhone/iPad)**

1. Transfiere el archivo `.p12` mediante **AirDrop** o guárdalo en la app **Archivos**
2. Toca el archivo en Archivos
3. Ve a **Ajustes** → **Perfil 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

**Consejo**

**Consejo:** Si Safari no muestra el aviso del certificado, prueba primero con navegación privada, luego con el modo normal.

**Opción 2/5 — macOS**

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"

**Información**

**Nota:** Puede que necesites configurar el certificado como "Confiar Siempre" en Acceso a Llaveros si se solicita.

**Opción 3/5 — Windows**

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.

**Opción 4/5 — Android**

1. Guarda el `.p12` en el almacenamiento del dispositivo
2. Ve a **Ajustes** → **Seguridad** → **Cifrado y credenciales**
3. Toca **Instalar un certificado** → **Certificado 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

**Opción 5/5 — Linux (curl)**

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

```bash
# 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**

| 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

```bash
# 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

```bash
# 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

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

**Salida — Ejemplo de Entrada en el Log de Errores**

```text
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

**Advertencia**

**Crítico:** Tu clave privada de la CA (`ca.key`) es la joya de la corona. Si se ve comprometida, un atacante puede emitir certificados válidos. Protégela con:

- Permisos de archivo: `chmod 400 ca.key`
- Separada del servidor web (aislada en un equipo sin red para alta seguridad)
- HSM para despliegues empresariales

### 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

- [Nginx ngx_http_ssl_module](https://nginx.org/en/docs/http/ngx_http_ssl_module.html) — Todas las directivas SSL/TLS
- [Documentación de CA de OpenSSL](https://docs.openssl.org/3.5/man1/openssl-ca/) — Comandos de la certificate authority

### Tutoriales y Guías

- [Smallstep Hello mTLS](https://smallstep.com/hello-mtls) — Tutorial interactivo de mTLS
- [Guía de mTLS de SSL.com](https://www.ssl.com/article/authenticating-users-and-iot-devices-with-mutual-tls/) — Autenticación de IoT y usuarios
- [Cloudflare: ¿Qué es mTLS?](https://www.cloudflare.com/learning/access-management/what-is-mutual-tls/) — Resumen conceptual

### Herramientas

- [step-ca](https://smallstep.com/docs/step-ca/) — Certificate authority moderna
- [cfssl](https://github.com/cloudflare/cfssl) — Kit de herramientas PKI de Cloudflare
- [easy-rsa](https://github.com/OpenVPN/easy-rsa) — Scripts simples de gestión de CA

