1639 words
8 minutes
HashiCorp Vault + MariaDB: Eliminating Static Credentials with Dynamic Secrets

One of the most powerful — and underutilized — features of HashiCorp Vault is its Database Secrets Engine. Instead of embedding static database passwords in your .env files, application configs, or CI/CD pipelines, Vault generates short-lived, on-demand credentials that are automatically revoked when they expire.

This guide walks through the full setup:

  1. Installing Vault in dev mode (local testing)
  2. Running MariaDB via Docker Compose
  3. Enabling and configuring the Database Secrets Engine
  4. Creating db-admin and db-read-only roles
  5. Generating and verifying dynamic credentials against MariaDB
  6. Managing leases and manual revocation

Official reference: Vault MySQL/MariaDB Database Secrets Engine


Prerequisites#

  • Vault CLI installed on your machine
  • Docker & Docker Compose (to run MariaDB)
  • A Unix-like terminal (Linux, macOS, or WSL on Windows)

Configuration Placeholders#

The table below lists the placeholder variables used throughout this guide. Replace them with your environment’s actual values:

PlaceholderDescriptionExample
<MARIADB_HOST>IP or hostname of the MariaDB server192.168.1.100
<MARIADB_PORT>MariaDB port3306
<MARIADB_ROOT_PASSWORD>Root password for MariaDBYourStrongPassword
<VAULT_HOST>IP or hostname of the Vault server (non-dev mode)192.168.1.50
<VAULT_PORT>Vault API port8200

Step 1: Install Vault (Dev Mode)#

For local testing and exploration, you don’t need a full production Vault setup (unsealing, persistent storage backends, etc.). The dev server starts pre-unsealed with an in-memory storage backend — perfect for learning.

macOS (Homebrew)#

Terminal window
brew tap hashicorp/tap
brew install hashicorp/tap/vault

Linux (Debian/Ubuntu)#

Terminal window
sudo apt update
sudo apt install -y gnupg wget lsb-release
wget -O- https://apt.releases.hashicorp.com/gpg \
| gpg --dearmor \
| sudo tee /usr/share/keyrings/hashicorp-archive-keyring.gpg > /dev/null
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" \
| sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update
sudo apt install -y vault

Verify Installation#

Terminal window
vault --version

Output:

Vault v2.0.4 (c9e9d1d4ddd4b55aae79a8949adffa9e96338720), built 2026-08-03T16:14:36Z

Start the Dev Server#

Open a dedicated terminal window and run:

Terminal window
vault server -dev -dev-root-token-id="root"

Output:

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 variables:
$ 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: c+pRvn/2UC6tGpAH5ucQ9Qejufbxnpl/u0UuHTWnoKc=
Root Token: root
Development mode should NOT be used in production installations!

Vault will start listening on http://127.0.0.1:8200 with the root token root.

Warning: Dev mode stores all data in memory and resets on restart. Never use dev mode in production.

In a second terminal, export the required environment variables so the CLI knows where to connect:

Terminal window
export VAULT_ADDR='http://127.0.0.1:8200'
export VAULT_TOKEN='root'

Verify connectivity:

Terminal window
vault status

Output:

Key Value
--- -----
Seal Type shamir
Initialized true
Sealed false
Total Shares 1
Threshold 1
Version 2.0.4
Build Date 2026-08-03T16:14:36Z
Storage Type inmem
Cluster Name vault-cluster-b032d240
Cluster ID 3ae78de0-3009-c9b6-5993-caba26940ba5
HA Enabled false

Expected output will show Sealed: false — confirming Vault is running and unsealed.


Alternative: Run Vault Without Dev Mode (via config.hcl)#

Dev mode is great for quick exploration, but it stores everything in-memory (wiped on process exit) and starts fully unsealed automatically — which doesn’t reflect how Vault behaves in a real environment. If you want a more realistic setup (persistent data, manual init/unseal), use a configuration file instead.

1. Create config.hcl#

storage "file" {
path = "/opt/vault/data"
}
listener "tcp" {
address = "<VAULT_HOST>:<VAULT_PORT>"
tls_disable = true
}
api_addr = "http://<VAULT_HOST>:<VAULT_PORT>"
ui = true

Configuration breakdown:

FieldPurpose
storage "file"Persists all Vault data to disk. For HA environments, use storage "raft" instead
listener "tcp" addressReplace <VAULT_HOST> with the server’s IP/hostname and <VAULT_PORT> with the desired port
tls_disable = trueAllows plain HTTP access without a certificate — testing/development only
api_addrThe address Vault advertises to clients and cluster peers; must match listener

Create the storage directory:

Terminal window
sudo mkdir -p /opt/vault/data
sudo chown -R $(whoami) /opt/vault/data

2. Start Vault with the Config File#

Terminal window
vault server -config=config.hcl

Output:

==> Vault server configuration:
Cgo: disabled
Go Version: go1.26.5
Listener 1: tcp (addr: "<VAULT_HOST>:8200", cluster address: "<VAULT_HOST>:8201", tls: "disabled")
Mlock: supported: true, enabled: false
Recovery Mode: false
Storage: file
Version: Vault v2.0.4, built 2026-08-03T16:14:36Z
==> Vault server started! Log data will stream in below:
[INFO] proxy environment: http_proxy="" https_proxy="" no_proxy=""
[INFO] incrementing seal generation: generation=1
[INFO] core: Initializing version history cache for core
[INFO] events: Starting event system
[INFO] core: security barrier not initialized
[INFO] core: seal configuration missing, not initialized

In a new terminal, point the CLI at your Vault instance:

Terminal window
export VAULT_ADDR='http://<VAULT_HOST>:<VAULT_PORT>'

3. Initialize Vault#

Unlike dev mode, a fresh Vault instance must be initialized before use. This process generates the unseal keys and the initial root token — store them securely, as they are only shown once:

Terminal window
vault operator init

Example output:

Unseal Key 1: xxxxx
Unseal Key 2: xxxxx
Unseal Key 3: xxxxx
Unseal Key 4: xxxxx
Unseal Key 5: xxxxx
Initial Root Token: hvs.xxxxxxxxxxxxxxxx

By default, Vault requires 3 out of 5 unseal keys (configurable via -key-shares and -key-threshold flags on vault operator init).

4. Unseal Vault#

Run vault operator unseal once per key, up to the threshold (default: 3 times), providing a different key each time:

Terminal window
vault operator unseal # Enter Unseal Key 1
vault operator unseal # Enter Unseal Key 2
vault operator unseal # Enter Unseal Key 3

Verify the status:

Terminal window
vault status

Confirm that Sealed shows false before proceeding.

5. Log In with the Root Token#

Terminal window
vault login
# Enter the Initial Root Token from `vault operator init`

Once logged in, all subsequent steps — enabling the Database Secrets Engine, configuring MariaDB, creating roles, and generating credentials — are identical to the dev mode workflow below. Just make sure VAULT_ADDR points to http://<VAULT_HOST>:<VAULT_PORT> instead of 127.0.0.1:8200.

Security note: The setup above (tls_disable = true, single-node manual unseal) is appropriate only for testing and development on trusted/private networks. For production deployments, always enable TLS, consider auto-unseal (e.g., via cloud KMS), and restrict network access to the Vault port.


Step 2: Set Up MariaDB (Docker Compose)#

Create a docker-compose.yml file to spin up a MariaDB instance:

services:
database:
container_name: mariadb
image: mariadb:latest
restart: always
ports:
- "<MARIADB_PORT>:3306"
environment:
- MARIADB_ROOT_PASSWORD=<MARIADB_ROOT_PASSWORD>
volumes:
- mariadb-data:/var/lib/mysql
volumes:
mariadb-data:
name: mariadb-data

Start the container:

Terminal window
docker compose up -d

Verify it’s running:

Terminal window
docker ps

Output:

CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
de13ac6ba5cb mariadb:latest "docker-entrypoint.s…" 4 seconds ago Up 3 seconds 0.0.0.0:3306->3306/tcp mariadb

With this configuration, MariaDB is accessible at:

ParameterValue
Host<MARIADB_HOST> (or 127.0.0.1 if Vault runs on the same machine)
Port<MARIADB_PORT>
Userroot
Password<MARIADB_ROOT_PASSWORD>

The root credentials are what Vault uses as its management account — Vault will authenticate as root to create and revoke dynamic user accounts on your behalf.


Step 3: Configure Vault Database Secrets Engine#

Before proceeding, make sure that MariaDB at <MARIADB_HOST>:<MARIADB_PORT> is reachable from the machine where Vault is running.

3.1 Enable the Database Secrets Engine#

Terminal window
vault secrets enable database

Output:

Success! Enabled the database secrets engine at: database/

3.2 Configure the Database Connection#

Tell Vault how to connect to MariaDB. Vault uses the mysql-database-plugin, which is fully compatible with MariaDB:

Terminal window
vault write database/config/mariadb \
plugin_name=mysql-database-plugin \
connection_url="{{username}}:{{password}}@tcp(<MARIADB_HOST>:<MARIADB_PORT>)/" \
allowed_roles="*" \
username="root" \
password="<MARIADB_ROOT_PASSWORD>"

Parameter breakdown:

ParameterPurpose
connection_urlMariaDB address with {{username}} and {{password}} placeholders that Vault fills in automatically
allowed_roles="*"Permits all roles defined under this connection to use it. For production, restrict to explicit role names (e.g., "db-admin,db-read-only")
username / passwordThe management credentials Vault uses to create and drop dynamic users

3.3 Create an Admin Role#

This role generates users with full ALL PRIVILEGES — suitable for administrative or migration workloads:

Terminal window
vault write database/roles/db-admin \
db_name=mariadb \
creation_statements="CREATE USER '{{name}}'@'%' IDENTIFIED BY '{{password}}';" \
creation_statements="GRANT ALL PRIVILEGES ON *.* TO '{{name}}'@'%';" \
revocation_statements="DROP USER '{{name}}'@'%';" \
default_ttl="1h" \
max_ttl="1h"

3.4 Create a Read-Only Role#

This role generates users with SELECT privileges only — ideal for reporting dashboards, data pipelines, or read replicas:

Terminal window
vault write database/roles/db-read-only \
db_name=mariadb \
creation_statements="CREATE USER '{{name}}'@'%' IDENTIFIED BY '{{password}}';" \
creation_statements="GRANT SELECT ON *.* TO '{{name}}'@'%';" \
revocation_statements="DROP USER '{{name}}'@'%';" \
default_ttl="1h" \
max_ttl="1h"

Step 4: Generate Dynamic Credentials#

With the roles in place, request credentials on demand. You no longer need to know or manage any static passwords.

Admin credentials:

Terminal window
vault read database/creds/db-admin

Read-only credentials:

Terminal window
vault read database/creds/db-read-only

Example output:

Key Value
--- -----
lease_id database/creds/db-read-only/2f6a614c-4aa2-7b19-24b9-ad944a8d4de6
lease_duration 1h
lease_renewable true
password yY-57n3X5UQhxnmFRP3f
username v-root-db-read-onl-crBWVqVh2Hc1

Each invocation creates a brand-new MariaDB user with a randomly generated username and password. When the TTL expires, Vault automatically executes the revocation_statements to drop that user from MariaDB.


Step 5: Verify the Credentials#

Use the freshly generated credentials to authenticate against MariaDB:

Terminal window
mariadb -h <MARIADB_HOST> \
-u v-root-db-read-onl-crBWVqVh2Hc1 \
-pyY-57n3X5UQhxnmFRP3f \
-e "SHOW DATABASES;"

Expected behavior:

  • Credentials from db-admin can execute any SQL operation (DDL, DML, etc.)
  • Credentials from db-read-only: SELECT queries succeed; INSERT, DROP, etc., are rejected with an access denied error

If both behave as expected, the Vault and MariaDB integration is working correctly.


Step 6 (Optional): Lease Management & Manual Revocation#

List Active Leases#

Terminal window
vault list sys/leases/lookup/database/creds/db-read-only

Revoke a Specific Lease Early#

Terminal window
vault lease revoke database/creds/db-read-only/<lease_id>

After revocation, Vault immediately executes the DROP USER statement on MariaDB — the credentials are invalidated instantly, even before the TTL would have expired.

Revoke All Leases for a Role#

Terminal window
vault lease revoke -prefix database/creds/db-read-only/

This is useful in incident response scenarios where you want to instantly invalidate every active credential for a compromised role.


Why This Matters: Security Benefits#

Traditional ApproachVault Dynamic Secrets
Static password stored in .env or configNo static password exists anywhere
Password shared across multiple servicesEach service/request gets a unique credential
Manual rotation (easy to forget)Automatic revocation at TTL expiry
Audit trail unclearEvery credential tied to a Vault lease ID
Leaked password = permanent breach riskLeaked credential expires in 1 hour or less

What’s Next#

Once you’ve verified the basic flow, consider exploring these advanced capabilities:

  • Root credential rotation — Let Vault manage and rotate the root password it uses:

    Terminal window
    vault write -force database/rotate-root/mariadb

    After this command, only Vault knows the root password.

  • Static roles — For existing long-lived database users that need periodic password rotation rather than dynamic creation.

  • Vault Agent — A sidecar daemon that automatically fetches and renews credentials, injecting them into your application’s environment or config files with zero code changes.

  • Vault API / SDK — Integrate dynamic credential retrieval directly into your application at startup time using the Vault HTTP API or language-specific SDKs (Go, Python, Java, etc.).


Conclusion#

By integrating HashiCorp Vault’s Database Secrets Engine with MariaDB, you fundamentally change how applications access your database:

  • No more static passwords — credentials are generated on-demand and never stored in plaintext
  • Automatic isolation — every consumer gets a unique credential, making audit logs precise and breach impact minimal
  • Zero-config revocation — credentials expire automatically; incident response is a single vault lease revoke command away

This pattern is the foundation of secrets management at scale and applies equally well to PostgreSQL, MySQL, Oracle, MongoDB, and other supported database engines.

Reference: Vault MySQL/MariaDB Database Secrets Engine — HashiCorp Docs

HashiCorp Vault + MariaDB: Eliminating Static Credentials with Dynamic Secrets
https://im-gatan.com/posts/hashicorp-vault-mariadb-dynamic-credentials/
Author
Gatan
Published at
2026-08-19
License
CC BY-NC-SA 4.0