# 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](/docs/concepts/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

| Kind | What it rotates | Reaches out to |
| --- | --- | --- |
| `generated` | A random value seekrit is the source of truth for | nothing |
| `postgres` | An existing Postgres role's password | your cluster |
| `mysql` | An existing MySQL/MariaDB account's password | your cluster |
| `redis` | An existing Redis ACL user's password | your 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.

- **Postgres** — `ALTER 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/MariaDB** — `ALTER 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).
- **Redis** — `ACL 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](/docs/concepts/access-control) for that.

See the [rotation guide](/docs/guides/rotation) to set it up, the
[CLI reference](/docs/reference/cli) (`seekrit rotation`), and the
[API reference](/docs/reference/api) for the endpoints.
