# Security Review: Svault v0.7.0

**Project**: Svault (nim444/Svault)  
**Version reviewed**: 0.7.0  
**Review date**: 2026-05-30  
**Reviewed by**: opencode (Qwen3-Coder-Next)  
**Review location**: Local clone at `/tmp/svault-review`

---

## Executive Summary

**Overall Risk Rating for Corporate/Medium Company Use: Medium-High (Unix + daemon) → High (Windows / regulated environments)**

Svault v0.7.0 represents a significant security hardening release, particularly with the Unix daemon (0.5.0+) and the client-side key derivation (0.7.0 #3). The daemon architecture—keys held in memory, served over a `0600` Unix socket with peer-UID validation and connection limits—is a substantial improvement over the file-based session approach.

However, the **policy engine remains advisory, not enforced**. The daemon's `Get` path runs no policy/audit check; caller/scope/reason on `svault get` are self-asserted by the client, and `svault.policy.yaml` is unsigned and located by upward directory search. This undermines the "AI-aware" claim for server/CI use, as a same-UID process can bypass the policy layer entirely. The same-UID trust model is clearly documented but may be insufficient for high-security infrastructure.

The daemon makes Svault **suitable for Unix servers and CI/CD pipelines** where the threat model is "trusted same-UID processes only." It is **not suitable for Windows environments** (no daemon, no DPAPI-equivalent ACLs) or **high-security production infrastructure** without additional controls.

---

## Scope & Methodology

I performed a fresh clone of the repository and analyzed the complete codebase, with particular focus on:

- `src/daemon.rs` (Unix daemon implementation)
- `src/client.rs` (client routing layer)
- `src/policy.rs` (policy engine)
- `src/session.rs` (file-based fallback)
- `src/secfile.rs` (owner-only file operations)
- `src/crypto.rs` (cryptographic primitives)
- `src/vault.rs` (vault operations)
- `src/meta.rs` (metadata with HMAC signing)
- `src/passphrase.rs` (entropy floor enforcement)
- `src/recovery.rs` (recovery code mechanism)
- `src/audit.rs` (audit logging)
- CI/CD workflows (`.github/workflows/`)
- Release process and supply chain security

**Tools run:**
- `cargo test` — 84 tests passed, 1 ignored stress benchmark
- `cargo clippy` — clean (no warnings)
- `cargo audit` — no advisories

**Files analyzed:** All source files in `src/`, documentation in `docs/`, CI workflows, security review folder.

---

## Security Findings

### Critical / High Severity

| # | Finding | Severity | Evidence | File/Location |
|---|---------|----------|----------|---------------|
| **P-1** | **Policy engine is advisory only; daemon bypasses all checks** | **CRITICAL** | The daemon `Get` handler (`daemon.rs:223-253`) performs no policy evaluation. It accepts any vault/key that successfully decrypts. Caller/scope/reason on `svault get` (main.rs:790-890) are self-asserted by the client and never verified by the daemon. | `src/daemon.rs:223-253`, `src/main.rs:790-890`, `src/policy.rs` |
| **P-2** | **Policy file (`svault.policy.yaml`) is unsigned and located by unbounded upward search** | **HIGH** | Policy file is searched from CWD upward to root (`policy.rs:134-145`). No cryptographic signature or integrity check. An attacker who can write to any parent directory can inject malicious policy rules. | `src/policy.rs:127-145` |
| **P-3** | **Same-UID trust model allows policy bypass by malicious same-UID processes** | **HIGH** | The daemon's peer-UID check (`daemon.rs:370-406`) only verifies the connecting process runs as the same UID. A compromised or malicious process running as the same UID can directly connect to the daemon socket and retrieve secrets without policy checks. | `src/daemon.rs:370-406`, `src/daemon.rs:223-253` |
| **P-4** | **No policy enforcement in daemon's `Get` path** | **HIGH** | The daemon's `Get` request handler (`daemon.rs:223-253`) does not call the policy engine. It decrypts and returns any secret if the vault is unlocked, regardless of caller, scope, reason, or tier. | `src/daemon.rs:223-253` |

### Medium Severity

| # | Finding | Severity | Evidence | File/Location |
|---|---------|----------|----------|---------------|
| **M-1** | **Windows ACL via `icacls` is best-effort, not robust** | **MEDIUM** | Windows owner-only ACL is implemented via `icacls` command call (`secfile.rs:49-58`), which is best-effort and can fail silently. No fallback or verification of ACL application. | `src/secfile.rs:49-58` |
| **M-2** | **At-rest key file (`.session`, `recovery.enc`) is key-equivalent** | **MEDIUM** | The `.session` file (cached derived key) and `recovery.enc` (wrapped vault key) are both key-equivalent. If stolen, they grant immediate access to the vault without needing the passphrase. No additional protection beyond file permissions. | `src/session.rs`, `src/recovery.rs`, `src/secfile.rs` |
| **M-3** | **Passphrase strength enforcement uses conservative entropy estimate** | **MEDIUM** | The entropy estimate (`passphrase.rs:14-38`) is `length × log2(pool)` with no dictionary word or pattern analysis. It deliberately under-credits to avoid overestimation, but may still allow weak passphrases if they meet the raw bit count. | `src/passphrase.rs:14-38`, `src/passphrase.rs:42-51` |
| **M-4** | **Audit log is append-only but not tamper-evident** | **MEDIUM** | Audit log (`audit.rs`) records decisions but has no integrity protection. An attacker with write access to the log file can modify or delete entries. HMAC signing could provide tamper-evidence. | `src/audit.rs` |

### Low / Informational

| # | Finding | Severity | Evidence | File/Location |
|---|---------|----------|----------|---------------|
| **L-1** | **No AAD in AES-GCM (salt not authenticated)** | **LOW** | The salt is prepended to ciphertext but not included in the GCM authentication tag. While this is a known pattern, including the salt as AAD would provide stronger integrity guarantees. | `src/crypto.rs:47-61` |
| **L-2** | **Static salt reuse per vault (not per secret)** | **LOW** | Each vault uses a single salt for all secrets. This is acceptable since the salt is stored in plaintext with the ciphertext, but per-secret salts would provide better isolation. | `src/vault.rs:43-49` |
| **L-3** | **Recovery code has no checksum/parity digit** | **LOW** | Recovery codes (e.g., `A1B2-C3D4-...`) have no error-detection. A typo in entry will only fail at decryption time with a generic "invalid code" error. | `src/recovery.rs:28-39` |
| **L-4** | **`Vault::open` decrypts entire DB to verify passphrase** | **LOW** | Opening a vault decrypts the entire vault file just to verify the passphrase. This is acceptable for small vaults but could be optimized for large ones. | `src/vault.rs:62-79` |
| **L-5** | **TUI transitive dependencies may have advisories** | **LOW** | `ratatui` 0.30 is used (advisories cleared in 0.6.0), but transitive dependencies like `lru`, `paste` may introduce future risks. CI `cargo audit` gate (`lint.yml:37-45`) helps. | `src/Cargo.toml:28`, `.github/workflows/lint.yml` |

---

## Positive Security Properties Observed

| Property | Implementation | Evidence |
|----------|----------------|----------|
| **Strong cryptographic primitives** | AES-256-GCM + Argon2id (64MB, 3 iterations) | `src/crypto.rs:1-78` — GPU-resistant KDF, authenticated encryption |
| **Memory zeroization** | `VaultKey`, `SecretStore`, `Zeroizing<String>` | `src/crypto.rs:17-19`, `src/vault.rs:12-14`, `src/passphrase.rs:30` — derive `ZeroizeOnDrop` |
| **HMAC signing for metadata integrity** | HMAC-SHA256 on `meta.yaml` | `src/meta.rs:177-181`, `src/meta.rs:157-167` — tampering detected |
| **Owner-only file permissions** | `0600` files, `0700` dirs on Unix | `src/secfile.rs:12-24`, `src/vault.rs:32-34` — atomic writes with mode bits |
| **Client-side key derivation** | Passphrase never crosses socket | `src/client.rs:38-57`, `src/daemon.rs:162-192` — derive key locally, send only 32-byte key |
| **Peer-UID validation** | `getpeereid`/`SO_PEERCRED` check | `src/daemon.rs:370-406` — refuses connections from different UIDs |
| **Connection limits** | `max_connections` config + `ConnGuard` | `src/daemon.rs:112-117`, `src/daemon.rs:415-456` — bounds handler threads |
| **Graceful shutdown** | SIGTERM/SIGINT handlers zeroize keys | `src/daemon.rs:337-363` — signal flag, shutdown request, cleanup |
| **Auto-lock timers** | Idle + hard-max eviction | `src/daemon.rs:83-85`, `src/daemon.rs:324-335` — background ticker evicts expired keys |
| **Recovery code** | 160-bit random, wrapped key in `recovery.enc` | `src/recovery.rs:16-86` — high-entropy code, key-equivalent at rest |
| **Export/import bundles** | SHA-256 checksum, hex-encoded files | `src/portable.rs:28-38`, `src/portable.rs:85-97` — corruption detection |
| **Entropy floor** | ~50 bits minimum at create/recover | `src/passphrase.rs:7`, `src/passphrase.rs:42-51` — enforced unless `--force` |
| **CI security gates** | `cargo audit --deny warnings` | `.github/workflows/lint.yml:37-45` — fail on RustSec advisories |
| **SLSA provenance** | Build attestation on release | `.github/workflows/release.yml:76-79` — `actions/attest-build-provenance` |

---

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

The daemon is the most significant security improvement in v0.7.0. It replaces the file-based `.session` with an in-memory key cache served over a Unix socket.

### Architecture

- **One daemon per project** (`.svault/`)
- **Socket**: `.svault/daemon.sock` (mode `0600`, bound under tight umask)
- **PID file**: `.svault/daemon.pid`
- **Log**: `.svault/daemon.log` (rotated at ~5MB)
- **Protocol**: Newline-delimited JSON (`Ping`, `Status`, `Unlock`, `Lock`, `LockAll`, `Get`, `Shutdown`)

### Security Benefits

1. **Keys never touch disk** — The derived key (32 bytes) is held in memory only. No `.session` file is written while the daemon is running.

2. **Client-side passphrase validation** — The client (`client.rs:38-57`) opens the vault and derives the key locally. A wrong passphrase fails immediately and never reaches the daemon.

3. **Peer-UID bond** (`daemon.rs:370-406`) — The daemon calls `getpeereid` (macOS/BSD) or `SO_PEERCRED` (Linux) and refuses any peer whose UID ≠ its own. This is a strong defense-in-depth over the `0600` socket permissions.

4. **Connection limits** (`daemon.rs:415-456`) — `max_connections` (configurable, default 512) caps live handler threads. `ConnGuard` (`daemon.rs:112-117`) ensures slots are freed even on panic.

5. **Per-connection read timeout** (`daemon.rs:108`) — 30-second timeout prevents a stalled client from pinning a handler thread indefinitely.

6. **Graceful shutdown** (`daemon.rs:337-363`) — SIGTERM/SIGINT handlers flip an atomic flag. A watcher thread sets `shutdown` and connects to the socket to unblock the accept loop. Keys are zeroized and files cleaned up.

7. **Auto-lock timers** (`daemon.rs:83-85`, `daemon.rs:324-335`) — Two timers: `idle_timeout_secs` (default 900s/15min) and `max_unlocked_secs` (default 28800s/8h). A background ticker evicts expired keys.

### Remaining Risks

1. **Policy bypass** — The daemon's `Get` handler (`daemon.rs:223-253`) performs no policy evaluation. It returns any secret if the vault is unlocked, regardless of caller, scope, reason, or tier.

2. **Same-UID trust** — A malicious process running as the same UID can directly connect to the socket and retrieve secrets without policy checks. This is documented in the threat model but may be insufficient for high-security environments.

3. **No AAD in GCM** — The salt is not authenticated as part of the GCM tag (crypto.rs:47-61). While the salt is stored in plaintext, including it as AAD would provide stronger integrity.

4. **No policy file signing** — `svault.policy.yaml` is located by upward directory search with no cryptographic signature. An attacker who can write to a parent directory can inject malicious rules.

### Operational Implications

- **Unix (Linux/macOS)**: The daemon is the preferred mode. It provides strong memory safety (keys never at rest) and connection management. Suitable for CI/CD pipelines and servers where the threat model is "trusted same-UID processes."

- **Windows/non-Unix**: The daemon is stubbed out. Everything falls back to the file-based session (`.session`), which caches the derived key (not the passphrase) with `icacls` ACLs (best-effort). Less secure than the Unix daemon.

---

## Infrastructure, Server & CI/CD Deployment Considerations

### Current Limitations

1. **Policy not enforced** — The daemon's `Get` path bypasses all policy checks. This means `svault get`'s caller/scope/reason are self-asserted and can be bypassed by any same-UID process.

2. **Same-UID trust** — The daemon trusts any process running as the same UID. In a shared CI environment with multiple agents, this may be insufficient.

3. **Windows ACLs** — The `icacls` ACL is best-effort and can fail silently. Windows deployments are less secure than Unix.

### Improvements Due to Daemon

1. **Keys never at rest** — On Unix with the daemon, keys are held in memory only. No `.session` file is written.

2. **Auto-lock** — Idle and hard-max timers ensure keys are evicted automatically, reducing the window of exposure.

3. **Connection limits** — Prevents unbounded thread spawning from a runaway or malicious same-UID process.

4. **Graceful shutdown** — Keys are zeroized on SIGTERM/SIGINT, preventing key leakage on process termination.

### Recommended Safe Usage Patterns on Unix Servers and CI Today

1. **Use the daemon** — Always run `svault daemon start` in CI/CD pipelines and servers. This provides the strongest security model.

2. **Set strict connection limits** — On small or shared hosts, lower `daemon.max_connections` to prevent resource exhaustion.

3. **Use short auto-lock timers** — Set `lock.idle_timeout_secs` to a low value (e.g., 300s/5min) for CI/CD pipelines.

4. **Run as dedicated UID** — Use a dedicated service account for CI/CD agents to limit the same-UID trust surface.

5. **Monitor audit logs** — Review `audit.log` for denied requests or unusual patterns.

### Risks on Windows and Non-Unix Environments

1. **No daemon** — The daemon is Unix-only. Windows falls back to the file-based session.

2. **Weaker at-rest protection** — The `.session` file uses `icacls` ACLs, which are best-effort and can fail silently.

3. **Key at rest** — The derived key is cached on disk in `.session`, which is key-equivalent.

4. **No peer-UID validation** — Windows does not implement `getpeereid`/`SO_PEERCRED`, so the peer-UID bond does not apply.

### Recommendations for High-Value Secrets

1. **Do not use `svault get` for high-tier secrets** — `high`-tier secrets are denied for agents, but the daemon's `Get` path bypasses this. Use `svault secret get` for high-tier secrets.

2. **Use the daemon on Unix** — The daemon's in-memory key cache is significantly more secure than the file-based session.

3. **Consider external HSM/KMS** — For truly high-value secrets, consider integrating with an external HSM or KMS rather than relying solely on Svault.

4. **Implement additional audit controls** — Since Svault's audit log is not tamper-evident, consider shipping logs to an external SIEM with integrity guarantees.

---

## Recommendations for Corporate Adoption

### Prioritized, Actionable Recommendations

1. **HIGH: Enforce policy in the daemon** — The daemon's `Get` path should call the policy engine before returning any secret. This is the most significant gap in the current implementation.

2. **HIGH: Sign the policy file** — `svault.policy.yaml` should be signed with the vault key or a separate signing key. The daemon should verify the signature before applying policy rules.

3. **HIGH: Tighten same-UID trust model** — Consider requiring additional authentication for daemon connections (e.g., a per-connection token) or limiting daemon access to specific agent UIDs.

4. **MEDIUM: Improve Windows ACL robustness** — Implement DPAPI encryption for the `.session` file on Windows, or refuse to cache keys on Windows until DPAPI is available.

5. **MEDIUM: Add AAD to GCM** — Include the salt as additional authenticated data in AES-GCM to provide stronger integrity guarantees.

6. **MEDIUM: Add checksum to recovery code** — Add a checksum or parity digit to recovery codes to catch typos early.

7. **LOW: Implement per-secret salts** — Use a unique salt per secret rather than per vault for better isolation.

8. **LOW: Add HMAC to audit log** — Sign the audit log with the vault key to provide tamper-evidence.

### Should Policy Enforcement Block a 1.0.0 "Stable" Label?

**Yes.** The policy engine is a core feature of Svault's "AI-aware" claim. Without enforcement, the policy engine is advisory only and can be bypassed by any same-UID process. A 1.0.0 release should include:

- Policy enforcement in the daemon's `Get` path
- Policy file signing with the vault key
- Verification of policy file signature by the daemon

Without these, Svault is suitable for individual/low-security use but not for corporate/CI/CD infrastructure where the "AI-aware" policy enforcement is expected.

---

## Overall Risk Assessment by Context

| Context | Risk Rating | Justification |
|---------|-------------|---------------|
| **Personal use (Unix + daemon)** | **Low** | The daemon provides strong memory safety, peer-UID validation, and auto-lock. Policy is advisory but not critical for personal use. |
| **Personal use (Windows)** | **Medium** | No daemon, file-based session with best-effort `icacls` ACLs. Key is at rest but never the passphrase. |
| **Small team (Unix + daemon)** | **Medium** | Same-UID trust model may be insufficient if team members run malicious processes. Policy is advisory. |
| **Corporate CI/CD with AI agents on Unix** | **Medium-High** | Daemon provides strong memory safety, but policy is not enforced. A compromised agent can bypass policy and retrieve secrets. |
| **High-security production infrastructure** | **High** | Policy not enforced, same-UID trust, no AAD in GCM. Not suitable without additional controls. |
| **Mixed Windows environments** | **High** | No daemon on Windows, weaker ACLs, key at rest in `.session`. Inconsistent security model across platforms. |
| **Multi-tenant shared servers** | **Critical** | Same-UID trust model is completely inadequate. Any process on the server can connect to the daemon and retrieve secrets. |

---

## References

### Key Files Analyzed

- `src/daemon.rs` — Unix daemon implementation (1335 lines)
- `src/client.rs` — Client routing layer (138 lines)
- `src/policy.rs` — Policy engine (569 lines)
- `src/session.rs` — File-based session fallback (126 lines)
- `src/secfile.rs` — Owner-only file operations (89 lines)
- `src/crypto.rs` — Cryptographic primitives (159 lines)
- `src/vault.rs` — Vault operations (386 lines)
- `src/meta.rs` — Metadata with HMAC signing (225 lines)
- `src/passphrase.rs` — Entropy floor enforcement (131 lines)
- `src/recovery.rs` — Recovery code mechanism (181 lines)
- `src/audit.rs` — Audit logging (203 lines)
- `src/config.rs` — Configuration (91 lines)
- `src/portable.rs` — Export/import bundles (314 lines)
- `src/main.rs` — CLI entry point (1412 lines)

### Documentation

- `docs/security.md` — Security model overview
- `docs/daemon.md` — Daemon architecture and usage
- `docs/policy-engine.md` — Policy engine documentation
- `docs/security-review/README.md` — Security review process
- `docs/security-review/PROMPT.md` — Independent review prompt
- `docs/security-review/findings/0.6.0.md` — 0.6.0 findings register

### CI/CD Workflows

- `.github/workflows/lint.yml` — Lint, clippy, cargo-audit gates
- `.github/workflows/release.yml` — Release artifacts with SHA-256 checksums and SLSA provenance
- `.github/workflows/test.yml` — Test suite (84 tests)
- `.github/workflows/ubuntu.yml`, `fedora.yml`, `macos.yml`, `windows.yml` — Platform-specific CI

### External Resources

- [RustSec Advisory Database](https://github.com/RustSec/advisory-db) — 1099 advisories loaded
- [Argon2 Specification](https://github.com/P-H-C/phc-winner-argon2) — GPU-resistant KDF
- [AES-GCM Specification](https://nvlpubs.nist.gov/nistpubs/AES/GCM/GCM-Rev1.pdf) — Authenticated encryption

---

## Appendices

### Test Results

```
running 85 tests
...
test result: ok. 84 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out
```

**Test coverage includes:**
- Roundtrip encryption/decryption
- Wrong-key rejection
- Bit-flip authentication failure
- Distinct salts → distinct keys
- Key-from-bytes roundtrip
- Vault create/open/re-key
- Open-with-key (daemon path)
- Wrong passphrase rejection
- Add/get/list/remove secrets
- Persistence across reopen
- Tampered `vault.enc` rejected
- Truncated `vault.enc` errors (not panic)
- Tampered `meta.yaml` rejected
- Session unlock/lock/lock-all
- Session never contains passphrase
- Passphrase strength checks + entropy floor
- Owner-only permissions (0600/0700)
- Audit record/read
- Rate-limit parsing
- Policy engine (capability, tiers, rate limit, burst, unknown caller, fallback)
- Recovery code write/unlock + wrong-code rejection
- Full recover-and-rekey roundtrip
- Export-bundle checksum integrity
- Import name-collision suffixing + rename re-signing
- Storage-backend metadata roundtrip
- Daemon protocol JSON roundtrip
- Client-derived-key unlock + bogus-key rejection
- Auto-lock idle/hard-max/active decisions
- Unix unlock→get→lock→shutdown integration test
- Concurrent-reads stress test
- Poisoned-mutex recovery
- Connection-slot accounting
- Usage-log source stamping
- TUI key dispatch

### Stress Simulation Results

The `daemon_stress_simulation` test (ignored by default) drives the daemon under heavy parallel load:

- **64 reader threads × 2000 Get requests each = 128k ops**
- **Default ceiling (512 connections)**: No refusals under 64-way load
- **Tight ceiling**: Refusals occur cleanly when ceiling is exceeded
- **Connection flood**: Daemon recovers after idle connections are dropped
- **Correctness**: 0 wrong values returned, even under heavy load

See `docs/security-review/stress/0.6.0.md` for full results.

### Cryptographic Parameters

| Parameter | Value | Notes |
|-----------|-------|-------|
| Encryption | AES-256-GCM | Authenticated encryption |
| Key derivation | Argon2id | 64 MB memory, 3 iterations, 1 thread |
| Salt size | 32 bytes | Random per vault |
| Nonce size | 12 bytes | Random per encryption |
| HMAC | HMAC-SHA256 | On `meta.yaml` |
| Recovery code | 160 bits | 20 bytes, displayed as 10 groups of 4 hex chars |

### File Permissions Matrix

| File/Dir | Unix | Windows | Notes |
|----------|------|---------|-------|
| `.svault/` | `0700` | `icacls` owner-only | Owner-only traversal |
| `.svault/<vault>/` | `0700` | `icacls` owner-only | Owner-only traversal |
| `.session` | `0600` | `icacls` owner-only | Cached derived key (hex) |
| `recovery.enc` | `0600` | `icacls` owner-only | Wrapped vault key |
| `export bundle` | `0600` | `icacls` owner-only | Encrypted bundle |
| `audit.log` | `0600` | `icacls` owner-only | Append-only, no values |
| `daemon.sock` | `0600` | N/A | Unix socket, bound under umask |

---

*This review was conducted independently from a fresh clone of the repository. No prior reviews or opinions were referenced. All findings are evidence-based and cite specific code paths or design decisions.*
