HashiCorp Vault — PostgreSQL & MySQL
Hands-on setup of dynamic database credentials for PostgreSQL and MySQL — the two most common relational databases.
PostgreSQL — Complete Setup
Prerequisites
# Start PostgreSQL (Docker)
docker run -d --name postgres \
-e POSTGRES_USER=vault_admin \
-e POSTGRES_PASSWORD=vault_admin_pass \
-e POSTGRES_DB=myapp \
-p 5432:5432 \
postgres:16
# Verify connectivity
psql -h 127.0.0.1 -U vault_admin -d myapp -c "SELECT 1"
Configure Vault
# Enable database secrets engine
vault secrets enable database
# Configure PostgreSQL connection
vault write database/config/myapp-postgres \
plugin_name="postgresql-database-plugin" \
allowed_roles="pg-readonly,pg-readwrite" \
connection_url="postgresql://:@127.0.0.1:5432/myapp?sslmode=disable" \
username="vault_admin" \
password="vault_admin_pass"
# Rotate root credentials (Vault takes ownership)
vault write -f database/rotate-root/myapp-postgres
Create Roles
# Read-only role
vault write database/roles/pg-readonly \
db_name="myapp-postgres" \
creation_statements="CREATE ROLE \"\" WITH LOGIN PASSWORD '' VALID UNTIL ''; \
GRANT CONNECT ON DATABASE myapp TO \"\"; \
GRANT USAGE ON SCHEMA public TO \"\"; \
GRANT SELECT ON ALL TABLES IN SCHEMA public TO \"\"; \
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO \"\";" \
revocation_statements="REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA public FROM \"\"; \
REVOKE ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public FROM \"\"; \
REVOKE USAGE ON SCHEMA public FROM \"\"; \
REVOKE CONNECT ON DATABASE myapp FROM \"\"; \
DROP ROLE IF EXISTS \"\";" \
default_ttl="1h" \
max_ttl="24h"
# Read-write role
vault write database/roles/pg-readwrite \
db_name="myapp-postgres" \
creation_statements="CREATE ROLE \"\" WITH LOGIN PASSWORD '' VALID UNTIL ''; \
GRANT CONNECT ON DATABASE myapp TO \"\"; \
GRANT USAGE ON SCHEMA public TO \"\"; \
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO \"\"; \
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO \"\";" \
revocation_statements="REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA public FROM \"\"; \
REVOKE USAGE ON SCHEMA public FROM \"\"; \
REVOKE CONNECT ON DATABASE myapp FROM \"\"; \
DROP ROLE IF EXISTS \"\";" \
default_ttl="1h" \
max_ttl="24h"
Generate and Test Credentials
# Generate read-only credentials
vault read database/creds/pg-readonly
Expected output:
Key Value
--- -----
lease_id database/creds/pg-readonly/abc123
lease_duration 1h
lease_renewable true
password A1a-randompassword123
username v-token-pg-readon-abc123-1234567890
# Test the credentials
psql -h 127.0.0.1 -U v-token-pg-readon-abc123-1234567890 -d myapp
# Can SELECT
SELECT * FROM my_table;
# Cannot INSERT (read-only)
INSERT INTO my_table (name) VALUES ('test');
-- ERROR: permission denied
MySQL — Complete Setup
Prerequisites
# Start MySQL (Docker)
docker run -d --name mysql \
-e MYSQL_ROOT_PASSWORD=root_pass \
-e MYSQL_DATABASE=myapp \
-e MYSQL_USER=vault_admin \
-e MYSQL_PASSWORD=vault_admin_pass \
-p 3306:3306 \
mysql:8
# Verify connectivity
mysql -h 127.0.0.1 -u vault_admin -pvault_admin_pass myapp -e "SELECT 1"
Configure Vault
# Configure MySQL connection
vault write database/config/myapp-mysql \
plugin_name="mysql-database-plugin" \
allowed_roles="mysql-readonly,mysql-readwrite" \
connection_url=":@tcp(127.0.0.1:3306)/myapp" \
username="vault_admin" \
password="vault_admin_pass"
# Rotate root credentials
vault write -f database/rotate-root/myapp-mysql
Create Roles
# Read-only role
vault write database/roles/mysql-readonly \
db_name="myapp-mysql" \
creation_statements="CREATE USER ''@'%' IDENTIFIED BY ''; \
GRANT SELECT ON myapp.* TO ''@'%';" \
revocation_statements="REVOKE ALL PRIVILEGES, GRANT OPTION FROM ''@'%'; \
DROP USER IF EXISTS ''@'%';" \
default_ttl="1h" \
max_ttl="24h"
# Read-write role
vault write database/roles/mysql-readwrite \
db_name="myapp-mysql" \
creation_statements="CREATE USER ''@'%' IDENTIFIED BY ''; \
GRANT SELECT, INSERT, UPDATE, DELETE ON myapp.* TO ''@'%';" \
revocation_statements="REVOKE ALL PRIVILEGES, GRANT OPTION FROM ''@'%'; \
DROP USER IF EXISTS ''@'%';" \
default_ttl="1h" \
max_ttl="24h"
Generate Credentials
vault read database/creds/mysql-readonly
Application Integration
Python Example
import hvac
import psycopg2
# Authenticate to Vault
client = hvac.Client(url='http://127.0.0.1:8200', token='my-token')
# Get dynamic credentials
creds = client.secrets.database.generate_credentials('pg-readonly')
username = creds['data']['username']
password = creds['data']['password']
lease_id = creds['lease_id']
# Connect to database
conn = psycopg2.connect(
host='127.0.0.1',
database='myapp',
user=username,
password=password
)
# Use the connection...
cursor = conn.cursor()
cursor.execute("SELECT * FROM users")
# When done, revoke the lease
client.sys.revoke_lease(lease_id)
Quick Reference
| Command | Description |
|---|---|
vault write database/config/NAME ... |
Configure DB connection |
vault write -f database/rotate-root/NAME |
Rotate root password |
vault write database/roles/ROLE ... |
Create role |
vault read database/creds/ROLE |
Generate credentials |
vault lease revoke LEASE_ID |
Revoke credentials early |
Next: AWS Secrets Engine