TILLSECRETS · DYNAMIC SECRETS

Credentials, leased and expiring.

GA

A 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.

01Mental model

Backends, keys, leases

Three pieces:

  1. 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.
  2. 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.
  3. 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.

02Backends

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:

backend config
{
  "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}}\""
    ]
  }
}
FieldMeaning
default_ttl_secLease lifetime when a request names no TTL. Min 60s.
max_ttl_secHard ceiling. Any requested TTL is clamped to this; must be ≥ default.
role_prefixEvery minted role name starts with this (default tsl_) so leased roles are obvious in your DB.
grantsTemplated SQL run against each new role. See below.
SSRF-guarded, encrypted
The admin 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.
03Grants

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:

sql
-- 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.

04Leasing

Lease a credential

First declare a dynamic key that points at the backend (admin, session-gated):

bash
# 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:

bash
# 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:

bash
# 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.

05Why

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.
Static and dynamic together
A single environment mixes both freely: static keys for your API keys and flags, dynamic keys for anything backed by a database you control. Your app pulls the static set at boot and leases the dynamic ones on demand.

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.