# Svault 0.7.0 — Findings Register

> **Status: consolidated.** Three independent AI reviews of 0.7.0 were
> commissioned (see [PROMPT.md](../PROMPT.md)) and saved under
> [`../reviews/`](../reviews/). Their findings are de-duplicated into the **New
> findings** table below with maintainer dispositions. The carry-forward table
> restates the original 22 findings after the 0.6.0 + 0.7.0 hardening.

- **Version:** 0.7.0 (reviewed); fixes land in 0.8.0
- **Date:** 2026-05-30
- **Maintainer:** Nima Karimi
- **Reviewers:** Claude Opus 4.8 (Anthropic), GLM-5.1 (z-ai, via opencode), opencode (Qwen3-Coder-Next) — three independent fresh-clone reviews.
- **Tooling pass:** `cargo test` (84 pass, 1 ignored — all three reviewers reproduced this), `cargo clippy --all-targets --all-features -- -D warnings` (clean), `cargo audit` (clean, 0 advisories across 257 deps), `cargo fmt --check` (clean) — green on Linux/macOS/Windows/Fedora CI.

## Headline: all three reviews independently confirm the known gap

The single most important finding is unanimous across all three reviewers and
already tracked here as **#2/#5/#22**: **the policy engine is advisory, not
enforced.** The daemon `Get` path runs no policy or audit check, caller/scope/
reason are self-asserted, and `svault.policy.yaml` is unsigned and located by an
upward directory walk. A same-UID process bypasses the entire policy layer by
calling `svault secret get`, by speaking the socket protocol directly, or by
reading the `.session` key. **All three reviews explicitly state this should
block a 1.0.0 "stable / enforced policy" label.** The reviews validate the
priority and sharpen the requirements (N-1, N-2, N-5 fold into it). Per maintainer
decision this policy work is **deferred to 0.9.0** — 0.8.0 is
a hardening release that acts on everything *else* the review surfaced (see the
"0.8.0 scope" section below). Policy enforcement is deferred, not dropped, and
remains the gate for 1.0.0.

The same-UID trust model itself was judged **reasonable and clearly
communicated** — the issue is not the boundary, it is that the headline feature
is bypassable *within* it.

## Carry-forward from the 0.5.0/0.6.0 register

Status of the original 22 findings after the 0.6.0 + 0.7.0 hardening. **14 fixed,
5 accepted, 3 open** — all three open items are the policy-engine work scheduled
for 0.8.0, and were re-confirmed by every 0.7.0 reviewer.

| # | Finding (short) | Status after 0.7.0 |
|---|-----------------|--------------------|
| 1 | No socket peer-credential check | **Fixed (0.7.0)** — peer-UID bond (`SO_PEERCRED`/`getpeereid`); all 3 reviews verified it fail-closed on both Linux/macOS |
| 2 | Policy engine advisory, not enforced | **Open** — planned 0.8.0; **re-confirmed Critical/High by all 3 reviews** |
| 3 | Passphrase in cleartext over the socket | **Fixed (0.7.0)** — client-side key derivation; all 3 reviews verified passphrase never crosses socket |
| 4 | Raw passphrase on disk (incl. Windows, TUI) | **Fixed (0.6.0 + 0.7.0)** — key (not passphrase) at rest, mode 0600 / Windows `icacls` ACL (Windows residual → N-7) |
| 5 | Policy file unsigned + unbounded upward search | **Open** — planned 0.8.0 (with #2); re-confirmed High by all 3 reviews |
| 6 | Un-zeroized secret/passphrase strings | **Fixed (0.7.0)** — prompts, `get_secret`, TUI reveal all `Zeroizing` (residual transport/map copies → N-6) |
| 7 | Static salt reuse on vault writes | Accepted (random per-write nonce) |
| 8 | No connection limit on the daemon (DoS) | **Fixed (0.6.0)** — configurable ceiling + read timeout |
| 9 | No SCA / `cargo audit` in CI | **Fixed (0.7.0)** — `cargo audit` gate |
| 10 | Transitive advisories via `ratatui` | **Fixed (0.7.0)** — `ratatui` 0.30, audit clean |
| 11 | No release-artifact integrity/provenance | **Fixed (0.7.0)** — SHA-256 checksums + SLSA build provenance |
| 12 | Passphrase strength advisory only | **Fixed (0.7.0)** — entropy floor + `--force` (dictionary-modeling residual → N-13) |
| 13 | Mutex poisoning kills the daemon | **Fixed (0.6.0)** — poison-recovery lock |
| 14 | `recovery.enc` / export bundles world-readable | **Fixed (0.7.0)** — owner-only (CLI); **TUI export path was still default-umask → N-3, fixed 0.8.0** |
| 15 | Logs not tamper-evident | Accepted (advisory, no secret values; needs remote sink) — re-raised as N-8 |
| 16 | Socket TOCTOU + world-traversable `.svault/` | **Fixed (0.7.0)** — `.svault/` 0700, socket bound under umask (born 0600) |
| 17 | Daemon hygiene (signals, log rotation, etc.) | **Fixed/Improved (0.7.0)** — graceful `SIGTERM`/`SIGINT` zeroize + cleanup, `daemon.log` rotation (rotation perms residual → N-10, fixed 0.8.0) |
| 18 | Recovery code has no checksum digit | Accepted (backlog, UX) |
| 19 | `Vault::open` decrypts whole DB to verify | Accepted (backlog, perf) |
| 20 | `unwrap()` panic on truncated `vault.enc` | **Fixed (0.6.0)** — checked length guard |
| 21 | GCM uses no AAD | Accepted (salt change → auth failure anyway; format-breaking to add) |
| 22 | Fallback policy reads `meta.yaml` unverified | **Open** — tied to the policy-engine work (0.8.0) |

## New findings from the 0.7.0 independent review

De-duplicated across the three reviews. "Models" shows which reviewers raised
each (O = Claude Opus 4.8, G = GLM-5.1, Q = opencode/Qwen3). The big policy
finding is **not** repeated here — it is carry-forward #2/#5/#22 above.

| # | Finding | Sev | Models | Status | Disposition |
|---|---------|-----|--------|--------|-------------|
| N-1 | **Caller identity is unauthenticated / self-asserted** (`--caller`/`$SVAULT_CALLER`, no OS binding) | High | O,G,Q | **Open → 0.8.0** | Folds into the policy-enforcement work: the daemon must bind the caller to peer credentials (or a per-agent token) instead of trusting a self-asserted string. Tracked with #2. |
| N-2 | **Policy parse failure fails *open*** — a YAML typo silently drops to the allow-all fallback (`load()` uses `.ok()`) | Med-High | O | **Open → 0.8.0** | Real even for cooperative deployments. Distinguish "no policy file" (fallback) from "policy present but unparseable" (hard error, fail **closed**). Part of #2/#5. |
| N-3 | **TUI export wrote the bundle with default (world-readable) perms** (`tui/mod.rs` used `std::fs::write`, not `secfile`) | Med | G | **Fixed (0.8.0)** | Confirmed: export bundle wraps the vault key. The CLI path already used `secfile::write_owner_only`; the TUI path now does too. |
| N-4 | **Import created the vault dir with default `0755`** (`portable.rs` used `create_dir_all`) | Low | G | **Fixed (0.8.0)** | Bundle files are safe-to-commit ciphertext, but the dir is now `0700` (via `create_dir_owner_only`) for consistency with `Vault::init`, so a later `.session` is never reachable through a traversable parent. Regression test added. |
| N-5 | **Daemon `Get` writes no audit entry**; `svault secret get` (daemon path) evades the agent audit trail | Med | O,G,Q | **Open → 0.8.0** | The daemon is exactly where audit belongs. Emit an audit record on every `Get`, tagged with peer UID + request surface. Part of the #2 policy-in-daemon work. |
| N-6 | **Incomplete zeroization** — `Vault::load_secrets` builds a non-`Zeroizing` `HashMap<String,String>`; daemon `Response::Secret`/reply buffer are plain `String` | Low-Med | O,G | **Fixed — daemon transport (0.8.0); map residual accepted** | The daemon now wipes both the serialized reply buffer and the in-memory `Response::Secret` value after writing (`reply()` + `serve_conn`, N-6). The `load_secrets` `HashMap` plaintext is still freed-not-wiped; accepted under the at-rest/cooperative model and the README claim is worded accordingly. |
| N-7 | **Windows at-rest is weaker** — `restrict_to_owner` writes the file *then* runs `icacls` (creation-time TOCTOU) and silently no-ops on failure | Med | O,Q | **Accepted (backlog)** | Sharpens the Windows residual of #4/#14/#16. Unix (daemon) is the primary supported path; Windows hardening (atomic owner-only DACL at create + surface failures) is tracked, **not** a 1.0.0 blocker. Documented as weaker. |
| N-8 | **Audit log is not tamper-evident** (no HMAC / hash chain) | Med | Q | **Accepted (= #15)** | Re-raise of #15. The log holds no secret *values*; a same-UID writer can already alter it. The real fix is a remote/append-only sink; a local hash-chain is a later enhancement. |
| N-9 | Signal handler uses `libc::signal`, not `sigaction` | Low | G | **Fixed (0.8.0)** | Now installed via `libc::sigaction` (zeroed `sa_mask`, `SA_RESTART`) for well-defined cross-Unix semantics, on both Linux and macOS. |
| N-10 | **`daemon.log` rotation didn't set perms on the new file** | Low | G | **Fixed (0.8.0)** | The log lives inside `0700 .svault/` so it was already traversal-protected, but the file is now opened `0600` (belt-and-suspenders). |
| N-11 | No rate limit on daemon `Unlock` attempts | Low/Info | G | **Accepted** | Each attempt requires a full AES-256-GCM decrypt-and-verify of the vault, and the peer is already same-UID/peer-UID-bonded — brute force is impractical and not the threat. |
| N-12 | Argon2id params (64 MiB / t=3 / p=1) are fixed, not tunable | Low-Med | G | **Accepted (backlog)** | Sound for 2024–2026. Tunable params need a version field in the on-disk format (format change) — revisit before 1.0.0. |
| N-13 | Entropy estimate doesn't model dictionary/phrase attacks | Low | G,Q | **Accepted (= residual #12)** | Deliberately `length × log2(pool)`, which *under*-credits; layered with a separate common-word/variety check. The ~50-bit floor is meaningful against blind brute force, which is its job. |

## 0.8.0 scope (maintainer decision)

**0.8.0 is a security-review-response + hardening release. The policy engine is
deliberately deferred to 0.9.0 — it is NOT dropped.** It remains the
single substantive gap before a 1.0.0 "enforced policy / stable" label, exactly
as all three reviews flagged. Splitting it out keeps 0.8.0 a clean, shippable
hardening release rather than blocking on the largest piece of work.

**Landed in 0.8.0:**
- Owner-only TUI export (N-3), `0700` import dir (N-4 + regression test), `0600` `daemon.log` after rotation (N-10).
- Daemon transport zeroization — serialized reply buffer + in-memory `Response::Secret` wiped after write (N-6).
- `libc::sigaction` instead of `signal()` (N-9).

**Deferred to 0.9.0 (the policy thread — gates 1.0.0):**
- Move policy evaluation + audit into the daemon `Get` path (#2, N-5).
- Authenticate the caller out-of-process via peer creds / per-agent token (N-1).
- Sign/pin `svault.policy.yaml` and anchor discovery to the vault (#5).
- **Fail closed** on an unparseable policy (N-2).
- Verify `meta.yaml` on the fallback `get` path (#22).

**Accepted / backlog (not 1.0.0 blockers):** Windows atomic DACL (N-7),
tamper-evident audit sink (N-8/#15), `Unlock` rate-limit (N-11), tunable Argon2id
(N-12), dictionary-aware entropy (N-13), `load_secrets` map residual (N-6 part).
