Dynamic DB credentials with Hashicorp Vault

By Joonas Venäläinen

Vault loves Postgres

Vault is a great tool for managing and protecting sensitive data. One of its many features is ability to create database credentials dynamically. This gives you extra layer of security as you can define short TTL for the credentials and rotate them frequently. For this short demonstration, I will use a simple docker-compose file that setups Vault and PostgreSQL.

version: '3.9'
services:
vault:
image: vault:1.7.0
container_name: vault
ports:
- "8200:8200"
environment:
VAULT_ADDR: http://127.0.0.1:8200
VAULT_DEV_ROOT_TOKEN_ID: roottoken
VAULT_DEV_LISTEN_ADDRESS: 0.0.0.0:8200
VAULT_SKIP_VERIFY: "true"
cap_add:
- IPC_LOCK
postgres:
image: postgres:12-alpine
environment:
POSTGRES_USER: exampledb
POSTGRES_PASSWORD: exampledb
POSTGRES_DB: exampledb

Run compose and exec in to the Vault service:

docker-compose up -d
docker exec -it vault sh

Export VAULT_TOKEN so we don’t need to give it on every vault command:

export VAULT_TOKEN=roottoken

Enable the database secrets engine. If you want to enable the secret engine in a custom path use -path option:

vault secrets enable database

Write a configuration for the database. Since we are using PostgreSQL we want to use the PostgreSQL plugin. If you where using MySQL, plugin_name would be mysql-database-plugin. exampledb-pg role is allowed to access these credentials. If you want to allow multiple roles give them as a list.

vault write database/config/exampledb-pg \
plugin_name=postgresql-database-plugin \
allowed_roles="exampledb-pg" \
connection_url="postgresql://{{username}}:{{password}}@postgres:5432/exampledb?sslmode=disable" \
username=exampledb \
password=exampledb

Next, create the role defined in the previous step. db_name value doesn’t mean the actual db, but the config name created previously. creation_statements specifies the database statements executed to create and configure a user.

cat <<EOF > vault-postgres-creation.sql
CREATE ROLE "{{name}}" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}';
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO "{{name}}";
GRANT USAGE, SELECT, UPDATE ON ALL SEQUENCES IN SCHEMA public to "{{name}}";
EOF

Create role in Vault:

vault write database/roles/exampledb-pg \
db_name=exampledb-pg \
creation_statements=@vault-postgres-creation.sql \
default_ttl="5m" \
max_ttl="24h"

Now you should be able to read credentials with command:

vault read database/creds/exampledb-pg

If everything goes fine it should output the credentials for you:

Key                Value
--- -----
lease_id database/creds/exampledb-pg/wahjU6o4dDmkLzoPFwTTSMdk
lease_duration 5m
lease_renewable true
password I98JvcCYVf-xlLEgE4A5
username v-token-exampledb-p-pAWtSOmYYCqpEnP3XARY-1616423557

In a Kubernetes environment Vault Agent sidecar pod then could automatically fetch new credentials when 85% of the TTL is used. Then on the application side you could have custom middleware that would intercept the requests and check if the credentials are still valid and use new credentials if check fails. Outside Kubernetes where there is no Vault Agent available AppRole can be used to authenticate to Vault and get credentials. With AppRole authentication you can get extra layer of security by using secret_id_bound_cidrs setting which only allows logins from specific CIDR ranges.

Anders is a Finnish IT company, whose mission is sustainable software development with the greatest colleagues of all time.