Seamless file encryption & per-process access control for macOS.
Protect ~/.ssh, ~/.aws, ~/.kube, and other sensitive directories from supply chain attacks, malicious packages, and unauthorized processes. Files are encrypted at rest and only decrypted for verified, code-signed binaries.
Links: Website | Buy Me a Coffee
Trust & Quality
Zero telemetry. All data stays on your machine.
Powered by onllm.dev
Supply chain attacks like the litellm credential stealer (March 2026) demonstrate a critical gap: any process running as your user can read your SSH keys, AWS credentials, and Kubernetes configs. Standard Unix permissions offer no protection against same-user attacks.
A malicious PyPI/npm package runs as you. It reads ~/.ssh/id_rsa, ~/.aws/credentials, ~/.kube/config — and exfiltrates everything. FileVault doesn't help (it's full-disk, not per-process). No existing macOS tool combines file encryption with per-process access control.
onvault uses a two-layer defense:
Layer 1 — Encryption at Rest (macFUSE): Your sensitive directories are encrypted on disk. Files are only decrypted through a FUSE mount when onvault is running and you've authenticated. Kill the daemon? Files stay encrypted. Attacker reads disk? Ciphertext only.
Layer 2 — Per-Process Access Control (Endpoint Security Framework, when available): On builds/runtimes with the required ESF entitlement and permissions, only verified processes can read mounted plaintext. /usr/bin/ssh can read ~/.ssh/id_rsa. python3 cannot. Process identity is verified by Apple's code signing (cdHash + Team ID), not just the binary path.
Malicious package runs → tries to read ~/.ssh/id_rsa
→ Layer 2: python3 not in allowlist → DENIED
→ Even without Layer 2: file on disk is AES-256-XTS ciphertext
You run `ssh user@host`:
→ Layer 2: /usr/bin/ssh is in allowlist, Apple-signed → ALLOWED
→ Layer 1: FUSE decrypts on-the-fly → SSH reads the key
# macFUSE (one-time, requires reboot)
brew install --cask macfuse
# Approve the system extension in System Settings → Privacy & Security
# Rebootgit clone https://github.com/onllm-dev/onvault.git
cd onvault
make # Development build
make test # Run test suite (172 tests across 19 suites)
make dist # Distribution build (macFUSE still required at runtime)# 1. First-time setup — sets passphrase, generates a recovery key display
onvault init
# 2. Start the daemon (shows menu bar icon + web UI)
onvault start # with menu bar
onvault start --no-gui # headless (servers, CI)
# 3. Unlock — authenticates, loads the master key, and mounts configured vaults
onvault unlock
# 4. Protect directories — encrypts files, creates symlink
onvault vault add ~/.ssh --smart # encrypt + auto-populate allowlist
onvault vault add ~/.aws --smart # encrypt AWS credentials
# 5. Status — see what's protected
onvault status
# 6. Lock — unmount vaults, wipe keys from memory (passphrase required)
onvault lockRecovery keys are displayed during onvault init. If you lose your passphrase, use onvault recover with the recovery key to set a new passphrase. Touch ID unlock is available after the first passphrase unlock.
The daemon shows a menu bar icon and serves an interactive web UI. Click the lock icon or open http://127.0.0.1:<port>/menubar in any browser.
The web UI unlock flow returns a localhost-only bearer token; subsequent API calls use that token rather than unauthenticated localhost access.
Everything can be done from the menu bar — no CLI needed:
- Unlock/Lock — Touch ID first, passphrase fallback
- Add Vault — type a path or use quick-add buttons (~/.ssh, ~/.aws, etc.)
- Allow/Deny Process — inline input per vault with UID/GID options
- Global Rules — rules that apply across all vaults
- View Rules — per-vault and global rule management
- Watch Mode — start/stop directory observation from the Watch tab
- Audit Log — filterable log viewer (All / Denied)
- Settings — per-vault verify mode, key rotation, recovery key, log forwarding
- Recent Denials — denied access attempts with quick-allow button
- Auto-refresh — vault status updates every 5 seconds
The web UI port is written to ~/.onvault/http.port for scripting and testing.
onvault vault add ~/.ssh --smart
- Files in
~/.ssh/are encrypted (AES-256-XTS) and moved to~/.onvault/vaults/ssh/ - A nonce is stored in each file's xattr for key derivation
~/.sshis replaced with a symlink →~/.onvault/mnt/ssh/- Smart defaults auto-populate 7 allow rules (ssh, scp, sftp, ssh-add, ssh-agent, ssh-keygen, git)
- When unlocked: FUSE mount decrypts on-the-fly.
ssh,git, etc. work normally. - When locked or daemon stops: FUSE unmounts.
~/.sshsymlink points to nothing. Files are ciphertext.
To undo: onvault vault remove ssh decrypts everything back to the original location (passphrase required).
Destructive operations require passphrase verification via challenge-response. A malicious script running as your user cannot disable onvault without knowing your passphrase:
| Operation | Auth Required |
|---|---|
onvault lock |
Challenge-response passphrase proof |
onvault vault remove |
Challenge-response passphrase proof |
onvault allow/deny |
Challenge-response passphrase proof |
onvault policy import |
Challenge-response passphrase proof |
onvault rotate-keys |
Challenge-response passphrase proof |
onvault configure |
Local passphrase verification |
onvault unlock |
Passphrase (Argon2id) or Touch ID |
| Menu bar Lock/Unlock | Passphrase dialog or Touch ID |
onvault vault add |
Session (must be unlocked) |
onvault status/rules/log |
Requires unlocked daemon |
The daemon uses short-lived single-use nonces for challenge-response — proofs cannot be replayed.
Pass --smart when adding a vault for a known directory to auto-populate an allowlist of verified binaries. Smart defaults are opt-in; without --smart, the vault starts with default-deny rules until you add explicit allow entries.
| Path | Auto-allowed |
|---|---|
~/.ssh |
ssh, scp, sftp, ssh-add, ssh-agent, ssh-keygen, git |
~/.aws |
aws, terraform, pulumi |
~/.kube |
kubectl, helm, k9s |
~/.gnupg |
gpg, gpg2, gpg-agent, git |
~/.docker |
docker, Docker.app |
Only binaries that exist on your system are added. Each binary is hash-verified.
Only processes in the allowlist can read your encrypted files. Everything else is denied by default.
# Allow a specific binary to access a vault
onvault allow /usr/bin/vim ssh
# Allow with glob pattern (matches ssh, ssh-agent, sshd, etc.)
onvault allow "/usr/bin/ssh*" ssh
# Restrict to a specific user (UID) or group (GID)
onvault allow /usr/bin/vim ssh --uid 501
onvault allow /opt/homebrew/bin/aws aws --gid 20
# Require process ancestry verification
onvault allow /usr/bin/ssh ssh --chain-verify
# Global rules (apply across all vaults as fallback)
onvault allow /usr/bin/vim --global
onvault rules --global
# Deny a specific binary
onvault deny /usr/bin/python3 ssh
# View rules for a vault
onvault rules ssh
# Export/import policies as YAML (version-controllable)
onvault policy export > policies.yaml
onvault policy import policies.yaml
# See what processes access a path (learning mode — observe for 24h)
onvault vault watch ~/.ssh
onvault vault suggest ssh
# View audit log (all events or denied only)
onvault log
onvault log --denied
# Forward logs to external systems
onvault log --add-sink syslog
onvault log --add-sink json /var/log/onvault.json
# Compliance reports
onvault compliance --pci-dss
onvault compliance --hipaa
# Key management
onvault rotate-keys # Zero-downtime key rotation
onvault key-info # Key lifecycle metadata
onvault configure --rotation-interval 90d # Scheduled rotationonvault init First-time setup
onvault unlock [--touchid] Authenticate and mount vaults
onvault lock Unmount vaults, wipe keys (passphrase)
onvault status Show daemon and vault status
onvault health Machine-readable health check (JSON)
onvault vault add <path> [--smart] Encrypt and protect a directory
[--exclude "*.log"] Skip files matching pattern
onvault vault remove <vault_id> Decrypt and unprotect (passphrase)
onvault vault list List all vaults
onvault vault watch <path> Learning mode (24h observation)
onvault vault suggest <vault_id> Show watch suggestions
onvault allow <proc> <vault> [flags] Allow a process
[--uid <uid>] [--gid <gid>] Restrict to specific user/group
[--chain-verify] Verify process ancestry
[--global] Apply as global rule (no vault)
onvault deny <proc> <vault> [flags] Deny a process (same flags)
onvault rules <vault_id> Show rules for a vault
onvault rules --global Show global rules
onvault policy show Show all policies
onvault policy export Export policies as YAML
onvault policy import <file> Import policies from YAML
onvault rotate-keys Rotate encryption keys (zero downtime)
onvault key-info Key lifecycle metadata
onvault export-recovery Recovery key info
onvault recover Unlock with recovery key + new passphrase
onvault compliance --pci-dss|--hipaa|--sox Compliance report (JSON)
onvault log [--denied] View audit log
onvault log --sinks List active log sinks
onvault log --add-sink syslog|json|socket Add log forwarding sink
onvault log --remove-sink <type> Remove log forwarding sink
onvault configure <vault> --verify-mode Set per-vault verification mode
onvault configure --rotation-interval 90d Set scheduled key rotation
onvault --version Show version
┌──────────────────────────────────────────────────────┐
│ onvault CLI (C) │
│ init, unlock, lock, vault, allow, deny, configure │
├──────────────────────────────────────────────────────┤
│ Menu Bar (WKWebView + HTML/CSS/JS popover) │
│ Vault mgmt, allow/deny, rules, denials, lock/unlock │
├──────────────────────────────────────────────────────┤
│ Daemon — onvaultd (C) │
│ IPC Server │ HTTP Server │ Policy │ Auth │ Audit Log │
│ Integrity Monitor │ Rotation Scheduler │ Health API │
├──────────────────────────────────────────────────────┤
│ │
│ Layer 1: Encryption at Rest (macFUSE) │
│ AES-256-XTS (data) + AES-256-GCM (config/metadata) │
│ Per-file keys via HKDF-SHA512 + nonce in xattr │
│ No daemon = no mount = ciphertext only │
│ │
│ Layer 2: Per-Process Access Control (ESF, when │
│ entitlement + permission are available) │
│ AUTH_OPEN + AUTH_RENAME + AUTH_CREATE + more │
│ cdHash + Team ID + Signing ID + binary hash verify │
│ UID/GID rules, glob patterns, time-based access │
│ Process chain verification, binary tamper detection │
│ Global + per-vault policy hierarchy, default deny │
│ │
├──────────────────────────────────────────────────────┤
│ KMS Provider Abstraction │
│ Secure Enclave + Keychain (default, ECDH-wrapped) │
│ Pluggable: file-based KMS (dev), extensible to │
│ AWS KMS / Azure Key Vault / KMIP │
└──────────────────────────────────────────────────────┘
User Passphrase
→ [Argon2id, 64 MiB, 3 iter, 4 threads] → Master Key (AES-256)
→ Stored in Secure Enclave (ECDH-wrapped, non-exportable)
→ [HKDF-SHA512] → Config Key (policy/recovery/log encryption)
→ [HKDF-SHA512] → Per-Vault Key (one per protected directory)
→ [HKDF-SHA512 + nonce] → Per-File Key (unique per file)
Key rotation (online, zero-downtime): generates new master key, re-encrypts
all vault files with atomic temp+rename, swaps FUSE keys via rwlock.
Scheduled rotation available (e.g., every 90 days for compliance).
| Mode | Behavior |
|---|---|
codesign_preferred (default) |
Trust Apple code signing (cdHash + Team ID) for signed binaries. SHA-256 hash for unsigned. Survives brew updates. |
hash_only |
Always verify by SHA-256 binary hash. Every update requires re-approval. Maximum paranoia. |
codesign_required |
Only allow code-signed binaries. Reject all unsigned. |
| Threat | How |
|---|---|
| Supply chain attacks (litellm, malicious npm/pip packages) | Malicious code reads only ciphertext. Not in allowlist → DENIED. |
| Unauthorized daemon shutdown | onvault lock requires passphrase via challenge-response. IPC socket is owner-only. |
| Daemon killed / not running | FUSE auto-unmounts. Files remain AES-256 encrypted on disk. |
| Physical disk theft | Encrypted at rest. Master key in Secure Enclave (hardware-bound). |
| Root / su impersonation | Detected via audit_token — real UID vs effective UID comparison. |
| Binary swapping | Process identity verified by cdHash (Apple's content directory hash), not just path. |
| Config tampering | All policies and config encrypted with master key derivative. |
| Memory snooping | Keys mlock()'d (never swapped to disk), securely wiped via volatile memset + compiler barrier after use. |
| IPC replay attacks | Challenge-response with per-client single-use nonces for lock, vault remove, allow, deny, and policy import. |
| Policy enumeration | Read-only IPC commands require unlocked daemon. |
| Multiple daemon instances | PID lock file at ~/.onvault/onvaultd.pid. |
| Binary tampering | Runtime hash cache with mtime+inode+size invalidation. Modified binaries denied on next access. |
| Untrusted process launchers | Process chain verification walks ppid ancestry via sysctl. Trusted binary launched by malware → DENIED. |
| Daemon tampering | kqueue-based integrity monitor detects binary/config modifications, re-hashes after atomic rename. |
| Compliance | PCI-DSS, HIPAA, SOX reports verify encryption, key rotation, audit logging, access controls. |
| Key material on disk | Never plaintext. Secure Enclave wrapped (production) or debug-only file KMS (guarded by #ifndef NDEBUG). |
- Kernel-level compromise (ring-0 attacker with code execution)
- Hardware side-channel attacks (Spectre, cold boot on DRAM)
- Compromise of the Secure Enclave hardware itself
- DTrace/dtrace probing of the daemon process memory (requires SIP disabled)
- Attacks via debugger attachment (mitigated by
PT_DENY_ATTACHbut not bulletproof)
| Limitation | Detail | Workaround |
|---|---|---|
| macOS only | No Linux/Windows support. ESF and Secure Enclave are Apple-only. | Use platform-native tools on other OSes. |
| No filename encryption | File contents are encrypted (AES-256-XTS) but filenames remain visible in the vault directory (~/.onvault/vaults/). |
Use generic vault IDs and directory names to avoid leaking sensitive context through filenames. |
| ESF requires entitlement | Layer 2 (per-process access control) requires the com.apple.developer.endpoint-security.client entitlement, which Apple grants only to approved developers. Without it, Layer 1 (encryption at rest) still works fully. |
Apply for the entitlement via Apple Developer Program, or rely on Layer 1 encryption alone. |
| macFUSE dependency | Runtime dependency on macFUSE (kernel extension). Apple may restrict kexts in future macOS versions. | Monitor Apple's filesystem extension plans. macFUSE actively maintains compatibility. |
| Container detection is a stub on macOS | Docker Desktop on macOS runs containers inside a LinuxKit VM, so native PID-based container detection cannot identify containerized processes. The --container flag exists but always returns "not containerized" on macOS. |
Full container support planned for Linux builds. On macOS, use process path or signing identity rules instead. |
| Time-based rules use local time | Time window rules (e.g., "allow 9am-5pm") use the system's local timezone via localtime_r(). Timezone changes or DST transitions may cause brief windows of incorrect enforcement. |
Use UTC-aligned windows for critical policies, or avoid sub-hour precision near DST boundaries. |
| No centralized management | All configuration is local to each machine. No fleet-wide policy management server. | Use onvault policy export/import with configuration management tools (Ansible, Chef, etc.) to manage policies across machines. |
| Recovery key shown only at init | The 24-character recovery key is displayed once during onvault init and cannot be retrieved later. If lost, and the passphrase is also lost, data is unrecoverable. |
Write down the recovery key immediately and store it securely (e.g., password manager, physical safe). |
| Single-user design | onvault protects one user's directories. It does not support multi-user shared vaults or cross-user policy delegation. | Each user runs their own onvault instance with separate keys and policies. |
| HTTP API is localhost-only | The web UI / HTTP API binds to 127.0.0.1. Passphrase is sent in plaintext over the HTTP body. While not network-accessible, any local process can connect. |
The IPC socket (Unix domain, owner-only) provides stronger isolation for sensitive operations. Use CLI for highest security. |
| Hash cache TOCTOU | Binary tamper detection uses mtime+inode+size to decide whether to re-hash. A sophisticated attacker could theoretically modify a binary and restore the original mtime within the same second. | Enable codesign_required mode for maximum protection. Code signing verification is not subject to this race. |
| Symlinks in vaults | Only relative, non-traversing symlinks within a vault are preserved during encryption. Absolute symlinks and symlinks containing .. are silently skipped to prevent vault boundary escape. |
Convert absolute symlinks to relative before adding to vault. |
| Vault add on symlinks | Cannot add a symlink as a vault source — the path must be a real directory. onvault replaces the source with its own symlink, so a symlink source would create confusion. | Resolve the symlink first: onvault vault add $(readlink -f ~/.ssh) |
| Component | Algorithm | Standard |
|---|---|---|
| File data encryption | AES-256-XTS | NIST SP 800-38E |
| Config/metadata encryption | AES-256-GCM | NIST SP 800-38D |
| Key wrapping | AES-256-GCM | NIST SP 800-38D |
| Passphrase KDF | Argon2id (64 MiB, 3 iter, 4 threads) | RFC 9106, OWASP 2025 |
| Key derivation | HKDF-SHA512 | RFC 5869 |
| Process hashing | SHA-256 | FIPS 180-4 |
| Master key storage | Secure Enclave (ECDH P-256) | Apple CryptoKit |
| Auth proof | SHA-256(key || nonce) | Challenge-response |
~/.onvault/
├── salt # Argon2id salt (16 bytes)
├── auth.enc # Passphrase hash (encrypted with config key)
├── recovery.enc # Recovery key hash (encrypted)
├── key_meta.enc # Key lifecycle metadata (encrypted)
├── rotation.journal # Key rotation crash-recovery journal (temporary)
├── onvaultd.pid # PID lock (prevents multiple daemons)
├── onvault.sock # IPC socket (CLI ↔ daemon, owner-only)
├── http.port # HTTP server port for web UI
├── session # Session token + HMAC (15 min TTL)
├── policies.enc # Per-vault encrypted policy state
├── global_policy.enc # Global policy rules (cross-vault fallback)
├── logs/ # Encrypted audit logs (daily rotation)
├── watch/ # Learning mode discovery data (encrypted)
├── vaults/
│ ├── ssh/ # Ciphertext for ~/.ssh
│ │ ├── .onvault_source # Original path metadata
│ │ └── .onvault_exclusions # Excluded file patterns
│ └── aws/ # Ciphertext for ~/.aws
└── mnt/
├── ssh/ # FUSE mount → symlinked from ~/.ssh
└── aws/ # FUSE mount → symlinked from ~/.aws
- macOS 15 Sequoia or later (Apple Silicon)
- macFUSE 5.1+ (
brew install --cask macfuse) - Dist builds still require macFUSE at runtime
- Xcode Command Line Tools (
xcode-select --install) - OpenSSL 3 (
brew install openssl) - libargon2 (
brew install argon2) - macFUSE (
brew install --cask macfuse)
onvault/
├── src/
│ ├── common/ # Crypto, hashing, IPC, config, logging, types,
│ │ # compliance reports, policy YAML import/export
│ ├── fuse/ # Layer 1: macFUSE encrypted filesystem,
│ │ # vault lifecycle, multi-threaded encrypt pool
│ ├── esf/ # Layer 2: Endpoint Security per-process control,
│ │ # policy engine, integrity monitor, process chain,
│ │ # container detection
│ ├── keystore/ # Secure Enclave + Keychain, KMS provider abstraction
│ ├── daemon/ # onvaultd: IPC server, HTTP server, web UI (menubar.html)
│ ├── cli/ # onvault CLI + interactive configure
│ ├── menubar/ # macOS menu bar: WKWebView popover + notifications
│ ├── auth/ # Passphrase, sessions, Touch ID, recovery key,
│ │ # key rotation (journal-based crash-safe), key lifecycle
│ └── watch/ # Learning/discovery mode (ESF NOTIFY observer)
├── tests/ # 172 tests across 19 suites (keystore_stub for CI)
├── defaults/ # Smart default allowlists (ssh, aws, kube, gnupg, docker)
├── install/ # launchd plist, entitlements
├── Makefile
└── LICENSE # GPL-3.0
onvault includes production-grade features expected of enterprise file encryption solutions:
| Feature | Description |
|---|---|
| Glob pattern rules | Wildcard process matching (/usr/bin/ssh*, /opt/*/bin/python3) |
| UID/GID access control | Restrict rules to specific users or groups |
| Global policy hierarchy | Global rules as fallback when no vault-specific rule matches |
| Policy-as-code | Export/import policies as YAML for version control |
| Multi-threaded encryption | Parallel file encryption via thread pool for large vaults |
| File exclusion patterns | Skip *.log, .cache/* etc. during vault creation |
| Online key rotation | Zero-downtime re-encryption with atomic key swap (rwlock) |
| Scheduled rotation | Automatic rotation on configurable cadence (e.g., 90 days) |
| Crash-safe rotation | Journal-based recovery from interrupted key rotations |
| Syslog/SIEM integration | Forward logs to syslog, JSON files, or Unix sockets |
| Compliance reports | PCI-DSS, HIPAA, SOX reports (JSON) verifying encryption, rotation, logging |
| Key lifecycle tracking | Fingerprint, creation date, rotation count, last rotation timestamp |
| Agent self-protection | kqueue-based monitoring of daemon binary and config integrity |
| Process chain verification | Verify process ancestry (detect trusted binaries launched by malware) |
| Binary tamper detection | Runtime hash cache with mtime+inode+size invalidation |
| KMS provider abstraction | Pluggable key management (Secure Enclave default, extensible to cloud KMS) |
| Time-based access rules | Allow access only during specific hours/days |
| Backup mode | Authorized backup tools get plaintext access with strong identity verification |
| Health API | Machine-readable JSON endpoint for monitoring (/api/health) |
| Touch ID | Biometric unlock (after first passphrase unlock stores key in Keychain) |
| Recovery key | 24-character key for passphrase reset (displayed at init) |
No existing macOS product combines file encryption with per-process access control:
| Product | Encryption | Per-Process Control | Policy-as-Code | Compliance | Open Source |
|---|---|---|---|---|---|
| onvault | AES-256-XTS (FUSE) | ESF + code signing + UID/GID | YAML export/import | PCI-DSS, HIPAA, SOX | GPL-3.0 |
| FileVault | Full-disk (APFS) | None | No | No | No |
| Santa (NorthPoleSec) | None | ESF-based | Yes | No | Yes |
| Cryptomator | AES-256-GCM (FUSE) | None | No | No | Yes |
| CrowdStrike Falcon | None | Partial (behavioral) | Proprietary | Partial | No |
Contributions welcome. Please open an issue before submitting large PRs.
# Build and test
make clean && make && make test
# 172 tests across 19 suites — crypto, vault, auth, FUSE ops, config,
# policy, logging, IPC, HTTP, key rotation, glob matching, UID/GID,
# log sinks, online rotation, encrypt pool, security, compliance, KMS, audit
make testGNU General Public License v3.0
Powered by onllm.dev