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>
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user