Skip to content

Dynamic database credentials in .NET — no more hardcoded passwords

Why not just put the DB password in appsettings.json?

Section titled “Why not just put the DB password in appsettings.json?”

A static DB password has two failure modes:

  • Leak detection is silent. If someone copies your connection string, you don’t find out until audit review — weeks or months later.
  • Rotation is downtime. Updating the password means a coordinated rollout: change the secret, restart every service, cross your fingers no one connected in between.

Dynamic credentials inverts the trust model. The vault issues a fresh username/password pair with a 15-minute lease; if it leaks, it expires on its own. Renewal happens in the background at a configurable fraction of the TTL — your service never restarts, the connection pool transparently picks up the new credentials on next connection open.

You inject IDatabaseCredentialProvider and treat it as the source of truth:

public interface IDatabaseCredentialProvider
{
string Username { get; }
string Password { get; }
bool IsReady { get; }
}

In practice you rarely touch it directly — Granit’s persistence layer wires it into the connection factory for you. For adoption, three things matter:

[DependsOn(typeof(GranitVaultHashiCorpModule))] // or Azure / Aws / GoogleCloud
public class AppModule : GranitModule { }

The module registers IDatabaseCredentialProvider as a singleton and as an IHostedService — startup blocks until IsReady becomes true, so your app never opens a DB connection with a stale credential.

Each provider reads the credential pair from its native store. The configuration fragment depends on the provider — see Providers for the full reference.

3. Let the connection factory pick up rotations

Section titled “3. Let the connection factory pick up rotations”

Granit.Persistence wires the provider into its connection string factory — you don’t need to call Username / Password yourself. When the vault rotates, the next DbContext materialization uses the new pair; existing open connections finish their work and are returned to the pool normally.

Each provider implements its own renewal loop but exposes a common shape:

ProviderMechanismRenewal cadence
HashiCorp VaultDatabase secrets engine (/v1/{mount}/creds/{role}) issues a lease with TTL. Renewed at LeaseRenewalThreshold × TTL (default 75 %).Background service polls; lease-aware.
Azure Key VaultReads a JSON secret ({"username": "…", "password": "…"}) and polls for version changes.Vault:Azure:RotationCheckIntervalMinutes (default 5 min).
AWS Secrets ManagerDescribeSecret → compare VersionId with the cached one; re-read on change.Vault:Aws:RotationCheckIntervalMinutes (default 5 min).
GCP Secret ManagerAccessSecretVersion → compare resource name version with the cached one.Vault:GoogleCloud:RotationCheckIntervalMinutes (default 5 min).

HashiCorp is lease-aware — it learns the TTL from Vault and schedules renewal proactively. The three cloud providers rotate when you rotate the secret upstream; the polling interval is your staleness ceiling.

All four provider modules auto-disable when IHostEnvironment.IsDevelopment() — the interface resolves to a no-op that keeps IsReady = false, and your connection string falls back to whatever is in appsettings.Development.json. You can run the whole app locally without a vault server, no conditional registration code required.