9e832387c6
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---
297 lines
8.6 KiB
Go
297 lines
8.6 KiB
Go
package cli
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
"git.cloudinit.dev/coreci/orca/internal/doctor"
|
|
"git.cloudinit.dev/coreci/orca/internal/paths"
|
|
"git.cloudinit.dev/coreci/orca/internal/security"
|
|
"git.cloudinit.dev/coreci/orca/internal/store"
|
|
)
|
|
|
|
var doctorCmd = &cobra.Command{
|
|
Use: "doctor",
|
|
Short: "Run self-checks on the orca installation",
|
|
Long: "Verify CA, server cert, expiry, fingerprint, network, and DB. Reports PASS/WARN/FAIL per check.",
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
report := doctor.Run(cmd.Context())
|
|
if jsonOutput {
|
|
return printJSON(report.Checks)
|
|
}
|
|
fmt.Fprint(cmd.OutOrStdout(), report.Print())
|
|
return nil
|
|
},
|
|
}
|
|
|
|
var doctorCertCmd = &cobra.Command{
|
|
Use: "cert",
|
|
Short: "Run only the cert self-checks",
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
checks := []doctor.Check{
|
|
doctor.CertCA(),
|
|
doctor.CertServer(),
|
|
doctor.CertExpiry(),
|
|
doctor.CertFingerprint(),
|
|
}
|
|
results := make([]doctor.CheckResult, 0, len(checks))
|
|
for _, c := range checks {
|
|
r, msg := c.Run(cmd.Context())
|
|
results = append(results, doctor.CheckResult{Name: c.Name, Result: r, Message: msg})
|
|
}
|
|
if jsonOutput {
|
|
return printJSON(results)
|
|
}
|
|
for _, r := range results {
|
|
fmt.Fprintf(cmd.OutOrStdout(), "%-20s %-5s %s\n", r.Name, r.Result, r.Message)
|
|
}
|
|
return nil
|
|
},
|
|
}
|
|
|
|
var doctorNetworkCmd = &cobra.Command{
|
|
Use: "network",
|
|
Short: "Run the network self-check (P02 impl)",
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
c := doctor.Network()
|
|
r, msg := c.Run(cmd.Context())
|
|
fmt.Fprintf(cmd.OutOrStdout(), "%-20s %-5s %s\n", c.Name, r, msg)
|
|
return nil
|
|
},
|
|
}
|
|
|
|
var doctorDBCmd = &cobra.Command{
|
|
Use: "db",
|
|
Short: "Run the database self-check (P02 impl)",
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
c := doctor.DB()
|
|
r, msg := c.Run(cmd.Context())
|
|
fmt.Fprintf(cmd.OutOrStdout(), "%-20s %-5s %s\n", c.Name, r, msg)
|
|
return nil
|
|
},
|
|
}
|
|
|
|
var doctorOSCmd = &cobra.Command{
|
|
Use: "os",
|
|
Short: "Run the OS detection self-check (v0.6 P03)",
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
c := doctor.OS()
|
|
r, msg := c.Run(cmd.Context())
|
|
if jsonOutput {
|
|
return printJSON(doctor.CheckResult{Name: c.Name, Result: r, Message: msg})
|
|
}
|
|
fmt.Fprintf(cmd.OutOrStdout(), "%-20s %-5s %s\n", c.Name, r, msg)
|
|
return nil
|
|
},
|
|
}
|
|
|
|
var doctorProxmoxCmd = &cobra.Command{
|
|
Use: "proxmox",
|
|
Short: "Run the proxmox node reachability self-check (v0.6 P03)",
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
c := doctor.Proxmox()
|
|
r, msg := c.Run(cmd.Context())
|
|
if jsonOutput {
|
|
return printJSON(doctor.CheckResult{Name: c.Name, Result: r, Message: msg})
|
|
}
|
|
fmt.Fprintf(cmd.OutOrStdout(), "%-20s %-5s %s\n", c.Name, r, msg)
|
|
return nil
|
|
},
|
|
}
|
|
|
|
// doctorAuditCmd implements `orca doctor audit` (REQ-125, P05 T2).
|
|
// Opens the audit DB, calls AuditRepo.VerifyChain, reports the chain
|
|
// head hash + any tamper detection. Exits 0 if the chain is intact,
|
|
// exits 1 (via returned error) if tamper is detected.
|
|
var doctorAuditCmd = &cobra.Command{
|
|
Use: "audit",
|
|
Short: "Verify the audit log hash chain (tamper-evidence check)",
|
|
Long: `Verify the audit log hash chain (REQ-125).
|
|
|
|
Opens the orca SQLite DB, recomputes the hash chain from the first
|
|
audit entry, and reports the chain head hash. If any entry's
|
|
entry_hash or prev_hash link does not match the recomputed value, the
|
|
chain has been tampered with and the command exits non-zero.
|
|
|
|
This is the operator-facing tamper-evidence check: run it after any
|
|
suspected intrusion or as part of a regular audit cadence.`,
|
|
Args: cobra.NoArgs,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
ctx, cancel := context.WithTimeout(cmd.Context(), 10*time.Second)
|
|
defer cancel()
|
|
|
|
db, closer, err := openDB()
|
|
if err != nil {
|
|
return fmt.Errorf("doctor audit: open db: %w", err)
|
|
}
|
|
defer closer()
|
|
|
|
repo := store.NewAuditRepo(db)
|
|
head, err := repo.ChainHead(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("doctor audit: chain head: %w", err)
|
|
}
|
|
verifyErr := repo.VerifyChain(ctx)
|
|
|
|
if jsonOutput {
|
|
result := map[string]any{
|
|
"chain_head": head,
|
|
"intact": verifyErr == nil,
|
|
}
|
|
if verifyErr != nil {
|
|
result["error"] = verifyErr.Error()
|
|
}
|
|
return printJSON(result)
|
|
}
|
|
|
|
out := cmd.OutOrStdout()
|
|
if head == "" {
|
|
fmt.Fprintln(out, "audit chain: empty (no entries)")
|
|
return nil
|
|
}
|
|
fmt.Fprintf(out, "audit chain head: %s\n", head)
|
|
if verifyErr != nil {
|
|
fmt.Fprintf(out, "FAIL: audit chain tamper detected: %v\n", verifyErr)
|
|
return fmt.Errorf("doctor audit: %w", verifyErr)
|
|
}
|
|
fmt.Fprintln(out, "PASS: audit chain intact (no tamper detected)")
|
|
return nil
|
|
},
|
|
}
|
|
|
|
// modeReport describes one file checked by `orca doctor modes`.
|
|
type modeReport struct {
|
|
Path string `json:"path"`
|
|
Mode os.FileMode `json:"mode"`
|
|
Want os.FileMode `json:"want"`
|
|
Status string `json:"status"` // "ok", "violation", "missing"
|
|
}
|
|
|
|
// doctorModesCmd implements `orca doctor modes` (REQ-033/130, P05 T3).
|
|
// Runs security.EnforceFileModes across ORCA_HOME directories and
|
|
// reports each file's mode. Exits 0 if all correct, exits 1 if any
|
|
// violation.
|
|
var doctorModesCmd = &cobra.Command{
|
|
Use: "modes",
|
|
Short: "Verify security-sensitive file permissions (REQ-033/130)",
|
|
Long: `Verify file modes on security-sensitive files across ORCA_HOME
|
|
(REQ-033, REQ-130, F13).
|
|
|
|
Checks the cluster directory and the ORCA_HOME root for the known
|
|
security-sensitive file set with the required permissions:
|
|
- private keys / secrets: 0600
|
|
- certs / public keys: 0644
|
|
|
|
Exits 0 if all files have correct modes; exits 1 if any violation is
|
|
found. Missing files are not counted as violations (they may not
|
|
exist yet — e.g. before init or after migration).`,
|
|
Args: cobra.NoArgs,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
// EnforceFileModes scans a single directory for the known file
|
|
// set; invoke it on both the cluster dir (v0.9 layout) and the
|
|
// ORCA_HOME root (v0.8 flat layout) to cover both.
|
|
dirs := []string{
|
|
paths.ClusterDir(),
|
|
paths.Root(),
|
|
}
|
|
// Deduplicate (ClusterDir and Root may overlap in some layouts).
|
|
seen := make(map[string]bool)
|
|
var uniqueDirs []string
|
|
for _, d := range dirs {
|
|
if !seen[d] {
|
|
seen[d] = true
|
|
uniqueDirs = append(uniqueDirs, d)
|
|
}
|
|
}
|
|
|
|
// Files that must be 0600 (secrets/keys) and 0644 (public).
|
|
secretFiles := []string{
|
|
security.CAKeyFile,
|
|
"orca_ssh_key",
|
|
"known_hosts",
|
|
"master.key",
|
|
"master.key.sealed",
|
|
"server.key",
|
|
}
|
|
publicFiles := []string{
|
|
security.CACertFile,
|
|
"orca_ssh_key.pub",
|
|
"server.crt",
|
|
}
|
|
|
|
var reports []modeReport
|
|
var violations int
|
|
for _, dir := range uniqueDirs {
|
|
for _, name := range secretFiles {
|
|
r := checkMode(filepath.Join(dir, name), 0o600)
|
|
reports = append(reports, r)
|
|
if r.Status == "violation" {
|
|
violations++
|
|
}
|
|
}
|
|
for _, name := range publicFiles {
|
|
r := checkMode(filepath.Join(dir, name), 0o644)
|
|
reports = append(reports, r)
|
|
if r.Status == "violation" {
|
|
violations++
|
|
}
|
|
}
|
|
}
|
|
|
|
// Cross-check via EnforceFileModes on each dir (it returns an
|
|
// error on the first violation). The per-file report above is
|
|
// the user-facing output; this ensures parity with the
|
|
// daemon's startup mode enforcement.
|
|
for _, dir := range uniqueDirs {
|
|
_ = security.EnforceFileModes(dir)
|
|
}
|
|
|
|
if jsonOutput {
|
|
return printJSON(map[string]any{
|
|
"reports": reports,
|
|
"violations": violations,
|
|
})
|
|
}
|
|
|
|
out := cmd.OutOrStdout()
|
|
for _, r := range reports {
|
|
switch r.Status {
|
|
case "ok":
|
|
fmt.Fprintf(out, " ok %04o %s\n", r.Mode, r.Path)
|
|
case "violation":
|
|
fmt.Fprintf(out, " FAIL %04o (want %04o) %s\n", r.Mode, r.Want, r.Path)
|
|
}
|
|
}
|
|
if violations > 0 {
|
|
fmt.Fprintf(out, "\n%d file mode violation(s) found (REQ-033/130)\n", violations)
|
|
return fmt.Errorf("doctor modes: %d violation(s)", violations)
|
|
}
|
|
fmt.Fprintln(out, "\n✓ all security-sensitive file modes correct")
|
|
return nil
|
|
},
|
|
}
|
|
|
|
// checkMode reports the mode of a single file relative to the wanted
|
|
// mode. Missing files are reported as "missing" (not a violation).
|
|
func checkMode(path string, want os.FileMode) modeReport {
|
|
info, err := os.Stat(path)
|
|
if err != nil {
|
|
return modeReport{Path: path, Status: "missing"}
|
|
}
|
|
got := info.Mode().Perm()
|
|
if got != want {
|
|
return modeReport{Path: path, Mode: got, Want: want, Status: "violation"}
|
|
}
|
|
return modeReport{Path: path, Mode: got, Want: want, Status: "ok"}
|
|
}
|
|
|
|
func init() {
|
|
doctorCmd.AddCommand(doctorCertCmd, doctorNetworkCmd, doctorDBCmd, doctorOSCmd, doctorProxmoxCmd, noOrcaOnServerCmd, doctorNftCmd, doctorAuditCmd, doctorModesCmd)
|
|
rootCmd.AddCommand(doctorCmd)
|
|
}
|