Static and dynamic values
Every key in kyvlt.sh holds one of two kinds of value.
A static value is yours. You set it, and it stays whatever you set it to until you change it.
A dynamic value is kyvlt.sh's. It generated the value, it can replace it on a schedule, and nothing else may write it — a hand write to a dynamic key is refused rather than quietly thrown away at the next rotation.
Which one a key is, is the choice in the editor beside the value, and the
dynamic chip on the row is what says a key has taken it.
Why make anything dynamic
A signing key that has not changed in two years is not a secret any more. It is a fact several people know, sitting in a laptop backup, a CI log and an old screenshot. Replacing it on a schedule fixes that — and replacing it badly causes an outage at three in the morning, which is why most teams do not.
Making the key dynamic is what fixes that. kyvlt.sh produces a new value on your schedule, sends it everywhere the key already goes, and keeps the old one readable for long enough that nothing breaks while it catches up.
What can be dynamic, and what cannot
Only values kyvlt.sh itself generated. This is the whole of it, and the line is not blurry.
A STRIPE_SECRET_KEY is Stripe's. kyvlt.sh has no way to ask Stripe for a new
one, and writing a random string over it would replace a working credential
with something that authenticates nothing. The failure is silent and total, so
kyvlt.sh refuses and says where that credential actually gets rotated: at
Stripe, after which you write the new value here.
A JWT_SIGNING_KEY, a database password you chose, a shared secret between two
of your own services — those are yours, which means kyvlt.sh can be the one
that owns them. Make the key dynamic and it can replace the value from then on.
A value built out of other keys cannot be dynamic either. Generating over
postgres://user:${DB_PASSWORD}@host/db would throw the template away and leave
a random string, and everything reading through it would silently get that
string instead. Rotate DB_PASSWORD instead — every composed value that names
it changes with it, which is the point of composing them.
Making a key dynamic
Edit the key, switch the value from Static to Dynamic, and choose a shape:
| Shape | What it is | When to pick it |
| --- | --- | --- |
| Random text | letters, digits, - and _ | the default; densest, and safe in a URL and a connection string |
| Hexadecimal | 0–9, a–f | something at the other end expects hex |
| Letters and digits | no punctuation at all | something at the other end is fussy |
| UUID | one fixed shape | something expects a UUID |
Every one of them survives a URL and a connection string. That is not an
accident: a / in a generated password breaks a Postgres connection string,
and a + becomes a space in a query parameter, and both are discovered in
production by somebody who did not choose the password.
A prefix is optional and is prepended verbatim — sk_, whsec_ — so a value
is recognisable in a log without anybody having to reveal it. It adds no
randomness and does not count toward the length.
Making an existing key dynamic replaces what it holds right now. The old value stays in the key's history and can be restored, but anything using it stops working once the new one arrives. The editor says so before you save.
A key can also be created dynamic: choose it on a new row and kyvlt.sh generates the first value as the key is made, so there is no placeholder version in the history of a credential.
The schedule
Dynamic on its own means kyvlt.sh owns the value; it does not mean the value changes. Tick rotate it for that, and give it two numbers: how often, and how long the previous value stays available.
The interval is how long between rotations. Changing it re-dates from the last rotation rather than from today, so shortening a schedule brings the next rotation forward — which is what shortening it means.
The grace window is how long the previous value stays published beside the
current one, under the key's own name with _PREVIOUS on the end. A consumer
that reads both names keeps working through a cutover without any coordination:
the new value is there, the old one still verifies, and the old one goes away
on its own.
Grace can be zero, which is a hard cutover: the previous value is never published at all. It cannot be longer than the interval, because that would mean a key permanently holding two live values.
<KEY>_PREVIOUS is not a key. Nothing stores it, nothing versions it, and
nothing can write to it — it is the previous value, shown for as long as the
window is open. You cannot create a key with that name for the same reason: a
rotation would then appear to overwrite something somebody else made.
When the clock starts
Grace is measured from the moment every destination has the new value, not from the moment it was generated.
That distinction is the whole promise. If the window started at generation, it could be counting down while a cluster still held the old value — and could expire before that cluster ever received the new one, which is exactly a workload holding a credential that has stopped working.
So a rotation goes through three states:
- Delivering — the new value exists and is on its way. Nothing has been retired and no clock is running.
- In grace — every destination has it, and the previous value is published beside it until the window closes.
- Complete — the window closed, the publication stopped being sent, and every connector's ordinary prune removed it.
When something cannot be reached
A rotation that cannot get its new value to one of its destinations holds there indefinitely. It does not give up and it does not try again with a third value: the key is marked blocked, the destination is named, and no further rotation of that key starts however many intervals pass.
That is deliberate. Abandoning it would leave a value nothing could use; rotating again would stack up live credentials nobody is tracking. Holding is the only option that cannot break anything.
The way out is to fix the destination or disconnect it, which is its own deliberate act with its own record. kyvlt.sh will tell you: a blocked rotation raises a notification naming the key and the destination.
"Blocked" and "late" are different words on purpose. A late rotation will happen on the next pass. A blocked one will not happen at all until somebody does something.
Rotating now
There is also a button. Somebody who suspects a credential is exposed can replace it immediately, and the schedule restarts from today.
It needs only permission to write the value — somebody who could already replace it by typing can ask kyvlt.sh to replace it properly, and the moment you think a key has leaked is the worst possible moment to need to find an administrator. Setting the standing schedule needs permission to manage the environment, because that is an instruction that acts when nobody is present.
If a rotation is already under way, rotating now is refused rather than accepted. Two live rotations would mean three values for one key.
Putting it back
A rotation is a version like any other, so restoring an earlier version undoes it. The restored value goes to every destination the same way, and the history shows it as your restore rather than as another rotation — so a reader can tell that somebody intervened.
The key stays dynamic and the schedule stays set. Fixing an incident should not quietly cancel a rotation policy.
Handing a value back
Switching a dynamic key to Static gives you the value again. It is left exactly as it is and nothing redeploys — this is a statement about the future, not a reason to change what is running — and any schedule is removed with it.
What is written down
Every part of this is in the audit log, and none of it contains a value.
Making a key dynamic records the shape and the length. Setting a schedule records the interval and the grace window. A rotation records which version it replaced, which version it produced, and whether the schedule or a person asked for it. Opening and closing the grace window are recorded against that rotation.
Revealing <KEY>_PREVIOUS is recorded exactly as revealing any other value is:
one record per key, naming the suffixed name and the version behind it, for a
person. A connector pulling the whole environment writes the one transfer
record it always writes. Being synthesised rather than stored changes nothing
about what the log knows.