Utilizar credenciales de conexión a la base de datos generadas por Vault en una aplicación de Spring

Escrito por el .
java planeta-codigo programacion
Comentarios

Spring Cloud Vault facilita la integración con Vault, una de sus usos es utilizarlo para obtener unas credenciales de conexión a la base de datos generadas bajo demanda y con un tiempo de vida limitado en vez de embeberlas en la configuración de la aplicación y con u tiempo de vida indefinido.

Spring
Vault

La herramienta Vault de HashiCorp permite almacenar secretos, otra de sus funcionalidad es ser capaz de generar credenciales de forma dinámica. Habitualmente una aplicación para conectarse a una base de datos incluye en su configuración las credenciales, usuario y contraseña, para autenticarse y crear las conexiones, estas credenciales tiene un tiempo de vida indefinido y comprometidas proporcionan acceso a la base de datos.

Vault permite centralizar la seguridad, otorgar a cada aplicación los permisos para conectarse a una base de datos a través de la obtención de credenciales (usuario y contraseña), auditar su uso y limitar el tiempo de validez de las credenciales a unas horas en vez de forma indefinida.

Spring Cloud Vault permite a una aplicación Spring simplificar la integración con Vault para obtener unas credenciales generadas dinámicamente.

En el archivo de contrucción de la aplicación hay que incluir las dependencias de Spring para la integración con Vault.

1
2
3
4
5
...
implementation('org.springframework.cloud:spring-cloud-starter-config', excludeSpringBootStarterLogging)
implementation('org.springframework.cloud:spring-cloud-starter-vault-config', excludeSpringBootStarterLogging)
implementation('org.springframework.cloud:spring-cloud-vault-config-databases', excludeSpringBootStarterLogging)
...

Obtener la credenciales de conexión a la base de datos es transparente para el código de la aplicación, lo único que se necesita es el usuario y contraseña además de la URL de conexió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
25
26
27
28
29
30
31
32
33
34
35
package io.github.picodotdev.blogbitix.springcloudvault;

...

@SpringBootApplication
public class Main implements CommandLineRunner {

    @Value("${spring.datasource.username}")
    private String username;

    @Value("${spring.datasource.password}")
    private String password;

    @Override
    public void run(String... args) {
        System.out.printf("Username: %s%n", username);
        System.out.printf("Password: %s%n", password);

        try (Connection connection = DriverManager.getConnection("jdbc:postgresql://127.0.0.1:5432/app", username, password)) {
            if (connection != null) {
                System.out.println("Connected to the database!");
            } else {
                System.out.println("Failed to make connection!");
            }
        } catch (SQLException e) {
            System.err.format("SQL State: %s\n%s", e.getSQLState(), e.getMessage());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws Exception {
        SpringApplication.run(Main.class, args);
    }
}
1
2
3
4
5
6
...
10:37:47.013 [main] INFO  io.github.picodotdev.blogbitix.springcloudvault.Main - Started Main in 1.368 seconds (JVM running for 1.784)
Username: v-approle-app-gRlHQlNyPF8qJ3DCUz9U-1566549466
Passowrd: A1a-MPPFfvMD3OQUC00H
Connected to the database!
...

La parte más relevante está en la configuración necesaria de la aplicación. Hay que añadir como configuración la ubicación del servidor Vault, es necesario configurar un método de autenticación para Vault, para el caso de aplicaciones el recomendado es AppRole. Con AppRole cada aplicación necesita de un role-id y un secret-id que hay que generar previamente. Y el rol del que obtener las credenciales, app.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
spring.application.name: app
spring.cloud.vault:
  uri: http://127.0.0.1:8200
  authentication: APPROLE
  app-role:
    role-id: a248529d-882c-ef5f-f7e6-6a9d349afa57
    secret-id: 1ce82723-0f98-f885-9784-3d65d910350b
  database:
    enabled: true
    role: app
    backend: database
    username-property: spring.datasource.username
    password-property: spring.datasource.password

Para probarlo hay que iniciar en este caso el servidor Consul ya que en el ejemplo se utiliza como el lugar donde se guardan los secretos. También hay que iniciar Vault.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
$ consul agent -dev
==> Starting Consul agent...
==> Consul agent running!
           Version: 'v1.5.0'
           Node ID: '2f523068-b196-e3bb-0b09-dc34e20989d2'
         Node name: 'archlinux'
        Datacenter: 'dc1' (Segment: '<all>')
            Server: true (Bootstrap: false)
       Client Addr: [127.0.0.1] (HTTP: 8500, HTTPS: -1, gRPC: 8502, DNS: 8600)
      Cluster Addr: 127.0.0.1 (LAN: 8301, WAN: 8302)
           Encrypt: Gossip: false, TLS-Outgoing: false, TLS-Incoming: false
 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
$ vault server -dev -config vault.hcl
==> Vault server configuration:

             Api Address: http://127.0.0.1:8200
                     Cgo: disabled
         Cluster Address: https://127.0.0.1:8201
              Listener 1: tcp (addr: "127.0.0.1:8200", cluster address: "127.0.0.1:8201", max_request_duration: "1m30s", max_request_size: "33554432", tls: "disabled")
               Log Level: info
                   Mlock: supported: true, enabled: false
                 Storage: consul (HA available)
                 Version: Vault v1.1.1
             Version Sha: a3dcd63451cf6da1d04928b601bbe9748d53842e

WARNING! dev mode is enabled! In this mode, Vault runs entirely in-memory
and starts unsealed with a single unseal key. The root token is already
authenticated to the CLI, so you can immediately begin using Vault.

You may need to set the following environment variable:

    $ export VAULT_ADDR='http://127.0.0.1:8200'

The unseal key and root token are displayed below in case you want to
seal/unseal the Vault or re-authenticate.

Unseal Key: jsBAedtA88Lm7uU1pCSYOBK1980Ervf9GSky4usmEdQ=
Root Token: s.8wwm4fRuRRVFm1cbBUMgdNtE

Development mode should NOT be used in production installations!
1
2
3
4
5
6
ui = true

storage "consul" {
 address = "127.0.0.1:8500"
 path  = "vault"
}

La base de datos postgres se inicia como un contenedor de Docker.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
$ docker run --rm -it -p "5432:5432" -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=postgres postgres:alpine
docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                    NAMES
8fedf0ef1342        postgres:alpine     "docker-entrypoint.s…"   22 seconds ago      Up 20 seconds       0.0.0.0:5432->5432/tcp   serene_leavitt

$ docker exec -it 8fedf0ef1342 bash
bash-5.0# psql -U postgres
psql (11.4)
Type "help" for help.

postgres=# CREATE DATABASE app;
CREATE DATABASE
postgres=#

Para realizar la configuración de Vault primero hay que iniciar sesión, en el modo desarrollo del ejemplo utilizando el token root que es generado y emitido en la salida al iniciarlo.

1
2
$ export VAULT_ADDR='http://127.0.0.1:8200'
$ vault login s.8wwm4fRuRRVFm1cbBUMgdNtE

Como en el artículo Generar credenciales de conexión a base de datos bajo demanda con Vault hay que activar el backend de database para generar credenciales de bases de datos, en las que básicamente se especifica la cadena de conexión a la base de datos de postgres con un usuario y contraseña con permisos suficientes para crear usuarios y la sentencia SQL que los crea. Se habilita y configura el backend database del que obtener las credenciales.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
$ vault secrets enable database
$ vault write database/config/app \
    plugin_name=postgresql-database-plugin \
    allowed_roles="app" \
    connection_url="postgresql://{{username}}:{{password}}@localhost:5432/?sslmode=disable" \
    username="postgres" \
    password="postgres"
$ vault write database/roles/app \
     db_name=app \
     creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; GRANT SELECT ON ALL TABLES IN SCHEMA public TO \"{{name}}\";" \
     default_ttl="1h" \
     max_ttl="24h"

Para que la aplicación de Spring Boot obtenga las credenciales ha de autenticarse en Vault en este caso utilizando el método recomendado para las aplicaciones que es utilizando en mecanismo de autenticación AppRole que básicamente es un usuario y contraseña para las aplicaciones.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
$ vault auth enable approle
$ vault write auth/approle/role/app \
    secret_id_ttl=10m \
    token_num_uses=10 \
    token_ttl=20m \
    token_max_ttl=30m \
    secret_id_num_uses=40 \
    policies=database-app
$ vault read auth/approle/role/app/role-id
Key        Value
---        -----
role_id    a248529d-882c-ef5f-f7e6-6a9d349afa57
$ vault write -f auth/approle/role/app/secret-id
Key                   Value
---                   -----
secret_id             13b6c224-dc18-0404-7bc1-7c258c4c5a48
secret_id_accessor    fd9a0915-af6e-b0a8-4e6c-dbb6ee587903

En Vault los permisos se otorgan con las policy, los secretos se organiza en una estructura jerárquica de directorios y a cada una de los contextos se le otorga los permisos deseados. Spring obtiene las credenciales para la base de datos del contexto database/creds/app por lo que al rol utilizando para obtener las credenciales hay que asocialer un policy con permisos de lectura para este contexto.

1
$ vault policy write database-app database-app.hcl
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
path "secret/application" {
  capabilities = ["read"]
}

path "secret/app" {
  capabilities = ["read"]
}

path "database/creds/app" {
  capabilities = ["read"]
}

Obtenido un role-id y un secret-id so observa los policies asociados además de otras propiedades.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
$ vault write auth/approle/login role_id=a248529d-882c-ef5f-f7e6-6a9d349afa57 secret_id=13b6c224-dc18-0404-7bc1-7c258c4c5a48
Key                     Value
---                     -----
token                   s.yAKbv8Qz5tfXpc8n7C8oND01
token_accessor          GPHO70D2NFRHFFdsiNiftmzO
token_duration          20m
token_renewable         true
token_policies          ["database-app" "default"]
identity_policies       []
policies                ["database-app" "default"]
token_meta_role_name    app

En este caso la aplicación de Spring incluye en su configuración las credenciales del AppRole, también se puede proporcionar como variables de entorno y propiedades del sistema.

El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos de Blog Bitix alojado en GitHub.

Este artículo forma parte de la serie hashicorp:

  1. Introducción a Nomad para gestionar aplicaciones y microservicios
  2. Estrategias de despliegue para microservicios con Nomad
  3. Servicios con persistencia en el orquestador de microservicos Nomad
  4. Crear de forma sencilla y rápida máquinas virtuales de VirtualBox con Vagrant
  5. Registro y descubrimiento de servicios en contenedores de Docker con Consul y Registrator
  6. Administrar secretos y proteger datos sensibles con Vault
  7. Generar credenciales de conexión a base de datos bajo demanda con Vault
  8. Utilizar credenciales de conexión a la base de datos generadas por Vault en una aplicación de Spring

Este artículo forma parte de la serie spring-cloud:

  1. Datos de sesión externalizados con Spring Session
  2. Aplicación Java autocontenida con Spring Boot
  3. Configuración de una aplicación en diferentes entornos con Spring Cloud Config
  4. Información y métricas de la aplicación con Spring Boot Actuator
  5. Registro y descubrimiento de servicios con Spring Cloud y Consul
  6. Aplicaciones basadas en microservicios
  7. Registro y descubrimiento de servicios con Spring Cloud Netflix
  8. Servicio de configuración para microservicios con Spring Cloud Config
  9. Recargar sin reiniciar la configuración de una aplicación Spring Boot con Spring Cloud Config
  10. Almacenar cifrados los valores de configuración sensibles en Spring Cloud Config
  11. Tolerancia a fallos en un cliente de microservicio con Spring Cloud Netflix y Hystrix
  12. Balanceo de carga y resilencia en un microservicio con Spring Cloud Netflix y Ribbon
  13. Proxy para microservicios con Spring Cloud Netflix y Zuul
  14. Monitorizar una aplicación Java de Spring Boot con Micrometer, Prometheus y Grafana
  15. Exponer las métricas de Hystrix en Grafana con Prometheus de una aplicación Spring Boot
  16. Servidor OAuth, gateway y servicio REST utilizando tokens JWT con Spring
  17. Trazabilidad en microservicios con Spring Cloud Sleuth
  18. Implementar tolerancia a fallos con Resilience4j
  19. Iniciar una aplicación de Spring Boot en un puerto aleatorio
  20. Utilizar credenciales de conexión a la base de datos generadas por Vault en una aplicación de Spring