seekrit
Docs/Secret rotation

Secret rotation

seekrit can replace a stored secret's value on a schedule — a database password, an API key, a signing secret — and, for databases, change the credential on the system it authenticates to in the same step. Configure it once and the value is never older than your cadence.

This is the mirror image of temporary access. There, seekrit mints a credential nobody stored, and it expires. Here, seekrit replaces a long-lived credential you do store. Same targets, same admin credential, same verifier trick — opposite lifecycle. If you can use a short-lived credential instead, prefer it; rotation is for the credentials that have to be long-lived.

What rotation does

A rotation policy attaches to one secret. When it comes due, seekrit:

  1. generates a fresh high-entropy value,
  2. for a database kind, installs it on the target account, and
  3. writes the new value back as a new version of the secret — encrypted.

Consumers do nothing. The next seekrit run, GET /v1/resolve, or SDK call returns the new value, decrypted with the same environment key as always. Every rotation appends a secret_versions row, so the history shows exactly which versions rotation produced.

Kinds

KindWhat it rotatesReaches out to
generatedA random value seekrit is the source of truth fornothing
postgresAn existing Postgres role's passwordyour cluster
mysqlAn existing MySQL/MariaDB account's passwordyour cluster
redisAn existing Redis ACL user's passwordyour instance

The database kinds are what Vault calls static roles: the account is never created or dropped, only re-keyed. You name an account that already exists and seekrit changes its password in place, so every grant, ownership, and reference attached to it survives.

generated contacts nothing — it is for credentials whose consumer reads them from seekrit (internal API keys, webhook signing secrets, an encryption pepper). Rotating one changes the stored value; anything that validates it must read it from seekrit rather than hold its own copy.

The verifier trick, again

The database kinds keep the same property temporary access has: your database never receives the new password, only a one-way verifier of it.

  • PostgresALTER ROLE … WITH PASSWORD '<SCRAM-SHA-256$…>' stores the verifier verbatim. It can't authenticate: SCRAM login needs ClientKey and the verifier holds only SHA256(ClientKey).
  • MySQL/MariaDBALTER USER … IDENTIFIED WITH mysql_native_password AS '*<hash>' stores the double-SHA1 hash verbatim. It can't authenticate either (auth proves knowledge of the SHA1(password) preimage).
  • RedisACL SETUSER … on resetpass #<sha256-hex> stores the digest verbatim. resetpass clears the account's previous passwords first, so the old credential stops working the moment the new one lands, and SETUSER merges, so the user's key and command permissions are untouched.

So a dump of pg_authid, mysql.user, or the Redis ACL file still can't log in after a rotation — exactly as before one.

The trust boundary

Rotation is the one feature where something server-side must be able to encrypt into one of your environments, and therefore to decrypt it. There is no way around that: writing a new secret value means producing ciphertext under the environment's data key. We make the boundary as narrow and as visible as possible.

The rotator is just another principal. Every organization has a broker Durable Object — the same one that mints temporary credentials — with its own P-256 keypair whose private half never leaves its storage. Enabling rotation creates an ordinary key grant to that public key: an environment_keys row with principal_type = rotator. That grant is what lets the broker rotate, and it has the properties every other grant has.

What follows from that:

  • You establish it, not us. The wrapped key is computed on your machine (the CLI or your browser tab) from your own copy of the environment key, and uploaded already-wrapped. The API cannot produce it — enabling rotation requires someone who already holds the key, and it is audited when they do.
  • It is per environment. The broker can decrypt exactly the environments where you enabled rotation, and nothing else. An environment with no rotation has no rotator grant.
  • It ends when the feature does. Disabling the last rotation policy in an environment (or deleting the last rotating secret) drops the grant.
  • A database dump is still only ciphertext. The grant stores the key wrapped to a public key whose private half lives in Durable Object storage, not in D1.
  • The plaintext lives in one function. The new value is generated inside the broker, used to derive a verifier, encrypted, and discarded. What leaves the broker is an sc1. blob; what reaches your database is a verifier. Nothing writes it to storage or logs.
  • Revoking is immediate. The grant is re-read from the database on every rotation rather than cached, so removing it stops rotation at the first step.
note

This is a deliberate, scoped exception to the zero-knowledge invariant, in the same family as the admin credential a lease target holds (in_do execution) — and it is opt-in per environment. If you would rather seekrit never hold the ability to decrypt an environment at all, don't enable rotation for it: rotate by writing a new value yourself (seekrit secrets set), which stays fully client-side.

Scheduling, failure, and the honest caveat

The schedule runs on a one-minute sweep, so a policy rotates within a minute of coming due. Cadence is anything from 5 minutes to a year.

Rotation changes your database before it stores the new value, which is the safer of the two orders: the only failure window leaves the stored value stale rather than storing a password that was never installed. It is worth being precise about what that means:

  • If the write-back fails after the database was re-keyed, consumers hold a password that no longer works until the retry succeeds. seekrit records the failure, emails your admins, and retries with a fresh value on a backoff (15 minutes, doubling, capped at 6 hours). The retry re-keys and re-stores together, so the state converges.
  • After five consecutive failures the policy stops retrying and goes failed, so a broken target surfaces instead of retrying forever. Fix the cause and resume it — a successful rotation clears the streak.
  • Long-lived connections already authenticated with the old password are not affected by a rotation; new connections need the new one. Applications that resolve secrets once at boot should be able to re-resolve (or be restarted) after a rotation. Choose a cadence your deployment can absorb.

Every rotation writes a synchronous audit row (secret.rotated), as does every failure (secret.rotation_failed) and every configuration change (secret.rotation_configured / _updated / _disabled). Rotation also purges the cached resolve for the environment, so nothing serves the previous ciphertext.

Rotating the key, not the value

Rotation replaces a secret's value. That is different from rotating an environment's data key — issuing a new key, re-encrypting every secret under it, and re-wrapping it for the remaining principals — which is what cuts off a principal that may have cached the old key. See access & key grants for that.

See the rotation guide to set it up, the CLI reference (seekrit rotation), and the API reference for the endpoints.