En TLS normal solo el servidor demuestra su identidad con un certificado. Con TLS mutuo (mTLS) el cliente también presenta un certificado, y el servidor rechaza la conexión si no está firmado por una autoridad de certificación (CA) en la que confía. Es una forma robusta de proteger APIs internas, paneles de administración o la comunicación entre servicios sin depender de contraseñas ni tokens. En esta guía crearás una CA privada con OpenSSL, emitirás un certificado de servidor y otro de cliente, configurarás Nginx en Ubuntu 24.04 para exigirlos y aprenderás a bloquear un certificado comprometido.
Requisitos previos
- Un servidor con Ubuntu 24.04 LTS con Nginx instalado (
sudo apt install nginx), por ejemplo un VPS de CubePath. - Un usuario no root con privilegios
sudo. - Un nombre DNS para el servicio, en los ejemplos
api.your_domain, que resuelva a la IP del servidor. Para pruebas puedes añadirlo al/etc/hostsde tu equipo. - El puerto 443/TCP abierto:
sudo ufw allow 443/tcp. - Una aplicación escuchando en
127.0.0.1:8080a la que Nginx hará de proxy. Si aún no la tienes, puedes probar conpython3 -m http.server 8080 --bind 127.0.0.1.
Paso 1: Crear la autoridad de certificación
La CA es el par de clave y certificado con el que firmarás todos los demás certificados. Quien tenga su clave privada puede emitir certificados de cliente válidos, así que guárdala en un directorio al que solo accede root y, en producción, en una máquina distinta del servidor.
sudo -i
mkdir -p /root/mtls-ca
chmod 700 /root/mtls-ca
cd /root/mtls-ca
sudo -i abre una shell de root. A partir de aquí, hasta el paso 4, los comandos se ejecutan como root dentro de /root/mtls-ca. Genera la clave de la CA (ECDSA P-256) y un certificado autofirmado válido 10 años:
openssl ecparam -name prime256v1 -genkey -noout -out ca.key
chmod 600 ca.key
openssl req -x509 -new -key ca.key -sha256 -days 3650 -subj "/O=Your Company/CN=Your Company Internal CA" -out ca.crt
Con -x509, OpenSSL aplica la sección v3_ca de /etc/ssl/openssl.cnf, que marca el certificado como CA. Compruébalo:
openssl x509 -in ca.crt -noout -subject -ext basicConstraints
subject=O = Your Company, CN = Your Company Internal CA
X509v3 Basic Constraints: critical
CA:TRUE
Paso 2: Emitir el certificado del servidor
Los clientes modernos ignoran el campo CN y validan el nombre con la extensión Subject Alternative Name (SAN), así que el certificado del servidor debe incluirla. Crea un archivo de extensiones:
nano server.ext
basicConstraints = critical, CA:FALSE
keyUsage = critical, digitalSignature
extendedKeyUsage = serverAuth
subjectAltName = DNS:api.your_domain
Genera la clave, la solicitud de firma (CSR) y firma el certificado con la CA:
openssl ecparam -name prime256v1 -genkey -noout -out server.key
openssl req -new -key server.key -subj "/CN=api.your_domain" -out server.csr
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -days 397 -sha256 -extfile server.ext -out server.crt
-CAcreateserial crea el archivo ca.srl, donde OpenSSL lleva la cuenta de números de serie. Verifica que el certificado encadena con la CA:
openssl verify -CAfile ca.crt server.crt
server.crt: OK
Notasi el servicio es público, puedes usar un certificado de Let's Encrypt para el servidor y reservar la CA privada solo para los clientes. Nginx valida cada lado por separado.
Paso 3: Emitir un certificado de cliente
Cada cliente (una persona, un servicio, un equipo) recibe su propio certificado. El CN identifica al cliente y la extensión clientAuth limita su uso a autenticación de cliente:
nano client.ext
basicConstraints = critical, CA:FALSE
keyUsage = critical, digitalSignature
extendedKeyUsage = clientAuth
Emite el certificado para un cliente llamado billing-service, válido un año:
openssl ecparam -name prime256v1 -genkey -noout -out billing-service.key
openssl req -new -key billing-service.key -subj "/O=Your Company/CN=billing-service" -out billing-service.csr
openssl x509 -req -in billing-service.csr -CA ca.crt -CAkey ca.key -CAserial ca.srl -days 365 -sha256 -extfile client.ext -out billing-service.crt
openssl verify -CAfile ca.crt billing-service.crt
billing-service.crt: OK
Los navegadores y algunas herramientas importan el certificado de cliente en formato PKCS#12, un único archivo con certificado y clave protegido por contraseña:
openssl pkcs12 -export -in billing-service.crt -inkey billing-service.key -certfile ca.crt -name billing-service -out billing-service.p12
OpenSSL te pedirá una contraseña de exportación: usa una robusta y entrégala al cliente por un canal distinto al del archivo.
Paso 4: Instalar los certificados en Nginx
Nginx necesita el certificado y la clave del servidor, y solo el certificado de la CA para verificar a los clientes. La clave ca.key nunca debe copiarse a /etc/nginx:
install -d -m 750 -o root -g www-data /etc/nginx/mtls
install -m 644 ca.crt server.crt /etc/nginx/mtls/
install -m 640 -g www-data server.key /etc/nginx/mtls/
exit
El exit final cierra la shell de root que abriste en el paso 1.
Paso 5: Configurar mTLS en Nginx
Crea el sitio:
sudo nano /etc/nginx/sites-available/api.your_domain
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name api.your_domain;
ssl_certificate /etc/nginx/mtls/server.crt;
ssl_certificate_key /etc/nginx/mtls/server.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_client_certificate /etc/nginx/mtls/ca.crt;
ssl_verify_client on;
ssl_verify_depth 1;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Client-DN $ssl_client_s_dn;
proxy_set_header X-Client-Serial $ssl_client_serial;
proxy_set_header X-Client-Verify $ssl_client_verify;
}
}
ssl_client_certificate: la CA con la que se validan los certificados de cliente. Nginx solo acepta certificados firmados por ella.ssl_verify_client on: exige certificado. Sin él, Nginx responde400 Bad Request.ssl_verify_depth 1: tus certificados de cliente están firmados directamente por la CA, sin intermedias.- Las cabeceras
X-Client-*pasan a la aplicación la identidad del cliente para que pueda autorizar por CN. Asegúrate de que la aplicación solo es accesible desde Nginx, o cualquiera podría enviar esas cabeceras falsificadas.
Activa el sitio, comprueba la configuración y recarga:
sudo ln -s /etc/nginx/sites-available/api.your_domain /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
Paso 6: Probar la conexión
Copia a tu equipo ca.crt, billing-service.crt y billing-service.key (por ejemplo con scp). Primero intenta conectar sin certificado de cliente:
curl --cacert ca.crt https://api.your_domain/
<html>
<head><title>400 No required SSL certificate was sent</title></head>
...
Ahora con el certificado de cliente:
curl --cacert ca.crt --cert billing-service.crt --key billing-service.key https://api.your_domain/
La respuesta es la de tu aplicación. Un certificado firmado por otra CA produce 400 The SSL certificate error. Para ver qué CA pide el servidor durante el handshake:
openssl s_client -connect api.your_domain:443 -servername api.your_domain -CAfile ca.crt </dev/null 2>/dev/null | grep -A2 "Acceptable client certificate CA names"
Acceptable client certificate CA names
O = Your Company, CN = Your Company Internal CA
Paso 7: Revocar un certificado de cliente
Si se filtra la clave de un cliente o un servicio se da de baja, debes rechazar su certificado antes de que caduque. Para un número reducido de clientes, lo más sencillo es bloquear por número de serie en Nginx. Obtén el serial del certificado:
sudo openssl x509 -in /root/mtls-ca/billing-service.crt -noout -serial
serial=5C2E0A6B1F3D4E7A9B8C0D1E2F3A4B5C6D7E8F90
Crea la lista de seriales revocados, uno por línea seguido de 1;:
sudo nano /etc/nginx/mtls/revoked.map
5C2E0A6B1F3D4E7A9B8C0D1E2F3A4B5C6D7E8F90 1;
Declara el map en /etc/nginx/sites-available/api.your_domain, fuera del bloque server:
map $ssl_client_serial $client_revoked {
default 0;
include /etc/nginx/mtls/revoked.map;
}
Y dentro del bloque server, antes de location:
if ($client_revoked) {
return 403;
}
Recarga Nginx y repite desde tu equipo la petición con ese certificado:
sudo nginx -t && sudo systemctl reload nginx
curl --cacert ca.crt --cert billing-service.crt --key billing-service.key https://api.your_domain/
<html>
<head><title>403 Forbidden</title></head>
...
Si gestionas muchos clientes, conviene pasar a una CA con base de datos y listas de revocación (CRL), que Nginx admite con la directiva ssl_crl, o a una CA automatizada como step-ca que emite certificados de corta duración.
Paso 8: Renovar certificados antes de que caduquen
Un certificado caducado corta el servicio igual que uno revocado. Comprueba cuándo caducan y si lo harán en los próximos 30 días:
openssl x509 -in /etc/nginx/mtls/server.crt -noout -enddate -checkend 2592000
notAfter=Oct 27 10:02:11 2027 GMT
Certificate will not expire
Para renovar, repite el paso 2 o el 3 con una clave nueva, distribuye el certificado y, en el caso del servidor, recarga Nginx. Como la CA no cambia, los clientes existentes siguen funcionando sin tocar nada. Si algún día tienes que sustituir la CA, concatena la antigua y la nueva en ca.crt durante la transición para que Nginx acepte clientes de ambas.
Solución de problemas
400 No required SSL certificate was sentcon certificado. El cliente no lo está enviando: revisa las rutas de--certy--key. En navegadores, importa el.p12en el almacén del sistema y reinicia el navegador.400 The SSL certificate error. El certificado no encadena conssl_client_certificate, ha caducado o suextendedKeyUsageno incluyeclientAuth. Nginx registra el motivo con nivelinfo: pon temporalmenteerror_log /var/log/nginx/error.log info;en el bloqueserverpara verlo, o reprodúcelo conopenssl verify -CAfile ca.crt billing-service.crt.curl: (60) SSL certificate problem: unable to get local issuer certificate. El cliente no confía en el certificado del servidor. Pasa la CA con--cacert ca.crt.
Conclusión
Tienes una CA privada, certificados de servidor y de cliente, y un Nginx que solo acepta conexiones de clientes con un certificado válido y no revocado, pasando su identidad a la aplicación. Como siguientes pasos, usa el CN de X-Client-DN para autorizar por servicio en tu aplicación, lleva la clave de la CA fuera del servidor y automatiza la emisión y renovación, por ejemplo con step-ca, cuando el número de clientes crezca.
