Almacenar cifrados los valores de configuración sensibles en Spring Cloud Config

Escrito por el , actualizado el .
java planeta-codigo programacion spring
Comentarios

Spring
Java

Para no tener que hacer cambios en el código que implica recompilar la aplicación y generar de nuevos los artefactos al cambiar algunos valores de la aplicación se utilizan los archivos de configuración. Los archivos de configuración son archivos de texto plano que pueden seguir algún formato como properties, xml o yaml, externos a la aplicación pero que lee sus propiedades al iniciarse. Algunas propiedades de configuración de ejemplo pueden ser la cadena de conexión a una base de datos, el usuario y contraseña.

Dado que algunos valores de configuración son sensibles como en el ejemplo anterior la cadena de conexión, usuario y contraseña es recomendable por mayor seguridad almacenar estos valores cifrados y que la aplicación los descifre con la clave correspondiente al necesitar su valor original. Estos valores no deben estar en el código fuente para evitar un problema de seguridad aún con el código fuente compilado.

Spring Cloud Config permite guardar los archivos de configuración con algunos valores cifrados. Hay varias posibilidades de configuración para guardar los datos cifrados: mediante clave simétrica, clave privada-pública, guardarlos en el servicio externo Vault de Hashicorp, mantenerlos cifrados solo en el almacenamiento persistente o transmitirlos cifrados y que sea el cliente el que los descifre.

Utilizando la forma más simple para mantener los datos cifrados con una clave simétrica en el servicio de configuración hay que mantener en una propiedad de configuración la clave simétrica para cifrar y descifrar los datos, encrypt.key. En este ejemplo la clave simétrica y las propiedades cifradas están en archivos de configuración diferentes pero incluidos en el mismo servidor de configuración. Esto no parece que aporte mucha seguridad ya que si se tiene acceso al archivo de configuración de un servicio con una propiedad cifrada probablemente se tenga acceso al archivo con la clave cifrada y la medida de seguridad no es útil. Sin embargo, esto permite al estar separados los archivos de configuración añadir el archivo con la propiedad cifrada a un repositorio público sin peligro siempre y cuando la clave de cifrado se mantenga en secreto. Los archivos de configuración de los servicios en el servidor de configuración se podrían añadir a un repositorio de Git.

1
2
3
4
5
6
7
8
9
spring:
  cloud:
    config:
      server:
        native:
          searchLocations: file:./misc/config

encrypt:
  key: ma8FvTm1t8uWRlYE3ghPsQxxESaZwpOGdlsFwIyPNIWE25yNg1dsvvnd7orlZL9FH0qJyRkG8kcf5CBVdjmi8b2yxKzpXyfxpMXj

Definida la clave simétrica e iniciado el servidor de configuración este ofrece dos endpoints para cifrar y descifrar datos. Utilizando el de cifrado se obtiene el valor cifrado del dato sensible que se quiere proteger. Con el endpoint de descifrado se puede descifrar. Se observa que utilizando varias veces el endpoint de cifrado se devuelve en cada una un valor distinto, sin embargo, descifrando cada uno de estos valores con el endpoint de descifrado siempre se obtiene el valor original. Esto es debido seguramente a que en la operación de cifrado se utiliza la técnica del salt para que a los valores cifrados se les pueda aplicar un ataque de diccionario, el salt es incluido en el valor devuelto para que la operación de descifrado devuelva el valor original.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
$ curl localhost:8090/encrypt -d secret
fb416133716acf5b6e3ffb64a396d521cd68fb0c2cb58727d5e938badefa942b
$ curl localhost:8090/encrypt -d secret
2b5ab02e4653bebb1b325e88eb9711df0e97e8f7efbbffc0dd0c5ae532dceedc
$ curl localhost:8090/encrypt -d secret
df06bcf1e36d910851a6e3f866d0f014388d3ddcb479b9f0e43194d7cfb5d72c

$ curl localhost:8090/decrypt -d fb416133716acf5b6e3ffb64a396d521cd68fb0c2cb58727d5e938badefa942b
secret
$ curl localhost:8090/decrypt -d 2b5ab02e4653bebb1b325e88eb9711df0e97e8f7efbbffc0dd0c5ae532dceedc
secret
$ curl localhost:8090/decrypt -d df06bcf1e36d910851a6e3f866d0f014388d3ddcb479b9f0e43194d7cfb5d72c
secret

El valor cifrado obtenido por este endpoint se puede guardar en los archivos de configuración entrecomillándolo y precediéndolo con la cadena {cipher}.

1
2
3
config:
  key: dev
  password: '{cipher}fb416133716acf5b6e3ffb64a396d521cd68fb0c2cb58727d5e938badefa942b'

En este caso el servicio al iniciarse obtiene su configuración del servicio de configuración, los datos se transmiten en forma plana sin cifrar y el cifrado utilizando en el servidor de configuración es transparente para el cliente. Accediendo al endpoint del servidor de configuración que devuelve la configuración de un servicio con una propiedad cifrada se observa que al obtener el valor se devuelve en texto plano al cliente, esta petición es la misma que hace el servicio para obtener su configuración, de modo que aunque la información está cifrada en el servidor de configuración se transmite al servicio sin cifrar en texto plano. En este ejemplo se utiliza el protocolo inseguro HTTP, lo recomendable es utilizar el protocolo HTTPS para cifrar el tráfico entre el servidor de configuración y el cliente de modo que los valores sensibles queden protegidos también en la transmisión.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$ curl http://localhost:8090/client/default.yml
{
   "name":"client",
   "profiles":[
      "default.yml"
   ],
   "label":null,
   "version":null,
   "state":null,
   "propertySources":[
      {
         "name":"file:misc/config/client.yml",
         "source":{
            "server.port":"${port:8085}",
            "management.endpoints.web.exposure.include":"*",
            "circuitBreaker.requestVolumeThreshold":4,
            "circuitBreaker.errorThresholdPercentage":50,
            "metrics.rollingStats.timeInMilliseconds":10000,
            "config.key":"dev",
            "config.password":"secret"
         }
      }
   ]
}

Iniciado el servicio de descubrimiento, el de configuración y un servicio que tiene un dato cifrado de configuración el valor que obtiene está ya descifrado. En este caso el servicio client obtiene el valor de la propiedad config.password descifrado con el valor secret.

1
2
3
4
$ ./gradlew discoveryserver:run --args="--port=8761"
$ ./gradlew configserver:run --args="--port=8090"
$ ./gradlew service:run --args="--port=8080"
$ ./gradlew client:run
1
2
3
4
5
6
7
8
9
$ ./gradlew client:run
...
2018-09-30 00:56:08,193  INFO                    org.apache.coyote.http11.Http11NioProtocol Starting ProtocolHandler ["http-nio-8085"]
2018-09-30 00:56:08,283  INFO                    org.apache.tomcat.util.net.NioSelectorPool Using a shared selector for servlet write/read
2018-09-30 00:56:08,689  INFO  org.springframework.boot.web.embedded.tomcat.TomcatWebServer Tomcat started on port(s): 8085 (http) with context path ''
2018-09-30 00:56:08,691  INFO  netflix.eureka.serviceregistry.EurekaAutoServiceRegistration Updating port to 8085
2018-09-30 00:56:08,695  INFO        io.github.picodotdev.blogbitix.springcloud.client.Main Started Main in 19.901 seconds (JVM running for 22.013)
Valor de propiedad de configuración (config.key): dev
Valor de propiedad de configuración (config.password): secret
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package io.github.picodotdev.blogbitix.springcloud.client;

...

@SpringBootApplication
@EnableDiscoveryClient
@EnableCircuitBreaker
@EnableHystrixDashboard
public class Main implements CommandLineRunner {

    @Autowired
    private DefaultConfiguration configuration;

    @Autowired
    private ClientService service;
    
    @Override
    public void run(String... args) throws Exception {
        System.out.printf("Valor de propiedad de configuración (%s): %s%n", "config.key", configuration.getKey());
        System.out.printf("Valor de propiedad de configuración (%s): %s%n", "config.password", configuration.getPassword());

    ...
    }

    public static void main(String[] args) throws Exception {
        SpringApplication application = new SpringApplication(Main.class);
        application.setApplicationContextClass(AnnotationConfigApplicationContext.class);
        SpringApplication.run(Main.class, args);
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package io.github.picodotdev.blogbitix.springcloud.client;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;

@Configuration
public class DefaultConfiguration {

    @Value("${config.key}")
    String key;

    @Value("${config.password}")
    String password;

    public String getKey() {
        return key;
    }

    public void setKey(String key) {
        this.key = key;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}

El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos de Blog Bitix alojado en GitHub y probarlo en tu equipo ejecutando el comando ./gradle-run.sh, ./curl-1.sh.