mTLS en Spring Security: autenticación por certificado cliente

Avanzado
Spring Security
Spring Security
Actualizado: 19/04/2026

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 Subject y 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 password se 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 serialNumber o usa OIDs custom.
Alan Sastre - Autor del tutorial

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.