# Database Credentials Management: Best Practices

> Database credentials management best practices: store passwords and secrets in one place, rotate on a schedule, and separate the application path from the human path.

Tianzhou | 2026-07-31 | Source: https://www.bytebase.com/blog/database-credentials-management-best-practices/

---

Of all the secrets in a stack, the database password is almost always the least governed: pasted into an env file, committed once, forgotten until a breach or audit forces the question. [IBM's 2024 Cost of a Data Breach report](https://www.ibm.com/think/insights/cost-of-a-data-breach-2024-financial-industry) found stolen or compromised credentials to be the most common initial attack vector, and the slowest to catch:

> Breaches that start with a leaked credential take an average of **292 days** to identify and contain — longer than any other cause. In the financial industry specifically, the average breach now costs **$6.08 million**, 22% above the global average.

## Store the secret in one place

A credential should exist in exactly one system of record (a secrets manager like Vault, AWS Secrets Manager, GCP Secret Manager, Azure Key Vault, or Infisical), and everything else reads it from there at runtime. The moment a password lives in three env files, two CI variables, and someone's `.pgpass`, you can no longer rotate it, and you cannot even answer who actually holds this credential today.

```mermaid
flowchart LR
    BAD["<b>Scattered</b><br/>env file · CI variables · .pgpass<br/>source code · wiki / chat"]
    DB[("Database")]
    GOOD["<b>One source of record</b><br/>apps fetch one credential<br/>from a secrets manager"]

    BAD ==>|"a copy everywhere<br/>can't rotate"| DB
    DB --- GOOD

    style BAD fill:#fee2e2,stroke:#b91c1c
    style GOOD fill:#dcfce7,stroke:#15803d
```

Even with a secrets manager, credentials still leak in three places:

- **Code.** A credential in source is in every clone, every fork, and the git history forever. Deleting the line later does not remove it. Run a secret scanner (`gitleaks`, `trufflehog`, GitHub secret scanning) in CI, and if one slips through, rotate it.
- **CI/CD.** Use the platform's secret store, never plaintext in the workflow file. A proper store is write-only: GitHub Actions secrets cannot be read back even by an admin. The residual risk is exfiltration. Anyone who can edit a workflow can add a step that leaks the secret, so gate who changes pipelines. Better still, skip the stored secret with OIDC and mint a short-lived credential at run time.
- **Logs.** A connection string with an inline password gets logged by an ORM in debug mode, or by an exception handler dumping the config. Never put a password on a command line, and never log a full connection string.

## Rotate on a schedule, not on an incident

A credential that never changes has, in effect, already leaked. You just don't know when. Rotation bounds the blast radius: if a password is only valid for 30 days, then a leak on day one is dead by day thirty.

**Scheduled rotation.** The manager generates a new password, updates the database and the stored secret; apps pick it up on their next fetch. For a short grace window, the database should accept both the old and new password, so in-flight connections don't fail.

```mermaid
flowchart LR
    A["Generate<br/>new password"] --> B["Update<br/>database"]
    B --> C["Grace window<br/><b>old + new valid</b>"]
    C --> D["Apps fetch<br/>new password"]
    D --> E["Expire<br/>old password"]
    E -.->|next cycle| A

    style C fill:#fef3c7,stroke:#a16207
```


**Dynamic credentials.** Instead of rotating one shared password, the manager mints a fresh credential per consumer on demand and revokes it when the TTL expires. There is no standing secret left to steal. This fits short-lived work especially well: a CI migration, a batch ETL run, a serverless invocation.

```mermaid
flowchart LR
    A["Job starts"] --> B["Request<br/>credential"]
    B --> C["Mint scoped<br/>credential<br/><b>short TTL</b>"]
    C --> D["Job connects<br/>and runs"]
    D --> E["TTL expires,<br/>credential revoked"]

    style C fill:#fef3c7,stroke:#a16207
```

## Separate the application path from the human path

An application connects to the database the same way every time: the same credential, the same handful of statements, millions of times a day. A human connects differently every time, an ad-hoc query, a schema change, a 2 a.m. investigation. Two different workloads, two different risk profiles, and yet most teams hand them the same credential.

**The application path** is a machine-to-machine problem. The service account is fetched from the manager, known to no human, and scoped to exactly what it needs, and nothing more:

- `SELECT`, `INSERT`, `UPDATE`, `DELETE` on the tables it touches. No `DROP`. No `ALTER`.
- Migration accounts kept separate from runtime accounts.
- Read-only jobs on credentials that physically cannot write.
- One account per service, so a single compromise can't span two.

Least privilege won't stop a leak, but it turns a leaked credential from a catastrophe into a contained incident. Better yet, remove the password altogether. IAM-based authentication (RDS IAM, Cloud SQL IAM, Azure AD) lets a service authenticate with a short-lived token tied to its cloud identity, and impersonation on GCP or `AssumeRole` on AWS removes the static key on disk too.

**The human path** is an identity and authorization problem. A human should hold no standing password at all. They authenticate as themselves, get access scoped to the task, and have every action logged against their own name rather than a shared `analyst` login.

Conflate the two and you get the worst credential in any database: the shared admin password, known by the whole team, never rotated precisely because rotating it would page everyone at once. Separate the two paths, and that credential no longer has any reason to exist.

## Governing the human path

You can store a human's password in a vault, but the controls stop the moment they check it out. What happens after that is invisible to it.

This is the gap we built [Bytebase](/) to fill. Controls live at the statement level, not the connection:

- **Query-level authorization.** Access is evaluated per statement, against the specific principal, table, and action, not granted once at connect time.
- **Dynamic masking.** Sensitive columns return masked values on the human path, even when the underlying role has full `SELECT`.
- **Just-in-time access.** Access is requested, scoped to a database and a time window, and then expires on its own. No standing credential, nothing to rotate.
- **Audit by identity.** Every statement is logged against the person who ran it, so "who ran this query on March 3rd" has a name for an answer rather than a shared login.

```mermaid
flowchart LR
    H["Human"] --> BB
    AG["AI agent"] --> BB

    subgraph BB["Bytebase, governed path"]
        direction TB
        C1["Query-level authorization"] ~~~ C2["Dynamic data masking"] ~~~ C3["Just-in-time access"] ~~~ C4["Audit by identity"]
    end

    BB --> DB

    DB["<b>Databases</b><br/>PostgreSQL · MySQL · Oracle · SQL Server<br/>Snowflake · MongoDB · BigQuery · Redshift<br/>+ more"]

    BB -.->|"fetch DB credential"| SM["<b>Secrets manager</b><br/>Vault · AWS · GCP · Azure"]

    style SM fill:#fef3c7,stroke:#a16207

    style BB fill:#eff6ff,stroke:#1d4ed8
```

## Where to start

This is the order we suggest:

1. **Inventory the standing credentials.** Env files, CI variables, `.pgpass`, internal wikis. The list almost always runs longer than people expect.
2. **Move them into one secrets manager.** Then delete every copy that lives outside it.
3. **Turn on rotation.** Scheduled at a minimum, dynamic wherever the workload allows.
4. **Split application from human.** Scope the service accounts, and take humans off standing passwords and onto an audited, identity-based path.

## Related reading

- [Best Secrets Manager for Database Credentials in 2026: Vault vs Infisical vs Doppler](https://www.bytebase.com/blog/best-secrets-manager-for-database-credentials/)