# Temporary access

seekrit can mint **short-lived credentials** on demand — a temporary Postgres,
MySQL/MariaDB, or MongoDB login, a Redis ACL user, an SSH certificate, or an AWS
or GCP credential, that exists for an hour then disappears — without giving up
the zero-knowledge property that defines the rest of the product. Postgres,
MySQL/MariaDB, Redis, SSH, AWS, GCP, and MongoDB are all supported today; the
machinery is general.

## The problem it solves

A long-lived database password in a `.env` file is a standing liability: it
leaks, it lingers, it is hard to rotate. The Vault-style answer is **dynamic
secrets** — mint a credential per session, scoped and expiring. The catch is
that a naïve implementation has the control plane generate and hand out the
password, so the service sees your plaintext. seekrit avoids that.

## The key trick: ship the verifier, not the password

PostgreSQL stores passwords as **SCRAM-SHA-256 verifiers**, and `CREATE ROLE …
PASSWORD '…'` stores the string verbatim when it is already in verifier form —
it does not re-hash it. So the flow inverts:

1. The machine that will connect generates a random password **locally**.
2. It computes the SCRAM verifier locally (PBKDF2 → HMAC → SHA-256).
3. It sends seekrit **only the verifier**, plus a role name and a TTL.
4. seekrit runs `CREATE ROLE … PASSWORD '<verifier>' VALID UNTIL …`.
5. The machine connects to Postgres directly with the password it never shared.

The verifier is **not sufficient to authenticate** — SCRAM login requires
`ClientKey`, and the verifier only stores `SHA256(ClientKey)` (preimage-
resistant) plus `ServerKey`. So a dump of `pg_authid`, the query log, or a
backup cannot log in. The plaintext password lives only on the consumer.

> **Note:** This is zero-knowledge at **both** layers: the control plane relays only the verifier, and Postgres-at-rest holds only a verifier that can't log in. The password is high-entropy and machine-generated, so the SCRAM iteration count (which exists to slow dictionary attacks on human passwords) is irrelevant.

## MySQL & MariaDB

MySQL and MariaDB get the **same trick with a different hash**. The
`mysql_native_password` plugin stores an account's secret as
`*<UPPER(HEX(SHA1(SHA1(password))))>`, and
`CREATE USER … IDENTIFIED WITH mysql_native_password AS '*…'` stores that string
**verbatim** — it isn't re-hashed. So the client computes the hash locally and
sends only that. The double-SHA1 hash **cannot log in**: `mysql_native_password`
auth is a challenge-response that verifies knowledge of `SHA1(password)` (the
preimage of the stored inner hash), which a dump of `mysql.user` doesn't reveal.

Two MySQL-specific differences from Postgres:

- **No account expiry timestamp.** MySQL has no `VALID UNTIL` for an account, so
  the broker's alarm-driven `DROP USER` is the *only* thing that expires a lease
  (which is exactly what the broker is for).
- **Inline grants, no group role.** MySQL roles need activation
  (`SET DEFAULT ROLE`), an awkward fit for a plain connection URL — so the
  read-only / read-write presets apply their `GRANT`s **inline per user** at
  mint, scoped to the target database. There is no one-time setup query.

## Redis

Redis (6+) gets the **same trick with a third hash**. A Redis ACL user is
created with `ACL SETUSER <name> on #<digest>`, where `#<digest>` is the
**lowercase-hex SHA-256** of the password, stored **verbatim**. So the client
computes the digest locally and sends only that. The digest **cannot log in**:
`AUTH <user> <password>` hashes the *plaintext* it receives with SHA-256 and
compares — verifying a client needs the password, and SHA-256 is preimage-
resistant for the high-entropy machine-generated password, so a dump of the ACL
rules can't authenticate.

Like MySQL, Redis has **no per-user expiry** (the broker's alarm-driven
`ACL DELUSER` is the only thing that ends a lease) and **no role inheritance**,
so the read-only / read-write presets apply their ACL rules **inline per user**
at mint:

- **Read-only** — `~* +@read +@connection` (read every key; the client
  handshake commands work).
- **Read-write** — the above plus `+@write`.
- **Custom** — supply your own `ACL SETUSER` / `ACL DELUSER` command templates.

> **Note:** Redis provisioning speaks **RESP, not SQL** — each rendered statement is one Redis command line. The in-DO executor sends it over a `redis://`/`rediss://` socket; the remote executor's provisioner runs it against the admin endpoint it holds. Everything else (verifier-only minting, alarm expiry, the executor dial) is identical to the SQL providers.

## Provider tiers

Not every system supports verifier injection, so providers are classified by
how much zero-knowledge they can preserve for the credential they mint:

- **Tier 1 — verifier injection.** The target accepts a client-computed
  verifier; plaintext never leaves the consumer. **Postgres SCRAM**, **MySQL
  `mysql_native_password`**, **Redis ACL SHA-256**, SSH public keys, mTLS CSR
  signing, hashed API keys.
- **Tier 2 — client-encrypted delivery.** The target mints the secret itself,
  so the broker wraps it to the consumer's public key before returning it. The
  control plane relays ciphertext; the broker sees plaintext transiently.
  **AWS STS** (see [AWS credentials](#aws-credentials)), **GCP tokens** (see
  [GCP credentials](#gcp-credentials)), and **MongoDB** (see
  [MongoDB credentials](#mongodb-credentials)) ship today; Azure short-lived
  tokens are the same shape.
- **Tier 3 — opaque broker.** The target mints an opaque token the broker must
  hold; the best option is to never hand it out and proxy the requests instead.

## The broker

Each organization gets its own **broker** (a Cloudflare Durable Object,
one instance per org). It owns the lease lifecycle in its own isolated SQLite,
and uses **alarms** to expire leases — the sweeper that drops the role at its
deadline (managed Postgres has no in-database scheduler). It holds two secrets
carefully:

- its **own keypair**, generated once, private half never leaving the DO;
- each target's **admin credential**, stored only as ciphertext wrapped to that
  public key, decrypted transiently in memory to provision, never written back.

## The provisioning credential — the honest caveat

To run `CREATE ROLE` / `CREATE USER`, something must authenticate to the
database as a privileged role. That admin credential is a second, narrower trust
tier — the same trade every dynamic-secrets engine makes. seekrit minimizes it,
and lets you choose where it lives via the **executor**:

- **in-DO executor** — the broker connects to the database and runs the SQL
  itself. Convenient; the admin credential is decrypted transiently inside the
  DO.
- **remote executor** — the broker dispatches a signed command to a small
  `seekrit-provisioner` running inside your own network, which holds the admin
  credential. seekrit orchestrates but never sees it — **pure zero-knowledge
  even for provisioning**.

The credential you hand out stays zero-knowledge either way; the executor is the
dial for how much the *provisioning* credential is exposed.

The remote executor ships as a tiny container you run in your own network — the
[self-hosted provisioner guide](/docs/guides/provisioner) walks through minting
the shared key, registering a `remote` target, and running the daemon.

## Access levels & permissions

A freshly created role has **no privileges** — it can connect but read nothing,
by design (least privilege). You grant access by picking an **access level** when
you register a target:

- **Read-only** — leased credentials can only `SELECT`.
- **Read-write** — `SELECT / INSERT / UPDATE / DELETE` (plus sequence usage so
  inserts into serial/identity columns work).
- **Custom** — supply your own create/revoke SQL templates.

For the two presets, seekrit shows you a **one-time setup query** to run as an
admin. It creates a shared **group role** (`seekrit_readonly` /
`seekrit_readwrite`) with the right grants, including `ALTER DEFAULT PRIVILEGES`
so future tables are covered. It's idempotent — safe to re-run:

```sql
DO $$ BEGIN
  IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'seekrit_readonly') THEN
    CREATE ROLE seekrit_readonly NOLOGIN;
  END IF;
END $$;
GRANT USAGE ON SCHEMA "public" TO seekrit_readonly;
GRANT SELECT ON ALL TABLES IN SCHEMA "public" TO seekrit_readonly;
ALTER DEFAULT PRIVILEGES IN SCHEMA "public" GRANT SELECT ON TABLES TO seekrit_readonly;
```

Every leased credential is then created **`IN ROLE seekrit_readonly`** and
inherits those grants — so you configure permissions once, and the temp roles
stay disposable (they own nothing, so `DROP ROLE` at expiry is clean). The
dashboard badges each target and lease with its access level, so "this
credential can write" is always visible.

On **MySQL/MariaDB** the presets skip the group role: each leased user gets the
grant applied **inline** at mint (e.g. `GRANT SELECT ON \`app\`.* TO 'tmp_…'@'%'`),
scoped to the target database. There's no setup query to run, and `DROP USER` at
expiry removes the user and its grants together.

On **Redis** the presets are likewise inline — the leased user is created with
its ACL rules in the same `ACL SETUSER` command (e.g. `~* +@read +@connection`),
and `ACL DELUSER` at expiry removes the user and its rules together. No setup
step, no group.

## Provider compatibility

The verifier-injection trick relies on **standard** database behavior — a
pre-hashed password is stored verbatim.

**Postgres.** A pre-hashed `SCRAM-SHA-256$…` password is stored as-is on
**Render, AWS RDS, Cloud SQL, Azure, Crunchy Bridge, self-hosted**, and most
managed Postgres. The in-DO executor authenticates its own admin connection via
**SCRAM, MD5, or cleartext** (over TLS), so it works whether a provider uses the
modern SCRAM default or still challenges MD5 for login.

**MySQL / MariaDB.** `CREATE USER … IDENTIFIED WITH mysql_native_password AS
'*…'` stores the pre-hashed value as-is on **standard MySQL 8, MariaDB, AWS RDS
/ Aurora, Cloud SQL, Azure**, and self-hosted. The in-DO executor authenticates
its admin connection with **`mysql_native_password` or `caching_sha2_password`
(MySQL 8's default) over TLS**, so it works against a default MySQL 8 admin
account as well as MariaDB.

**Redis.** `ACL SETUSER … on #<sha256>` stores the digest as-is on **Redis 6+**
(where ACLs were introduced) and Redis-compatible servers — **self-hosted,
AWS ElastiCache/MemoryDB, Valkey**. Use `rediss://` for a TLS admin endpoint
(the default on managed providers); the admin user needs the `+@admin`
permission (or just `@dangerous`) to run `ACL SETUSER`/`ACL DELUSER`.

> **Warning:** **Control-plane-proxied databases aren't supported for the zero-knowledge path.** Some managed providers intercept role/user creation and demand a **plaintext** password, which is incompatible with sending only a verifier: **Neon** (Postgres — *"Neon only supports being given plaintext passwords"*) and **PlanetScale** (MySQL — user management goes through its own API, not `CREATE USER`). Use a database whose role/user creation isn't proxied by a control plane.

## SSH certificates

The same machinery issues **short-lived SSH certificates**, and it's an even
cleaner zero-knowledge story than Postgres — nothing secret is ever sent to or
returned by seekrit.

seekrit acts as an SSH **certificate authority**. When you register an SSH
target, a CA keypair is generated **in your browser/CLI**; only the CA *private*
key is wrapped to the broker (stored as ciphertext), and the CA *public* key is
printed for you to install on your hosts once:

```
# /etc/ssh/sshd_config
TrustedUserCAKeys /etc/ssh/seekrit_ca.pub
```

Minting a certificate then works like this:

1. The machine that will connect generates an **ephemeral SSH keypair locally**.
2. It sends seekrit **only the public key**, plus the login principals and a TTL.
3. The broker signs a certificate over that public key (`valid principals`,
   `valid before = now + TTL`) using the CA key, decrypted transiently in the DO.
4. seekrit returns the **certificate** — a public artifact — and you connect:

```
ssh -i id_ed25519 -o CertificateFile=id_ed25519-cert.pub deploy@host
```

The private key never leaves the requesting machine, and a certificate cannot
authenticate without it — so, like the Postgres verifier, what seekrit handles is
never sufficient to log in. This is tier 1 (verifier injection): the public key
is the "verifier". Certificates use Ed25519, and the CA-signing (`in_do`)
executor is the only mode for now.

> **Note:** **Revocation is by expiry.** A certificate is a self-contained bearer artifact, so seekrit v1 relies on **short TTLs** rather than a revocation list — keep lifetimes tight (minutes to a few hours). Revoking a lease records the event in the ledger and audit trail, but an already-issued certificate stays valid until it expires. A published KRL (`RevokedKeys`) for early revocation is planned.

## AWS credentials

AWS is the first **tier-2** provider. Unlike the databases and SSH — where the
consumer computes a verifier and the plaintext never leaves — AWS mints the
credential itself (an access key + secret + session token). So it can't be
verifier-only; instead the credential is **encrypted to the consumer before it
comes back**, which keeps it zero-knowledge at the control plane.

You register one **assumable IAM role** as a target. seekrit assumes it via STS
`AssumeRole` using a base IAM credential (the wrapped admin secret) whose only
required permission is `sts:AssumeRole` on that role. What the leased credential
can *do* lives entirely in IAM: the role's attached policies define its
permissions, optionally narrowed per target by an inline session policy.

Minting works like this:

1. The machine that will use the credential generates an **ephemeral P-256
   keypair locally** and sends seekrit **only the public key**, plus a TTL.
2. The broker calls STS `AssumeRole` with the base credential, decrypted
   transiently in the DO, and gets back the temporary credential.
3. The broker **wraps that credential to your public key** (the same `wd1.`
   envelope used for DEK grants) and returns only the ciphertext.
4. Your machine unwraps it with the private key it never shared:

```
eval "$(seekrit aws lease prod-deploy --ttl 1h)"
aws sts get-caller-identity
```

The control plane only ever relays ciphertext, and only your private key can
unwrap it — so, as with the other providers, what seekrit stores or returns is
never usable on its own. This is the one place the broker sees plaintext
transiently (it does the STS call and the wrap), the scoped exception tier 2
makes — the same trade every dynamic-secrets engine makes for the provisioning
credential.

> **Note:** **Revocation is by expiry.** STS session credentials can't be revoked individually, so — like SSH certificates — v1 relies on the short lifetime STS itself enforces (`DurationSeconds`, 15 minutes to 12 hours; the requested TTL is clamped to the role's `MaxSessionDuration`). Revoking a lease records the event in the ledger and audit trail, but an issued credential stays valid until it expires. `in_do` execution only — the remote provisioner speaks SQL, not the AWS API.

## GCP credentials

GCP is the second **tier-2** provider, and works exactly like AWS against
Google's IAM Service Account Credentials API. You register one **impersonable
service account** as a target. seekrit impersonates it with a base
**service-account key** (the wrapped admin secret) for a *source* service account
that holds `roles/iam.serviceAccountTokenCreator` on the target. What the leased
token can *do* lives entirely in GCP IAM: the target service account's roles
define its permissions, narrowed per lease by the OAuth `scopes` you grant.

Minting works the same as AWS:

1. The machine that will use the credential generates an **ephemeral P-256
   keypair locally** and sends seekrit **only the public key**, plus a TTL.
2. The broker signs a JWT with the source key, exchanges it for the source
   account's access token, and calls `generateAccessToken` to impersonate the
   target account — all with the admin key decrypted transiently in the DO.
3. The broker **wraps the returned OAuth token to your public key** (the same
   `wd1.` envelope used for DEK grants) and returns only the ciphertext.
4. Your machine unwraps it with the private key it never shared:

```
eval "$(seekrit gcp lease prod-deploy --ttl 1h)"
gcloud storage ls
```

The leased token is a standard OAuth 2.0 access token — the `export` lines set
`CLOUDSDK_AUTH_ACCESS_TOKEN` (for `gcloud`) and `GOOGLE_OAUTH_ACCESS_TOKEN` (for
the Google client libraries). As with every other provider, the control plane
only ever relays ciphertext, and only your private key can unwrap it.

> **Note:** **Revocation is by expiry.** Short-lived access tokens can't be revoked individually, so — like SSH certificates and AWS — v1 relies on the short `lifetime` GCP enforces (1 minute to 12 hours; tokens over 1 hour require the `constraints/iam.allowServiceAccountCredentialLifetimeExtension` org policy). Revoking a lease records the event in the ledger and audit trail, but an issued token stays valid until it expires. `in_do` execution only — the remote provisioner speaks SQL, not the Google APIs.

## MongoDB credentials

MongoDB is a **tier-2** provider, for a reason worth stating plainly: unlike
Postgres, MySQL, and Redis — where the target stores a client-computed verifier
verbatim — MongoDB's `createUser` **hashes the password server-side**, and there
is no supported way to inject a precomputed SCRAM credential. So the plaintext
can't stay on the consumer's machine the way it does for the tier-1 databases.
MongoDB takes the same shape as AWS instead: the credential is **encrypted to
the consumer before it comes back**.

You register one **cluster** as a target, with the database leased users get
access to and an access level. seekrit provisions with an admin `mongodb://`
credential (the wrapped admin secret) that needs `userAdmin` on that database.
Access levels map to MongoDB's built-in roles — `read` (read-only) or
`readWrite` — on the target database, or you supply an explicit `roles` array for
`custom`.

Minting works like this:

1. The machine that will use the credential generates an **ephemeral P-256
   keypair locally** and sends seekrit **only the public key**, plus a TTL.
2. The broker **generates a random password**, connects to MongoDB over its wire
   protocol (with the admin credential decrypted transiently in the DO), and runs
   `createUser` with the preset roles.
3. The broker **wraps the resulting credential to your public key** (the same
   `wd1.` envelope used for DEK grants) and returns only the ciphertext.
4. Your machine unwraps it with the private key it never shared:

```
eval "$(seekrit mongodb lease prod-app --ttl 1h)"
mongosh "$MONGODB_URI"
```

The control plane only ever relays ciphertext, and only your private key can
unwrap it. The broker sees the password transiently (it generates it and runs
`createUser`), the scoped exception tier 2 makes — the same trade the tier-1
providers make for their *admin* credential.

> **Note:** **Revocation is real.** Unlike AWS/SSH, a MongoDB lease is ended by dropping the user: the broker's alarm runs `dropUser` at expiry, and revoking a lease drops it immediately. `in_do` execution only — the remote provisioner speaks SQL, not the MongoDB wire protocol. `mongodb+srv://` admin URLs aren't supported (they need a DNS SRV lookup); register an explicit `host:port`.

See the [CLI reference](/docs/reference/cli) (`seekrit pg`, `seekrit mysql`,
`seekrit redis`, `seekrit ssh`, `seekrit aws`, `seekrit gcp`, `seekrit mongodb`) and the
[API reference](/docs/reference/api) for the endpoints.
