DEV Community

Ivan Rossouw
Ivan Rossouw

Posted on

Verified Is Not Active: Designing Safer Credential Rotation

A successful connection test answers a technical question: can these credentials work?

It does not answer an operational question: should this configuration become active now?

Those questions are easy to collapse into one button. An administrator enters new values, clicks “Test”, receives a green result, and the system saves them as the live configuration. The flow feels efficient. It also gives a diagnostic operation authority it should not have.

Recent committed work reminded me that the safer design is deliberately less convenient: stage, verify, and activate must be separate intentions.

The dangerous shortcut

Consider a service that already has a working credential set. An operator wants to rotate it.

A naive workflow looks like this:

  1. Overwrite the stored values.
  2. call the external service;
  3. keep the new values if the call succeeds;
  4. try to restore the old values if it fails.

The problem is not only the failure path. A network timeout may leave the result unknown. Two administrators may work from different browser tabs. A retry may repeat the write. A role or entitlement may change between page load and submission. “Test” has become both an experiment and a production mutation.

The safer rule is: verification produces evidence; only an explicit command changes authority.

Give the candidate its own lifecycle

Model the replacement as a candidate rather than editing the active version in place:

stage candidate
      |
      v
verify exact candidate
      |
      v
explicitly activate or promote
      |
      v
retire previous active version
Enter fullscreen mode Exit fullscreen mode

The current configuration remains authoritative while the candidate is being prepared and tested. The verifier can read the exact candidate and return one of a small set of results: verified, rejected, or indeterminate. It cannot update the active reference.

That constraint matters. It makes the boundary structural, not merely a convention in a UI event handler.

Only a separate activation or promotion command performs cutover. It checks that the evidence is still fresh and belongs to the exact candidate. If cutover succeeds, the previous active version can be retired. Until then, normal work continues with the known-good version.

Re-authorise at the effect

An approval screen is a snapshot. The mutation happens later.

At submission time, the server should derive the actor and scope from trusted request context again. Do not accept either as an editable form value. Then re-check:

  • the actor still has permission;
  • the target still belongs to the resolved scope;
  • the relevant entitlement is still valid;
  • the candidate is the one that was reviewed;
  • its verification evidence is still fresh; and
  • the target revision has not changed.

This is especially useful for long-lived administrative pages. A tab opened before a role change should not retain yesterday’s authority.

A short-lived, server-issued action token can bind the intended command to the actor, scope, target, and expected revision. That is not a substitute for authorisation. It is a way to prove that the submitted mutation still matches the state that was presented for review.

Make retries boring

Administrative commands are retried more often than we think: double-clicks, mobile reconnects, reverse-proxy retries, and impatient refreshes all happen.

Record the outcome against a stable command identity. An exact replay can return the same result without creating another candidate or repeating the cutover. Reusing that identity with changed input should conflict. Submitting it after the target revision changes should also conflict.

The goal is not to make every conflict disappear. It is to turn ambiguity into an explicit outcome the UI can explain.

Blank is not an intention

Credential forms have another subtle boundary: what does an empty field mean?

For an existing secret, blank could mean “keep the stored value”, “replace it with an empty value”, or “clear it”. A single string cannot safely represent all three.

Use an explicit edit command:

Keep
Replace(new value)
Clear
Enter fullscreen mode Exit fullscreen mode

This costs a little more mapping code, but it removes accidental erasure and prevents the server from guessing the operator’s intent.

Test the boundary, not only the happy path

The committed tests I reviewed pin the behaviours that matter:

  • staging and verification leave the current active version untouched;
  • only explicit promotion changes the active reference;
  • the previous version is retired after successful cutover;
  • an exact replay does not duplicate state;
  • changed input with the same command identity conflicts;
  • stale target state conflicts;
  • cross-scope access fails closed; and
  • losing authority removes enabling actions while preserving safety-reducing actions.

I inspected those tests but did not run them during this read-only review. They are design evidence, not a claim about production behaviour.

The trade-off

This design adds candidate states, verification records, token expiry, concurrency handling, audit rows, UI steps, and more tests. Operators perform one additional deliberate action.

In exchange, a green check cannot silently become a production decision. The known-good version remains available during rotation. Stale tabs and changed permissions are re-evaluated at the point of effect. Retries become explainable rather than mysterious.

For high-impact settings, that is usually a worthwhile trade.

The practical question is simple: in your administrative tooling, does “test” only gather evidence, or can it also change what is live?

Top comments (0)