Each driver is real-service code only (no test awareness; tests inject just an HTTPClient/Bin), following the established patterns: - mongo: in-place changeUserPassword over the old connection; no secret on argv (host/db on the URI, both passwords on stdin, output scrubbed). - npm: create token -> /-/whoami -> delete-by-key; blob records the key. - gcp: service-account KEY rotation; mints an RS256 JWT signed with the SA's own private key (crypto/rsa, stdlib) to self-auth the IAM create/verify/delete. - twilio: API Key create -> get -> delete, Basic auth (KeySid/secret). - flyio: GraphQL — a managing token mints/deletes the rotated deploy token. - k8s: create a service-account-token Secret, await the populated token, verify against the SA, delete the old Secret (invalidates the old token). All 6 register their honest proof level (MOCK-ONLY) in proofs.go + ROTATION-PROOFS.md; mongo/npm/k8s are LIVE-VM candidates pending real-target VM POCs. Emulators enforce credential validity (gcp verifies the JWT signature), tests assert Verify(old) fails after revoke + a secret-leak canary. Full suite: 103 green, -race clean on rotate/sink/vault. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
5.7 KiB
Rotation cutover proofs
This file is the human-facing mirror of internal/rotate/proofs.go. That Go table is
the single source of truth (incredigo rotate surfaces it as the PROOF column); this
document explains what each level means and records the basis for every driver's level.
Keep the two in sync — if you change one, change the other.
Why this exists
Every Rotator in internal/rotate contains only real rotation code: it talks to a
real service (HTTP API, DB client, SSH, local files). None of them contain simulation
branches or test awareness — the only thing a test injects is an endpoint / HTTPClient /
binary path, which a real deployment also configures.
What differs between drivers is how their real-cutover path has been validated. That is
data, not behaviour, so it lives in one table (here + proofs.go) instead of as prose
scattered through — and easily confused with — the driver code. This is the guardrail that
stops a mock-only driver from ever being mistaken for a live one.
Levels
| Level | Meaning |
|---|---|
| LIVE-VM | Cutover proven against the real target software running in the sandbox VM (real PostgreSQL / MariaDB / Redis / wg / Gitea / local files). |
| MOCK-ONLY | Cutover proven only against an emulator — our own httptest / in-process SSH server, or a third-party mock (e.g. moto for AWS). NOT proven against the real target service. |
| UNPROVEN | No cutover proof recorded (e.g. the dry-run noop helper). |
Honesty note: running in the VM does not automatically make a driver LIVE-VM. AWS ran in the VM but only against moto (a mock AWS), so it is MOCK-ONLY. LIVE-VM means the real target software validated the cutover.
Current status
LIVE-VM — proven against real target software in the sandbox VM
| Driver | Real target | Provisioner |
|---|---|---|
postgres |
real PostgreSQL | lab-provision-pg.sh |
mysql |
real MariaDB | lab-provision-dbclones.sh |
redis |
real redis-server |
lab-provision-dbclones.sh |
wireguard |
real wg |
lab-provision-wg.sh |
gitea |
real Gitea | lab-provision-gitea.sh |
appsecret |
real local files | lab-provision-appsec.sh |
MOCK-ONLY — proven only against an emulator / mock
| Driver | Emulator | Why not (yet) LIVE-VM |
|---|---|---|
aws |
moto mock AWS in VM |
real AWS never hit |
sshkey |
in-process SSH server | live VM POC not run |
openwrt |
in-process SSH server | live QEMU deferred |
mullvad |
httptest emulator |
needs a paid account |
cloudflare |
httptest emulator |
no self-host |
ghactions |
httptest emulator (holds the libsodium box keypair, decrypts the sealed PUT) |
no self-host |
gitlab |
httptest emulator |
GitLab CE too heavy for the VM |
mongo |
fake-mongosh shell stub (auths against a state file) |
real-MongoDB VM POC not yet run |
npm |
httptest registry emulator |
real registry never hit |
gcp |
httptest IAM emulator (verifies the driver's RS256 JWT against the key's public key) |
no self-host |
twilio |
httptest emulator (Basic-auth Keys resource) |
no self-host |
flyio |
httptest GraphQL emulator |
no self-host |
k8s |
httptest apiserver emulator (token-Secret create/delete) |
real k3s POC not yet run |
What the MOCK-ONLY emulators actually prove
The emulators are not stubs that rubber-stamp success — each enforces the real protocol so the test exercises the driver's true cutover logic:
- gitlab —
self/rotatemints a new PAT and revokes the caller's old one; the test assertsVerify(old)fails afterRotate, proving the revoke really happened. - cloudflare —
PUT .../valuerolls the value for the same token id and invalidates the previous value; the test asserts the id is unchanged andVerify(old)fails. - ghactions — the emulator owns a real NaCl
boxkeypair, serves the public key, and onPUTdecrypts the sealed value and stores the plaintext; the test asserts the decrypted value equals the new value the driver put in the rebuilt blob — proving the sealed-box encryption is correct, not simulated. - npm / twilio / k8s — each emulator enforces credential validity (a token/key
authenticates only while it exists) and the test asserts
Verify(old)fails afterRevokeOld— proving the create→verify→revoke cutover really happened, old credential dead, new one alive. - gcp — the emulator verifies the RS256 JWT signature the driver mints against the
public key recorded for the key's
kid; it only issues an access token for a JWT that actually verifies, andDELETEremoves the key — so the test proves real service-account self-auth (genuinecrypto/rsasigning) and the cutover (revoked key can no longer mint a token). - flyio — a managing token mints/deletes the deploy token over GraphQL; the deploy token authenticates the app query only while it exists, so the test proves the managing-token → deploy-token rotation and that the revoked deploy token stops working.
- mongo — the fake
mongoshauthenticatesdb.authagainst a password held in a state file and onlychangeUserPasswordupdates it; the test proves the in-place cutover (old password rejected after rotation). No password ever reachesmongosh's argv.
Promoting any of these to LIVE-VM only requires standing up the real service (a self-hosted GitLab CE / a real Cloudflare token / a throwaway GitHub repo / a real MongoDB / a real npm registry / a k3s cluster) and re-running the same driver against it — no driver code changes.