Almost every production app need credentials to access their database.
A common approach is to create a database user with a password and provide that credential to the application through environment variable, configuration file, or secret management system.
The application then uses the same credential until it is manually changed.
This approach is simple, but the credential itself becomes a long lived security risk until someone rotates it.
Though credential rotation can also become a new problem.
A typical workflow might look like this:
The problem is not only how the credential is stored.
We also need to consider its entire lifecycle.
Who creates it?
How long should it remain valid?
How is it renewed while the application is running?
How do we replace it without interrupting the application?
What happens to the old credential after the replacement is ready?
We wanted to approach this differently.
Instead of giving the application a permanent database password, we wanted the database credential to be temporary by design.
This is where dynamic credentials become useful.
With HashiCorp Vault, an application can request a database credential when it needs one. Vault generates a temporary database user and returns the credential together with a lease.
The application can then use the credential while it is valid and renew its lease while the credential is still in use.
When the application needs to replace the credential, it can request another one instead of modifying a permanent password.
The workflow becomes:
This changes credential management from a manually maintained configuration value into a lifecycle that can be managed by the application.
Project Goals
We wanted an application whose database credentials are temporary by design, automatically renewed while in use, and replaced periodically without storing a static password.
The application should be able to:
Authenticate to Vault without using a Vault root token.
Obtain database credentials dynamically.
Connect to Database using those credentials.
Renew the credential lease while the application is running.
After three successful renewals, request a new credential.
Verify the new credential against Database.
Switch to the new credential.
Revoke the old lease.
Continue serving requests without restarting the application.
The three renewal threshold is intentionally used as a simple rotation trigger for this project. It gives us a deterministic lifecycle that can be observed and tested without introducing a more complex rotation policy.
The complete implementation will show how these pieces work together, from the initial authentication and credential request to lease renewal and the final zero downtime credential switch.
The code snippets in this article focus on the important parts of the implementation. For the complete configuration you can find the full source code in the repository below.
GitHub Repository: https://github.com/muhammadyulasfipahrizal/vault-setup.git
Architecture
The project consists of three main components, each component has a specific responsibility in the credential lifecycle.
Vault
HashiCorp Vault is responsible for managing the database credentials used by the application.
Vault generates temporary credentials and manages the lease associated with each credential. The application communicates with Vault to authenticate, obtain credentials, and maintain the credential lease while it is running.
Application
The application is a small Go API that consumes credentials provided by Vault.
It authenticates to Vault, requests a dynamic PostgreSQL credential, and uses that credential to establish a connection to the database.
The application also manages the runtime credential lifecycle. While the credential is being used, it renews the lease. When the rotation condition is reached, the application requests a new credential and verifies the new connection before switching away from the existing one.
This allows the application to replace its database credential while continuing to run.
Database
PostgreSQL is the database accessed by the application.
The database is configured so that Vault can create temporary users for the application. These users are created with the permissions required by the application and are removed when their associated credentials are revoked.
The application does not communicate with PostgreSQL through Vault. Instead, Vault provides the application with the credentials, and the application connects directly to PostgreSQL.
Component Interaction
The interaction between the three components can be summarized as:
This architecture keeps credential management separate from database access. Vault provides the temporary identity needed to access the database, while the application remains responsible for using that identity throughout its lifecycle.
Project Structure
Each part of the repository has a specific responsibility.
app/contains the Go application, including its source code, Go dependencies, Dockerfile, and Docker Compose configuration.config/contains the Vault server configuration.db/contains the PostgreSQL Docker Compose configuration and the SQL script used to initialize the database.docker-compose.ymlat the repository root defines the Vault service and connects it to the shared Docker network.
Implementation
PostgreSQL
The PostgreSQL service is defined in db/docker-compose.yml.
The configuration uses PostgreSQL 17 and persists its data through a Docker volume. The initialization directory is mounted into PostgreSQL's /docker-entrypoint-initdb.d directory so the database can be prepared when the container is initialized.
The service is also attached to the external Docker network used by the other components, allowing Vault and the application to communicate with PostgreSQL through its container name.
services:
postgres:
image: postgres:17
container_name: db-postgres
restart: unless-stopped
environment:
POSTGRES_DB: ${YOUR_DB_NAME}
POSTGRES_USER: ${YOUR_DB_USER}
POSTGRES_PASSWORD: ${YOUR_DB_PASSWORD}
ports:
- "5432:5432"
volumes:
- postgres-data:/var/lib/postgresql/data
- ./postgres/init:/docker-entrypoint-initdb.d:ro
networks:
- ${YOUR_DOCKER_NETWORK}
Database Initialization
The initialization script is located at:
db/postgres/init/01-init.sql
Its purpose is to create the administrative database user that Vault will use when managing dynamic PostgreSQL users.
The script grants this user permission to connect to the target database and create objects in the public schema.
The repository contains the following initialization:
Vault
Vault is defined in the root docker-compose.yml.
The service uses the official hashicorp/vault image, exposes ports 8200 and 8201, mounts the Vault data and configuration directories, and starts Vault using the repository's HCL configuration file.
services:
vault:
image: hashicorp/vault:latest
container_name: vault
restart: unless-stopped
ports:
- "8200:8200"
- "8201:8201"
volumes:
- ./data:/vault/data
- ./config:/vault/config:ro
cap_add:
- IPC_LOCK
command: vault server -config=/vault/config/vault.hcl
networks:
- ${YOUR_DOCKER_NETWORK}
Vault Configuration
The Vault server configuration is stored under config/.
ui = true
disable_mlock = true
api_addr = "https://0.0.0.0:8200"
cluster_addr = "https://0.0.0.0:8201"
listener "tcp" {
address = "0.0.0.0:8200"
cluster_address = "0.0.0.0:8201"
tls_disable = true
}
storage "raft" {
path = "/vault/data"
node_id = "vault-1"
}
The configuration enables the Vault UI and uses Raft storage for Vault's persistent data. The TCP listener accepts connections on ports 8200 and 8201.
Enable Database Secrets Engine
The Database Secrets Engine allows Vault to generate database credentials.
For this project, the database plugin is:
postgresql-database-plugin
The database connection is configured with the name:
vault-postgres
Vault connects to PostgreSQL through the Docker network using:
postgresql://{{username}}:{{password}}@vault-postgres:5432/appdb?sslmode=disable
The {{username}} and {{password}} values are provided by Vault's database plugin when it establishes the administrative connection.
Dynamic Database Role
When the application requests database/creds/app-role, Vault generates a new role and password.
The role is configured as a dynamic role with:
TTL: 1 hour
Max TTL: 1 day
When the application requests:
database/creds/app-role
Vault generates a new database username and password.
The creation statements are:
CREATE ROLE "{{name}}"
WITH LOGIN
PASSWORD '{{password}}'
VALID UNTIL '{{expiration}}';
GRANT CONNECT ON DATABASE YOUR_DATABASE_NAME
TO "{{name}}";
Vault replaces the template variables with generated values before executing the statements against PostgreSQL.
The role also defines a revocation statement:
DROP ROLE IF EXISTS "{{name}}";
This is what allows Vault to clean up the generated PostgreSQL user when the lease is revoked.
Policy
The application also needs permission to request them from Vault.
We can create ACL policy named app-policy:
This policy deliberately exposes only the credential endpoint required by the application.
AppRole Authentication
The application should not authenticate to Vault using the root token.
Instead, we enable the AppRole authentication method.
The configured authentication method is:
The application receives two values:
Role ID
Secret ID
These values are provided to the application through its runtime environment rather than embedding them directly. The application can therefore authenticate as a service identity without receiving the Vault root token.
App
The application is defined in app/docker-compose.yml.
The Compose configuration provides the Vault address, AppRole credentials, and PostgreSQL connection information through environment variables.
The application communicates with Vault using:
http://vault:8200
and database with:
db-postgres:5432
Both services are connected through the same external Docker network.
This keeps the application configuration independent from hard coded container IP addresses.
Application Lifecycle
Authenticate
When the application starts, it creates a Vault client and authenticates using AppRole.
The application sends its Role ID and Secret ID to:
auth/approle/login
Vault validates the credentials and returns a Vault token.
That token is then used for subsequent requests to the Vault API.
Request Dynamic Credentials
After authentication, the application requests:
database/creds/app-role
Vault creates a new Database role and returns:
username
password
lease ID
lease duration
The application stores these values in its runtime state.
Connect to Database
The generated username and password are then used to create a database connection.
The application verifies the credential by establishing the connection and performing a database ping.
Lease Renewal
The application obtains the lease duration directly from Vault when it requests the credentials.
Instead of using a fixed renewal interval, the renewal loop waits for approximately half of the current lease duration before attempting a renewal.
When the renewal succeeds, the application updates its lease information and increments its renewal counter.
Rotation
For this project, rotation occurs after three successful lease renewals.
The counter in this project is intentionally simple so the complete lifecycle can be observed during testing.
After the third successful renewal:
The application requests another credential from:
database/creds/app-role
Vault creates a second database user with a different username, password, and lease.
At this point, two credentials can temporarily exist:
The application does not immediately revoke the old credential.
First, it verifies that the new credential actually works.
Zero Downtime Credential Switch
The application creates a new PostgreSQL connection using the newly generated credential.
If the connection succeeds, the application switches its active database connection to the new one.
Only after the new connection is verified does the application revoke the old lease.
The old credential remains available until the replacement has been successfully verified. If the new credential cannot connect to database, the application keeps using the existing connection instead of disrupting the running application.
Observing the Rotation and Database Profile
The application exposes its current lifecycle state through an API handler, allowing us to observe information such as the current credential identity, lease information, expiration, and renewal count.
We can then compare that application state with the state visible from the Vault CLI.
The observation focuses on two events:
Lease renewal.
Credential rotation.
Lease Renewal
The application state should show that the lease is still associated with the same dynamic credential while the renewal count increases.
This demonstrates the distinction between renewal and rotation: the lease is extended, but the database credential itself has not yet been replaced.
Credential Rotation
After the third successful renewal, the application requests a new credential.
The final state should show that the application has successfully moved to the new credential while continuing to run.
Conclusion
Managing database credentials is not only about keeping passwords secret, but also about managing their entire lifecycle.
In this project, Vault provides temporary Database credentials through AppRole authentication, while the application handles lease renewal and credential rotation automatically.
After three renewals, the application requests a new credential, verifies the connection, switches to it, and revokes the old lease. This allows credentials to be rotated without restarting the application or interrupting database access.
The three renewal policy is only a demonstration. In production, the same pattern can be adapted to rotation intervals and security requirements.
The idea is to treat database credentials as temporary runtime rather than static configuration values.
The code snippets in this article focus on the important parts of the implementation. For the complete configuration you can find the full source code in the repository below.
















Top comments (0)