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.
What does my code look like?
Section titled “What does my code look like?”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:
1. Declare a provider module
Section titled “1. Declare a provider module”[DependsOn(typeof(GranitVaultHashiCorpModule))] // or Azure / Aws / GoogleCloudpublic 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.
2. Point the provider at the right source
Section titled “2. Point the provider at the right source”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.
How does the lease renewal work?
Section titled “How does the lease renewal work?”Each provider implements its own renewal loop but exposes a common shape:
| Provider | Mechanism | Renewal cadence |
|---|---|---|
| HashiCorp Vault | Database secrets engine (/v1/{mount}/creds/{role}) issues a lease with TTL. Renewed at LeaseRenewalThreshold × TTL (default 75 %). | Background service polls; lease-aware. |
| Azure Key Vault | Reads a JSON secret ({"username": "…", "password": "…"}) and polls for version changes. | Vault:Azure:RotationCheckIntervalMinutes (default 5 min). |
| AWS Secrets Manager | DescribeSecret → compare VersionId with the cached one; re-read on change. | Vault:Aws:RotationCheckIntervalMinutes (default 5 min). |
| GCP Secret Manager | AccessSecretVersion → 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.
What if I’m running in Development?
Section titled “What if I’m running in Development?”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.
See also
Section titled “See also”- Providers — per-provider setup (HashiCorp, Azure, AWS, GCP)
- Secret retrieval —
ISecretStorefor non-DB secrets - Persistence module — how dynamic credentials flow into EF Core