rotate: 4 SaaS-token/app-signing drivers + data-separated proof tracking
Add gitlab, cloudflare, ghactions (MOCK-ONLY) and appsecret (LIVE-VM) one-file Rotators, each with a table-driven cutover-proof + leak-check test. gitlab/ cloudflare/ghactions are in-place SaaS-token rolls (self/rotate, value-roll, sealed-secret overwrite) so RevokeOld is a no-op; ghactions seals via nacl/box.SealAnonymous (no new go.mod dep). appsecret regenerates a local app signing secret and rewrites it in place across every target file (atomic, mode- preserving, redacted errors), discoverable via exact-match app-signing key names in env.go. Keep real-rotation code distinct from mock code: how each driver's cutover was validated lives as DATA in internal/rotate/proofs.go (single source of truth) + docs/ROTATION-PROOFS.md, surfaced as a PROOF column in `incredigo rotate` so a MOCK-ONLY driver can never be mistaken for a LIVE-VM one. appsecret proven LIVE-VM against real local files in the sandbox VM. 82 tests green, -race clean on rotate/sink/vault. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
# 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 |
|
||||
|
||||
## 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.
|
||||
|
||||
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) and re-running the same
|
||||
driver against it — no driver code changes.
|
||||
Reference in New Issue
Block a user