Skip to main content

05 โ€” Credentials

The promiseโ€‹

No DB credential ever leaves the gateway. Not in responses, not in logs, not in errors. The agent โ€” and the developer โ€” has no way to obtain a connection string from this system.

What the gateway holdsโ€‹

For every (server, database) pair declared in config, the gateway needs:

  • host, port, db name
  • a Postgres (or MySQL/MSSQL) role
  • the password / cert / IAM-token-issuer for that role

These come from one of four sources, resolved at config load. Unresolved references abort boot โ€” the gateway never serves a request it can't authenticate to.

  1. Inline literal in config.yaml (dev only โ€” rejected if env=production).
  2. Environment variable reference โ€” ${ENV:DB_PROD_RO_PASSWORD} โ€” resolved from the process env at startup.
  3. File reference โ€” ${FILE:/run/secrets/db-prod-ro-password} โ€” read from disk at startup. Pairs with k8s sealed-secrets / external-secrets-operator / Vault Agent sidecar (all of which materialise secrets as files). Re-read on pool open so file rotation works without a restart.
  4. Secret manager reference โ€” vault:secret/prod/db/ro_password, aws-sm:arn:..., gcp-sm:projects/.../secrets/.... Recognised today; backend integrations land later. Until then, the gateway refuses to start rather than failing on first DB connect.

Per-database role designโ€‹

Every database gets its own Postgres role. Not one role per server. Not one role for everything.

AspectChoice
PrivilegeSELECT on intended schemas; nothing else by default
statement_timeoutSet on the role (ALTER ROLE โ€ฆ SET statement_timeout) โ€” defense in depth on top of the gateway's own timeout
idle_in_transaction_session_timeoutShort โ€” read-only debugging doesn't need long transactions
Row capEnforced gateway-side via LIMIT rewriting and by streaming the cursor and disconnecting at N
Namingmcp_gateway_<env>_<db>_ro โ€” boring, traceable in DB-side logs

The repo will ship example SQL for provisioning these roles (see 09-deployment).

Why not IAM auth alone?โ€‹

IAM auth (AWS RDS IAM, GCP Cloud SQL IAM) is supported as a credential source โ€” the gateway swaps in a short-lived token instead of a password โ€” but the role concept stays. IAM auth + a role with SELECT is the recommended setup on managed Postgres.

Write accessโ€‹

Off by default. To enable:

  1. The role granted to the gateway must have the write grants โ€” gateway will not provision them for you.
  2. Config must mark the (server, database) permission entry as mode: read-write.
  3. The permission must be scoped to a specific group, with require_reason: true recommended.
  4. Audit log captures the full SQL of every write.

This is intentionally several steps so it can't be enabled by accident.

Connection poolโ€‹

Per (server, database):

  • Max connections in config (default 5)
  • Idle timeout (default 5 min)
  • Acquire timeout (default 10s โ€” fail fast so the agent gets a clear error)
  • TLS required by default; reject non-TLS unless explicitly opted out for local dev

Rotationโ€‹

Credentials are re-read from their source on SIGHUP (config reload). Live connections aren't killed โ€” new connections will use the new credential, old ones drain naturally. For forced rotation, restart the gateway; with two replicas, this is zero-downtime.

What this rules outโ€‹

  • No "share my session credential with my coworker" โ€” there's nothing to share.
  • No connection-string export tool. Ever.
  • No "let me see what credential you're using" admin endpoint. Logs show role name (not password) and only to operators.
  • No DSN in a boot failure. A driver connect error can quote the URL it was handed (and thus the password); the gateway logs the error type only and exits with a credential-free message. Same discipline as the admin handlers.