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.
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 construcció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)
...
|
build.gradle
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);
}
}
|
Main.java
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!
...
|
System.out
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: 13b6c224-dc18-0404-7bc1-7c258c4c5a48
database:
enabled: true
role: app
backend: database
username-property: spring.datasource.username
password-property: spring.datasource.password
|
bootstrap.yml
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
|
consul.sh
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!
|
vault.sh
ui = true
storage "consul" {
address = "127.0.0.1:8500"
path = "vault"
}
vault.hcl
La base de datos PostgreSQL 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=#
|
postgresql.sh
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
|
vault-login.sh
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 PostgreSQL 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"
|
vault-database.sh
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
|
vault-approle.sh
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 asociarle un policy con permisos de lectura para este contexto.
1
2
|
$ vault policy write database-app database-app.hcl
|
vault-policy.sh
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"]
}
|
database-app.hcl
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
|
vault-role-id.sh
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.