Credentials, leased and expiring.
GAA dynamic secret has no stored value. Instead of a long-lived database password sitting in your config, an app leases a fresh credential when it needs one — minted on your backend, valid for minutes, and revoked automatically when the lease ends. TillSecrets keeps only a reference to the lease; it never stores the credential itself.
Backends, keys, leases
Three pieces:
- A backend is registered once per project — an upstream that can mint and revoke credentials. Today that’s Postgres; the type is an enum with room to grow (
mysql,aws_iam,redis,custom). Its admin connection config is stored envelope-encrypted. - A dynamic key is a normal env-var-style KEY (e.g.
DATABASE_URL) whose kind is dynamic and which references a backend, with an optional TTL. - A lease is a single issuance: a short-lived credential handed to a caller, with an expiry. We persist a lease reference (the minted role name) and the expiry — nothing that can be replayed.
A pull lists dynamic keys under dynamic: [...] so your app knows they exist — but their values only come from a lease.
Register a Postgres backend
A backend is registered from the dashboard under Secrets → Dynamic backends (admin only). You provide an admin connection — one privileged enough to CREATE ROLE and DROP ROLE — plus the grants each minted role should get, and the TTL bounds:
{
"name": "app-db",
"type": "postgres",
"default_ttl_sec": 3600, // 1h — used when a lease asks for no TTL
"max_ttl_sec": 86400, // 24h — hard ceiling; requests are clamped
"config": {
"host": "db.internal.example.com",
"port": 5432,
"database": "app",
"admin_user": "tillsecrets_admin",
"admin_password": "…", // encrypted under your org DEK before it lands
"ssl": true,
"role_prefix": "tsl_", // every minted role starts with this
"grants": [
"GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO \"{{name}}\"",
"GRANT USAGE ON SCHEMA public TO \"{{name}}\""
]
}
}| Field | Meaning |
|---|---|
default_ttl_sec | Lease lifetime when a request names no TTL. Min 60s. |
max_ttl_sec | Hard ceiling. Any requested TTL is clamped to this; must be ≥ default. |
role_prefix | Every minted role name starts with this (default tsl_) so leased roles are obvious in your DB. |
grants | Templated SQL run against each new role. See below. |
host is checked against an SSRF allow-list before TillSecrets ever connects, and the whole config — password included — is envelope-encrypted under your org key. The lease service decrypts it in memory only for the moment it connects.Templated SQL grants
When TillSecrets mints a role it runs each entry in grants as SQL, substituting {{name}} with the generated role name. That’s how a leased credential gets exactly the privileges you decide — and nothing more:
-- Read/write on the app schema for the life of the lease:
GRANT USAGE ON SCHEMA public TO "{{name}}";
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO "{{name}}";
-- Or a read-only analyst role:
GRANT SELECT ON ALL TABLES IN SCHEMA public TO "{{name}}";Behind the scenes each lease is a CREATE ROLE <prefix><random> LOGIN PASSWORD … VALID UNTIL <expiry>, followed by your grants. On revoke — or when the sweeper reaps an expired lease — TillSecrets reassigns owned objects and DROP ROLEs it. Even if a revoke never reaches your database, the role’s own VALID UNTIL is the backstop: the login stops working at expiry regardless.
Lease a credential
First declare a dynamic key that points at the backend (admin, session-gated):
# Declare a dynamic KEY that references the backend (admin-gated).
curl -X POST https://tilldev.dev/api/secrets/environments/env_…/dynamic \
-H "content-type: application/json" \
--cookie "$SESSION" \
-d '{ "key": "DATABASE_URL", "backend_id": "bk_…", "ttl_seconds": 900 }'Then any app holding a ts_… token pinned to that environment can lease it. Leasing is served by the TillSecrets lease service (a Node service, because it opens a real connection to your database) at POST /v1/lease:
# Lease a credential. Auth is the same ts_… token used to pull.
curl -X POST "$TILLSECRETS_LEASE_URL/v1/lease" \
-H "authorization: Bearer $TILLSECRETS_TOKEN" \
-H "content-type: application/json" \
-d '{ "secret_key": "DATABASE_URL", "ttl_seconds": 900 }'
# → {
# "lease_id": "ls_…",
# "credential": {
# "username": "tsl_9f2c…", "password": "…",
# "host": "db.internal.example.com", "port": "5432", "database": "app"
# },
# "expires_at": "2026-07-10T12:15:00Z",
# "ttl_seconds": 900
# }The credential is returned once — assemble your connection string from it and use it until expires_at. Pass secret_key (resolved within the token’s environment) or a backend_id directly. The requested ttl_seconds is clamped to the backend’s max_ttl_sec.
Revoking early
A lease expires on its own, but a read_write token can hand one back sooner:
# Give the credential back early (needs a read_write token).
curl -X POST "$TILLSECRETS_LEASE_URL/v1/lease/ls_…/revoke" \
-H "authorization: Bearer $TILLSECRETS_TOKEN"
# → { "ok": true }A background sweep also runs on a schedule, revoking and marking any lease whose expires_at has passed — so nothing lingers even if a client forgets to release it.
Why lease instead of store
- Short blast radius. A leaked leased credential is useless within minutes; there is no shared, long-lived database password to rotate in a panic.
- Per-instance identity. Every app instance leases its own role, so your database logs attribute activity to a real, time-boxed identity.
- Nothing to exfiltrate at rest. TillSecrets stores a lease reference and an expiry, never the password. There is no credential in Postgres to steal.
See Config store for static values and the pull API, or Security for how backend configs and leases are protected. Back to the TillSecrets overview.