La autenticación mutua TLS (también llamada mTLS o two-way TLS) sustituye o complementa a las contraseñas usando certificados X.509 como credencial. Es la opción estándar en escenarios donde no hay un humano detrás del cliente: comunicación entre microservicios, integraciones B2B o dispositivos IoT.
Cuándo usar mTLS
mTLS aporta tres ventajas frente a la autenticación basada en token:
- 1. Identidad fuerte vinculada al hardware: el certificado vive en el HSM o en un keystore protegido y es difícil de exfiltrar.
- 2. Defensa en profundidad: incluso si un atacante consigue un JWT, no podrá completar el handshake TLS sin el certificado.
- 3. Auditoría confiable: el
Subjecty el número de serie quedan registrados a nivel de red, antes incluso de que la petición llegue a la aplicación.
mTLS no reemplaza la autorización. El certificado dice quién eres, pero los permisos siguen viniendo de tu base de datos o de un claim del JWT que viaje sobre la conexión.
Arquitectura de un handshake mTLS
sequenceDiagram
participant C as Cliente
participant S as Servidor
C->>S: ClientHello
S->>C: ServerHello + Certificate + CertificateRequest
C->>S: Certificate + ClientKeyExchange + CertificateVerify
S->>C: Finished
Note over C,S: A partir de aqui el canal esta cifrado y el cliente autenticado
Lo distinto respecto a un TLS estándar es el CertificateRequest: el servidor exige al cliente que envíe su certificado y que firme un challenge con su clave privada para demostrar que es el legítimo poseedor.
Generar la PKI de pruebas
Para entender el flujo conviene crear una autoridad raíz interna y firmar tanto el certificado del servidor como el del cliente. Con openssl el procedimiento es directo.
openssl req -x509 -newkey rsa:4096 -days 3650 -nodes \
-keyout ca.key -out ca.crt -subj "/CN=DemoRootCA"
openssl req -newkey rsa:2048 -nodes \
-keyout server.key -out server.csr -subj "/CN=api.demo.local"
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key \
-CAcreateserial -out server.crt -days 365
openssl req -newkey rsa:2048 -nodes \
-keyout cliente.key -out cliente.csr -subj "/CN=svc-pagos"
openssl x509 -req -in cliente.csr -CA ca.crt -CAkey ca.key \
-CAcreateserial -out cliente.crt -days 365
Java necesita los certificados en formato PKCS12. Convertimos el del servidor.
openssl pkcs12 -export -in server.crt -inkey server.key \
-out server.p12 -name api -password pass:secret
Y construimos el truststore con la CA, que el servidor usará para validar a los clientes.
keytool -import -trustcacerts -alias ca \
-file ca.crt -keystore truststore.p12 \
-storetype PKCS12 -storepass secret -noprompt
En producción, la CA suele ser interna (Vault PKI, AWS Private CA, Azure Key Vault) o pública (Let's Encrypt para servidores, certificados emitidos por la organización para clientes).
Configurar mTLS en Spring Boot
application.yaml declara los dos almacenes y exige certificado al cliente.
server:
port: 8443
ssl:
enabled: true
key-store: classpath:server.p12
key-store-password: secret
key-store-type: PKCS12
key-alias: api
trust-store: classpath:truststore.p12
trust-store-password: secret
trust-store-type: PKCS12
client-auth: need
El parámetro client-auth admite tres valores:
none: TLS estándar, el cliente no envía certificado.want: el servidor lo pide, pero acepta también clientes anónimos. Útil para mezclar mTLS con OAuth2.need: obligatorio. Sin certificado válido, el handshake falla.
Para escenarios mixtos donde solo /api/internal/** exige mTLS conviene poner want y filtrar a nivel de aplicación.
Mapear certificado a UserDetails
Spring Security ya trae X509AuthenticationFilter y un X509PrincipalExtractor por defecto que lee el CN del Subject. Activar la integración es declarativo.
@Bean
SecurityFilterChain mtls(HttpSecurity http,
UserDetailsService users) throws Exception {
http
.securityMatcher("/api/internal/**")
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
.x509(x509 -> x509
.subjectPrincipalRegex("CN=(.*?)(?:,|$)")
.userDetailsService(users))
.csrf(csrf -> csrf.disable());
return http.build();
}
El regex extrae el CN. Por ejemplo, CN=svc-pagos produce username = "svc-pagos". El UserDetailsService lo carga y devuelve los GrantedAuthority.
@Bean
UserDetailsService usuariosMtls() {
UserDetails pagos = User.withUsername("svc-pagos")
.password("")
.authorities("ROLE_INTERNAL", "SCOPE_pagos:write")
.build();
return new InMemoryUserDetailsManager(pagos);
}
El
passwordse ignora cuando la autenticación viene de un certificado. La cadena vacía es habitual; lo que importa son las autoridades.
Personalizar la extracción del principal
Para escenarios complejos, escribimos nuestra propia implementación de X509PrincipalExtractor. Por ejemplo, identificando al cliente por OID custom del certificado.
public class OidPrincipalExtractor implements X509PrincipalExtractor {
@Override
public Object extractPrincipal(X509Certificate cert) {
String dn = cert.getSubjectX500Principal().getName();
return Stream.of(dn.split(","))
.map(String::trim)
.filter(s -> s.startsWith("OU="))
.map(s -> s.substring(3))
.findFirst()
.orElseThrow(() -> new BadCredentialsException("OU ausente"));
}
}
Y se inyecta en la configuración con .x509(x509 -> x509.x509PrincipalExtractor(new OidPrincipalExtractor())).
Revocación: CRL y OCSP
Un certificado puede comprometerse antes de su expiración. La PKI debe contemplar la revocación mediante una de dos técnicas.
- CRL (Certificate Revocation List): la CA publica una lista firmada de números de serie revocados. El servidor la descarga periódicamente.
- OCSP (Online Certificate Status Protocol): el servidor consulta a la CA por el estado de cada certificado en tiempo real. Se puede mejorar con OCSP stapling para reducir latencia.
En Java se activa con propiedades del sistema.
System.setProperty("com.sun.net.ssl.checkRevocation", "true");
Security.setProperty("ocsp.enable", "true");
En producción casi nadie habilita CRL/OCSP por su impacto en latencia y disponibilidad. La práctica común es rotar certificados con vidas cortas (24-72 horas) emitidos por una CA automatizada (Vault, cert-manager, SPIRE).
mTLS en pruebas con WebClient y RestClient
Probar un endpoint mTLS desde otro servicio Spring requiere configurar el WebClient o RestClient con un keystore de cliente.
@Bean
RestClient pagosClient() throws Exception {
var keyStore = KeyStore.getInstance("PKCS12");
try (var in = new FileInputStream("cliente.p12")) {
keyStore.load(in, "secret".toCharArray());
}
var sslContext = new SslContextBuilder()
.loadKeyMaterial(keyStore, "secret".toCharArray())
.loadTrustMaterial("ca.crt".toCharArray())
.build();
var requestFactory = new HttpComponentsClientHttpRequestFactory(
HttpClients.custom().setSSLContext(sslContext).build());
return RestClient.builder()
.baseUrl("https://api.demo.local:8443")
.requestFactory(requestFactory)
.build();
}
Para curl:
curl --cacert ca.crt --cert cliente.crt --key cliente.key \
https://api.demo.local:8443/api/internal/pagos
Combinar mTLS con JWT
El patrón más robusto en empresas mezcla mTLS para identidad de servicio y JWT para identidad de usuario. El microservicio verifica que la conexión TLS viene de un servicio autorizado y, dentro, lee el JWT del usuario que originó la petición.
@Bean
SecurityFilterChain hybrid(HttpSecurity http) throws Exception {
http
.securityMatcher("/api/internal/**")
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
.x509(Customizer.withDefaults())
.oauth2ResourceServer(oauth -> oauth.jwt(Customizer.withDefaults()));
return http.build();
}
Spring evalúa los dos filtros y la Authentication final contiene tanto el certificado como el JWT, accesibles vía SecurityContextHolder para auditoría.
Errores frecuentes
- Olvidar
client-auth: need: el servidor acepta peticiones anónimas y todo el modelo de seguridad cae. - Hacer pública la clave privada del cliente: tratarla como secret y rotarla con frecuencia.
- Usar la misma CA para producción y staging: un certificado de staging robado vale para producción. Separa siempre las jerarquías.
- Confiar el regex del CN ciegamente: si la CA emite certificados con CN duplicados, dos clientes distintos compartirán identidad. Combina CN con
serialNumbero usa OIDs custom.
Alan Sastre
Ingeniero de Software y formador, CEO en CertiDevs
Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Spring Security es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.
Más tutoriales de Spring Security
Explora más contenido relacionado con Spring Security y continúa aprendiendo con nuestros tutoriales gratuitos.
Aprendizajes de esta lección
Configurar mTLS en Spring Boot con keystore y truststore PKCS12, mapear certificados X.509 a UserDetails y validar la cadena de certificación contra una autoridad interna o pública.