59af53dcc0
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>
105 lines
3.3 KiB
Go
105 lines
3.3 KiB
Go
package rotate
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"incredigo/internal/discover"
|
|
"incredigo/internal/vault"
|
|
)
|
|
|
|
// writeFakePsql writes a stub `psql` that emulates password auth against a state
|
|
// file holding the role's CURRENT password. ALTER updates the state (only if the
|
|
// caller authenticated with the current password); SELECT 1 succeeds only when
|
|
// PGPASSWORD matches the current password. This lets a test prove a real cutover:
|
|
// after Rotate the OLD password must stop authenticating.
|
|
func writeFakePsql(t *testing.T, statePath string) string {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
p := filepath.Join(dir, "psql")
|
|
script := `#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
sql="$(cat)"
|
|
cur="$(cat "` + statePath + `" 2>/dev/null || true)"
|
|
case "$sql" in
|
|
*"ALTER USER"*|*"ALTER ROLE"*)
|
|
if [ "${PGPASSWORD:-}" != "$cur" ]; then echo "FATAL: password authentication failed" >&2; exit 2; fi
|
|
np="$(printf '%s' "$sql" | sed -E "s/.*PASSWORD '([^']*)'.*/\1/")"
|
|
printf '%s' "$np" > "` + statePath + `"
|
|
echo "ALTER ROLE"; exit 0 ;;
|
|
*"SELECT 1"*)
|
|
if [ "${PGPASSWORD:-}" != "$cur" ]; then echo "FATAL: password authentication failed" >&2; exit 2; fi
|
|
echo "1"; exit 0 ;;
|
|
*) echo "unrecognized" >&2; exit 1 ;;
|
|
esac
|
|
`
|
|
if err := os.WriteFile(p, []byte(script), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return p
|
|
}
|
|
|
|
func TestPostgresDetect(t *testing.T) {
|
|
pg := &Postgres{}
|
|
if !pg.Detect(discover.Credential{Source: "postgres"}) {
|
|
t.Error("should detect Source=postgres")
|
|
}
|
|
if pg.Detect(discover.Credential{Source: "env"}) {
|
|
t.Error("should not detect Source=env")
|
|
}
|
|
}
|
|
|
|
func TestPostgresRotateRealCutover(t *testing.T) {
|
|
state := filepath.Join(t.TempDir(), "pw")
|
|
if err := os.WriteFile(state, []byte("oldpw"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Setenv("PGPASSWORD", "") // ensure the driver, not the env, supplies it
|
|
pg := &Postgres{Bin: writeFakePsql(t, state)}
|
|
|
|
v := vault.New()
|
|
defer v.Purge()
|
|
oldDSN := "postgres://labapp:oldpw@127.0.0.1:5432/labdb"
|
|
cred := discover.Credential{Source: "postgres", Identity: "labapp@labdb", Secret: v.Store([]byte(oldDSN))}
|
|
ctx := context.Background()
|
|
|
|
newH, err := pg.Rotate(ctx, cred, v)
|
|
if err != nil {
|
|
t.Fatalf("Rotate: %v", err)
|
|
}
|
|
|
|
// The fixture's current password must have changed.
|
|
if cur, _ := os.ReadFile(state); string(cur) == "oldpw" {
|
|
t.Fatal("password not changed by ALTER")
|
|
}
|
|
|
|
// New secret authenticates...
|
|
if err := pg.Verify(ctx, newH, v); err != nil {
|
|
t.Errorf("Verify(new) should pass: %v", err)
|
|
}
|
|
// ...and the OLD credential is now dead (real cutover).
|
|
if err := pg.Verify(ctx, cred.Secret, v); err == nil {
|
|
t.Error("Verify(old) must fail after rotation — old password should be revoked")
|
|
} else if strings.Contains(err.Error(), "oldpw") {
|
|
// sanity: error must not leak the secret
|
|
t.Error("error leaked the old password")
|
|
}
|
|
|
|
if err := pg.RevokeOld(ctx, cred, v); err != nil {
|
|
t.Errorf("RevokeOld (no-op) returned: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestPostgresRejectsNonPostgresDSN(t *testing.T) {
|
|
pg := &Postgres{Bin: "/bin/false"}
|
|
v := vault.New()
|
|
defer v.Purge()
|
|
cred := discover.Credential{Source: "postgres", Secret: v.Store([]byte("mysql://u:p@h/db"))}
|
|
if _, err := pg.Rotate(context.Background(), cred, v); err == nil {
|
|
t.Error("Rotate should reject a non-postgres scheme")
|
|
}
|
|
}
|