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:
- The machine that will connect generates a random password locally.
- It computes the SCRAM verifier locally (PBKDF2 → HMAC → SHA-256).
- It sends seekrit only the verifier, plus a role name and a TTL.
- seekrit runs
CREATE ROLE … PASSWORD '<verifier>' VALID UNTIL …. - 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.
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 UNTILfor an account, so the broker's alarm-drivenDROP USERis 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 theirGRANTs 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 DELUSERcommand templates.
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), GCP tokens (see GCP credentials), and MongoDB (see 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-provisionerrunning 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 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:
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.
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:
- The machine that will connect generates an ephemeral SSH keypair locally.
- It sends seekrit only the public key, plus the login principals and a TTL.
- The broker signs a certificate over that public key (
valid principals,valid before = now + TTL) using the CA key, decrypted transiently in the DO. - 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.
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:
- The machine that will use the credential generates an ephemeral P-256 keypair locally and sends seekrit only the public key, plus a TTL.
- The broker calls STS
AssumeRolewith the base credential, decrypted transiently in the DO, and gets back the temporary credential. - The broker wraps that credential to your public key (the same
wd1.envelope used for DEK grants) and returns only the ciphertext. - 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.
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:
- The machine that will use the credential generates an ephemeral P-256 keypair locally and sends seekrit only the public key, plus a TTL.
- The broker signs a JWT with the source key, exchanges it for the source
account's access token, and calls
generateAccessTokento impersonate the target account — all with the admin key decrypted transiently in the DO. - The broker wraps the returned OAuth token to your public key (the same
wd1.envelope used for DEK grants) and returns only the ciphertext. - 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.
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:
- The machine that will use the credential generates an ephemeral P-256 keypair locally and sends seekrit only the public key, plus a TTL.
- The broker generates a random password, connects to MongoDB over its wire
protocol (with the admin credential decrypted transiently in the DO), and runs
createUserwith the preset roles. - The broker wraps the resulting credential to your public key (the same
wd1.envelope used for DEK grants) and returns only the ciphertext. - 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.
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 (seekrit pg, seekrit mysql,
seekrit redis, seekrit ssh, seekrit aws, seekrit gcp, seekrit mongodb) and the
API reference for the endpoints.