4b70e31cf4
Critical fixes:
- logs --job: validate ^[A-Za-z0-9_-]+$ + shellQuote (was %q backtick RCE)
- pprof: isLoopback treats empty host as bind-all (was :6060 bypass)
- backup restore: filepath.Rel containment check (was tar-slip via a/../..)
- WebAuthn reg auth deferred to P04 (requires session infra)
High fixes:
- txn rollback/show/apply: validate ^T-[0-9a-f]{16}$ + shellQuote
- nft diff --against: validate txn ID before filepath.Join
- drain stopAlloc: validate allocID ^[A-Za-z0-9_-]+$
- cluster_compat: shellQuote peer dir name
- podman image: shellQuote (was %q backtick injection)
- nft TrustedProbes: net.ParseIP/CIDR validation + split v4/v6 sets
- sudoers: validate --proxmox-user/--proxmox-role ^[a-zA-Z_][a-zA-Z0-9_-]{0,31}$
fixed path /etc/sudoers.d/orca; shellQuote pveum/useradd; validateSudoers
checks actual file
- nft country block: validate ^[A-Z]{2}$ (was len==2 only)
New file: internal/cli/validate.go (shared validators + shellQuote)
All 38 Go test packages pass. go vet + gofmt clean.
---ci---
project: orca
phase: 2
milestone: v0.13
status: complete
requirements:
covered: [150]
---/ci---
420 lines
11 KiB
Go
420 lines
11 KiB
Go
package cli
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log/slog"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
"git.cloudinit.dev/coreci/orca/internal/emit"
|
|
"git.cloudinit.dev/coreci/orca/internal/model"
|
|
)
|
|
|
|
var noOrcaOnServerCmd = &cobra.Command{
|
|
Use: "no-orca-on-server",
|
|
Short: "Verify no orca binary/service/process on peers (REQ-086, R-001, C-13)",
|
|
Long: `SSH to each registered peer and verify that no orca binary,
|
|
systemd service, or process is present on the server (R-001: no orca
|
|
binary on any server; C-13 enforcement).
|
|
|
|
Checks per peer:
|
|
1. command -v orca → must return nothing (no orca in PATH)
|
|
2. systemctl list-units 'orca*' (excluding orca-alloc-*) → must be empty
|
|
3. pgrep orca → must return nothing (no orca process)
|
|
4. /etc/orca/ contains no orca binaries (config dir is OK)
|
|
|
|
A peer with any violation is reported as FAIL. The exit code is non-zero
|
|
if any peer fails.`,
|
|
Args: cobra.NoArgs,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
return runNoOrcaOnServer(cmd)
|
|
},
|
|
}
|
|
|
|
type noOrcaPeerResult struct {
|
|
Node string `json:"node"`
|
|
Peer string `json:"peer"`
|
|
Pass bool `json:"pass"`
|
|
Violations []string `json:"violations,omitempty"`
|
|
}
|
|
|
|
func runNoOrcaOnServer(cmd *cobra.Command) error {
|
|
ctx, cancel := context.WithTimeout(cmd.Context(), 5*time.Minute)
|
|
defer cancel()
|
|
|
|
reg, closer, err := nodeRegistry()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer closer()
|
|
|
|
nodes, err := reg.List(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("list nodes: %w", err)
|
|
}
|
|
|
|
ex, err := drainExecFromCtx(cmd.Context())
|
|
if err != nil {
|
|
return fmt.Errorf("ssh transport: %w", err)
|
|
}
|
|
|
|
log := newLogger()
|
|
results := make([]noOrcaPeerResult, 0, len(nodes))
|
|
var failedNodes []string
|
|
|
|
for i := range nodes {
|
|
n := nodes[i]
|
|
peer := peerAddrForNode(n)
|
|
if peer == "" {
|
|
continue
|
|
}
|
|
r := noOrcaPeerResult{Node: n.Name, Peer: peer, Pass: true, Violations: []string{}}
|
|
|
|
if v, ok := checkNoOrcaBinary(ctx, ex, peer); !ok {
|
|
r.Pass = false
|
|
r.Violations = append(r.Violations, v)
|
|
}
|
|
if v, ok := checkNoOrcaService(ctx, ex, peer); !ok {
|
|
r.Pass = false
|
|
r.Violations = append(r.Violations, v)
|
|
}
|
|
if v, ok := checkNoOrcaProcess(ctx, ex, peer); !ok {
|
|
r.Pass = false
|
|
r.Violations = append(r.Violations, v)
|
|
}
|
|
if v, ok := checkNoOrcaBinInEtc(ctx, ex, peer); !ok {
|
|
r.Pass = false
|
|
r.Violations = append(r.Violations, v)
|
|
}
|
|
|
|
if !r.Pass {
|
|
failedNodes = append(failedNodes, n.Name)
|
|
log.Warn("no-orca-on-server: violations",
|
|
slog.String("node", n.Name), slog.Any("violations", r.Violations))
|
|
}
|
|
results = append(results, r)
|
|
}
|
|
|
|
summary := map[string]any{
|
|
"results": results,
|
|
"failed": failedNodes,
|
|
}
|
|
|
|
if jsonOutput {
|
|
return printJSON(summary)
|
|
}
|
|
out := cmd.OutOrStdout()
|
|
for _, r := range results {
|
|
status := "PASS"
|
|
if !r.Pass {
|
|
status = "FAIL"
|
|
}
|
|
fmt.Fprintf(out, "%-20s %-5s %s\n", r.Node, status, strings.Join(r.Violations, "; "))
|
|
}
|
|
if len(failedNodes) > 0 {
|
|
fmt.Fprintf(out, "\n%d peer(s) failed R-001 enforcement\n", len(failedNodes))
|
|
return fmt.Errorf("no-orca-on-server: %d peer(s) have violations", len(failedNodes))
|
|
}
|
|
fmt.Fprintf(out, "\n✓ all peers clean (R-001 enforced)\n")
|
|
return nil
|
|
}
|
|
|
|
func checkNoOrcaBinary(ctx context.Context, ex drainExecer, peer string) (string, bool) {
|
|
out, err := ex.Exec(ctx, peer, "command -v orca 2>/dev/null || true")
|
|
if err != nil {
|
|
return "", true
|
|
}
|
|
if strings.TrimSpace(string(out)) != "" {
|
|
return fmt.Sprintf("orca binary in PATH: %s", strings.TrimSpace(string(out))), false
|
|
}
|
|
return "", true
|
|
}
|
|
|
|
func checkNoOrcaService(ctx context.Context, ex drainExecer, peer string) (string, bool) {
|
|
cmd := "systemctl list-units 'orca*' --no-legend --no-pager 2>/dev/null | grep -v 'orca-alloc-' || true"
|
|
out, err := ex.Exec(ctx, peer, cmd)
|
|
if err != nil {
|
|
return "", true
|
|
}
|
|
trimmed := strings.TrimSpace(string(out))
|
|
if trimmed != "" {
|
|
return fmt.Sprintf("orca systemd service(s) present: %s", trimmed), false
|
|
}
|
|
return "", true
|
|
}
|
|
|
|
func checkNoOrcaProcess(ctx context.Context, ex drainExecer, peer string) (string, bool) {
|
|
out, err := ex.Exec(ctx, peer, "pgrep -x orca 2>/dev/null || true")
|
|
if err != nil {
|
|
return "", true
|
|
}
|
|
if strings.TrimSpace(string(out)) != "" {
|
|
return fmt.Sprintf("orca process running: pid(s) %s", strings.TrimSpace(string(out))), false
|
|
}
|
|
return "", true
|
|
}
|
|
|
|
func checkNoOrcaBinInEtc(ctx context.Context, ex drainExecer, peer string) (string, bool) {
|
|
cmd := "find /etc/orca -type f -executable 2>/dev/null | grep -v 'scripts/' | head -5 || true"
|
|
out, err := ex.Exec(ctx, peer, cmd)
|
|
if err != nil {
|
|
return "", true
|
|
}
|
|
trimmed := strings.TrimSpace(string(out))
|
|
if trimmed != "" {
|
|
return fmt.Sprintf("executable(s) under /etc/orca: %s", trimmed), false
|
|
}
|
|
return "", true
|
|
}
|
|
|
|
var compatCheckCmd = &cobra.Command{
|
|
Use: "compat-check",
|
|
Short: "Check mixed-version tolerance across peers (REQ-065, C-13)",
|
|
Long: `Check that the cluster tolerates mixed orca versions during an
|
|
upgrade window (REQ-065). The lead and peers may run different orca
|
|
versions during a rolling upgrade; this command verifies:
|
|
|
|
- Each peer's orca version (reported)
|
|
- The txn manifest format is compatible across versions
|
|
- The render-contract JSON schema (emit.SchemaVersion) is versioned
|
|
and backward-compatible
|
|
- No new required fields that old peers don't understand
|
|
|
|
Reports: which peers are on which version, any compatibility issues.`,
|
|
Args: cobra.NoArgs,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
return runCompatCheck(cmd)
|
|
},
|
|
}
|
|
|
|
type compatPeerResult struct {
|
|
Node string `json:"node"`
|
|
Peer string `json:"peer"`
|
|
Version string `json:"version"`
|
|
LeadVersion string `json:"lead_version,omitempty"`
|
|
Compatible bool `json:"compatible"`
|
|
Issue string `json:"issue,omitempty"`
|
|
}
|
|
|
|
func runCompatCheck(cmd *cobra.Command) error {
|
|
ctx, cancel := context.WithTimeout(cmd.Context(), 5*time.Minute)
|
|
defer cancel()
|
|
|
|
reg, closer, err := nodeRegistry()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer closer()
|
|
|
|
nodes, err := reg.List(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("list nodes: %w", err)
|
|
}
|
|
|
|
ex, err := drainExecFromCtx(cmd.Context())
|
|
if err != nil {
|
|
return fmt.Errorf("ssh transport: %w", err)
|
|
}
|
|
|
|
leadVersion := version
|
|
results := make([]compatPeerResult, 0, len(nodes))
|
|
var issues []string
|
|
versionSet := map[string]int{}
|
|
|
|
for i := range nodes {
|
|
n := nodes[i]
|
|
peer := peerAddrForNode(n)
|
|
if peer == "" {
|
|
continue
|
|
}
|
|
peerVersion := detectPeerOrcaVersion(ctx, ex, peer)
|
|
versionSet[peerVersion]++
|
|
r := compatPeerResult{
|
|
Node: n.Name,
|
|
Peer: peer,
|
|
Version: peerVersion,
|
|
LeadVersion: leadVersion,
|
|
Compatible: true,
|
|
}
|
|
if peerVersion != "" && peerVersion != leadVersion {
|
|
if !versionsCompatible(leadVersion, peerVersion) {
|
|
r.Compatible = false
|
|
r.Issue = fmt.Sprintf("peer %s (%s) incompatible with lead (%s)",
|
|
n.Name, peerVersion, leadVersion)
|
|
issues = append(issues, r.Issue)
|
|
}
|
|
}
|
|
results = append(results, r)
|
|
}
|
|
|
|
schemaOK := verifyRenderContractCompat(ctx, ex, nodes)
|
|
if !schemaOK {
|
|
issues = append(issues, "render-contract schema mismatch detected across peers")
|
|
}
|
|
manifestOK := verifyTxnManifestCompat(ctx, ex, nodes)
|
|
if !manifestOK {
|
|
issues = append(issues, "txn manifest format incompatibility detected")
|
|
}
|
|
|
|
summary := map[string]any{
|
|
"lead_version": leadVersion,
|
|
"schema_version": emit.SchemaVersion,
|
|
"results": results,
|
|
"versions_seen": versionSet,
|
|
"issues": issues,
|
|
"schema_ok": schemaOK,
|
|
"manifest_ok": manifestOK,
|
|
}
|
|
|
|
if jsonOutput {
|
|
return printJSON(summary)
|
|
}
|
|
out := cmd.OutOrStdout()
|
|
fmt.Fprintf(out, "lead version: %s (schema %s)\n", leadVersion, emit.SchemaVersion)
|
|
for _, r := range results {
|
|
mark := "✓"
|
|
if !r.Compatible {
|
|
mark = "✗"
|
|
}
|
|
fmt.Fprintf(out, " %s %-20s %s\n", mark, r.Node, r.Version)
|
|
if r.Issue != "" {
|
|
fmt.Fprintf(out, " %s\n", r.Issue)
|
|
}
|
|
}
|
|
if len(issues) > 0 {
|
|
fmt.Fprintf(out, "\n%d compatibility issue(s) found\n", len(issues))
|
|
return fmt.Errorf("compat-check: %d issue(s)", len(issues))
|
|
}
|
|
fmt.Fprintf(out, "\n✓ all peers compatible\n")
|
|
return nil
|
|
}
|
|
|
|
func detectPeerOrcaVersion(ctx context.Context, ex drainExecer, peer string) string {
|
|
out, err := ex.Exec(ctx, peer, "orca version --json 2>/dev/null || true")
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
s := strings.TrimSpace(string(out))
|
|
if s == "" {
|
|
return ""
|
|
}
|
|
var parsed map[string]any
|
|
if err := json.Unmarshal([]byte(s), &parsed); err == nil {
|
|
if v, ok := parsed["version"]; ok {
|
|
if vs, ok := v.(string); ok && vs != "" {
|
|
return vs
|
|
}
|
|
}
|
|
}
|
|
for _, line := range strings.Split(s, "\n") {
|
|
line = strings.TrimSpace(line)
|
|
if strings.Contains(line, "version") {
|
|
fields := strings.Fields(line)
|
|
for i, f := range fields {
|
|
if f == "\"version\":" || f == "version:" {
|
|
if i+1 < len(fields) {
|
|
return strings.Trim(fields[i+1], "\",")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return s
|
|
}
|
|
|
|
func versionsCompatible(lead, peer string) bool {
|
|
if lead == "" || peer == "" {
|
|
return true
|
|
}
|
|
li := versionMinor(lead)
|
|
pi := versionMinor(peer)
|
|
if li == 0 || pi == 0 {
|
|
return true
|
|
}
|
|
diff := li - pi
|
|
if diff < 0 {
|
|
diff = -diff
|
|
}
|
|
return diff <= 1
|
|
}
|
|
|
|
func versionMinor(v string) int {
|
|
s := strings.TrimPrefix(v, "v")
|
|
parts := strings.Split(s, ".")
|
|
if len(parts) < 2 {
|
|
return 0
|
|
}
|
|
var n int
|
|
for _, c := range parts[1] {
|
|
if c >= '0' && c <= '9' {
|
|
n = n*10 + int(c-'0')
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
return n
|
|
}
|
|
|
|
func verifyRenderContractCompat(ctx context.Context, ex drainExecer, nodes []*model.Node) bool {
|
|
for i := range nodes {
|
|
n := nodes[i]
|
|
peer := peerAddrForNode(n)
|
|
if peer == "" {
|
|
continue
|
|
}
|
|
out, err := ex.Exec(ctx, peer, "test -f /etc/orca/cluster/render-contract.json && cat /etc/orca/cluster/render-contract.json || true")
|
|
if err != nil {
|
|
continue
|
|
}
|
|
s := strings.TrimSpace(string(out))
|
|
if s == "" {
|
|
continue
|
|
}
|
|
if !strings.Contains(s, emit.SchemaVersion) && !strings.Contains(s, "schema_version") {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func verifyTxnManifestCompat(ctx context.Context, ex drainExecer, nodes []*model.Node) bool {
|
|
for i := range nodes {
|
|
n := nodes[i]
|
|
peer := peerAddrForNode(n)
|
|
if peer == "" {
|
|
continue
|
|
}
|
|
out, err := ex.Exec(ctx, peer, "test -d /etc/orca/cluster/txns && ls /etc/orca/cluster/txns | head -1 || true")
|
|
if err != nil {
|
|
continue
|
|
}
|
|
first := strings.TrimSpace(string(out))
|
|
if first == "" {
|
|
continue
|
|
}
|
|
// F7: first is a directory name parsed from remote `ls` output
|
|
// and is therefore attacker-controlled (stored injection from a
|
|
// malicious peer). Shell-quote it before interpolation.
|
|
man, err := ex.Exec(ctx, peer, fmt.Sprintf("cat /etc/orca/cluster/txns/%s/manifest.json 2>/dev/null || true", sshQuote(first)))
|
|
if err != nil {
|
|
continue
|
|
}
|
|
s := strings.TrimSpace(string(man))
|
|
if s == "" {
|
|
continue
|
|
}
|
|
if !strings.Contains(s, "txn_id") || !strings.Contains(s, "files") {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func sshQuote(s string) string {
|
|
return "'" + strings.ReplaceAll(s, "'", "'\\''") + "'"
|
|
}
|