Comunicación con Mutual TLS en Spring Boot

Mutual TLS con Spring Boot

Mutual TLS con Spring Boot


En esta nueva entrada de refactorizando sobre Comunicación con Mutual TLS en Spring Boot, vamos a comunicar dos servicios con certificados autofirmados mediante un WebClient con SSL para poder establecer una comunicación. Tanto el lado del cliente como el lado del servidor tendrán certificados y ambos deberán ser válidos para poder comunicarse.

La securización de las aplicaciones y la comunicación entre ellas es cada vez más importante, y el uso de certificados para permitir esas comunicaciones y seguridad es de vital importancia para encriptar la comunicación.

¿Qué es la comunicación bidireccional o mutual TLS ?

Por lo general estamos familiarizados con la comunicación unidireccional, es decir, aquella en la que el servidor presenta su certificado y el cliente añadiría este certificado a la lista de certificados de confianza. Este uso es el más cotidiano, por ejemplo, cuando navegamos por internet es probable que en nuestro browser (el cliente), tengamos certificados guardados.

Y por otro lado, y es el tema de este artículo (Comunicación con Mutual TLS en Spring Boot), tenemos la comunicación bidireccional, en el que tanto cliente como servidor tienen que confiar el uno en el otro. Por lo que en la creación de los certificados ambos tendrán relación. Cuando se hace esta forma de autenticación el servidor presenta un certificado al cliente, y el cliente presenta un certificado al servidor para que ambos se encuentren autenticados.

El siguiente diagrama explica la autenticación mutua entre servidor y cliente:

Diagrama Mutual TLS | Comunicación con Mutual TLS en Spring Boot
Diagrama Mutual TLS

Como crear los certificados para Mutual TLS

A continuación vamos a crear los certificados necesarios para establecer la comunicación mutua entre cliente y servidor. Estos certificados van a ser autofirmado self signed, para entornos de pruebas es perfecto, pero para entornos de producción mejor tener unos firmados por una CA.

Antes de crear los certificados de cliente y servidor vamos a crear el jks:

keytool -v -genkeypair -dname "CN=Refactorizando,OU=Madrid,O=Refactorizando,C=SP" -keystore identity.jks -storepass secret -keypass secret -keyalg RSA -keysize 2048 -alias server -validity 3650 -deststoretype pkcs12 -ext KeyUsage=digitalSignature,dataEncipherment,keyEncipherment,keyAgreement -ext ExtendedKeyUsage=serverAuth,clientAuth -ext SubjectAlternativeName:c=DNS:localhost,IP:127.0.0.1

Crear certificado en servidor

A continuación vamos a exportar el certificado del servidor:

keytool -v -exportcert -file server.cer -alias server -keystore identity.jks -storepass secret -rfc

Una vez hemos generado el certificado para el servidor vamos a crear un trustore para el cliente y añadir el certificado en este trustore.

keytool -v -importcert -file server.cer -alias server -keystore truststore.jks -storepass secret -noprompt

Crear certificado Cliente

Ya tenemos el certificado del servidor y el trustore del cliente en el que tenemos su servidor, pero ahora nos queda crear el certificado del cliente para poder establecer la comunicación y que el servidor confíe en el cliente.

keytool -v -genkeypair -dname "CN=Refactorizando,OU=Madrid,O=Refactorizando,C=SP" -keystore identity.jks -storepass secret -keypass secret -keyalg RSA -keysize 2048 -alias client -validity 3650 -deststoretype pkcs12 -ext KeyUsage=digitalSignature,dataEncipherment,keyEncipherment,keyAgreement -ext ExtendedKeyUsage=serverAuth,clientAuth -ext SubjectAlternativeName:c=DNS:localhost,IP:127.0.0.1

Y a continuación vamos a exportarlo para tener el certificado del cliente y poder añadirlo en el trustore del servidor y así poder tener establecida la comunicación:

keytool -v -exportcert -file client.cer -alias client -keystore identity.jks -storepass secret -rfc

Acabamos de exportar el certificado del cliente, pero aún así todavía nos faltaría el trustore del servidor y poder añadir el certificado:

keytool -v -importcert -file client.cer -alias client -keystore truststore.jks -storepass secret -noprompt

Una vez hemos realizado todos estos pasos sería el momento de establecer la comunicación entre nuestras aplicaciones cliente y servidor.

Comunicación con mutual TLS y WebClient en Spring Boot

Una vez hemos generado todos los pasos anteriores y tenemos los trustore y los key store es el momento de crear dos aplicaciones en Spring Boot en las que estableceremos la comunicación a través de WebClient.

Dependencias Maven para comunicación mutual TLS en Spring Boot

Dependencias servidor

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

Las dependencia anterior será imprescindible para el servidor.

Dependencias cliente

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-webflux</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter</artifactId>
 		</dependency>		
                <dependency>
			<groupId>org.apache.httpcomponents</groupId>
			<artifactId>httpcore</artifactId>
		</dependency>

		<dependency>
			<groupId>org.apache.httpcomponents</groupId>
			<artifactId>httpclient</artifactId>
		</dependency>

Las cuatro dependencias anteriores serán imprescindibles para comunicar através de webclient con el servidor.

Creación del Servidor

A continuación vamos a crear un servicio muy sencillo con spring boot, el cual tendrá el certificado cliente creado anteriormente y será llamado a través de un endpoint.

Añadir key store y truststore al classpath del servidor

Key y truststore

Configuración del application.yaml para comunicación con TLS

Configuración Servidor con Mutual TLS en Spring Boot
Configuración Servidor con Mutual TLS en Spring Boot

En la imágen anterior podemos ver la configuración necesaria para activar la configuración en el servidor.

Configuración SSL:

  • enabled: Para activar o desactivar la comunicación ssl.
  • client-auth: La autenticación de cliente es obligatoria.
  • key-store: Ruta al keystore.
  • key-store-password: Password del keystore.
  • key-alias: Alias establecido.
  • key-store-type: Tipo del keystore.
  • key-store-provider: Proveedor del keystore.
  • trust-store: Ruta del truststore.
  • trust-store-password: Password del truststore.
  • trust-store-type: Tipo del truststore.

Creación de endpoint

Vamos a crear un simple endpoint que será invocado por el cliente, el cual simplemente le devolverá un String.

@RestController
public class ServiceVerificationResource {

  @GetMapping("certificate-verify")
  public ResponseEntity<String> tlsServiceVerification() {

    return ResponseEntity.ok("verified");

  }
}

Creación del Cliente

Como hemos visto, la configuración del servidor es bastante sencilla, ya que Spring hará casi todo el trabajo por nosotros. La configuración del cliente require algo más de implementación ya que vamos a añadir un WebClient con seguridad.

El primer paso será realizar la configuración del WebClient para que pueda tener comunicación por TLS.

Configuración WebClient con TLS/SSL.

Para la creación de un WebClient con TLS vamos a crear un @Bean en el arranque del servicio, de esta manera luego podrá ser invocado cuando se necesite.

@Configuration
public class ServiceRegistryConfig {

  @Value("${server.base-url}")
  private String baseUrl;

  @Value("${server.endpoint}")
  private String endpoint;

  @Bean
  public WebClient configureWebclient(@Value("${server.ssl.trust-store}") String trustStorePath,
      @Value("${server.ssl.trust-store-password}") String trustStorePass,
      @Value("${server.ssl.key-store}") String keyStorePath,
      @Value("${server.ssl.key-store-password}") String keyStorePass,
      @Value("${server.ssl.key-alias}") String keyAlias) {

    SslContext sslContext;

    final PrivateKey privateKey;

    final X509Certificate[] certificates;

    try {
      final KeyStore trustStore;

      final KeyStore keyStore;

      trustStore = KeyStore.getInstance(KeyStore.getDefaultType());

      trustStore.load(new FileInputStream(ResourceUtils.getFile(trustStorePath)), trustStorePass.toCharArray());

      keyStore = KeyStore.getInstance(KeyStore.getDefaultType());

      keyStore.load(new FileInputStream(ResourceUtils.getFile(keyStorePath)), keyStorePass.toCharArray());

      List<Certificate> certificateList =
          Collections.list(trustStore.aliases()).stream()
              .filter(
                  t -> {
                    try {
                      return trustStore.isCertificateEntry(t);
                    } catch (KeyStoreException exception) {
                      throw new RuntimeException("Error reading truststore", exception);
                    }
                  })
              .map(
                  t -> {
                    try {
                      return trustStore.getCertificate(t);
                    } catch (KeyStoreException exception) {
                      throw new RuntimeException("Error reading truststore", exception);
                    }
                  })
              .collect(Collectors.toList());

      certificates = certificateList.toArray(new X509Certificate[certificateList.size()]);

      privateKey = (PrivateKey) keyStore.getKey(keyAlias, keyStorePass.toCharArray());

      Certificate[] certChain = keyStore.getCertificateChain(keyAlias);

      X509Certificate[] x509CertificateChain =
          Arrays.stream(certChain)
              .map(certificate -> (X509Certificate) certificate)
              .collect(Collectors.toList())
              .toArray(new X509Certificate[certChain.length]);

      X509Certificate certificate = x509CertificateChain[0];

      validateCertificate(certificate);

      sslContext =
          SslContextBuilder.forClient()
              .keyManager(privateKey, keyStorePass, x509CertificateChain)
              .trustManager(certificates)
              .build();

      HttpClient httpClient =
          HttpClient.create().secure(sslContextSpec -> sslContextSpec.sslContext(sslContext));

      return webClientConfiguration(httpClient);

    } catch (KeyStoreException
        | CertificateException
        | NoSuchAlgorithmException
        | IOException
        | UnrecoverableKeyException e) {

      throw new RuntimeException(e);
    }

  }

  private boolean validateCertificate(X509Certificate certificate) {

    var certificateExpirationDate =
        certificate.getNotAfter().toInstant().atZone(ZoneId.systemDefault()).toLocalDate();

    var certificateStartDate =
        certificate.getNotBefore().toInstant().atZone(ZoneId.systemDefault()).toLocalDate();

    if (LocalDate.now().isAfter(certificateExpirationDate)) {

      throw new ServiceExpirationDateException("Service date expiration");
    }

    if (LocalDate.now().isBefore(certificateStartDate)) {

      throw new ServiceStartDateException(
          "Service cannot be used until " + certificateStartDate.toString());
    }

    var subject =
        Arrays.stream(certificate.getSubjectDN().getName().split(","))
            .map(i -> i.split("="))
            .collect(Collectors.toMap(element -> element[0].trim(), element -> element[1].trim()));

    if (!subject.get("O").equalsIgnoreCase("Refactorizando")) {

      throw new OrganizationNameException("Organization is not correct");
    }

    return true;
  }

  private WebClient webClientConfiguration(HttpClient httpClient) {

    ClientHttpConnector connector = new ReactorClientHttpConnector(httpClient);

    var webClient =
        WebClient.builder()
            .clientConnector(connector)
            .baseUrl(baseUrl)
            .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
            .build();

    var reponse =
        webClient.get().uri(endpoint).retrieve().bodyToMono(String.class).block();

    assert Objects.requireNonNull(reponse).equalsIgnoreCase("verified");
    
    return webClient;
  }
}

En la clase ServiceRegistryConfig, vamos a realizar varias comprobaciones en el certificado y añadir la seguridad en el webclient.

Para realizar comprobaciones sobre el certificado vamos a hacer uso del objeto X509Certificate, que nos proporciona un API con varios métodos para poder realizar comprobaciones sobre el certificado, como por ejemplo:

  • NotAfter(): Método para ver la fecha de expiración del certificado.
  • NotBefore(): Desde cuando el certificado estará vigente.
  • SubjectDN(): Obtiene los subject del certificado.

Para poder securizar nuestro WebClient vamos a crear un SslContext, al que le añadimos el keyManager a partir del privateKey, el keyStorePass y los certificados que tenemos en el classpath mediante un array de objetos de tipo X509Certificate, y el trustManager al que le añadimos la lista de los certificados que hemos generado.

Posibles errores con comunicación TLS

Al crear los certificados e intentar establecer la comunicación punto a punto, si no hemos generado bien los certificados o tenemos algo mal configurado podremos tener errores como los siguientes:

  • No subject alternative names present: Este error por lo general se produce cuando no hemos configurado el SubjectAlternativeName.
  • No subject alternative names matching IP… : La Ip no esta incluida en el SubjectAlternativeName.
  • PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target: Certificado firmado por otra autoridad de la que se encuentra en el truststore.
  • Software caused connection abort: recv failed: Esto dice que el servidor no confía en el cliente, por lo general es debido a que o el cliente no tiene certificado, o que es incorrecto el certificado del cliente.
  • Received fatal alert: handshake_failure: Tanto cliente como servidor se deben de cifrar haciendo uso del mismo cifrado si no ha sido así saldrá este error.

Conclusión

En esta entrada sobre la Comunicación con Mutual TLS en Spring Boot, hemos comprobado lo fácil que es generar haciendo uso de certificados autofirmados, una comunicación segura.

El uso necesario de aumentar la seguridad en las comunicaciones, hace que cada vez sea más necesario implementar comunicaciones, con contextos seguros. Como por ejemplo haciendo uso del WebClient de Spring añaiendo SSLContext.

Si quieres puedes ver los ejemplos funcionando en nuestro Github.

Si necesitas más información puedes escribirnos un comentario o un correo electrónico a refactorizando.web@gmail.com y te ayudaremos encantados!


No te olvides de seguirnos en nuestras redes sociales Facebook o Twitter para estar actualizado.


Deja una respuesta

Tu dirección de correo electrónico no será publicada.