Files
orca/.ciagent/REQUIREMENTS.md
T
Jon Chery 3be86e6daf
Release / ci (push) Failing after 5m13s
Release / container-orca (push) Has been skipped
Release / container-traefik (push) Has been skipped
fix(P1): Gitea Actions clone auth + .coreci.yml native format rewrite (REQ-183,184)
REQ-183: Fix .gitea/workflows/release.yml — the git clone of the private
coreci repo failed because the clone command had no credentials. The
actions/checkout@v4 step only injects auth for the orca repo. Fix: pass
GITEA_TOKEN env to the Install CoreCI step and embed it in the clone
URL (https://cloudinit-bot:${GITEA_TOKEN}@git.cloudinit.dev/...).

REQ-184: Rewrite .coreci.yml from the invalid pipelines:/steps:/image:/
commands: format to CoreCI's native jobs:/plugin:/invoke:/vars: format
with a proper DAG (needs:). CoreCI's Pipeline struct only recognizes
jobs:/services:/env: top-level keys — unknown fields are silently dropped
by yaml.Unmarshal, producing an empty Jobs map → zero jobs execute.
The rewrite:
- 8 jobs: go-vet → fan-out (verify-reqs, gosec, govulncheck, gitleaks)
  → build → test → release
- plugin: docker://golang:1.25.12 + invoke: on each job (container path
  with shell-isolated fallback — Go is installed on the runner)
- GITEA_TOKEN via vars: with ${{ secrets.GITEA_TOKEN }} (resolved from
  env via CoreCI's secret resolver os.Getenv fallback)
- CI_COMMIT_BRANCH (tag name on tag push) and CI_COMMIT_SHA for version
  injection — no ${VAR} interpolation in YAML fields (shell expansion
  only works inside invoke: via sh -c)
- No apk add (runner is ubuntu, not alpine — uses curl for tool downloads)
- Release job handles duplicate release (ship workflow creates release
  first with title+body; coreci run attaches binary assets later via API
  fallback if tea releases create fails)
- Release job verifies asset count ≥ 2 (REQ-097 gate C-21) with retry

Root cause: all 87 releases in repo history had zero binary assets
because coreci run never executed any jobs (empty Jobs map from the
invalid format) and the Gitea Actions workflow failed before reaching
coreci run (private repo clone had no credentials).

---ci---
project: orca
phase: 1
milestone: v0.16
status: execute
requirements:
  covered: [183, 184]
  partial: []
---/ci---
2026-08-12 21:05:21 +00:00

482 lines
71 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Requirements: Orca
The canonical requirements table. Each row carries the REQ-ID, the
milestone it belongs to, the requirement summary, priority, the phase
that addresses it, and the current status. This single table is the
source of truth — superseded any per-milestone status tables in
earlier versions of this file.
| ID | Requirement | Priority | Phase | Status |
|----|-------------|----------|-------|--------|
| REQ-001 | Go 1.25+ toolchain support | High | v0.1 P01 | **Complete** |
| REQ-002 | CLI-first interface for all operations (single binary) | High | v0.1 P01 | **Complete** |
| REQ-003 | Offline-first operational mode (no cloud deps) | High | v0.1 | **Complete** |
| REQ-004 | Basic task deployment (single-node process execution) | Medium | v0.1 P03 | **Complete** (single-node); multi-node dispatch in v0.2 P02 |
| REQ-005 | Local state storage via modernc/sqlite (CGO-free) | Medium | v0.1 P02 | **Complete** |
| REQ-006 | Security-first audit logging via `log/slog` | High | v0.1 P04 | **Complete** |
| REQ-007 | CoreCI full release flow integration via `.coreci.yml` | High | v0.1 P06 | **Complete** (per-phase releases) |
| REQ-008 | Structured JSON logging (slog) | High | v0.1 P05 | **Complete** |
| REQ-009 | HCL/YAML job spec parsing | Medium | v0.1 P03 | **Complete** |
| REQ-010 | `--json` output flag for machine consumption | High | v0.1 P01 | **Complete** |
| REQ-011 | mTLS for inter-node communication | Medium | **v0.2 P01** | **Complete** (P01 shipped v0.2.1) |
| REQ-012 | `~/.orca/config.hcl` and `/etc/orca/orca.hcl` config locations | Low | v0.1 P01 | **Complete** (CLI uses `~/.orca/` + `ORCA_DB` env) |
| REQ-013 | Pre-push git hook triggers CoreCI on every push | High | v0.1 P01 | **Complete** |
| REQ-014 | `gosec` + `govulncheck` in CI pipeline | High | v0.2 P03 | **Complete** (P10 shipped v0.2.3) |
| REQ-015 | MIT LICENSE | Low | v0.1 P01 | **Complete** |
| REQ-016 | README.md with quickstart | Medium | v0.1 P01 | **Complete** |
| REQ-017 | `context.Context` propagation in all I/O | High | v0.1 | **Complete** |
| REQ-018 | Error wrapping with `fmt.Errorf("...: %w", err)` | High | v0.1 | **Complete** |
| REQ-019 | Cobra CLI framework | High | v0.1 P01 | **Complete** |
| REQ-020 | HCL parser integration (`hashicorp/hcl`) | Medium | v0.1 P03 | **Complete** |
| REQ-021 | `os/exec` with `WaitDelay` (Go 1.25+) | Medium | v0.1 P03 | **Complete** |
| REQ-022 | `iter.Seq` for streaming job lists (Go 1.25+) | Low | **v0.3 P01** | **Complete** (v0.3 P01 shipped v0.3.1) |
| REQ-023 | Self-signed mTLS cert generation | Medium | **v0.2 P01** | **Complete** (P01 shipped v0.2.1) |
| REQ-024 | `Makefile` with standard targets | High | v0.1 P01 | **Complete** |
| REQ-025 | Bounded cert rotation history: retain last N=3 server certs per node for rollback | Medium | **v0.2 P01** | **Complete** (P01 shipped v0.2.1) |
| REQ-026 | Trusted-CA fingerprint pinned in config; daemon refuses to start on mismatch | High | **v0.2 P01** | **Complete** (P01 shipped v0.2.1) |
| REQ-027 | `govulncheck` runs in offline mode in CI (no `vuln.go.dev` calls; pre-mirrored DB or `-format json` + `jq` gate) | High | v0.2 P03 | **Complete** (P10 shipped v0.2.3) |
| REQ-028 | HCL/YAML schema for `NodeCapacity` declaration (`orca node join` flag and/or `~/.orca/node.hcl`) | High | v0.2 P02 | **Complete** (P09 shipped v0.2.2; `orca node capacity` CLI) |
| REQ-029 | `gitleaks` baseline file committed to repo to suppress pre-existing `.env` SHA-1 leak in git history | Medium | v0.2 P03 | **Complete** (P10 shipped v0.2.3) |
| REQ-030 | `--watch` output format mode: table (default) vs streaming one-line JSON per event | Low | **v0.3 P01** | **Complete** (v0.3 P01 shipped v0.3.1) |
| REQ-031 | `go test -race` enabled in CI for all v0.2 packages | High | v0.2 P01P04 | **Complete** (P10; `.coreci.yml` test pipeline runs `-race`) |
| REQ-032 | `orca doctor` subcommand for diagnostics (CA/cert health, db integrity, peer reachability) | Medium | **v0.2 P01 / v0.3 P02** | **Complete** (cert checks P01 v0.2.1; network + db P02 v0.3.2) |
| REQ-033 | Cert file mode enforcement: 0600 for keys, 0644 for certs (refuses to start on violation) | High | **v0.2 P01** | **Complete** (P01 shipped v0.2.1) |
| REQ-034 | Cert proactive rotation alarm: structured slog WARN 30 days before `not_after` | Medium | **v0.2 P01** | **Complete** (P01 shipped v0.2.1) |
| REQ-035 | `orca cert show` redacts private key material from default and `--json` output | High | **v0.2 P01** | **Complete** (P01 shipped v0.2.1) |
| REQ-036 | Server cert SAN validation: SAN entries (DNS + IP) populated at sign-time; refuses to sign a CSR without them | High | **v0.2 P01** | **Complete** (P01 shipped v0.2.1) |
| REQ-037 | `X-Orca-Idempotency-Key` header on cross-node POST; dispatcher retries only when header is present | Medium | v0.2 P02 | **Complete** (P09 shipped v0.2.2; `internal/transport/idempotency.go`) |
| REQ-038 | Structured slog fields for mTLS failures: `event=mtls.handshake`, `peer`, `cert_fp`, `err` | Medium | **v0.2 P01** | **Complete** (P01 shipped v0.2.1) |
| REQ-039 | `.gitleaks.toml` extended with stopwords for test data paths and CA cert PEM blocks | Medium | v0.2 P03 | **Complete** (P10 shipped v0.2.3) |
| REQ-040 | `.golangci.yml` unified lint config superseding per-tool invocations | Low | v0.2 P03 | **Complete** (P10 shipped v0.2.3) |
| REQ-041 | Unified namespace root via `ORCA_HOME` for all components (db, certs, init, daemon) | High | **v0.5 P1** | **Complete** (P1 shipped v0.4.2) |
| REQ-042 | `--system` flag selects system-level namespace root `/root/.orca` | High | **v0.5 P1** | **Complete** (P1 shipped v0.4.2) |
| REQ-043 | `install.sh` 1-liner pulling release binary from public Gitea URL; user-level default, `--system` for system-level | High | **v0.5 P2** | **Complete** (P2 shipped v0.4.3) |
| REQ-044 | `install.sh` in-place update preserves config/state; idempotent re-run | High | **v0.5 P2** | **Complete** (P2 shipped v0.4.3) |
| REQ-045 | Gitea repo + releases publicly accessible (unauthenticated download) | High | **v0.5 P0** | **Complete** (P0 ship: repo + org visibility public) |
| REQ-046 | Docker image published to Gitea container registry per release | Medium | **v0.5 P3** | **Complete** (P3 shipped v0.4.4) |
## v0.1 Milestone Summary
**Status: Complete** — all 6 phases shipped (P00P06) plus P07 backfill,
4-layer verification passed at every phase, tagged `v0.2.0` per
`run.md` versioning logic (next-minor after all feature-patches
v0.1.1..v0.1.7 ship).
**Coverage**: 21/24 v0.1-declared requirements complete by v0.1 ship;
the 3 deferred (REQ-011, REQ-014, REQ-022, REQ-023) all moved to v0.2.
Plus REQ-025..REQ-040 (16 net-new) added by v0.2 IDEATE stage.
## v0.2 Milestone Summary
**Status: Functionally Complete (pending merge to main)** — P08 (mTLS),
P09 (scheduling), P10 (security scan) all shipped to the
`milestone/v0.2-networking-observability-security` branch as v0.2.1,
v0.2.2, v0.2.3. The milestone branch has NOT been merged to main yet.
REQ-022/030 (iter.Seq streaming) and REQ-032 (doctor network/db) were
deferred to v0.3.
## v0.3 Milestone Summary
**Status: Complete** — P01 (iter.Seq streaming, v0.3.1) and P02 (doctor
network+db, v0.3.2) both shipped. REQ-022, REQ-030, REQ-032 all complete.
Re-init SPECIFY audit confirmed all other v0.2-deferred REQs (014, 027,
028, 029, 031, 037, 039, 040) already shipped in P08-P10.
## Deferred to v0.4
- pprof endpoint on `orca daemon` (idea I-308, 0.70 confidence): deferred
to keep v0.2 lean; revisit in v0.3 once P02's dispatcher is stable.
## v0.5 Milestone Summary
**Status: Complete** — all 3 execution phases + final review shipped.
P0 (v0.4.1), P1 (v0.4.2), P2 (v0.4.3), P3 (v0.4.4), P4 final (v0.4.5).
REQ-041..046 all complete. Repo + releases publicly accessible (REQ-045).
Docker image published to Gitea container registry (REQ-046).
- **P0** (v0.4.1): pre-execution + repo visibility flipped to public (REQ-045).
- **P1** (v0.4.2): namespace unification — `ORCA_HOME` + `--system` (REQ-041/042).
- **P2** (v0.4.3): `install.sh` 1-liner + in-place update (REQ-043/044) + README quickstart (REQ-016).
- **P3** (v0.4.4): Docker release — distroless image + Gitea container registry (REQ-046).
- **P4** (v0.4.5): final review + audit + milestone release.
## v0.6 Requirements — Node Bootstrap & Proxmox
| ID | Requirement | Priority | Phase | Status |
|----|-------------|----------|-------|--------|
| REQ-047 | `orca init` auto-provisions CA + server cert + DB migrations + localhost node (idempotent; safe re-run) | High | **v0.6 P1** | **Complete** (P1 shipped v0.5.1) |
| REQ-048 | `orca init` registers a default `localhost` node with auto-detected OS via `/etc/os-release ID` | High | **v0.6 P1** | **Complete** (P1 shipped v0.5.1) |
| REQ-049 | Node schema extension: `nodes.kind` (localhost\|linux\|proxmox) + `nodes.os` columns (migration 0006, backward-compatible) | High | **v0.6 P1** | **Complete** (P1 shipped v0.5.1) |
| REQ-050 | `orca node join --type proxmox` SSH bootstrap via `golang.org/x/crypto/ssh` (new direct dep); password auth, deploy orca pubkey, create `orca` user (config-overridable), assign PVE role, drop sudoers allowlist; idempotent | High | **v0.6 P2** | **Complete** (P2 shipped v0.5.2) |
| REQ-051 | Proxmox least-privilege `OrcaOperator` PVE role (VM.Audit, Datastore.AllocateSpace, SDN.Use) + `orca` user + `/etc/sudoers.d/orca` allowlist (pct, qm, pvesh, apt-get, dpkg); config-overridable user/role names | High | **v0.6 P2** | **Complete** (P2 shipped v0.5.2; refined: pvesh excluded per AD-020, orca@pam per AD-019) |
| REQ-052 | `orca doctor` extensions: `doctor os` (verify localhost OS detection matches stored node row) + `doctor proxmox` (SSH-probe each `kind=proxmox` node with `pveversion`/`pvecmd status`, 3s timeout, PASS/WARN/FAIL); audit log all bootstrap + join actions | Medium | **v0.6 P3** | **Complete** (P3 shipped v0.5.3) |
## v0.6 Milestone Summary
**Status: Complete** — all 3 execution phases + final review shipped.
P0 (v0.5.0), P1 (v0.5.1), P2 (v0.5.2), P3 (v0.5.3), P4 final (v0.5.4).
REQ-047..052 all complete.
- **P0** (v0.5.0): pre-execution (specify → clarify → research → plan). 8 decisions (D-030..D-037).
- **P1** (v0.5.1): `orca init` full bootstrap + schema 0006 (REQ-047/048/049).
- **P2** (v0.5.2): Proxmox SSH join + OrcaOperator role + sudoers (REQ-050/051).
- **P3** (v0.5.3): `doctor os` + `doctor proxmox` + audit logging (REQ-052).
- **P4** (v0.5.4): final review + audit + milestone release.
## v0.7 Requirements — Hardening & Completion
| ID | Requirement | Priority | Phase | Status |
|----|-------------|----------|-------|--------|
| REQ-053 | `orca cert` command tree registered on root command (`cert ca-init`, `cert gen`, `cert show`, `cert renew`, `cert fingerprint`) — code exists in `internal/cli/cert.go` but is never AddCommand'd; unreachable today | High | **v0.7 P1** | **Complete** (P1 shipped v0.6.1) |
| REQ-054 | HCL config file parsing: `internal/config` package loads `~/.orca/config.hcl` / `/etc/orca/orca.hcl` (keys: db_path, listen_addr, ca_path, server_cert_path, server_key_path, node_capacity); merge precedence flag > env > file > default; `--config` flag on root command | High | **v0.7 P2** | **Complete** (P2 shipped v0.6.2) |
| REQ-055 | Test coverage uplift: every package ≥ 50% — adds tests for `internal/engine` (executor, dispatcher, peer), `internal/transport` (mtls, dispatch, handshake_log), `internal/proxmox` (bootstrap SSH path), `internal/audit` | Medium | **v0.7 P3** | **Complete** (P3 shipped v0.6.3) |
| REQ-056 | `--pprof <addr>` opt-in flag on `orca daemon` (default disabled); `net/http/pprof` mounted on a separate mux, never on the mTLS daemon listener | Low | **v0.7 P4** | **Complete** (P4 shipped v0.6.4) |
## v0.8 Requirements — Coverage & Trust Hardening
| ID | Requirement | Priority | Phase | Status |
|----|-------------|----------|-------|--------|
| REQ-057 | Test coverage uplift round 2: raise `internal/engine` (8.3%), `internal/proxmox` (5.1%), `internal/cli` (27.6%), `internal/transport` (26.3%), `internal/store` (46.7%), `internal/jobspec` (47.6%) to ≥ 70%; add first tests for `internal/audit`, `internal/certpaths`, `cmd/orca` (currently 0%) to ≥ 50% (D-047 tiered floor) | High | **v0.8 P1** | **Complete** (P1 shipped v0.7.1; all 9 packages exceeded floor) |
| REQ-058 | `--host-key-fingerprint <SHA256:base64>` pre-pin flag on `orca node join` (validated when `--type proxmox`): when supplied, join fails fast if the SSH host key's OpenSSH SHA-256 fingerprint does not match; supersedes TOFU (D-035) for pre-pinned deployments (D-044, D-045) | Medium | **v0.8 P2** | **Complete** (P2 shipped v0.7.2) |
| REQ-059 | `orca node key-reset <node>` command: clears the persisted SSH host key entry for the node from `~/.orca/known_hosts` only (local, not remote authorized_keys — D-046); audit-logs `event=node.key_reset`; next `doctor proxmox`/dispatch re-pins via TOFU or `--host-key-fingerprint` | Low | **v0.8 P2** | **Complete** (P2 shipped v0.7.2) |
| REQ-060 | Requirement-status hygiene sweep: REQUIREMENTS.md v0.7 rows were stale ("Pending" after ship); add a verify-stage assertion that every REQ listed as `Complete` in ROADMAP.md has a matching `Complete` row in REQUIREMENTS.md, enforced by `make verify-reqs` | Medium | **v0.8 P3** | **Complete** (P3 shipped v0.7.3) |
## v0.9/v0.10 Requirements — Re-architecture Foundation & Production Hardening
The v0.9/v0.10 milestones supersede the shipped v0.1v0.8 architecture per the
adopted PRD (`.ciagent/PRD_v0.9.md`). The re-architecture is justified on six
grounds recorded in the PROJECT.md Supersession Table. 30 net-new requirements
(REQ-061..REQ-090) derive from the v0.9 IDEATION; their phase placement and
binding grill conditions (C-01..C-19) are documented in `IDEATION_v0.9.md`
and `GRILL_v0.9.md`.
| ID | Requirement | Priority | Phase | Status |
|----|-------------|----------|-------|--------|
| REQ-061 | `orca daemon` deprecation command and build-tag removal path: v0.9 emits deprecation warning + still runs (dual-write window); v1.0 repurposes to `orca daemon drain-and-stop` (stops v0.8 daemons on peers via SSH, confirms workloads survive via systemd); post-v1.0 the command and `internal/daemon/` are deleted. `// Deprecated` Go doc comments + `slog.Warn` on every run (I-M-001) | High | **v0.11 P14b** (drain-and-stop + rotate-lead) | **Complete** |
| REQ-062 | Coverage follow-ups: 3 zero-test packages (`internal/audit`, `internal/certpaths`, `cmd/orca`) + `internal/cli` to 70% floor; once `daemon.go` is deprecated/removed the exclusion reason disappears and the floor applies to the whole package; all net-new subsystems carry a 70% floor from their first phase (I-M-002) | Medium | **v0.9 P0X** + each new pkg | Complete |
| REQ-063 | `known_hosts` flock concurrency gap (deferred P1 from REVIEW_v0.8 A2): add `flock`-style advisory lock (stdlib `syscall.Flock` wrapper) around the read-modify-write in `TOFUHostKeyCallback` capture path (`bootstrap.go:290-302`) and `ResetHostKey` (`bootstrap.go:479-523`); lock file at `cluster/known_hosts.lock` (R-002) (I-M-003) | Medium | **v0.9 P0a1** | Complete |
| REQ-064 | HCL→Markdown jobspec adapter/bridge layer: keep `internal/jobspec/spec.go` as legacy HCL path behind `// Deprecated`; add `internal/jobspec/markdown.go` (canonical) + `internal/jobspec/dispatch.go` (extension-based dispatcher: `.md`→Markdown, `.hcl`→legacy, `.yaml`→Markdown-with-empty-body); unified `*WorkloadSpec` populated via adapter; preserves `orca job run old-spec.hcl` during migration window (I-M-004) | High | **v0.9 P0b** | Complete |
| REQ-065 | `orca doctor --legacy-paths` detection: detects v0.8 residue (orca.db at ORCA_HOME root, ca.crt/ca.key, config.hcl, flat server.crt, namespace column in any *.db); outputs list of legacy artifacts with migration recommendations; the detection half of v0.10-P14 (I-M-005) | Medium | **v0.11 P14c** | **Complete** |
| REQ-066 | Legacy CA state migration to step-ca: `orca upgrade --to-v1.0 --import-ca` reads `~/.orca/ca.key`, initializes step-ca with it, re-issues workload SVIDs; preserves audit history even if live trust root changes (I-M-006). **Gated by C-07** | High | **v0.11 P14a** | **Complete** |
| REQ-067 | Fuzz test harness for Markdown frontmatter parser: `testing.F` fuzz target in `internal/jobspec/markdown_test.go` round-trips random frontmatter+body through `ParseMarkdown` asserting byte-exact body preservation; corpus of adversarial fixtures (CRLF, BOM, no-frontmatter, empty-frontmatter, frontmatter-with-only-separator) (I-M-007) | Medium | **v0.9 P0b** | Complete |
| REQ-068 | Deprecation warnings on removed/repurposed CLI subcommands: each removed/changed command (`orca cert`, `orca node join` mTLS semantics, `orca job run <spec.hcl>`) emits `slog.Warn` deprecation banner with v1.0 replacement except under `orca upgrade`; `--no-deprecation-warnings` global flag via `root.go` `PersistentPreRunE` (I-M-008) | Low | **v0.9 P0X** + v0.10 P13 | Complete |
| REQ-069 | `internal/config/config.go` HCL config demotion via adapter: keep `internal/config/` as `legacy_config.go` with `// Deprecated`; add `internal/config/markdown.go` for new Markdown-frontmatter loader (R-014); `root.go` dispatches on file extension (`.hcl`→legacy, `.md`→new); `--config` semantics: `.hcl` read-only legacy, `.md` canonical (I-M-009) | High | **v0.9 P0a1** | Complete |
| REQ-070 | `internal/certpaths/` replacement with multi-namespace path resolver: new `internal/paths` package with `paths.NamespaceDir(ns)`, `paths.ClusterDir()`, `paths.CacheDB()`, `paths.MasterKey()`, `paths.NSDb(ns)`, `paths.NSEnv(ns)`, `paths.NSSecrets(ns)`; keep `certpaths` as thin shim for v0.8 compat then remove post-v1.0 (R-002) (I-M-010) — highest blast radius | High | **v0.9 P0a1** | Complete |
| REQ-071 | `internal/store/` schema: per-namespace DBs, drop namespace column: `store.Open` gains namespace parameter (or caller passes `paths.NSDb(ns)`); `migrate.go` runs migrations per namespace DB; `cert_repo` (0004) removed (step-ca handles certs); audit_log moves to CLI-side cache DB (R-008) (I-M-011) | High | **v0.9 P0a1** + v0.10 P06 | Complete |
| REQ-072 | `internal/transport/` deletion + SSH-push package: delete `mtls.go`, `dispatch.go`, `handshake_log.go`; extract retry/idempotency patterns into `internal/sshpush/`; existing `transport.IdempotencyStore` directly reusable (I-M-012). Deletion deferred to v0.10-P14 to keep dual-write window open | High | **v0.9 P00** (delete v0.10 P14) | Complete |
| REQ-073 | SSH-push transport layer design: connection pooling (reuse `*ssh.Client` per peer), idempotency (content-addressed filenames), retry (exponential backoff 100ms×2 cap 5s max 5), timeout (30s SCP, 10s exec), fan-out (errgroup bounded concurrency default 8), known_hosts reuse `proxmox.TOFUHostKeyCallback` (I-B-001) | High | **v0.9 P01** (design P0a1) | Complete |
| REQ-074 | Emitter template system (Layer 4): `internal/emitter/` package with `Emitter` interface `Render(spec *WorkloadSpec, node *Node) ([]File, error)`; implementations systemdEmitter/traefikEmitter/syncthingEmitter/socketEmitter; SSH-push SCPs `[]File` atomically (write-to-tmp + rename); emitters registered per kind + runtime (I-B-002) | High | **v0.9 P0c** | Complete |
| REQ-075 | Lead applier execution model: CLI renders transaction bundle (tarball + apply.sh + verify.sh) on operator host, SCPs to lead's `/run/orca/txns/<txn-id>/`, lead's systemd timer runs `apply.sh` idempotently, CLI polls txn status via SSH; bash scripts generated by emitter not hand-written (I-B-003). **Gated by C-09** | High | **v0.11 P10a** | **Complete** |
| REQ-076 | step-ca integration: `orca init` runs `step ca init` on lead; CLI SSHs to lead, installs step-ca via apt, stores step-ca.json; workload SVIDs via `step ca token` (JWE minted by CLI) → `step ca certificate`; SPIFFE ID as SAN; new `internal/stepca/` package wraps `step` CLI via SSH (I-B-004). Reverses AD-010 per override justification ground 2 | High | **v0.9 P07** + v0.10 P02 | Complete |
| REQ-077 | Traefik dynamic config generation + atomic reload: Traefik emitter renders `/etc/traefik/dynamic/orca-<ns>-<svc>.yaml` with backends (socket paths R-007), health checks, mTLS config pointing at step-ca root; atomic reload via tmpfile+fsync+rename triggering fsnotify; drain writes `weight=0` or removes backend (I-B-005). **Gated by C-10** | High | **v0.9 P02** | Complete |
| REQ-078 | Runtime abstraction interface (5 backends): `Runtime` interface in `internal/runtime/` with Prepare/Start/Stop/Status; processRuntime (wraps existing executor.go), wasmRuntime (wasmtime via SSH), podmanRuntime, pveVMRuntime (qm via proxmox SSH), pveCTRuntime (pct); runtimeRegistry keyed by `runtime:` frontmatter value; Alloc carries runtime field changeable on migration (I-B-006). Split P07a/b/c per PC-10. **P07b gated by C-01** | High | **v0.9 P07a/b/c** | Complete |
| REQ-079 | Transaction bundle format + N-peer atomicity: bundle = tarball with desired-state.json + apply.sh + verify.sh + rollback.sh + manifest.sig (signed with master.key); content-addressed `<txn-id>=sha256(desired-state.json)` stored in `cluster/txns/<txn-id>/`; lead applies to self first then fans out; failure on any peer runs rollback.sh on applied peers (I-B-007). **Gated by C-09** | High | **v0.11 P10a** | **Complete** |
| REQ-080 | Master key management + HKDF-SHA256 per-line .env.secrets encryption: `cluster/master.key` 32-byte random (generated at `orca init` using WriteAtomic pattern); each line `base64(nonce||ciphertext||tag)`, nonce=random(12 bytes), AES-256-GCM with AAD=line-number (prevents line-swap); HKDF-SHA256 derives per-namespace sub-keys; `orca secrets set/get`; v0.8 `internal/security/redact.go` reusable (I-B-008). **Gated by C-19** | High | **v0.11 P03** | **Complete** |
| REQ-081 | Syncthing config rendering + folder-ID content-addressing: per-namespace Syncthing folder `orca-<ns>` with content-addressed folder ID `sha256(ns + master-key-fingerprint)`; CLI renders config.xml per peer; Syncthing runs as systemd unit (emitted by systemd emitter); CLI discovers peers via `cluster/peers/`; migration works because new node joins folder and syncs before workload starts (I-B-009). **Gated by C-02 + C-14** | Medium | **v0.9 P09** (spike v0.9 P00) | Complete |
| REQ-082 | Namespace inheritance resolver algorithm: DFS parent walker with visited set for cycle detection; `_defaults/` implicit root (always exists, no parent); merge semantics: child overrides parent for scalars, arrays unioned (child adds to parent); pure function (no I/O) taking `map[nsName→*NSConfig]` returning `map[nsName→*ResolvedNS]` (I-B-010) | High | **v0.9 P0a2** | Complete |
| REQ-083 | CLI-side scheduler redesign: `Score(node, workload) (score int, fits bool)` where `fits` checks runtime compatibility + constraints, `score` is bin-packing (most free capacity = highest); Services pick `count` distinct nodes (anti-affinity default); DaemonSets pick all matching nodes; Job = one-shot; CLI-side not daemon-side (R-001) (I-B-011) | High | **v0.9 P05** (skeleton P0c) | Complete |
| REQ-084 | `orca job lint` category-driven lint engine: `Linter` runs `Rule` checks returning `Finding{Category, Severity, Message, Explanation}`; categories schema/runtime/security/migration/best-practice; `--explain` prints rationale; pure (no I/O) checks against static rules (I-B-012) | Medium | **v0.11 P11** | **Complete** |
| REQ-085 | v0.8→v1.0 migration ordering: v0.9 ships new parser + kinds + runtime + SSH-push alongside old daemon (dual-write window); `orca job run` dispatches on extension (`.md`→SSH-push, `.hcl`→old daemon); v0.10-P05 drains old daemons; v0.10-P14 converts remaining `.hcl` specs and removes daemon (I-C-001). **Most important cross-cutting idea** | High | **v0.9 P00** → v0.10 P14 | Complete |
| REQ-086 | "No orca on server" enforcement: `orca doctor no-orca-on-server` SSHs to each peer verifying no `orca` binary in PATH, no `orca` systemd service, no `orca` process, no `/etc/orca/` directory; runs after v0.10-P05 before v0.10-P16; reuses v0.8 `proxmox` SSH session infrastructure (I-C-002). Implements grill C-13 | High | **v0.11 P14c** | **Complete** |
| REQ-087 | Test infrastructure: hermetic 3-linux + 1-proxmox cluster pipeline: `test/integration/` with docker-compose/vagrant creating 4 containers/VMs; Go test harness SSHes to each, runs CLI, asserts end-to-end workflows (ns create → workload submit → migrate → drain); proxmox simulated via mock pct/qm; v0.8 e2e tests (bootstrapE2ESetup) are foundation (I-C-003) | Medium | **v0.11 P08** | **Complete** |
| REQ-088 | Security-engineer + network-engineer persona reactivation: reactivate security-engineer (step-ca provisioner model, SSH-push blast radius, Traefik edge, .env.secrets crypto) and network-engineer (socket exposure R-007, Syncthing P2P ports, Traefik routing); cross-cutting review not single phase (I-C-004). Implements grill C-05 | High | **v0.9 P00** → v0.10 P16 | Complete |
| REQ-089 | Documentation rewrite: ARCHITECTURE.md/PROJECT.md/README + AD-010 supersession: v0.9-P00 adds "v0.9 Architecture (Supersedes v0.8)" section + banners + Superseded Decisions table; v0.10-P15 rewrites README quickstart for new curl|sh + orca init + orca ns create flow (I-C-005) | Medium | **v0.9 P00** + v0.10 P15/P16 | Complete |
| REQ-090 | Dual-write window: v0.9 `orca job run` dispatches on extension (`.md`→SSH-push new path, `.hcl`→old daemon path) via parser dispatcher (REQ-064); daemon not removed until v0.10-P05; SSH-push path writes to separate systemd unit namespace (`orca-v1-<alloc>.service`) while daemon uses `orca-<job>.service` — no unit name overlap = no conflict (I-C-006) | High | **v0.9 P00** | Complete |
## v0.10 Docs & Install Milestone Requirements
The following requirements are scoped to the v0.10 docs/cli-examples
milestone. They cover the CLI reference documentation, jobspec
reference, ingress guide, full-stack example jobspecs, README refresh,
namespace.md v0.9 layout update, and the release/install pipeline fix
that guarantees every Gitea release carries a Linux binary asset.
| ID | Requirement | Priority | Phase | Status |
|----|-------------|----------|-------|--------|
| REQ-091 | `docs/cli.md` comprehensive CLI reference: every command/subcommand with synopsis, flags (name/type/default/description), and one-line example; global flags (`--json`, `--system`, `--config`, `--no-deprecation-warnings`); output modes (text vs `--json`, `--watch` table vs NDJSON); exit codes; deprecated surface (`orca daemon`, `orca cert`, `orca node join` mTLS path, legacy `.hcl` jobspec) flagged with callout boxes pointing to v0.10 removal | High | **v0.10 P2** | **Complete** |
| REQ-092 | `docs/jobspec.md` markdown frontmatter schema reference: all top-level keys, block reference (runtime, ports, env/secrets, volumes, restart, update, service, health, lifecycle, constraints, affinity, tasks), kinds matrix (Job/Service/DaemonSet required vs allowed), CEL subset grammar, body byte-exact preservation (R-015), deprecated HCL form callout | High | **v0.10 P2** | **Complete** |
| REQ-093 | `docs/ingress.md` Traefik ingress reference: `kind: Service` implies Traefik route (D-175), R-007 socket-vs-TCP-bind semantics, generated Traefik YAML shape (routers/services/healthCheck), atomic reload (C-10), drain (`weight: 0`), TLS (certResolver, trust domain, step-ca), worked-example pointer to `examples/full-stack/`, v0.10 forward limitations (socket activation, transactional update) | High | **v0.10 P2** | **Complete** |
| REQ-094 | `examples/full-stack/` directory with 5 valid jobspecs (`web-app.md`, `api.md`, `worker.md`, `log-shipper.md`, `postgres.md`) exercising ports/service/health/restart/update/constraints/affinity/lifecycle/task-groups/volumes/replication/DaemonSet; `rendered/` subdir showing the Traefik dynamic YAML + systemd units orca generates; `README.md` walkthrough (init → node join → capacity set → ns create → job run → list --watch → inspect rendered) | High | **v0.10 P3** | **Complete** |
| REQ-095 | README.md refresh: status line (v0.9 complete, v0.10 in progress), install `--version` example updated to current tag, subcommand table expanded to all commands with deprecation markers, update-in-place example updated, development targets complete (`verify-reqs`, `security-scan`, `test-race`, `changelog`), new Documentation + Examples sections linking all `docs/*.md` and `examples/` | High | **v0.10 P4** | **Complete** |
| REQ-096 | `docs/namespace.md` v0.9 multi-namespace layout update: replace v0.8 flat path table with v0.9 layout (`cluster/`, `_defaults/`, per-ns `db/jobs/alloc/ns.md`), `ORCA_HOME`/`--system` resolution, `orca ns` subcommand cross-link, v0.8 flat layout flagged deprecated | Medium | **v0.10 P4** | **Complete** |
| REQ-097 | `scripts/release.sh` release pipeline fix: cross-build `linux-amd64` tarball regardless of host arch (`GOOS=linux GOARCH=amd64 go build`); post-create asset verification (query `/releases/tags/$VERSION`, assert the tarball in attachments, retry/fail loudly if missing). Guarantees every Gitea release carries the Linux binary asset (root cause of v0.4.5 install) | High | **v0.10 P1** | **Complete** |
| REQ-098 | `scripts/install.sh` asset fallback walk: if the latest/pinned release lacks the matching `orca-<ver>-<os>-<arch>.tar.gz`, walk backward through `/releases?limit=20` to the most recent release that has it, with a clear warning. Keeps pulling from releases (not main). Optional `--check` dry-run mode | High | **v0.10 P1** | **Complete** |
## v0.11 Production Hardening Milestone Requirements
The following requirements (REQ-099…REQ-NN) are scoped to the v0.11
production-hardening milestone. They cover the ingress hybrid default
(R-017), drift detection (R-018/R-019/R-020), the systemd Path unit
implementation (D-227…D-237), and five net-new CLI commands added per
operator decision Q2=C.
### Ingress hybrid (R-017, D-215…D-226)
| ID | Requirement | Priority | Phase | Status |
|----|-------------|----------|-------|--------|
| REQ-099 | `internal/emitter/nft.go`: nftables emitter renders `/etc/nftables.d/orca.nft` with DNAT (`:443``127.0.0.1:8443`, `:80``127.0.0.1:8080`), SYN-flood `tcp-flags` filter, `ora_rl` rate-limit meter (default 100/s burst 200), `orca_trusted_probes` set; idempotent `nft -f` apply; atomic rule-set swap (R-017, D-217, D-218, D-222) | High | **v0.11 P15.5** | **Complete** |
| REQ-100 | Traefik static config emitter update: `entryPoints.websecure.address` changes from `:443` to `127.0.0.1:8443` (default); `entryPoints.web.address` changes to `127.0.0.1:8080`; `--public-binding=traefik-on-public-ip` opt-out emits `:443`/`:80` instead; certs/mTLS/dynamic config unchanged (R-017, D-220, D-216) | High | **v0.11 P15.5** | **Complete** |
| REQ-101 | `orca doctor nft`: checks `table inet orca-ingress` exists, expected DNAT rules present, rate-limit meter present, `/etc/nftables.d/orca.nft` parses cleanly (`nft -c -f`), file hash matches latest applied txn; drift detection via hash comparison (R-018 critical_paths, D-221, D-226) | High | **v0.11 P15.5** | **Complete** |
| REQ-102 | `orca nft` CLI: `show [--peer]`, `diff --against <txn-id>`, `doctor` (alias for `orca doctor nft`), `country block add <cc-list>` (opt-in GeoIP), `rate limit set --rate N/s`; all Layer-5 orchestrators that SSH into peers and parse `nft` output (D-223, D-222) | Medium | **v0.11 P15.5** | **Complete** |
### Drift detection (R-018/R-019/R-020, D-227…D-237)
| ID | Requirement | Priority | Phase | Status |
|----|-------------|----------|-------|--------|
| REQ-103 | `internal/drift` package: `Detector` interface (`Watch`, `Aggregate`, `Remediate`, `Acknowledge`), `Event`, `Config`, `PathSpec`, `RemediationPolicy` types; `iter.Seq2[Event, error]` per D-017; `signal.NotifyContext` per D-023 (R-018, D-236) | High | **v0.11 P10** | **Complete** |
| REQ-104 | `orca drift` CLI tree: `watch [--interval=2s] [--paths=...] [--json]`, `show [--peer]`, `acknowledge <peer> <path>`, `remediate <peer> <path> [--force]`, `config show`, `config validate`; uses `iter.Seq2` + `signal.NotifyContext` (D-236) | High | **v0.11 P10** | **Complete** |
| REQ-105 | systemd Path unit emitter: for each critical path, emit `orca-drift-<name>.path` (`PathChanged=`, `RateLimitIntervalSec=1s`, `RateLimitBurst=5`) + `orca-drift-<name>.service` (`Type=oneshot`, `ExecStart=/usr/local/bin/orca-drift-notify.sh %f`, `User=orca`, security hardening: `NoNewPrivileges`, `ProtectSystem=strict`); R-001-clean (R-018, D-227, D-228) | High | **v0.11 P10** | **Complete** |
| REQ-106 | `scripts/orca-drift-notify.sh`: receives changed path as `$1`, computes sha256 (or "DELETED"), writes event JSON to `/etc/orca/state/drift-events/<event-id>.json` (event_id, ts, host, path, status, new_sha256, latest_txn, triggered_by); stateless, idempotent; `flock` for serialization (D-228) | High | **v0.11 P10** | **Complete** |
| REQ-107 | `scripts/orca-aggregate.sh` extension: existing 10s aggregator cadence (C-11) now also rsyncs each peer's `/etc/orca/state/drift-events/`, validates event hashes against `/etc/orca/state/applied/<txn>/manifest.json`, triggers `orca-remediate.sh` for auto-remediable paths, consumes (deletes) event files on peers (D-229, D-237) | High | **v0.11 P09** | **Complete** |
| REQ-108 | `scripts/orca-remediate.sh`: re-pushes latest applied txn's per-peer render tree via rsync, runs peer-side applier; 5-min cooldown per path applies ONLY on successful remediation (transient failures retry next tick); cooldown state at `/etc/orca/state/remediation-cooldown/` (D-231, D-232 refined per CLARIFY C4) | High | **v0.11 P10** | **Complete** |
| REQ-109 | Drift cadence config in `config.md` (`kind: ClusterConfig`): `drift.polling.{enabled,default_interval,max_concurrent_peers}`, `drift.paths.{critical,standard,excluded}` (each with `systemd_path_unit`, `interval`, `paths` list), `drift.remediate.{auto,auto_paths,require_approval_paths,notify_on_remediation}`; critical defaults: Traefik dynamic, nftables, sudoers, orca-alloc services; secrets + `/run/orca/*` + drift-events dir excluded (R-018, D-231, D-234) | High | **v0.11 P10** | **Complete** |
| REQ-110 | Pre-flight consistency gate in applier: `orca-pull.sh` (C-09) refuses new txns if drift detected on the target peer/namespace; `--force` flag overrides; per-namespace scoping means a drifted peer in ns-A does not block ns-B (R-020, Q4=A) | High | **v0.11 P10** | **Complete** |
| REQ-111 | `orca` system user on peers: peer-setup emits `useradd -r orca` (system account, no login shell); `orca-drift-*.service` runs as `User=orca Group=orca`; SSH key access to lead for aggregator; idempotent at peer setup (net-new operational requirement from doc 5) | High | **v0.11 P10** | **Complete** |
| REQ-112 | NFS detection at peer setup: `orca node join` / peer-setup detects NFS mounts on orca state dirs; if `/etc/orca` is on NFS, systemd Path units are disabled for those paths and polling is the only detection; logs a warning (D-233) | Medium | **v0.11 P10** | **Complete** |
| REQ-113 | `orca job restart <name>`: restarts an allocation to pick up EnvironmentFile drift; goes through normal allocation lifecycle (not file-level remediation); triggers on drift of `/etc/orca/allocs/<id>/env` (D-235) | Medium | **v0.11 P10** | **Complete** |
### Net-new CLI surface (Q2=C — all five commands added to v0.11)
| ID | Requirement | Priority | Phase | Status |
|----|-------------|----------|-------|--------|
| REQ-114 | `orca cluster rotate-lead`: moves cluster CA + lead state to a new bare-Linux peer (R-003 enforces bare-Linux-only lead); workloads keep running (certs already distributed); SSH key rotation; idempotent (Q2=C, folds into P14b daemon cutover) | High | **v0.11 P14b** | **Complete** |
| REQ-115 | `orca upgrade --to-vX`: thin wrapper around `install.sh` + `orca restore` (binary upgrade only, not full cluster rolling upgrade); handles Traefik binding cutover from `:443` to `127.0.0.1:8443` for existing v0.9/v0.10 clusters (R-017 migration path, CLARIFY C1, C2=a thin wrapper); full cluster-rolling-upgrade defers to v1.x (Q2=C) | High | **v0.11 P14a** | **Complete** |
| REQ-116 | `orca job migrate <name> --to <node>`: drain+reschedule composite (uses P05 drain + P06 alloc history); live-migrate with storage replication defers to v1.x (CLARIFY C3=a); idempotent (Q2=C) | Medium | **v0.11 P05** | **Complete** |
| REQ-117 | `orca logs --all-nodes --since 5m`: aggregates journald logs across peers via SSH; uses P06 alloc-history cache DB; `iter.Seq` streaming per D-017; `--since` duration flag; `--all-nodes` fans out (Q2=C, folds into P06) | Medium | **v0.11 P06** | **Complete** |
| REQ-118 | `orca doctor mTLS`: verifies trust chain (CA → server cert → workload SVIDs exist + not expired) AND live mTLS handshake probe to each peer (reuses P01 metrics endpoint + P01.5 SPIFFE spike infra); both chain verification + live probe (CLARIFY C5, Q2=C, folds into P15.5) | High | **v0.11 P15.5** | **Complete** |
### Scope notes
- REQ-099…REQ-118 = 20 net-new requirements (REQ count grows 98→118).
- No new phases added (Q3=A folds ingress into P15.5; Q2=C folds CLI commands into existing phases).
- P09 expands (REQ-107 aggregator extension); P10 expands (REQ-103…REQ-113, the largest phase); P15.5 expands (REQ-099…REQ-102 ingress + REQ-118 mTLS doctor).
- P05 gains REQ-116 (migrate); P06 gains REQ-117 (logs --all-nodes); P14a gains REQ-115 (upgrade); P14b gains REQ-114 (rotate-lead).
## v0.12 Milestone Summary — Security Hardening (Zero-Trust Identity)
**Status**: complete (shipped as v0.11.x tags; milestone release v0.11.28). 30 net-new requirements (REQ-119..REQ-148)
derived from the v0.12 threat-model review (25 findings F1..F25) and the
zero-trust identity model (R-021). See ROADMAP.md for the 29-phase plan
(P0 + P01..P27 + P28 final) and RESEARCH_v0.12.md for the full threat model.
### Wave A — Critical injection & traversal
| ID | Requirement | Priority | Phase | Status |
|----|-------------|----------|-------|--------|
| REQ-119 | Command injection fix in `internal/runtime/podman.go` & `wasm.go`: shell-quote `cmdStr` via `shellQuote` in SSH exec interpolation (`podman.go:57`, `wasm.go:39`); add injection regression tests (bats + Go) covering `;`, `\|`, `$()`, backticks, newline injection (F3) | High | **v0.12 P01** | complete |
| REQ-120 | Namespace path traversal fix: `validateNamespaceName` in `internal/ns/` rejects `..`, `/`, leading `-`, null bytes, control chars in `ns create`/`ns inherit`/`ns set-constraint`; add fuzz test (F4) | High | **v0.12 P02** | complete |
| REQ-121 | Txn apply path allowlist: `apply.sh` python heredoc validates every `path` in `desired-state.json` against a prefix allowlist (`/etc/orca/`, `/etc/traefik/orca*`, `/etc/systemd/system/orca-*`, `/etc/nftables.d/orca*`, `/etc/syncthing/orca*`); rejects otherwise; HMAC-signed manifest unchanged (F5) | High | **v0.12 P03** | complete |
### Wave B — Zero-trust identity
| ID | Requirement | Priority | Phase | Status |
|----|-------------|----------|-------|--------|
| REQ-122 | ACL enforcement wiring: `acl.Check` invoked in daemon handlers (read/write/admin by route) and SSH-push applier (validates `ORCA_OIDC_TOKEN` env var against JWKS before applying any txn); deny-by-default enforced; actor recorded in audit (F1, foundational for REQ-145) | High | **v0.12 P06** | complete |
| REQ-123 | Daemon auth hardening: mandatory mTLS (remove plaintext mode entirely); OIDC bearer accepted as second factor on human-facing endpoints; `MaxBytesReader` body limits; pprof loopback-only by default, refuse non-loopback without `--pprof-allow-public` confirmation (F6, F24) | High | **v0.12 P09** | complete |
| REQ-124 | HTTP request body size limits: `http.MaxBytesReader` on all JSON-decoding handlers; `MaxHeaderBytes` set; rejects oversized bodies (F24) | Medium | **v0.12 P09** | complete |
| REQ-125 | Audit log tamper-evidence: hash-chained entries (`prev_hash = sha256(prev_row \|\| payload)`), HMAC-SHA256 under master key on the chain head; `orca doctor audit` verifies the chain; append-only enforcement via SQLite trigger blocking UPDATE/DELETE; actor field carries OIDC `sub` or SPIFFE SVID (F2) | High | **v0.12 P10** | complete |
| REQ-126 | SVID chain validation: `VerifySVID` validates the full cert chain against the CA pool, not just the URI SAN; reject certs signed by unknown CAs even with correct URI (F9) | High | **v0.12 P11** | complete |
| REQ-127 | Backup symlink validation: `Restore` rejects `Linkname` that's absolute, contains `..`, or points outside `ORCA_HOME`; add regression test with crafted tarball (F7) | High | **v0.12 P12** | complete |
| REQ-128 | step-ca /tmp hardening: `step ca certificate` writes to 0600 temp under `ClusterDir()/step-tmp/` (or `TMPDIR` override), not world-readable `/tmp`; cleanup in `defer` (F10) | High | **v0.12 P13** | complete |
| REQ-129 | Master key rotation: `orca secrets rotate-master` re-encrypts all namespace secrets under a new master key; new master key re-sealed to OIDC as part of the same operation; `--dry-run` + atomic + automatic rollback to old sealed key on any ns failure; no passphrase (R-021) (F12) | High | **v0.12 P14** | complete |
| REQ-130 | File-mode audit expansion: `EnforceFileModes` extended to SSH key, master key (sealed blob), server cert/key, known_hosts; `orca doctor modes` checks all; startup refuses to run on violation (F13) | Medium | **v0.12 P15** | complete |
| REQ-131 | aggregate.sh JSON injection fix + drift-gate parse fix: replace `printf` interpolation with `jq`-based JSON construction (or Go-side aggregator emitting JSON); fix `orca-pull.sh` R-020 parsing to use `jq` instead of grep (F11, F18) | High | **v0.12 P16** | complete |
| REQ-132 | install.sh checksum+GPG verification: release.sh publishes `SHA256SUMS` + `SHA256SUMS.asc` (GPG-signed) alongside tarball; install.sh verifies before `tar -xzf`; fail closed on mismatch (F14) | High | **v0.12 P17** | complete |
| REQ-133 | nftables ruleset hardening: add conntrack bounds (`ct state established,related accept`), input default-deny on orca chain, drop invalid packets; `orca doctor nft` audits live ruleset against emitted one (F21) | Medium | **v0.12 P18** | complete |
| REQ-134 | sudoers hardening: add NOEXEC to `apt-get`/`dpkg` (or remove if unused); `orca doctor proxmox` audits sudoers file against expected allowlist (F22) | Medium | **v0.12 P19** | complete |
| REQ-135 | System user consistency: Proxmox bootstrap creates `nologin` system user (`-r -s /usr/sbin/nologin`), matching peer-setup; `orca doctor` flags inconsistency on existing peers; `orca upgrade` migrates (F23) | Medium | **v0.12 P20** | complete |
| REQ-136 | SQLite file-mode + at-rest encryption: `store.Open` sets DB file mode 0600; optional `--encrypt-db` (CGO-free fallback per C-31: file-mode 0600 + documented threat if SQLCipher needs CGO); no CGO (F8) | High | **v0.12 P21** | complete |
| REQ-137 | Migration safety: `copyFile` -> atomic temp+rename; `migrateDBSchema` runs in transaction with `foreign_keys(ON)`; pre-migration backup step (uses `internal/backup`); document manual rollback; v0.11->v0.12 identity migration: `orca upgrade` refuses clusters using `--password`/bare-tokens without `--accept-identity-migration` (F19, C-34) | High | **v0.12 P22** | complete |
| REQ-138 | Legacy CA/mTLS/daemon + step-ca password-provisioner deletion: remove `internal/security/ca.go` legacy CA, `internal/transport/mtls.go` deprecated path, daemon plaintext mode; migrate `orca init`/`orca cert *` to step-ca exclusively; `certpaths` (v0.8 layout) removed; delete step-ca `--password-file` provisioner (replaced by OIDC provisioner); **gate: P06/P08/P09/P11 all shipped** (F16) | High | **v0.12 P23** | complete |
| REQ-139 | known_hosts tightening + transport hardening: `Flock` tightens pre-existing looser perms to 0600; `classifyDialErr` switched from substring to typed errors; add SSH-exec rate limiting (token bucket per peer) (F15, F25) | Medium | **v0.12 P24** | complete |
| REQ-140 | Drift event authentication: drift events signed with per-peer HMAC key (derived from master key); aggregator rejects unsigned/forged events; `orca-drift-notify.sh` reads key from 0600 file owned by `orca` (F18) | Medium | **v0.12 P25** | complete |
| REQ-141 | Security integration test suite: hermetic harness exercising injection, traversal, symlink, drift-forgery, audit-tamper, daemon-auth-negative, OIDC mock-IdP flow, ACL-with-OIDC-claims negative tests, unseal/seal, WebAuthn virtual-authenticator ceremony, password-removal regression (assert `--password` is rejected); gates in `.coreci.yml` `validate` (C-33) | High | **v0.12 P26** | complete |
| REQ-142 | Zero-trust + OIDC + WebAuthn + threat-model docs: `docs/threat-model.md` (STRIDE + zero-trust model + OIDC data-flow), `docs/oidc.md` (configure your IdP, Dex offline quickstart, claim-to-namespace mapping), `docs/webauthn.md` (passkey registration, RP ID, secure context), `docs/security-runbook.md` (unseal/seal, master key rotation, incident response, sudoers audit, nft audit); README security section names "no orca credentials" as an invariant | Medium | **v0.12 P27** | complete |
| REQ-143 | Final review + ship + audit: multi-persona review across all phases, `ciagent-audit` reconstruction test, milestone merge to main, tag `v0.11.29` (= v0.12 milestone release per feature-milestone rule) | High | **v0.12 P28** | complete |
| REQ-144 | OIDC client + bundled Dex: `orca auth login`/`logout`/`status`/`init-idp`; OIDC config block (`oidc.issuer`, `client_id`, `client_secret`, `scopes`); bundled Dex systemd unit + Traefik route on the lead; BYO external IdP override via `oidc.issuer` repoint; JWKS caching + refresh; token storage at `~/.orca/credentials.json` (0600); `--oidc` flag on commands requiring identity; browser auth-code + PKCE + local loopback redirect; headless device-code fallback (D-238..D-247) | High | **v0.12 P04** | complete |
| REQ-145 | ACL rewrite to OIDC claims: remove `KindToken` entirely; `KindSpiffe` stays for machine identity; new `KindOidc` maps `sub`+`groups` -> namespace permissions; `acl.Check` takes OIDC claims struct; deny-by-default enforced in daemon + SSH-push applier; `acl.json` mode tightened to 0600 (F1) | High | **v0.12 P06** | complete |
| REQ-146 | Remove all password/token paths (breaking): delete `--password`/`$ORCA_PROXMOX_PASSWORD` from Proxmox join (replace with pre-staged-key-only or `step ssh` OIDC cert exchange); delete step-ca `--password-file` provisioner (migrate to OIDC provisioner); delete any bare-token CLI paths; documented in migration guide (R-021, C-34) | High | **v0.12 P07** | complete |
| REQ-147 | Master key seal-to-OIDC + Shamir recovery: master key encrypted with key derived from OIDC token exchange at unseal; `orca cluster unseal`/`seal`; sealed blob at `ClusterDir()/master.key.sealed` (0600); raw key never on disk; Shamir 3-of-5 shards printed at seal time; recovery via `--recovery` + 3 shards; mTLS-only offline path derives seal key from cluster CA (D-241, C-35) | High | **v0.12 P08** | complete |
| REQ-148 | WebAuthn connector for Dex (passkeys): `orca-webauthn-connector` (~300 LoC Go, `go-webauthn`); register/login ceremonies at `/orca/webauthn/{register,login}` behind Traefik; `orca auth register` browser flow; passkey storage SQLite `ClusterDir()/webauthn-credentials.db` (0600, public keys only); RP ID = cluster Traefik domain; secure context via step-ca cert; headless device-code fallback; virtual-authenticator integration tests (D-240, D-243, D-244, C-38) | High | **v0.12 P05** | complete |
### Scope notes (v0.12)
- REQ-119..REQ-148 = 30 net-new requirements (REQ count grows 118 -> 148).
- 29 phases (P0 + P01..P27 + P28 final); GRILL may split/merge.
- P04 (OIDC+Dex) and P05 (WebAuthn) are the new `feat` phases; the rest are `fix`/`chore`/`test`/`docs`/`refactor`. Milestone type = feature (at least one `feat`).
- Tags on v0.11.x patch line: `v0.11.0` (P0) ... `v0.11.29` (P28 final = v0.12 milestone release).
- v1.0.0 production-ready tag stays deferred for post-v0.12 UAT (per v0.11 PRD).
---
## Milestone v0.13: Production Hardening Round 2 + UAT Plan
**Status**: complete (2026-08-10). v0.12 (Security Hardening) is
COMPLETE; v0.13 is the final hardening round before the v1.0.0
production-ready tag. v1.0.0 is gated on the UAT signoff script
(`scripts/uat-signoff.sh`) delivered by this milestone.
### Wave A — Toolchain & injection hardening
| ID | Requirement | Priority | Phase | Status |
|----|-------------|----------|-------|--------|
| REQ-149 | Go toolchain bump to 1.25.12+ (closes 24 stdlib vulns: archive/tar GO-2025-4014/GO-2026-4869, crypto/tls GO-2026-5856/GO-2025-4008, crypto/x509 GO-2026-5037/4947/4946/GO-2025-4175/4155/4013, net/http GO-2026-4918/GO-2025-4012, net/url GO-2026-4601/4341/GO-2025-4010, encoding/pem GO-2025-4009, os GO-2026-4602); `govulncheck -show verbose` triage of 6 imported third-party vulns; bump deps with reachable traces | High | **v0.13 P01** | complete |
| REQ-150 | Input validation & injection hardening: (a) `orca logs --job` validate against `^[A-Za-z0-9_-]+$`, use `shellQuote` not `%q` (critical: backtick RCE via SSH fanout); (b) pprof `isLoopback(":6060")` treat empty host as non-loopback/bind-all, reject unless explicit public-allow flag wired; remove phantom `--pprof-allow-public` references, make loopback-only a hard invariant; (c) backup restore tar-slip fix: use `filepath.Rel(target, dest)` containment check instead of `HasPrefix(name, "..")`; (d) `orca txn rollback` validate txn ID against `^T-[0-9a-f]{16}$`; (e) `orca nft diff --against` validate txn ID before `filepath.Join`; (f) `drain stopAlloc` validate `allocID` against `^[A-Za-z0-9_-]+$` before `systemctl stop`; (g) `cluster_compat` `shellQuote(first)` for peer dir name; (h) `runtime/podman.go` use `shellQuote(image)` not `%q`; (i) nft `TrustedProbes` validate each entry with `net.ParseIP`/`net.ParseCIDR`; (j) sudoers: validate `--proxmox-user`/`--proxmox-role` against `^[a-z_][a-z0-9_-]{0,31}$`; write to fixed `/etc/sudoers.d/orca`; `shellQuote` all pveum/useradd; `validateSudoers` check the actual file written; (k) `nft country block add` validate `^[A-Z]{2}$` | Critical | **v0.13 P02** | complete |
### Wave B — Scheduler wiring & jobspec parser (architectural)
| ID | Requirement | Priority | Phase | Status |
|----|-------------|----------|-------|--------|
| REQ-151 | Scheduler/deployment wiring: wire `internal/scheduler.Schedule()` into `orca job run` — replace local `exec.CommandContext` path with: evaluate constraints/capacity/affinity via scheduler → render systemd units via `internal/emitter` → SSH-push to target via `internal/sshpush`; `--target` overrides scheduler selection; capacity enforced (reject job if no node fits); CEL constraints evaluated; affinity weighted scoring; `systemd-analyze verify` on rendered unit before deploy; `job run` without `--target` uses scheduler bin-packing across registered nodes | Critical | **v0.13 P03** | complete |
| REQ-152 | jobspec parser fixes: add `case "schedule":` and `case "timeout":` to top-level switch in `internal/jobspec/markdown.go` (currently silently dropped); fix DaemonSet — parser must not default Count to 1 for DaemonSet (validator rejects Count!=0); DaemonSet schedule block actually parsed and stored; `timeout:` on Jobs parsed and enforced (kill after duration); `restart:` policy translated to systemd `Restart=`/`StartLimitBurst` in emitter; add `job lint` warnings for advisory-only fields (cron, health, update, affinity) with honest "not enforced in this version" message | Critical | **v0.13 P03** | complete |
### Wave C — Zero-trust enforcement wiring
| ID | Requirement | Priority | Phase | Status |
|----|-------------|----------|-------|--------|
| REQ-153 | ACL enforcement + WebAuthn registration auth: (a) wire `acl.Check` into all 5 daemon handlers (`dispatch`/`jobs`/`nodes`/`tasks`/`health`) — extract OIDC sub/SPIFFE SVID from mTLS peer cert, check against ACL for namespace+verb, deny-by-default; (b) wire `acl.Check` into sshpush applier + txn apply path (validate `ORCA_OIDC_TOKEN` bearer against JWKS); (c) thread OIDC sub/SVID into audit `actor` field (replaces "cli"/"daemon"); (d) fix `acl.json` mode 0644→0600; (e) fix WebAuthn unauthenticated registration — `/orca/webauthn/register` requires existing authenticated session or admin bootstrap token; do not allow overwriting existing credentials without re-auth; (f) add flock on `acl.json` for concurrent grant/revoke | Critical | **v0.13 P04** | complete |
| REQ-154 | Seal/audit CLI + chain race + key zeroing: (a) implement `orca cluster seal`/`unseal` (OIDC token exchange→unwrap master key→zeroed on shutdown; Shamir 3-of-5 shards printed at seal time; sealed blob at `ClusterDir()/master.key.sealed` 0600); (b) implement `orca doctor audit` (invokes `AuditRepo.VerifyChain`); (c) implement `orca doctor modes` (invokes `EnforceFileModes` across ORCA_HOME); (d) fix audit hash-chain race — `Append` uses `BEGIN IMMEDIATE` transaction; (e) fix `secrets rotate-master` to actually re-seal to OIDC; (f) zero master key / namespace keys / SVID private keys after use (defense-in-depth against pprof heap extraction) | High | **v0.13 P05** | complete |
| REQ-155 | auth init-idp real + auth register: (a) implement `orca auth init-idp` — render Dex systemd unit + config template + Traefik dynamic route from `internal/webauthn/` connector at `https://<cluster>/orca/webauthn/{register,login}`; RP ID = cluster Traefik domain (C-38); HTTPS secure context via step-ca cert; atomic deploy with rollback; (b) implement `orca auth register` (browser flow to WebAuthn registration endpoint); (c) `loadOIDCConfig` config-file loading (`oidc.issuer` in config, not flags-only); (d) `orca doctor oidc` health check | High | **v0.13 P06** | complete |
### Wave D — Concurrency, transport, migration safety
| ID | Requirement | Priority | Phase | Status |
|----|-------------|----------|-------|--------|
| REQ-156 | Concurrency safety: (a) SQLite `busy_timeout(5000)` + `SetMaxOpenConns(1)` on all DSNs (store, cache, recovery, webauthn); (b) secrets file flock (concurrent `secrets set` on same ns no longer loses data); (c) upgrade lock file (refuse concurrent `orca upgrade`); (d) backup lock file; (e) cache invalidation by write commands (`node join`/`leave`, `ns create`/`delete`, `job run`/`stop` invalidate relevant cache class — read-after-write consistency); (f) `Executor.Run` mutex scope fix (hold only for DB inserts, not whole job duration); (g) `ns create` atomic dir+ns.md write; (h) `writeCurrentLead` atomic write; (i) consolidate 3 divergent `writeAtomic` impls onto `security.WriteAtomic`; (j) WebAuthn session stores guarded with `sync.Mutex` | High | **v0.13 P07** | complete |
| REQ-157 | Transport & SSH safety: (a) replace substring matching in `transport.IsTransient` AND `sshpush.isTransient` with typed sentinels (`errors.Is`); (b) `rotateSSHKeys` 2-phase atomic swap (stage new key on all peers → atomic swap → verify → cleanup old); (c) `known_hosts` flock field actually read by `dial()` (TOFU callback uses new field, not v0.8 `certpaths.KnownHostsPath()`); (d) IPv6 `net.JoinHostPort` in proxmox SSH dial + drain `splitHostPort`; (e) explicit timeouts for all SSH commands (peer-setup, drift remediate/ack, txn rollback, job restart — use `context.WithTimeout`); (f) `verifyCutover` use `security.ClientTLSConfig` with orca CA pool; (g) OIDC callback server `ReadHeaderTimeout: 5s`; (h) root SIGINT/SIGTERM handler for non-watch commands (clean SSH session + temp file cleanup) | High | **v0.13 P08** | complete |
| REQ-158 | Migration & operational safety: (a) migration transaction + torn-write fix — `migrateDBSchema` wraps ALTER TABLE in transaction; crash after `os.Rename` but before schema fixup is recoverable; (b) `job stop` real `systemctl stop` via SSH (matches `job restart` pattern; honest semantics); (c) DB retention/compaction for `jobs`/`tasks`/`audit_log` tables (retention policy + `orca doctor db` compaction check); (d) `orca logs --lines` cap + `--since` upper bound (prevent OOM from unbounded journalctl output); (e) cache DB mode 0600 (matches `store.Open`); (f) `upgrade.go` cutover backup-file + atomic-rename (replace direct `sed -i`) | High | **v0.13 P09** | complete |
### Wave E — Observability, docs, UAT
| ID | Requirement | Priority | Phase | Status |
|----|-------------|----------|-------|--------|
| REQ-159 | Observability expansion: metrics add `orca_jobs_by_state` histogram, `orca_drift_events_total` counter, `orca_ssh_errors_total` counter, `orca_txn_apply_total`/`orca_txn_rollback_total` counters, `orca_acl_denials_total` counter, `orca_audit_chain_head` gauge; new `docs/metrics.md` with Prometheus scrape config; security headers middleware on daemon (`X-Content-Type-Options`, `X-Frame-Options`) | Medium | **v0.13 P10** | complete |
| REQ-160 | Doc drift round 2: (a) README — update status banner (v0.12+v0.13 complete), latest tag, subcommand table (add `auth`/`nft`/`peer-setup`/`secrets rotate-master`), correct "mTLS by default" claim (SSH-push is canonical, mTLS deprecated), add missing docs to table; (b) `docs/cli.md` — complete rewrite covering all ~40 subcommands; (c) CHANGELOG regen; (d) help text fixes (`job run` HCL→markdown, `job stop` daemon→SSH-push); (e) `docs/webauthn.md` add `auth register`; (f) `docs/namespace.md` add `inherit`/`set-constraint`; (g) `docs/install.md`+`docker.md` update version refs; (h) `docs/security-runbook.md` match P05 reality; (i) fix `verify-reqs` bold-format regex (currently bypasses v0.12); (j) fix ROADMAP/REQUIREMENTS v0.12 status hygiene; (k) `docs/security-scanning.md` gosec.json; (l) `internal/proxmox/bootstrap.go` comments (password→key auth); (m) deprecate `orca status` stub; (n) `make verify-docs` target (cli.md ↔ `orca --help` consistency) | High | **v0.13 P11** | complete |
| REQ-161 | `--type linux` SSH-join: implement `NodeKindLinux` path (reserved at `model/node.go:29`); new `internal/linux/bootstrap.go` mirroring Proxmox pattern — orca pubkey deploy → `orca` system user → drift-events dir → no PVE role; key-auth only (R-021); `orca node join --type linux --host <ip> --ssh-user root --ssh-key <path>`; `peer-setup.go` kept as documented fallback | High | **v0.13 P12** | complete |
| REQ-162 | UAT plan: `docs/uat.md` — 3-host topology (lead Ubuntu 22.04 + pve01 Proxmox VE 8/9 + worker01 Ubuntu 22.04); step-by-step with exact commands (bootstrap→onboard Proxmox→onboard Ubuntu worker→capacity→namespace→deploy full stack→migrate between hosts→exercise every claim); claim matrix mapping ~35 feature claims to UAT steps; signoff procedure (run `scripts/uat-signoff.sh`, paste output) | Critical | **v0.13 P12** | complete |
| REQ-163 | UAT signoff script: `scripts/uat-signoff.sh` — idempotent, `set -euo pipefail`, ~35 named assertions covering all feature claims; read + non-mutating only (doctor, list, --dry-run); exit 0 iff all pass; `scripts/uat-smoke.sh` — pure-CLI subset for CI `validate` (version, acl file mode, doctor modes, no-password grep, metrics shape); tests for both scripts | Critical | **v0.13 P12** | complete |
### Scope notes (v0.13)
- REQ-149..REQ-163 = 15 net-new requirements (REQ count grows 148 -> 163).
- 14 phases (P0 + P01..P12 + P13 final); "no limit on phases" per operator.
- P03 (scheduler wiring) and P12 (`--type linux` + UAT) are the `feat` phases; the rest are `fix`/`chore`/`test`/`docs`/`refactor`. Milestone type = feature (at least one `feat`).
- Tags on v0.12.x patch line: `v0.12.0` (P0) ... `v0.12.13` (P13 final = v0.13 milestone release).
- v1.0.0 production-ready tag stays deferred for post-v0.13 UAT signoff (operator runs `scripts/uat-signoff.sh`, paste output back).
### Accepted residual risks (documented in threat-model, not fixed)
- OIDC tokens plaintext at rest (0600) — sealing on every CLI invocation conflicts with "no orca binary on servers" model
- HSTS on daemon — mTLS-only API, no browser-facing surface on daemon itself
- DNS resolution timeout — bounded by `net.Dialer{Timeout: 15s}`
- Temp file cleanup on SIGKILL — orphaned temp files, operator-visible, low impact
- Flock timeout on NFS — stuck holder is rare; `tryFlockEx` exists if needed later
- "WASM-first" pillar aspirational — document as "WASM runtime available, process is default"
- arm64/armv7 release — D-193 deferred; install.sh detection is forward-looking
- OIDC callback slowloris — loopback, short-lived, single CLI invocation
## Milestone v0.14: Ingress Bootstrap Completeness
**Scope**: ensure that linux & proxmox types are properly bootstrapped with
traefik during cluster init or node join. All cluster endpoints are
provisioned as sockets (R-007); routing between jobs and services depends on
traefik being present on the host and properly configured. The v0.13 traefik
deployment shipped only a binary + systemd unit + empty dynamic dir — it
never wrote the static config nor applied nft rules, so `orca-traefik.service`
fails to start on a fresh `orca init` and `orca doctor nft` FAILs. v0.14
replaces the binary+systemd model with a **podman container** running a custom
`orca-traefik` image, and completes the nft SNAT+DNAT ingress stack on every
node type.
**New load-bearing rule**:
- **R-024** — Traefik runs exclusively as a podman container, deployed from
the `orca-traefik` image published per release. Every orca-managed ingress
surface bootstraps: nft DNAT (`:443→127.0.0.1:8443`,
`:80→127.0.0.1:8080`) + SNAT/MASQUERADE postrouting + `podman run -d
--restart=unless-stopped --network host -v /etc/traefik/traefik.yml:/etc/traefik/traefik.yml:ro
-v /etc/traefik/dynamic:/etc/traefik/dynamic:ro
-v /etc/orca/step-ca-root.crt:/etc/orca/step-ca-root.crt:ro
git.cloudinit.dev/coreci/orca-traefik:<tag>`. No node joins without a
functional podman-traefik ingress data plane. The image's baked static
config is a default; host-side `traefik.yml` mounted `:ro` overrides it
(preserves `traefik-on-public-ip` opt-out, REQ-100).
**Three topologies** (per operator constraints):
- **Linux**: host → nft → `podman run orca-traefik` (host network)
- **Proxmox Native**: host → nft → LXC (nesting=1) → `podman run orca-traefik`
- **Proxmox Floating-IP**: LXC (owns floating IP) → nft (inside LXC) →
`podman run orca-traefik`
| ID | Requirement | Priority | Phase | Status |
|----|-------------|----------|-------|--------|
| REQ-171 | `Dockerfile.traefik` + release pipeline: build + publish `git.cloudinit.dev/coreci/orca-traefik:<version>` alongside the orca image per release; `.coreci.yml` `container-publish-traefik` step; image bakes default `traefik.yml` (entrypoints `websecure` 127.0.0.1:8443, `web` 127.0.0.1:8080, `traefik` 127.0.0.1:8081 + file provider watching `/etc/traefik/dynamic` + json log/accessLog); host-side `/etc/traefik/traefik.yml` mounted `:ro` overrides baked config (preserves `traefik-on-public-ip` opt-out REQ-100); no `certificatesResolvers` (traefik v3.3 only supports acme/tailscale); `tls: {}` in dynamic config for v0.14 (real mTLS deferred to v0.15) | Critical | **v0.14 P1** | complete |
| REQ-172 | Replace `internal/traefik/install.go` binary+systemd install with a podman-container reconciler: `podman pull orca-traefik:<tag>` + `podman run -d --restart=unless-stopped --network host --name orca-traefik -v /etc/traefik/traefik.yml:/etc/traefik/traefik.yml:ro -v /etc/traefik/dynamic:/etc/traefik/dynamic:ro -v /etc/orca/step-ca-root.crt:/etc/orca/step-ca-root.crt:ro <image>`; idempotent (pull+run if absent, start if stopped); install podman if absent (C-50); v0.13→v0.14 upgrade: detect+stop+disable+remove legacy `orca-traefik.service` + `/usr/local/bin/traefik` (C-57); works locally + over SSH-push; enable `podman-restart.service`; remove systemd unit generation | Critical | **v0.14 P2** | complete |
| REQ-173 | nft SNAT+DNAT ruleset render+apply: extend `internal/emitter/nft.go` with postrouting masquerade chain; new `internal/ingress/bootstrap.go` renders `orca.nft` + applies `nft -f` + ensures `/etc/traefik/dynamic` dir + pushes step-ca root CA + invokes podman traefik reconciler; wired into `orca init` (localhost lead) | Critical | **v0.14 P3** | complete |
| REQ-174 | Remote ingress bootstrap via SSH-push for `orca node join --type linux`: push step-ca root CA, render+apply nft remotely, invoke podman traefik reconciler remotely; register node as `linux` | Critical | **v0.14 P4** | complete |
| REQ-175 | Proxmox native ingress mode (`--ingress-mode native`, default): on PVE host, render+apply nft (vmbr-compatible, separate `orca-ingress` table avoids pve-firewall conflict); create unprivileged LXC with `--features nesting=1,keyctl=1` running podman+orca-traefik; push step-ca root CA into LXC; nft DNAT target = LXC bridge IP; register PVE host as `proxmox` node; add `IngressMode` field to `model.Node` + schema migration | Critical | **v0.14 P5** | complete |
| REQ-176 | Proxmox floating-IP mode (`--ingress-mode floating-ip --floating-ip --gateway --mac [--net-prefix]`): `pct create` Ubuntu LXC named `ingress` with `net0 bridge=vmbr0,hwaddr=<mac>,ip=<floating-ip>/<prefix>,gw=<gateway> --features nesting=1,keyctl=1 --onboot 1`; install podman + run orca-traefik inside LXC; apply nft DNAT+SNAT inside LXC; register LXC as managed `linux` node (name=`ingress`, addr=`<floating-ip>:8443`); interactive prompt for params when flags absent + not `--json`; validate IP/MAC/gateway; PVE host also registered as `proxmox` for workload dispatch | Critical | **v0.14 P6** | complete |
| REQ-177 | `orca doctor ingress [--peer]`: verify orca-traefik container running (`podman inspect`), nft DNAT+SNAT applied, `/etc/traefik/dynamic` exists, step-ca root CA mounted; extend `scripts/uat-signoff.sh` with ingress assertions (40 podman_traefik, 41 nft_dnat_snat, 42 linux_worker, 43 proxmox_native_lxc / floating_ip_lxc) | High | **v0.14 P7** | complete |
| REQ-178 | Docs: `docs/cli.md` (`--ingress-mode` + floating-IP flags + `doctor ingress`), `docs/uat.md` (native + floating-IP topologies), `docs/ingress.md` (podman-traefik image + volume mounts + certResolver), `docs/docker.md` (orca-traefik image), `ARCHITECTURE.md` (R-024 + ingress bootstrap section) | High | **v0.14 P7** | complete |
| REQ-179 | Integration tests: hermetic harness fakes SSH; asserts init→podman traefik running + nft applied; linux join→remote podman+nft; proxmox native→LXC created with nesting + podman traefik; floating-ip→`pct create` with correct net0 args + LXC registered as linux node; release.sh builds orca-traefik image (Dockerfile.traefik parses) | Critical | **v0.14 P7** | complete |
### Scope notes (v0.14)
- REQ-171..REQ-179 = 9 net-new requirements (REQ count grows 163 -> 172).
- 9 phases (P0 + P1..P7 + P8 final); feature milestone (multiple `feat` phases).
- Tags on v0.13.x patch line: `v0.13.0` (P0) ... `v0.13.8` (P8 final = v0.14 milestone release).
- Milestone branch: `milestone/v0.14-ingress-bootstrap`.
## Milestone v0.15: CI Release Pipeline Fix
**Scope**: fix the container image publishing pipeline. v0.14 shipped
`Dockerfile.traefik` + `Dockerfile` but no container images were
published to the Gitea registry because: (1) no Gitea Actions workflow
existed to trigger on tag pushes, (2) the CoreCI trigger script
stripped tag refs, (3) the `.coreci.yml` container-publish steps used
Docker-in-Docker (`docker:24-cli`) which is prohibited. v0.15 adds a
Gitea Actions workflow that triggers on tag pushes, installs the
`coreci` binary on the runner, and runs `coreci run`. The
`.coreci.yml` container-publish steps are rewritten to use kaniko
(no Docker daemon required).
| ID | Requirement | Priority | Phase | Status |
|----|-------------|----------|-------|--------|
| REQ-180 | Create `.gitea/workflows/release.yml` that triggers on `push: tags: ['v*']`, installs the `coreci` binary (from `git.cloudinit.dev/coreci/coreci`), injects `PAT_TOKEN` secret as `GITEA_TOKEN` env var, and runs `coreci run` — which executes the full `.coreci.yml` pipeline (validate, build, test, release) locally on the Gitea Actions runner | Critical | **v0.15 P1** | complete |
| REQ-181 | Replace `docker:24-cli` DinD steps in `.coreci.yml` with kaniko (`gcr.io/kaniko-project/executor:debug`): write `/kaniko/.docker/config.json` from `GITEA_TOKEN` (base64 auth), run `/kaniko/executor --dockerfile=<Dockerfile> --context=dir://. --destination=<registry/image:tag> --skip-tls-verify-registry`. Applies to both `container-publish` (orca image) and `container-publish-traefik` (orca-traefik image) | Critical | **v0.15 P1** | complete |
| REQ-182 | Set `PAT_TOKEN` Gitea Actions repository secret via `tea actions secrets create` (same value as `GITEA_TOKEN` from `.env`). Gitea reserves the `GITEA_` prefix for built-in secrets, so the secret must be named `PAT_TOKEN`, not `GITEA_PAT` | High | **v0.15 P0** | complete |
### Scope notes (v0.15)
- REQ-180..REQ-182 = 3 net-new requirements (REQ count grows 172 -> 175).
- 3 phases (P0 + P1 + P2 final); fix milestone (no `feat` phases — CI infrastructure).
- Tags on v0.14.x patch line: `v0.14.0` (P0) ... `v0.14.2` (P2 final = v0.15 milestone release).
- Milestone branch: `milestone/v0.15-ci-release-pipeline`.
- REQ-182 is complete: `PAT_TOKEN` secret created via `tea actions secrets create PAT_TOKEN <value> --repo coreci/orca`.
## Milestone v0.16: Release Binary Asset Fix
**Scope**: fix the root cause of releases shipping with zero binary
assets. v0.15 added a Gitea Actions workflow but it never executed
successfully due to two compounding bugs: (1) the `git clone` of the
private `coreci` repo in the workflow had no credentials, causing the
"Install CoreCI" step to fail; (2) the `.coreci.yml` used an invalid
`pipelines:`/`steps:`/`image:`/`commands:` format that CoreCI does not
recognize (CoreCI's native format is `jobs:` with `plugin:`/`invoke:`
/`vars:` and a DAG via `needs:`). Both issues must be fixed for the
release pipeline to actually build and upload binaries.
| ID | Requirement | Priority | Phase | Status |
|----|-------------|----------|-------|--------|
| REQ-183 | Fix `.gitea/workflows/release.yml` "Install CoreCI" step: the `git clone` of the private `coreci` repo fails because the clone command has no credentials. The `actions/checkout@v4` step only injects auth for the orca repo (via `http.https://git.cloudinit.dev/.extraheader`), not for the subsequent bare `git clone` of the coreci repo. Fix: embed the `PAT_TOKEN` in the clone URL (`https://cloudinit-bot:${GITEA_TOKEN}@git.cloudinit.dev/coreci/coreci.git`) and pass `GITEA_TOKEN: ${{ secrets.PAT_TOKEN }}` as env to the "Install CoreCI" step | Critical | **v0.16 P1** | complete |
| REQ-184 | Rewrite `.coreci.yml` from the invalid `pipelines:`/`steps:`/`image:`/`commands:` format to CoreCI's native `jobs:`/`plugin:`/`invoke:`/`vars:` format with a proper DAG (`needs:`). CoreCI's `Pipeline` struct only has `Jobs`/`Services`/`Env` fields — unknown top-level keys and unknown job fields are silently dropped by `yaml.Unmarshal`, producing an empty `Jobs` map. `coreci run` then executes zero jobs (validate does not reject empty jobs). The rewrite must: (a) convert each pipeline to a job with `plugin: docker://golang:1.25.12` and `invoke:` for the commands, (b) use `needs:` for DAG ordering (validate→build→test→release), (c) pass `GITEA_TOKEN` via `vars: { GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} }` (resolved from env via CoreCI's secret resolver `os.Getenv` fallback), (d) use `CI_COMMIT_BRANCH` (tag name on tag push, from CoreCI's github.go CI context) and `CI_COMMIT_SHA` for version injection, (e) handle the case where the release already exists (created by the CIAgent ship workflow with title+body but no binary) by falling back to Gitea API asset attachment, (f) verify assets are actually attached after release creation (REQ-097 gate C-21) | Critical | **v0.16 P1** | complete |
### Scope notes (v0.16)
- REQ-183..REQ-184 = 2 net-new requirements (REQ count grows 175 -> 177).
- 3 phases (P0 + P1 + P2 final); fix milestone (no `feat` phases — CI infrastructure).
- Tags on v0.15.x patch line: `v0.15.0` (P0) ... `v0.15.2` (P2 final = v0.16 milestone release).
- Milestone branch: `milestone/v0.16-release-binary-fix`.
- Root cause analysis confirmed: all 87 releases in the repo's history have zero binary assets — this has never worked. The releases are created by the CIAgent ship workflow (via Gitea API, title+body only); the binary upload is exclusively the `.coreci.yml` release job's job, and that job has never executed.