Skip to content

Database credentials

Where the password lives

Keel provisions RDS with manage_master_user_password = true. That means AWS — not Keel — generates the master password, stores it in Secrets Manager, and rotates it on a schedule.

The consequence worth internalising: Keel never holds the password. It is not in keel.yml, not in OpenTofu state, not in ~/.keel, and not in any log Keel writes. Nothing you can grep will produce it, because nothing on your machine has ever had it.

The running tasks do not hold it either. Each service's task definition names the secret by ARN and ECS injects the values at container start:

VariableSource
DATABASE_USERthe secret's username key
DATABASE_PASSWORDthe secret's password key
DATABASE_CREDENTIALSthe whole JSON payload, for a client that wants it
DATABASE_HOSTthe endpoint, from the RDS resource
DATABASE_PORTthe port, from the RDS resource
DATABASE_NAMEthe database name
DATABASE_SCHEMEpostgres or mysql2, per the engine

So an application never needs the admin credentials, and should not use them: it already has what it needs, and it keeps working across a rotation.

Note there is no DATABASE_URL: the parts are injected separately and an app assembles its own. (The cache does get CACHE_URL and REDIS_URL. The asymmetry is finding F5 in docs/findings/rails-app-deployment.md, half-implemented.)

Seeing that it exists

The config views list it alongside the variables someone set, because a service's environment carries both:

$ keel config list

NAME              TYPE           VERSION
SECRET_KEY_BASE   SecureString   3

Managed by AWS — injected into every service, not editable here:

  @database credentials  (active)
    DATABASE_USER ← username               the master username
    DATABASE_PASSWORD ← password           generated by AWS, rotated on a schedule
    DATABASE_CREDENTIALS                   the whole JSON secret, for a client that wants it
    AWS Secrets Manager, generated and rotated by RDS
    rotated by AWS every 7 days — do not copy the value
    secret   arn:aws:secretsmanager:us-west-2:123456789012:secret:rds!db-1a2b3c-AbCdEf
    values   keel db credentials

The dashboard's Config tab shows the same thing, in a block below the variables table and outside the cursor's range — the selection there drives unset, and none of this can be unset.

No value is read to produce that listing: it comes from DescribeSecret, which returns metadata only. So a viewer or a developer sees the secret exists, what it injects and how often it rotates, without holding any permission that could read it.

keel config set and keel config unset refuse these names rather than reporting a missing SSM parameter:

$ keel config unset DATABASE_PASSWORD
Error: DATABASE_PASSWORD is not a configuration variable and cannot be unset: it comes
from @database credentials, which AWS generates and rotates. Read the values with
'keel db credentials'.

Reading them as a human

For the cases an application does not cover — connecting with psql, running a one-off query, creating a second database user — there are two commands. Both need admin access, because both call secretsmanager:GetSecretValue.

$ keel db credentials

acme (production) — postgres

  host       acme-production-db.abc123.us-west-2.rds.amazonaws.com
  port       5432
  database   acme
  username   keeladmin
  password   ••••••••••••••••   (--show to reveal)

  secret     arn:aws:secretsmanager:us-west-2:123456789012:secret:rds!db-1a2b3c-AbCdEf

The password is masked by default: checking an endpoint or the admin username is the common case, and it should not put a credential into a shared screen or a scrollback buffer. --show prints it.

For a connection string:

$ psql "$(keel db url)"

keel db url is the only Keel command that emits a password, and it emits it only when asked. Do not store what it prints — the password rotates, and a saved URL stops working.

Reaching the database at all

The database sits in private subnets, and its security group only admits the service security groups on the engine's port. It is not reachable from a laptop, whatever credentials you hold.

The route in is a task inside the VPC:

$ keel exec web -- sh -lc \
    'psql "postgres://$DATABASE_USER:$DATABASE_PASSWORD@$DATABASE_HOST:$DATABASE_PORT/$DATABASE_NAME"'

That uses the service's own injected values, needs only developer access, and does not involve the admin password at all. It is the right answer for almost every "I need to look at the database" moment. keel db credentials is for the cases where you genuinely need the master user — creating roles, granting to a new user, an out-of-band migration.

Rotation

AWS rotates the master password on the schedule the secret carries. Two things follow:

  • Copies expire. Anything you paste into a password manager, a .env, or a CI variable will stop working, silently, at a time nobody chose. Read the secret each time instead.
  • Nothing needs redeploying when it rotates. The task definitions reference the secret by ARN, so ECS picks up the new value the next time a task starts.

If keel db credentials says the output is missing

A stack applied before Keel emitted the database_secret_arn output has no record of where the secret is. Re-applying adds the output:

$ keel up --env production

Nothing is recreated — an output is not a resource.

Last updated:

Keel — the AWS CLI you've always wanted.