Dynamic Database Credentials (JIT) for PostgreSQL and MySQL
Why JIT database access
Shared database passwords are one of the most common credential-rot patterns in any infrastructure. "The prod DB password is in our 1Password vault" — everyone who's ever had prod access has it, nobody revokes it when someone leaves, and rotating it requires a coordinated app restart. JIT flips the model: every access creates an ephemeral user scoped to a single session, which is dropped when the session ends. Nothing to rotate because nothing persists.
Prerequisites
- A PostgreSQL 13+ or MySQL 8.0+ database you have superuser access to. Local Docker Postgres works fine for this tutorial:
docker run -d -p 5432:5432 -e POSTGRES_PASSWORD=admin postgres:17. - A CoreLink tenant with tenant-admin role.
psqlormysqlCLI installed locally.tbcl-cliset up (optional — UI-only works too).
Step 1 — Create a role template on the database
CoreLink will create users on the fly and assign them to an existing role. Create that role in Postgres:
psql -h localhost -U postgres -d postgres
postgres=# CREATE ROLE corelink_readonly;
postgres=# GRANT CONNECT ON DATABASE postgres TO corelink_readonly;
postgres=# GRANT USAGE ON SCHEMA public TO corelink_readonly;
postgres=# GRANT SELECT ON ALL TABLES IN SCHEMA public TO corelink_readonly;
postgres=# ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO corelink_readonly;
This is the ceiling of what CoreLink-provisioned users can do — SELECT-only on the public schema. Separate role templates cover read-write, DDL, and admin profiles.
Step 2 — Grant CoreLink a provisioning user
CoreLink needs a user that can CREATE ROLE and DROP ROLE:
postgres=# CREATE ROLE corelink_provisioner WITH LOGIN CREATEROLE PASSWORD 'replace-with-strong-pw';
Store that password as a CoreLink secret before you do anything else with it — never paste it directly into the DB connection config:
tbcl-cli secrets create --name demo-db-provisioner --value "replace-with-strong-pw"
Step 3 — Register the database connection
UI: Sessions → Database Connections → New Connection. Fill in:
- Name:
demo-postgres - Engine:
postgresql - Host:
localhost(or the DB host CoreLink can reach) - Port:
5432 - Database:
postgres - Provisioning User:
corelink_provisioner - Provisioning Password: select existing secret
demo-db-provisioner - SSL Mode:
requirein production;disableis fine for local dev - Role Templates:
- Name
readonly, assigns rolecorelink_readonly
- Name
Save. CoreLink attempts a test connection using the provisioning user. You should see Status: Verified within a second or two.
Step 4 — Request JIT credentials
On the connection's detail page, click Request Access:
- Role template:
readonly - Duration:
15 minutes
CoreLink generates a random username (tbcl_a8f3c2d1_readonly), creates the user in Postgres with GRANT corelink_readonly TO ..., returns the credentials to you, and schedules a drop-user job for 15 minutes from now.
Expected output:
Access request granted.
username: tbcl_a8f3c2d1_readonly
password: hg83K!f2nZjLp9qWb4vM7eRyUtX0sC5a
host: localhost
port: 5432
database: postgres
role: corelink_readonly
expires: 2026-04-24T15:42:18Z
You can also request via CLI:
tbcl-cli db-access request --connection demo-postgres --role readonly --duration 15m
Step 5 — Connect with the ephemeral credentials
PGPASSWORD='hg83K!f2nZjLp9qWb4vM7eRyUtX0sC5a' \
psql -h localhost -U tbcl_a8f3c2d1_readonly -d postgres
postgres=> SELECT current_user;
# tbcl_a8f3c2d1_readonly
postgres=> SELECT * FROM pg_tables WHERE schemaname = 'public';
# ...
postgres=> CREATE TABLE evil (x int);
# ERROR: permission denied for schema public
The ephemeral user can read but not write, because the role template only grants SELECT. Anything higher needs a different role template with a different grant set.
Step 6 — Verify automatic revocation
Wait for the 15-minute TTL (or click Revoke Now in the UI to finish early). CoreLink's revocation scheduler runs REASSIGN OWNED BY ... TO corelink_provisioner; DROP OWNED BY ...; DROP ROLE tbcl_a8f3c2d1_readonly;. The user is gone from Postgres and cannot reconnect:
PGPASSWORD='hg83K!f2nZjLp9qWb4vM7eRyUtX0sC5a' \
psql -h localhost -U tbcl_a8f3c2d1_readonly -d postgres
# psql: error: connection to server at "localhost" failed: FATAL: password authentication failed for user "tbcl_a8f3c2d1_readonly"
Even if the credential had leaked to a log file or a screenshot during the 15 minutes, it's useless now. Rotation isn't needed because revocation happened by design.
What's happening in the audit trail
Four events landed in the audit log for this flow, hash-chained per the tamper-evident audit log design:
db_access.requested(actor: you; resource:demo-postgres; role: readonly)db_access.granted(username:tbcl_a8f3c2d1_readonly; TTL: 15m)db_access.connected(if your app uses the CLI/SDK; directpsqlconnections don't emit this)db_access.revoked(automatic, with reason "ttl_expired")
That's the compliance evidence: who requested, when, with what privilege, for how long, and the revocation confirmation.
MySQL is the same story
Engine mysql on the connection, role templates map to MySQL roles or direct GRANT statements. Commands are slightly different (CREATE USER ... IDENTIFIED BY, GRANT ... ON db.* TO, DROP USER) but the CoreLink-facing UX is identical. MongoDB is on the roadmap.
Next steps
- Gate JIT access behind an approval workflow: Approvals → Database Access. A peer or manager approves each request before CoreLink provisions.
- Pair with SSH session brokering to cover privileged access for both infrastructure layers.
- Read the session brokering use case for the full session lifecycle including recording and long-lived session policies.