Files
incredigo/internal/rotate/dryrun.go
T
leetcrypt 59af53dcc0 rotate: nine rotation drivers + verify-before-revoke execute spine
Implements the Rotator interface across all four rotation patterns, each a
self-contained one-file driver self-registering via init():

  in-place DB password   postgres, mysql (3-stmt unprivileged fallback), redis
  local keypair + propagate  sshkey (ed25519), wireguard (clamped curve25519)
  provider-API token         gitea PAT, mullvad device key
  cloud-key self-identifying  aws IAM access key (hand-rolled SigV4, no SDK dep)

Spine (execute.go) enforces Hard Rule #2: backup -> rotate -> verify(new) ->
store -> re-read+verify -> revoke-old; dryrun.go keeps --execute gated. Each
driver ships a table-driven test proving real cutover (old secret stops
authenticating) and asserting no secret substring leaks to argv/Meta/errors.
Promotes golang.org/x/crypto to a direct dependency.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-18 14:48:24 -07:00

66 lines
1.9 KiB
Go

package rotate
import (
"context"
"incredigo/internal/discover"
"incredigo/internal/vault"
)
// DryRunResult records, without any secret, what the rotation spine did for one
// credential during a dry run.
type DryRunResult struct {
Credential discover.Credential
Driver string
Rotated bool // a new secret was minted in-vault
Verified bool // the new secret passed Verify
Revoked bool // RevokeOld was invoked (no real effect for the noop driver)
Err error // first error encountered for this credential, if any
}
// DryRun walks the post-backup rotation spine — Detect → Rotate → Verify →
// RevokeOld — for every credential that driver d claims, WITHOUT storing
// anything in gopass and WITHOUT persisting the minted secret (it is Forgotten
// from the vault immediately after). It exists so the full flow can be exercised
// against the noop driver (docs/ROTATION.md §6 step 1) touching no service.
//
// The MANDATORY backup gate (Snapshot) is the caller's responsibility and must
// already have succeeded; DryRun does not perform it.
func DryRun(ctx context.Context, d Rotator, creds []discover.Credential, v *vault.Vault) []DryRunResult {
var out []DryRunResult
for _, c := range creds {
if !d.Detect(c) {
continue
}
res := DryRunResult{Credential: c, Driver: d.Name()}
newH, err := d.Rotate(ctx, c, v)
if err != nil {
res.Err = err
out = append(out, res)
continue
}
res.Rotated = true
// Verify-before-revoke: only proceed to revoke once the new secret checks out.
if err := d.Verify(ctx, newH, v); err != nil {
res.Err = err
v.Forget(newH)
out = append(out, res)
continue
}
res.Verified = true
// A real run stores the new secret in gopass here; a dry run does not.
if err := d.RevokeOld(ctx, c, v); err != nil {
res.Err = err
} else {
res.Revoked = true
}
v.Forget(newH) // never persist the minted secret in a dry run
out = append(out, res)
}
return out
}