# Independent Security Review: Svault 0.9.9

**Project**: Svault (nim444/Svault)  
**Version reviewed**: 0.9.9 from `origin/feat/0.9.9-conditional-access-seal` at commit `c1696e9`  
**Review date**: 2026-06-02 04:34:49 EEST  
**Reviewed by**: OpenAI GPT-5.5 via OpenCode  
**Review location**: Local clone at `/var/folders/08/34qvzr6s3wx7mzcrnm79_qk80000gn/T/opencode/svault-review-eA3YE4`

Important branch note: `origin/main` is currently version `0.9.8` at `20f816f`, and the latest remote tag I observed is `v0.9.8`. The 0.9.9 code exists on `origin/feat/0.9.9-conditional-access-seal`; I switched to that branch before completing this review.

## Executive Summary

Overall risk rating for corporate use: **Moderate** for cooperative-agent developer and CI workflows on Unix, **not yet suitable as a hard security boundary against same-UID hostile agents or high-value production secrets**.

The 0.9.9 branch materially improves Svault's posture: the daemon remains the strongest Unix path, policy and judge material are encrypted at rest, session expiry is bounded to six hours, MCP routes through the same gate, and conditional access/sealing is implemented in the encrypted policy. I found no critical/high implementation flaw in the reviewed code path. The main residual issues are architectural: caller identity is self-asserted, cached same-UID sessions can still be used by local agents, and seal/escalate is an intentional availability lockout that can be triggered by repeated denied requests.

## Scope & Methodology

Reviewed branch and version:

| Item | Value |
|---|---|
| Repository | `https://github.com/nim444/Svault` |
| Branch reviewed | `feat/0.9.9-conditional-access-seal` |
| Commit reviewed | `c1696e9` |
| Cargo version | `0.9.9` |
| Latest tag observed | `v0.9.8` |

Files and areas reviewed:

| Area | Files |
|---|---|
| Daemon and client routing | `src/daemon/mod.rs`, `src/daemon/client.rs` |
| Policy, gate, judge | `src/core/policy.rs`, `src/core/gate.rs`, `src/core/judge.rs` |
| Storage, crypto, metadata | `src/core/vault.rs`, `src/core/crypto.rs`, `src/core/meta.rs`, `src/core/secfile.rs` |
| Master/keyslot/session model | `src/core/master.rs`, `src/core/session.rs`, `src/core/keyring.rs`, `src/core/recovery.rs` |
| YubiKey | `src/core/yubikey.rs` |
| MCP | `src/mcp/mod.rs` |
| CLI privileged actions | `src/cli/mod.rs` |
| CI/CD | `.github/workflows/*.yml` |
| Docs and review prompt | `docs/security.md`, `docs/daemon.md`, `docs/policy-engine.md`, `docs/mcp.md`, `docs/security-review/PROMPT.md` |

Commands run on the 0.9.9 branch:

| Command | Result |
|---|---|
| `cargo test --locked` | Passed: 140 passed, 1 ignored stress test |
| `cargo clippy --all-targets -- -D warnings` | Passed |
| `cargo clippy --all-targets --all-features -- -D warnings` | Passed |
| `cargo fmt --all --check` | Passed |
| `cargo audit --deny warnings` | Completed without reported advisories |

## Security Findings

### Critical / High Severity

No critical or high-severity findings identified in the reviewed 0.9.9 branch.

### Medium Severity

| ID | Finding | Evidence | Impact | Recommendation |
|---|---|---|---|---|
| M-1 | `require_callers` is based on self-asserted caller identity, not OS-bound identity | `policy.rs:416-423` enforces `rule.require_callers` against `req.caller`; CLI/MCP accept caller from user input or environment | If an agent knows or guesses an allowed caller name, it can impersonate that caller in the cooperative model | Treat `require_callers` as a cooperative policy control, not strong identity. For 1.0, bind caller rules to OS process identity, signed tool identity, parent process, executable hash, or a controlled launcher |
| M-2 | A same-UID agent can clear seals if it can use an unlocked file/master session | `cmd_approve` in `cli/mod.rs:1443-1460` clears encrypted seals after `open_unlocked_or_prompt`; session fallback opens without fresh human confirmation | The claim "agent cannot clear seal without master" is true for MCP/gated paths, but not for a same-UID process with shell access and an existing session | Require explicit fresh master re-authentication for `svault approve`, or at least document that cached sessions can approve. Consider refusing approval from non-TTY contexts |
| M-3 | Seal/escalate is an intentional availability DoS surface | `gate.rs:231-265` seals medium/high secrets after five denials in 300 seconds; `daemon/mod.rs:369-374` calls it on every denied gated request | A cooperative or compromised agent can force human intervention by making denied requests against a medium/high secret | Keep the short-circuit once sealed, but consider per-caller and per-peer throttling before seal, cooldowns, audit escalation without immediate lockout, or configurable thresholds for CI |
| M-4 | Rate/burst limiting remains caller-string scoped and can be bypassed by caller rotation | `policy.rs:484-509` counts prior allowed requests by `caller`; `audit.rs:121-140` filters by `entry.caller` | An agent can rotate `--caller` to evade rate/burst limits, although 0.9.9 seal counting correctly counts denials by secret across callers | Count rate/burst by peer UID in daemon mode, by secret, or by both caller and peer UID. Keep the current cross-caller seal logic |

### Low / Informational

| ID | Finding | Evidence | Impact | Recommendation |
|---|---|---|---|---|
| L-1 | 0.9.9 is not currently tagged as a release in the remote repository | `origin/main` is 0.9.8 and latest tag observed is `v0.9.8`; 0.9.9 is only on `feat/0.9.9-conditional-access-seal` | Release state is ambiguous for downstream security reviewers and adopters | Merge/tag 0.9.9 before calling it current released state, or update the prompt to say "review the 0.9.9 feature branch" |
| L-2 | AI judge prompt injection remains a soft-control risk | `judge.rs:177-199` embeds caller-provided `reason` into user prompt; parser accepts first JSON block | A malicious reason can try to manipulate the model; base policy and thresholds mitigate but do not eliminate this | Keep fail-closed for high tier. Consider stronger structured prompt separation, model response schema enforcement, and prompt-injection tests |
| L-3 | Windows owner-only ACL path is best effort | `secfile.rs:46-57` uses `icacls` and ignores failures | Windows protection is weaker than Unix permissions and daemon model is Unix-only | Return and surface ACL failures on Windows for sensitive files, or clearly downgrade Windows support for corporate use |
| L-4 | `serde_yaml` is deprecated in dependency output | Build uses `serde_yaml v0.9.34+deprecated` | Not an immediate vulnerability per `cargo audit`, but a maintenance signal | Plan migration away from deprecated YAML parsing or confine YAML to non-sensitive metadata |

## Positive Security Properties Observed

| Area | Positive property |
|---|---|
| Daemon | Unix socket is created under tight umask and chmodded `0600`; peer UID is checked with `SO_PEERCRED` or `getpeereid`; daemon drops different/unknown UIDs |
| Agent path | `svault get` uses `GetGated`; daemon evaluates `gate::authorize` before returning values |
| MCP | `svault_get_secret` routes through daemon `get_gated` or local `gate::gated_get`; it does not prompt for the master passphrase |
| Policy at rest | `VaultPolicyData` including classification, caller rules, judge assignment, conditional windows, and seals is encrypted in `vault.enc` |
| Metadata | `meta.yaml` is HMAC-verified and intentionally excludes policy-relevant data |
| Sessions | `.session`, `.master.session`, and `.keyring.session` carry timestamped key material and expire via `MAX_SESSION_SECS` |
| Keyslots | Master model is clear: passphrase, recovery, and YubiKey are OR slots, not 2FA |
| Crypto | AES-256-GCM and Argon2id are used appropriately for the reviewed design; wrong keys fail closed via GCM authentication |
| Tests | Branch includes targeted tests for conditions, seals, MCP generic denial, daemon gate behavior, session expiry, and encrypted-at-rest policy |
| CI | Workflows run rustfmt, clippy with all features, cargo-audit, multi-OS tests, release checksums, and SLSA provenance attestation |

## The Unix Daemon (0.5.0+) - Detailed Analysis

The Unix daemon is a meaningful security improvement over file-only sessions. It holds vault keys in memory, serves over `.svault/daemon.sock`, checks peer UID, and applies policy inside the process that holds the key.

Key observations:

| Control | Assessment |
|---|---|
| Socket permissions | Good. `daemon/mod.rs:742-749` binds under `umask(077)` and sets socket mode `0600` |
| Peer UID | Good for same-user boundary. `daemon/mod.rs:542-577` uses Linux `SO_PEERCRED` or macOS/BSD `getpeereid`; `daemon/mod.rs:596-599` refuses nonmatching or unknown UID |
| Passphrase over socket | Good. Client sends 32-byte unwrapped/derived key via `Unlock`, not plaintext passphrase |
| Gated access | Good. `GetGated` builds a policy request, resolves judge, calls `gate::authorize`, audits, and only then returns the secret |
| Human path | Expected but important. `Request::Get` is ungated and corresponds to `svault secret get`, the human path |
| Connection controls | Good baseline. Connection ceiling, per-connection read timeout, retrying client send, and poisoned mutex recovery are present |
| Auto-lock | Good. The ticker evicts idle or hard-max-aged keys; hard cap aligns with six-hour session expiry |
| Shutdown | Good. Signal watcher sets shutdown and accept loop clears held keys and removes socket/pid files |
| Residual risk | Same-UID hostile processes remain out of scope; a hostile same-UID process can attack daemon memory, use approved CLI/session paths, or drive denial/seal workflows |

The daemon substantially improves corporate risk posture on Unix because the best path avoids writing vault DEKs to `.session`. It does not turn Svault into a sandbox.

## Infrastructure, Server & CI/CD Deployment Considerations

Recommended safe usage patterns on Unix:

| Scenario | Guidance |
|---|---|
| Developer workstation | Use daemon mode, avoid file sessions where possible, keep vaults under `0700` `.svault`, and use `svault lock --all` after sensitive work |
| CI with cooperative agents | Use short-lived runners, daemon mode, scoped low/medium secrets, strong caller rules, and audit collection |
| Build agents | Prefer ephemeral environments and vaults containing only task-specific credentials |
| High-value secrets | Keep high-tier secrets human-only unless a strongly controlled runner and judge configuration exist |
| MCP | Use MCP only after human unlock; do not give the MCP process general shell authority if seal approval must remain human-only |

Windows and non-Unix:

| Environment | Risk |
|---|---|
| Windows | Higher risk because the Unix daemon and peer UID bond do not apply; ACL enforcement is best-effort |
| Non-Unix unsupported/partial | Treat as file-session mode with weaker local protections |
| Shared CI hosts | Avoid if multiple jobs/users share the same OS account or persistent workspace |

## Recommendations for Corporate Adoption

Priority recommendations:

| Priority | Recommendation |
|---|---|
| P0 | Do not market 0.9.9 as "current released" until it is merged/tagged, or explicitly tell reviewers to inspect `feat/0.9.9-conditional-access-seal` |
| P1 | Bind caller identity to something stronger than a self-asserted string before treating `require_callers` as an enterprise access-control feature |
| P1 | Require fresh human master re-authentication for `svault approve`, especially if seals are positioned as human-clear-only escalation |
| P1 | Count rate and burst limits by daemon peer UID and secret, not only caller string |
| P2 | Add operational controls for seal thresholds, cooldowns, and "audit-only escalation" in CI where denial-induced lockout can break builds |
| P2 | Surface Windows ACL failures instead of ignoring `icacls` errors |
| P2 | Add prompt-injection regression tests for `reason`, `description`, and judge criteria |
| P3 | Replace deprecated `serde_yaml` over time |

1.0.0 stable label assessment:

| Criterion | Assessment |
|---|---|
| Enforced policy engine | Mostly sound for cooperative/semi-trusted agents |
| Same-UID hostile boundary | Correctly documented as out of scope |
| Corporate workstation use | Reasonable with daemon mode and training |
| Corporate CI use | Conditionally acceptable for scoped secrets on ephemeral Unix runners |
| High-security production use | Not ready without stronger caller identity and fresh human approval controls |
| 1.0 readiness | Close, but I would address caller identity semantics and seal approval re-auth before a security-stable 1.0 claim |

## Overall Risk Assessment by Context

| Context | Risk | Assessment |
|---|---:|---|
| Personal use, Unix daemon | Low-Medium | Good at-rest protection and useful agent gate |
| Small team, Unix | Medium | Adoptable with clear same-UID limitations |
| Corporate developer workstations | Medium | Reasonable pilot candidate with daemon-only guidance |
| Corporate CI/CD with AI agents on Unix | Medium | Use ephemeral runners and scoped secrets; avoid high-value prod secrets |
| High-security production infrastructure | High | Not suitable as primary control boundary |
| Mixed Windows environments | Medium-High | Weaker local security model and no Unix daemon protections |
| Hostile same-UID threat model | High | Explicitly out of scope |

## References

Key reviewed files:

| File | Relevance |
|---|---|
| `src/daemon/mod.rs` | Unix daemon, socket, peer UID, `GetGated`, seal write-on-deny |
| `src/daemon/client.rs` | CLI routing through daemon |
| `src/core/policy.rs` | Base policy, conditional access, caller rules, seal constants |
| `src/core/gate.rs` | Enforced gate, judge fail modes, seal check/write |
| `src/core/judge.rs` | OpenRouter judge prompt, response parser, fail modes |
| `src/core/master.rs` | Master keyslot model, recovery, YubiKey slot |
| `src/core/keyring.rs` | Encrypted judge registry/config/API keys |
| `src/core/yubikey.rs` | Optional FIDO2 hmac-secret support |
| `src/core/session.rs` | Timestamped file sessions and six-hour cap |
| `src/core/secfile.rs` | Owner-only file/dir handling |
| `src/mcp/mod.rs` | MCP tool routing and generic denials |
| `.github/workflows/lint.yml` | fmt, clippy all-features, cargo-audit |
| `.github/workflows/release.yml` | release build, checksums, provenance |

External resources consulted:

| Resource | Use |
|---|---|
| RustSec advisory database via `cargo audit` | Dependency advisory check |
| GitHub remote metadata via `git remote show origin` and `git ls-remote` | Branch/tag verification |
