This document is the canonical statement of envroll's threat model and the vulnerability disclosure process. The threat-model text below is reproduced verbatim in the README's "What envroll does and does not protect against" section so it's the first thing every new user sees. But if you're reading the source, this is the file to start with.
- A passive attacker who reads the configured vault remote (public GitHub repo, leaky S3 bucket, accidental tweet of the URL). All env contents are age-encrypted; without the passphrase the attacker sees only ciphertext + commit metadata (timestamps, commit messages, env names).
- A lost or stolen laptop with full-disk encryption enabled and locked. Vault content is still encrypted at the envroll layer, so even if FDE is later defeated the attacker still needs the passphrase.
- Casual
.envexposure in chat, screenshots, screen-shares, or tickets, provided the user pasted the encrypted.agefile, not the plaintext checkout. envroll cannot prevent users from pasting plaintext. - Unauthorized writes to the vault remote: age messages have a built-in HMAC, so any tampered ciphertext fails to decrypt with
file corrupt or tampered.
- An active attacker with shell access on the user's machine. They can read
.checkout/<name>directly. - A malicious remote serving a rolled-back but still valid commit. age's MAC catches modification but not "an older legitimate ciphertext is the current HEAD". Mitigation:
git logthe vault and verify expected history. v0.2 may add signed tags. - Keyloggers or compromised terminal emulators. The passphrase is typed in the user's terminal; if that terminal is compromised, so is the vault.
- Weak passphrases. scrypt makes brute-force expensive but not impossible. We recommend
>= 6random words from a long wordlist or equivalent entropy. - The user committing plaintext somewhere envroll can't see (e.g., pasting
.envinto a chat, configuring their shell to log env vars). - Side channels (timing, CPU usage). Out of scope.
- envroll's own dependency supply chain. Mitigation: pin
Cargo.lock, usecargo audit/cargo denyin CI.
- Algorithm. age in
scrypt::Recipient/scrypt::Identitymode. The on-disk format is binary age v1 (the wire format declared by<vault>/.envroll-version = "1"). - Work factor.
agecrate default, currently calibrated to ~1 second per derivation on commodity hardware. Not user-tunable in v0.1. - Authenticity. age messages carry a per-message HMAC. Any modification to a
.agefile (single bit, swapped header, truncated tail) fails decryption withfile corrupt. Exit code 11. - Passphrase handling in process memory. Passphrases are held only in
secrecy::SecretString. They are never.clone()-ed into aString, never written to disk, never logged, never displayed.SecretStringzeroizes its bytes on drop. - No key material on disk. envroll stores no key file, no recovery key, no wrapped passphrase. The only key is the one in your head (and in your password manager).
- Vault root and per-project
.checkout/directories: mode0700. - Plaintext checkout files and all
.ageblobs (canary + per-env): mode0600. - Plaintext metadata (
manifest.toml,.gitignore,.envroll-version): mode0644. - Atomic writes everywhere: tempfile-in-same-directory +
fsync+rename+ parentfsync. An interrupted write leaves the destination unchanged; the orphan tempfile is reaped on the next vault-locking command. .checkout/is in the vault's.gitignoreand is never committed. A test intests/env_switching.rs::sync_never_includes_checkout_in_commitswalks every commit pushed to a remote and asserts no.checkout/path is present.- Plaintext only ever leaves the project-scoped
.checkout/directory through three deliberate routes: a symlink at./.env(default), a copy at./.env(copy-mode), or as an environment variable injected into a child process viaenvroll exec(in-memory only, never on disk).
| Stream | Contents |
|---|---|
| stdout | The command's primary output (an env name, a key value, a JSON document). Never a passphrase. Never a plaintext env body, except envroll get <KEY> which prints exactly the value the user asked for. |
| stderr | Status lines, warnings, and structured errors of the form envroll: <category>: <message>. Never a passphrase. Never a plaintext value, except envroll status --show-values and envroll diff --show-values which the user has explicitly opted into. |
| Logs | None on disk by default. With --log debug or RUST_LOG=debug, full anyhow chains go to stderr. envroll never writes its own log files. |
| Telemetry | None. envroll makes no network call other than git fetch/git push to the user-configured origin. |
envroll ships with an undocumented-on-purpose presentation mode for recording screencasts and conference demos. It activates only when BOTH of these env vars are set:
ENVROLL_DEMO_MODE=1
ENVROLL_PASSPHRASE=<your demo vault's passphrase>
When active, the interactive rpassword prompt is replaced by a visible
animation: the prompt label appears (envroll passphrase:), pauses
briefly, then 8 bullet glyphs (•) type out one at a time. The
passphrase actually used is $ENVROLL_PASSPHRASE. The bullets are
purely cosmetic and the bytes printed are literally the • glyph.
- Canary verification still runs. The passphrase from
$ENVROLL_PASSPHRASEis still fed to the samecrypto::verify_canarypath that every encrypted-content operation uses. A wrong passphrase fails withEnvrollError::WrongPassphrase(exit 10) just like in normal mode. - No new logging surface. The actual passphrase bytes never reach
the terminal, stdout, stderr, or any logger. Only the
•bullets do. - No new attack surface. The animation reads from an env var, which was already a supported (and documented) passphrase source. Demo mode adds zero new code paths into the crypto / lock / vault layers.
- In CI or production. Demo mode requires
$ENVROLL_PASSPHRASEto be set, and a CI environment with that env var leaked into a process group is exactly the failure mode envroll's threat model warns about. CI should use--passphrase-stdininstead. - On a real vault with secrets you care about. Demo mode is for scratch vaults whose passphrase you wouldn't mind leaking. Even though demo mode adds no new exposure vector, treating it as a "test fixture" reduces the chance of accidentally leaving the env vars set in your shell session and then doing something real.
Keeping demo mode in main instead of a fork means demos exercise the
same passphrase code path that real users hit, modulo the prompt
animation. A fork would drift over time and quietly stop being
representative. The full implementation is in src/prompt.rs; search
for DEMO_MODE_ENV.
If you believe you've found a security issue in envroll (anything that violates the "envroll protects against" list above, leaks plaintext or passphrase material, or undermines the on-disk integrity guarantees), please report it privately rather than opening a public issue.
Email: adrianjpr@gmail.com
Or open a private security advisory on GitHub: https://github.com/AdriiiPRodri/envroll/security/advisories/new.
What helps us triage quickly:
- A clear description of the vulnerability and the impact you observed.
- The smallest reproduction you can produce (a few commands or a tiny test case is ideal).
- The envroll version (
envroll --version), platform, and any non-default environment (vault on a network mount, exotic FS, custom--passphrase-envconvention, etc.). - Your timeline expectations and whether you'd like public credit when the fix ships.
We'll acknowledge receipt within 3 business days and aim to ship a fix or a documented mitigation within 30 days for high-severity issues.
The following are deliberately not vulnerabilities (they're documented limitations or accepted v0.1 trade-offs):
- "An attacker with root on my laptop can read
.checkout/<env>." Yes, the threat model excludes active local attackers. - "If I forget my passphrase, I can't recover my envs." Yes, that's the single security guarantee the entire design is anchored to.
- "Concurrent
envroll setcalls from two terminals can interleave." Yes, v0.1 has a vault-wide advisory lock, not per-env locks. v0.2 may revisit. - "Pushing the vault to a public repo leaks env names and timestamps." Yes, the threat model says exactly this; a passive attacker sees ciphertext and commit metadata. If env names are themselves sensitive, don't sync.
If you're not sure whether something is in scope, err on the side of reporting it. We'd rather triage a non-issue than miss a real one.