Rotate a secret
Replace a credential's secret object under optimistic locking and prove the rotation took.
A vendor key leaked, expired, or simply aged out of policy. Rotation here is a whole-object replacement of the secret under optimistic locking — no partial merges, no status changes.
Goal
End with the same credential record (same id) carrying a new secret: version incremented, mask changed, configuration intact.
Prerequisites
- An active credential from scenario 02; its
idand currentversion.
Steps
1. Read the current state
Call GET /v1/credentials/{id} and keep version and masked_credentials for comparison.
2. Rotate
Call PATCH /v1/credentials/{id} with the complete new secret object and the current version:
curl -s -X PATCH "$CREDS_BASE/v1/credentials/$CRED_ID" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"credentials": {"api_key": "sk-live-fedcba9876543210"},
"version": 1
}'
r = httpx.patch(f"{CREDS_BASE}/v1/credentials/{cred_id}", headers=headers, json={
"credentials": {"api_key": "sk-live-fedcba9876543210"},
"version": current_version,
})
assert r.status_code == 200
rotated = r.json()
Expected 200: version bumped, masked_credentials visibly different (the mask is recomputed from the new plaintext), is_default and status untouched. The old secret is gone — re-encrypted storage holds only the new object.
3. Prove the lock protects you
Repeat the same PATCH with the old version:
curl -s -X PATCH "$CREDS_BASE/v1/credentials/$CRED_ID" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"credentials": {"api_key": "sk-live-stale-write"}, "version": 1}'
Expected 409 with error.code = "CREDENTIAL_VERSION_CONFLICT" — a concurrent editor cannot silently overwrite your rotation. Re-read, then retry, is the contract.
Rotating an organization or platform key is the same call
The identical PATCH exists on /v1/org-credentials and the internal admin plane; only the ownership and the gate differ.
Verified by the test test_s03_rotate_a_secret.