Set up HashiCorp Vault's database secrets engine to automatically generate short-lived credentials for PostgreSQL and MySQL databases, improving security by eliminating static passwords and enabling automated credential rotation.
Prerequisites
- Running Vault server with admin access
- PostgreSQL or MySQL database server
- Network connectivity between Vault and databases
What this solves
Static database passwords are a security risk that creates operational overhead when they need rotation. Vault's dynamic secrets engine generates temporary database credentials on-demand, automatically rotating them and revoking access when the lease expires. This eliminates the need to store long-lived database passwords in configuration files or environment variables.
Prerequisites
You'll need a running Vault server and PostgreSQL or MySQL database server. This tutorial assumes you have administrative access to both systems and basic familiarity with database user management.
Step-by-step configuration
Install database clients
Install the database client tools needed to test connections and manage database users.
sudo apt update
sudo apt install -y postgresql-client mysql-client
Enable the database secrets engine
Enable Vault's database secrets engine at the database/ path. This engine manages database connections and credential generation.
vault auth -method=userpass username=admin
vault secrets enable database
Create PostgreSQL database connection
Configure Vault to connect to your PostgreSQL database. Replace the connection details with your actual database server information.
vault write database/config/postgresql \
plugin_name=postgresql-database-plugin \
connection_url="postgresql://{{username}}:{{password}}@203.0.113.10:5432/postgres?sslmode=disable" \
allowed_roles="readonly,readwrite" \
username="vault" \
password="vault_user_password"
Create MySQL database connection
Configure Vault to connect to your MySQL database using the MySQL database plugin.
vault write database/config/mysql \
plugin_name=mysql-database-plugin \
connection_url="{{username}}:{{password}}@tcp(203.0.113.20:3306)/" \
allowed_roles="readonly,readwrite" \
username="vault" \
password="vault_user_password"
Create PostgreSQL database users
Create the Vault service user in PostgreSQL with permissions to create and manage other database users.
psql -h 203.0.113.10 -U postgres -c "CREATE USER vault WITH PASSWORD 'vault_user_password';"
psql -h 203.0.113.10 -U postgres -c "GRANT CREATE ON DATABASE postgres TO vault;"
psql -h 203.0.113.10 -U postgres -c "ALTER USER vault CREATEROLE;"
Create MySQL database users
Create the Vault service user in MySQL with permissions to create and manage other database users.
mysql -h 203.0.113.20 -u root -p -e "CREATE USER 'vault'@'%' IDENTIFIED BY 'vault_user_password';"
mysql -h 203.0.113.20 -u root -p -e "GRANT CREATE USER ON . TO 'vault'@'%' WITH GRANT OPTION;"
mysql -h 203.0.113.20 -u root -p -e "GRANT SELECT ON . TO 'vault'@'%';"
mysql -h 203.0.113.20 -u root -p -e "FLUSH PRIVILEGES;"
Create PostgreSQL readonly role
Define a readonly role that generates database users with SELECT permissions only. The SQL statements define what permissions the generated users will have.
vault write database/roles/postgresql-readonly \
db_name=postgresql \
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"
Create PostgreSQL readwrite role
Define a readwrite role that generates database users with SELECT, INSERT, UPDATE, and DELETE permissions.
vault write database/roles/postgresql-readwrite \
db_name=postgresql \
creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; \
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO \"{{name}}\";"\
default_ttl="1h" \
max_ttl="24h"
Create MySQL readonly role
Define a MySQL readonly role that creates users with SELECT permissions across all databases.
vault write database/roles/mysql-readonly \
db_name=mysql \
creation_statements="CREATE USER '{{name}}'@'%' IDENTIFIED BY '{{password}}'; \
GRANT SELECT ON . TO '{{name}}'@'%';" \
revocation_statements="DROP USER '{{name}}'@'%';" \
default_ttl="1h" \
max_ttl="24h"
Create MySQL readwrite role
Define a MySQL readwrite role that creates users with comprehensive data manipulation permissions.
vault write database/roles/mysql-readwrite \
db_name=mysql \
creation_statements="CREATE USER '{{name}}'@'%' IDENTIFIED BY '{{password}}'; \
GRANT SELECT, INSERT, UPDATE, DELETE ON . TO '{{name}}'@'%';" \
revocation_statements="DROP USER '{{name}}'@'%';" \
default_ttl="1h" \
max_ttl="24h"
Create access policies
Create Vault policies that control which applications can request specific database roles.
path "database/creds/postgresql-readonly" {
capabilities = ["read"]
}
path "database/creds/mysql-readonly" {
capabilities = ["read"]
}
vault policy write db-readonly /tmp/db-readonly-policy.hcl
Create readwrite policy
Create a more privileged policy for applications that need write access to databases.
path "database/creds/postgresql-readwrite" {
capabilities = ["read"]
}
path "database/creds/mysql-readwrite" {
capabilities = ["read"]
}
vault policy write db-readwrite /tmp/db-readwrite-policy.hcl
Test credential generation
Generate dynamic credentials for PostgreSQL and MySQL to verify the configuration works correctly.
vault read database/creds/postgresql-readonly
vault read database/creds/mysql-readonly
The output shows the generated username, password, and lease information.
Verify your setup
Test that the generated credentials work by connecting to your databases and verifying the appropriate permissions.
# Generate PostgreSQL credentials
vault read database/creds/postgresql-readonly
Test PostgreSQL connection (replace with actual generated credentials)
psql -h 203.0.113.10 -U v-token-postgre-2Rt7X9Y4kZ -d postgres -c "SELECT current_user;"
Generate MySQL credentials
vault read database/creds/mysql-readonly
Test MySQL connection (replace with actual generated credentials)
mysql -h 203.0.113.20 -u v-token-mysql-8Kx3M2N7qL -p -e "SELECT USER();"
Check that credentials automatically expire and are cleaned up:
# List active leases
vault list sys/leases/lookup/database/creds/postgresql-readonly
Check lease details
vault lease lookup database/creds/postgresql-readonly/lease_id_here
Configure credential rotation
Set up automatic rotation of the root database credentials that Vault uses to connect to your databases.
Enable root credential rotation
Configure Vault to automatically rotate the root database credentials periodically for enhanced security.
vault write database/config/postgresql \
plugin_name=postgresql-database-plugin \
connection_url="postgresql://{{username}}:{{password}}@203.0.113.10:5432/postgres?sslmode=disable" \
allowed_roles="readonly,readwrite" \
username="vault" \
password="vault_user_password" \
password_policy="vault_password_policy"
Create password policy
Define a password policy for generated credentials to ensure they meet security requirements.
length = 20
rule "charset" {
charset = "abcdefghijklmnopqrstuvwxyz"
min-chars = 1
}
rule "charset" {
charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
min-chars = 1
}
rule "charset" {
charset = "0123456789"
min-chars = 1
}
rule "charset" {
charset = "!@#$%^&*"
min-chars = 1
}
vault write sys/policies/password/vault_password_policy policy=@/tmp/password-policy.hcl
Rotate root credentials
Perform an immediate rotation of the root credentials to test the rotation functionality.
vault write -force database/rotate-root/postgresql
vault write -force database/rotate-root/mysql
Application integration examples
Here are examples of how applications can request and use dynamic database credentials.
Python application example
import hvac
import psycopg2
import os
Initialize Vault client
client = hvac.Client(url='http://127.0.0.1:8200')
client.token = os.environ['VAULT_TOKEN']
Get dynamic PostgreSQL credentials
response = client.read('database/creds/postgresql-readonly')
credentials = response['data']
Connect to database using dynamic credentials
conn = psycopg2.connect(
host="203.0.113.10",
database="postgres",
user=credentials['username'],
password=credentials['password']
)
Use the connection
cursor = conn.cursor()
cursor.execute("SELECT version();")
result = cursor.fetchone()
print(result)
Shell script example
#!/bin/bash
Get PostgreSQL credentials from Vault
CREDS=$(vault read -format=json database/creds/postgresql-readonly)
DB_USER=$(echo $CREDS | jq -r '.data.username')
DB_PASS=$(echo $CREDS | jq -r '.data.password')
Export for use by other applications
export PGUSER=$DB_USER
export PGPASSWORD=$DB_PASS
export PGHOST=203.0.113.10
export PGDATABASE=postgres
echo "Database credentials set for user: $DB_USER"
Common issues
| Symptom | Cause | Fix |
|---|---|---|
| "permission denied for database" error | Vault user lacks CREATE permissions | Grant CREATE and CREATEROLE to vault user in PostgreSQL |
| "Access denied" in MySQL | Vault user missing user management privileges | Grant CREATE USER and WITH GRANT OPTION to vault MySQL user |
| "plugin not found" error | Database plugin not available | Verify Vault version includes database plugins or reinstall |
| Generated users can't connect | Network connectivity or host restrictions | Check firewall rules and database host-based authentication |
| Credentials expire too quickly | Default TTL too short | Increase default_ttl and max_ttl in role configuration |
| "role does not exist" error | Role name mismatch or not created | Verify role exists with vault list database/roles |
Security considerations
Implement these security best practices when using Vault dynamic secrets in production:
- Use TLS encryption for all Vault and database connections
- Implement proper authentication methods like AppRole instead of tokens
- Monitor credential usage and lease activity through Vault audit logs
- Set appropriate TTL values based on application needs and security requirements
- Regularly rotate the root database credentials used by Vault
- Use network policies to restrict database access to authorized applications only
For enhanced security in production environments, consider implementing Vault PKI for certificate management alongside database secrets.
Next steps
- Set up Vault auto-unseal with cloud KMS
- Monitor PostgreSQL performance with Prometheus
- Integrate Vault with Kubernetes service accounts
- Configure Vault audit logging for compliance
- Set up Vault disaster recovery and replication
Running this in production?
Automated install script
Run this to automate the entire setup
#!/usr/bin/env bash
set -euo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
# Global variables
VAULT_ADDR="${VAULT_ADDR:-http://127.0.0.1:8200}"
POSTGRES_HOST=""
MYSQL_HOST=""
POSTGRES_PORT="5432"
MYSQL_PORT="3306"
VAULT_USER_PASS=""
usage() {
echo "Usage: $0 [OPTIONS]"
echo "Options:"
echo " -p, --postgres-host HOST PostgreSQL server hostname/IP"
echo " -m, --mysql-host HOST MySQL server hostname/IP"
echo " --postgres-port PORT PostgreSQL port (default: 5432)"
echo " --mysql-port PORT MySQL port (default: 3306)"
echo " --vault-password PASS Password for Vault database user"
echo " -h, --help Show this help message"
echo ""
echo "Example:"
echo " $0 --postgres-host 192.168.1.10 --mysql-host 192.168.1.20 --vault-password mypassword"
exit 1
}
log_info() {
echo -e "${GREEN}[INFO]${NC} $1"
}
log_warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
cleanup() {
if [[ $? -ne 0 ]]; then
log_error "Script failed. Rolling back changes..."
vault secrets disable database 2>/dev/null || true
fi
}
trap cleanup ERR
detect_distro() {
if [ -f /etc/os-release ]; then
. /etc/os-release
case "$ID" in
ubuntu|debian)
PKG_MGR="apt"
PKG_INSTALL="apt install -y"
PKG_UPDATE="apt update"
;;
almalinux|rocky|centos|rhel|ol|fedora)
PKG_MGR="dnf"
PKG_INSTALL="dnf install -y"
PKG_UPDATE="dnf check-update || true"
;;
amzn)
PKG_MGR="yum"
PKG_INSTALL="yum install -y"
PKG_UPDATE="yum check-update || true"
;;
*)
log_error "Unsupported distribution: $ID"
exit 1
;;
esac
else
log_error "Cannot detect distribution. /etc/os-release not found."
exit 1
fi
}
check_prerequisites() {
echo "[1/8] Checking prerequisites..."
if [[ $EUID -ne 0 ]]; then
log_error "This script must be run as root or with sudo"
exit 1
fi
if ! command -v vault &> /dev/null; then
log_error "Vault CLI not found. Please install Vault first."
exit 1
fi
if [[ -z "$POSTGRES_HOST" && -z "$MYSQL_HOST" ]]; then
log_error "At least one database host (PostgreSQL or MySQL) must be specified"
usage
fi
if [[ -z "$VAULT_USER_PASS" ]]; then
log_error "Vault database user password is required"
usage
fi
log_info "Prerequisites check completed"
}
install_database_clients() {
echo "[2/8] Installing database client tools..."
$PKG_UPDATE
if [[ -n "$POSTGRES_HOST" ]]; then
if [[ "$PKG_MGR" == "apt" ]]; then
$PKG_INSTALL postgresql-client
else
$PKG_INSTALL postgresql
fi
fi
if [[ -n "$MYSQL_HOST" ]]; then
if [[ "$PKG_MGR" == "apt" ]]; then
$PKG_INSTALL mysql-client
else
$PKG_INSTALL mysql
fi
fi
log_info "Database clients installed successfully"
}
enable_database_secrets_engine() {
echo "[3/8] Enabling Vault database secrets engine..."
export VAULT_ADDR
if ! vault auth -method=userpass username=admin 2>/dev/null; then
log_warn "Failed to authenticate with userpass. Trying token auth..."
if [[ -z "${VAULT_TOKEN:-}" ]]; then
log_error "Please set VAULT_TOKEN or configure userpass authentication"
exit 1
fi
fi
vault secrets enable -path=database database || {
log_warn "Database secrets engine may already be enabled"
}
log_info "Database secrets engine enabled"
}
configure_postgresql() {
echo "[4/8] Configuring PostgreSQL connection..."
if [[ -z "$POSTGRES_HOST" ]]; then
log_info "Skipping PostgreSQL configuration (no host specified)"
return
fi
vault write database/config/postgresql \
plugin_name=postgresql-database-plugin \
connection_url="postgresql://{{username}}:{{password}}@${POSTGRES_HOST}:${POSTGRES_PORT}/postgres?sslmode=disable" \
allowed_roles="postgresql-readonly,postgresql-readwrite" \
username="vault" \
password="$VAULT_USER_PASS"
log_info "PostgreSQL connection configured"
}
configure_mysql() {
echo "[5/8] Configuring MySQL connection..."
if [[ -z "$MYSQL_HOST" ]]; then
log_info "Skipping MySQL configuration (no host specified)"
return
fi
vault write database/config/mysql \
plugin_name=mysql-database-plugin \
connection_url="{{username}}:{{password}}@tcp(${MYSQL_HOST}:${MYSQL_PORT})/" \
allowed_roles="mysql-readonly,mysql-readwrite" \
username="vault" \
password="$VAULT_USER_PASS"
log_info "MySQL connection configured"
}
create_postgresql_roles() {
echo "[6/8] Creating PostgreSQL roles..."
if [[ -z "$POSTGRES_HOST" ]]; then
log_info "Skipping PostgreSQL roles (no host specified)"
return
fi
vault write database/roles/postgresql-readonly \
db_name=postgresql \
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 write database/roles/postgresql-readwrite \
db_name=postgresql \
creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO \"{{name}}\";" \
default_ttl="1h" \
max_ttl="24h"
log_info "PostgreSQL roles created"
}
create_mysql_roles() {
echo "[7/8] Creating MySQL roles..."
if [[ -z "$MYSQL_HOST" ]]; then
log_info "Skipping MySQL roles (no host specified)"
return
fi
vault write database/roles/mysql-readonly \
db_name=mysql \
creation_statements="CREATE USER '{{name}}'@'%' IDENTIFIED BY '{{password}}'; GRANT SELECT ON *.* TO '{{name}}'@'%';" \
default_ttl="1h" \
max_ttl="24h"
vault write database/roles/mysql-readwrite \
db_name=mysql \
creation_statements="CREATE USER '{{name}}'@'%' IDENTIFIED BY '{{password}}'; GRANT SELECT, INSERT, UPDATE, DELETE ON *.* TO '{{name}}'@'%';" \
default_ttl="1h" \
max_ttl="24h"
log_info "MySQL roles created"
}
verify_configuration() {
echo "[8/8] Verifying configuration..."
log_info "Listing database connections:"
vault list database/config
log_info "Listing database roles:"
vault list database/roles
if [[ -n "$POSTGRES_HOST" ]]; then
log_info "Testing PostgreSQL readonly credential generation..."
vault read database/creds/postgresql-readonly
fi
if [[ -n "$MYSQL_HOST" ]]; then
log_info "Testing MySQL readonly credential generation..."
vault read database/creds/mysql-readonly
fi
log_info "Configuration verification completed successfully!"
}
main() {
while [[ $# -gt 0 ]]; do
case $1 in
-p|--postgres-host)
POSTGRES_HOST="$2"
shift 2
;;
-m|--mysql-host)
MYSQL_HOST="$2"
shift 2
;;
--postgres-port)
POSTGRES_PORT="$2"
shift 2
;;
--mysql-port)
MYSQL_PORT="$2"
shift 2
;;
--vault-password)
VAULT_USER_PASS="$2"
shift 2
;;
-h|--help)
usage
;;
*)
log_error "Unknown option: $1"
usage
;;
esac
done
detect_distro
check_prerequisites
install_database_clients
enable_database_secrets_engine
configure_postgresql
configure_mysql
create_postgresql_roles
create_mysql_roles
verify_configuration
log_info "Vault database dynamic secrets configuration completed!"
echo ""
echo "Next steps:"
echo "1. Create the vault service user in your databases:"
if [[ -n "$POSTGRES_HOST" ]]; then
echo " PostgreSQL: psql -h $POSTGRES_HOST -U postgres -c \"CREATE USER vault WITH PASSWORD '$VAULT_USER_PASS';\""
echo " psql -h $POSTGRES_HOST -U postgres -c \"GRANT CREATE ON DATABASE postgres TO vault;\""
echo " psql -h $POSTGRES_HOST -U postgres -c \"ALTER USER vault CREATEROLE;\""
fi
if [[ -n "$MYSQL_HOST" ]]; then
echo " MySQL: mysql -h $MYSQL_HOST -u root -p -e \"CREATE USER 'vault'@'%' IDENTIFIED BY '$VAULT_USER_PASS';\""
echo " mysql -h $MYSQL_HOST -u root -p -e \"GRANT CREATE USER ON *.* TO 'vault'@'%' WITH GRANT OPTION;\""
fi
echo "2. Generate credentials: vault read database/creds/postgresql-readonly"
}
main "$@"
Review the script before running. Execute with: bash install.sh