Files
incredigo/docs/ROTATION-PROOFS.md
T
leetcrypt 35f19c007a rotate: 6 service-API drivers (mongo, npm, gcp, twilio, flyio, k8s)
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>
2026-06-18 15:57:59 -07:00

95 lines
5.7 KiB
Markdown

# 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/rotate` mints a new PAT *and revokes the caller's old one*; the test
asserts `Verify(old)` fails after `Rotate`, proving the revoke really happened.
- **cloudflare** — `PUT .../value` rolls the value for the same token id and invalidates
the previous value; the test asserts the id is unchanged and `Verify(old)` fails.
- **ghactions** — the emulator owns a real NaCl `box` keypair, serves the public key, and
on `PUT` **decrypts 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 after
`RevokeOld` — 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, and `DELETE` removes the key — so the test proves real service-account
self-auth (genuine `crypto/rsa` signing) *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 `mongosh` authenticates `db.auth` against a password held in a state
file and only `changeUserPassword` updates it; the test proves the in-place cutover (old
password rejected after rotation). No password ever reaches `mongosh`'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.