feat(P05): seal/audit CLI + chain race fix + key zeroing (REQ-154)

New CLI commands:
- orca cluster seal: OIDC/CA-derived seal + Shamir 3-of-5 shards
- orca cluster unseal: OIDC/CA unseal + --recovery Shamir path
- orca doctor audit: VerifyChain + chain head report
- orca doctor modes: EnforceFileModes across ORCA_HOME

Fixes:
- audit hash-chain race: Append uses BEGIN IMMEDIATE transaction
  (concurrent appends no longer corrupt tamper-evidence)
- secrets rotate-master: re-seals to OIDC on sealed clusters
  (was writing raw key, docstring claimed re-seal)
- key zeroing: ZeroKey helper + defer after master/namespace key use
  (defense-in-depth against pprof heap extraction)
- store.Open: busy_timeout(5000) pragma (concurrent writers wait)

Tests: 18 new test functions (seal round-trip, Shamir recovery, doctor
audit tamper detection, doctor modes 0644 rejection, concurrent append
chain integrity, rotate-master re-seal, key zeroing).

---ci---
project: orca
phase: 5
milestone: v0.13
status: complete
requirements:
  covered: [154]
---/ci---
This commit is contained in:
Jon Chery
2026-08-07 21:06:39 +00:00
parent 5232fcb808
commit 9e832387c6
14 changed files with 1457 additions and 29 deletions
+64 -19
View File
@@ -53,21 +53,19 @@ func computeEntryHash(prevHash, timestamp, actor, action, resource, result, errM
return hex.EncodeToString(h.Sum(nil))
}
// getLastEntryHash returns the entry_hash of the most recent audit_log
// entry, or "" if the table is empty.
func (r *AuditRepo) getLastEntryHash(ctx context.Context) (string, error) {
var prevHash string
err := r.db.QueryRowContext(ctx,
`SELECT entry_hash FROM audit_log ORDER BY id DESC LIMIT 1`).Scan(&prevHash)
if err == sql.ErrNoRows {
return "", nil
}
if err != nil {
return "", fmt.Errorf("get last entry hash: %w", err)
}
return prevHash, nil
}
// Append adds a new audit entry to the log. The read of the previous
// entry's hash and the insert of the new row are wrapped in a single
// BEGIN IMMEDIATE transaction executed on a single dedicated
// connection so concurrent appends serialize: BEGIN IMMEDIATE acquires
// a RESERVED write lock immediately, blocking other writers until
// COMMIT. Without this, two concurrent Append calls could both read
// the same prev_hash and produce two entries with the same prev_hash
// link — corrupting the chain (REQ-125, P05 T4).
//
// We pin a single connection from the pool (db.Conn) and run
// BEGIN IMMEDIATE / SELECT / INSERT / COMMIT on it so the transaction
// state stays on one connection (database/sql does NOT propagate
// transaction state across pooled connections).
func (r *AuditRepo) Append(ctx context.Context, e *AuditEntry) error {
if e.Timestamp.IsZero() {
e.Timestamp = time.Now().UTC()
@@ -78,19 +76,50 @@ func (r *AuditRepo) Append(ctx context.Context, e *AuditEntry) error {
metaJSON, _ := json.Marshal(e.Metadata)
tsStr := e.Timestamp.UTC().Format(time.RFC3339Nano)
// Compute the hash chain (REQ-125, F2).
prevHash, err := r.getLastEntryHash(ctx)
// Pin a single connection so the transaction state is consistent.
conn, err := r.db.Conn(ctx)
if err != nil {
return fmt.Errorf("audit hash chain: %w", err)
return fmt.Errorf("audit append: acquire conn: %w", err)
}
defer conn.Close()
// BEGIN IMMEDIATE acquires a RESERVED lock right away, serializing
// concurrent writers. Other BEGIN IMMEDIATE callers block (with
// the configured busy_timeout) until we COMMIT.
if _, err := conn.ExecContext(ctx, "BEGIN IMMEDIATE"); err != nil {
return fmt.Errorf("audit append: begin immediate: %w", err)
}
committed := false
defer func() {
if !committed {
_, _ = conn.ExecContext(ctx, "ROLLBACK")
}
}()
// Read the chain head (last entry's hash) within the transaction.
var prevHash string
err = conn.QueryRowContext(ctx,
`SELECT entry_hash FROM audit_log ORDER BY id DESC LIMIT 1`).Scan(&prevHash)
if err == sql.ErrNoRows {
prevHash = ""
} else if err != nil {
return fmt.Errorf("audit append: get last entry hash: %w", err)
}
// Compute the new entry hash (REQ-125, F2).
entryHash := computeEntryHash(prevHash, tsStr, e.Actor, e.Action, e.Resource, e.Result, e.Error, string(metaJSON))
_, err = r.db.ExecContext(ctx,
_, err = conn.ExecContext(ctx,
`INSERT INTO audit_log (timestamp, actor, action, resource, result, error, metadata, prev_hash, entry_hash) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
e.Timestamp, e.Actor, e.Action, e.Resource, e.Result, e.Error, string(metaJSON), prevHash, entryHash)
if err != nil {
return fmt.Errorf("insert audit: %w", err)
}
if _, err := conn.ExecContext(ctx, "COMMIT"); err != nil {
return fmt.Errorf("audit append: commit: %w", err)
}
committed = true
return nil
}
@@ -135,6 +164,22 @@ func (r *AuditRepo) VerifyChain(ctx context.Context) error {
return rows.Err()
}
// ChainHead returns the entry_hash of the most recent audit_log entry,
// or "" if the table is empty. Used by `orca doctor audit` to report
// the chain head hash (REQ-125, P05 T2).
func (r *AuditRepo) ChainHead(ctx context.Context) (string, error) {
var head string
err := r.db.QueryRowContext(ctx,
`SELECT entry_hash FROM audit_log ORDER BY id DESC LIMIT 1`).Scan(&head)
if err == sql.ErrNoRows {
return "", nil
}
if err != nil {
return "", fmt.Errorf("chain head: %w", err)
}
return head, nil
}
func (r *AuditRepo) List(ctx context.Context, limit int) ([]*AuditEntry, error) {
if limit <= 0 {
limit = 100