Project 5: env-vault (Secrets and Env Runner)
Build a secure CLI that stores secrets safely and injects them into child processes without leaks.
Quick Reference
| Attribute | Value |
|---|---|
| Difficulty | Level 2 (Intermediate) |
| Time Estimate | 1 week |
| Main Programming Language | Go (Alternatives: Rust, Python) |
| Alternative Programming Languages | Rust, Python |
| Coolness Level | Level 3: Practical security |
| Business Potential | 3: SaaS tooling |
| Prerequisites | CLI basics, config, process execution |
| Key Topics | Secret storage, env inheritance, redaction |
1. Learning Objectives
By completing this project, you will:
- Store secrets securely using encryption or OS keychain.
- Inject secrets into child processes without leaking them.
- Design safe input flows that avoid shell history leakage.
- Implement redaction and audit-friendly logging.
- Provide deterministic behavior and exit codes for automation.
2. All Theory Needed (Per-Concept Breakdown)
2.1 Secret Storage and Key Management
Fundamentals
Secret storage is about protecting sensitive values at rest. For a CLI, you must decide whether to store secrets in encrypted files or delegate to the OS keychain. Both approaches require a key management strategy. If you encrypt files, you need a master key, which may come from a passphrase or an OS keychain. If you delegate to a keychain, you rely on platform APIs. The primary rule is to never store secrets in plaintext. The secondary rule is to make retrieval and usage secure and user-friendly.
Deep Dive into the concept
Encrypted file storage is attractive because it is portable and simple to implement, but it shifts the burden of key management to you. A good approach is to derive an encryption key from a user-provided passphrase using a key derivation function (KDF) like scrypt or Argon2. The derived key should be used with an authenticated encryption algorithm such as AES-GCM or ChaCha20-Poly1305. Authenticated encryption ensures that tampering is detected. You must store a salt and nonce with the ciphertext. The salt ensures that the same passphrase yields different keys across users, and the nonce ensures that repeated encryption of the same secret yields different ciphertexts.
Keychain storage delegates key management to the OS. On macOS, this is Keychain Access; on Windows, Credential Manager; on Linux, Secret Service. These systems handle encryption and protect keys with the user’s login credentials. The trade-off is portability and dependency on platform APIs. A hybrid approach is common: store an encrypted file and protect the master key using the OS keychain when available.
A CLI must also consider the threat model. The primary risk is that secrets leak to disk, logs, or shell history. If you prompt for secrets in a TTY, you should disable echo. If you allow secrets via stdin, you must avoid storing them in command history by not accepting secrets as flags. The safest design is to read secrets from stdin or from a prompt and never accept them as flags.
You must also define what happens if the key is lost. If the user forgets the passphrase, encrypted secrets are unrecoverable. That is correct for security, but you should communicate it clearly. Provide a reset command that wipes all secrets and reinitializes the store. Provide an export command only if it is encrypted or explicitly unsafe with warnings.
Finally, storage format matters. A simple JSON file that maps names to encrypted blobs is fine. It should also include metadata such as created_at and updated_at. The file must be written atomically to avoid corruption, and the metadata should never include the plaintext secret. A minimal schema is enough, but it must be stable.
How this fit on projects
This concept determines how env-vault stores and retrieves secrets, how it initializes its vault, and how it handles key rotation or resets.
Definitions & key terms
- KDF: Key derivation function (scrypt, Argon2).
- AEAD: Authenticated encryption with associated data.
- Nonce: Unique per-encryption value.
- Keychain: OS-managed secure storage.
Mental model diagram (ASCII)
Secret -> KDF(passphrase) -> AEAD encrypt -> ciphertext -> vault file
How it works (step-by-step)
- Ask user for a passphrase (no echo).
- Derive key using KDF and salt.
- Encrypt secret with AEAD and a fresh nonce.
- Store ciphertext, nonce, and metadata in vault file.
- Decrypt on demand using the same derived key.
Minimal concrete example
vault.json
{
"API_KEY": {"nonce":"sQ8pR6o9mM0=","ciphertext":"p5m4nYlG3V6wK2rJxQ==","created_at":"2026-01-01"}
}
Common misconceptions
- “Base64 is encryption.” -> It is only encoding.
- “Using AES without authentication is fine.” -> It allows silent tampering.
- “Secrets can be passed as CLI flags.” -> They leak into history.
Check-your-understanding questions
- Why is a KDF required for passphrase-based keys?
- What does AEAD protect against?
- Why should vault writes be atomic?
Check-your-understanding answers
- It slows down brute-force attacks and derives a strong key.
- It ensures confidentiality and integrity of the ciphertext.
- To prevent file corruption on crashes.
Real-world applications
- Tools like
gopassandpassuse encryption and keychains. - Secrets managers store encrypted blobs with metadata.
Where you will apply it
- See §3.5 Data Formats and §4.3 Data Structures.
- Also used in: Project 2: task-nexus and Project 10: distro-flow.
References
- OWASP Cryptographic Storage Cheat Sheet
- libsodium documentation
Key insights
Secure storage is less about algorithms and more about disciplined key management.
Summary
To store secrets safely, use authenticated encryption and a clear key management strategy, and never store plaintext.
Homework/Exercises to practice the concept
- Implement a simple AEAD encrypt/decrypt flow.
- Add a KDF step with a random salt.
- Write and read a vault file with metadata.
Solutions to the homework/exercises
- Use AES-GCM or ChaCha20-Poly1305.
- Use Argon2 or scrypt with a per-vault salt.
- Store ciphertext and nonce in JSON.
2.2 Environment Inheritance and Process Execution
Fundamentals
When you run a command from a CLI, the child process inherits the environment variables from the parent. This is how env-vault injects secrets. The challenge is to do it safely: you must avoid leaking secrets to logs, and you must ensure that secrets are only visible to the child process, not the parent shell or other processes. The correct strategy is to build a new environment array and execute the child process directly, without invoking a shell.
Deep Dive into the concept
Process execution APIs typically allow you to pass both the command and the environment. In Go, this is exec.Command with cmd.Env. In Rust, it is std::process::Command with env. The key is to avoid sh -c or shell string interpolation because it introduces injection vulnerabilities and can expose secrets in the command string. By passing arguments as a slice and environment variables as a map, you keep secrets out of logs and avoid shell parsing issues.
Environment inheritance works by copying the parent’s environment and then adding or overriding specific variables. This means that your CLI should start with os.Environ() and append new KEY=VALUE pairs. You should also consider removing variables that might conflict or leak secrets. For example, you may want to avoid exporting your own vault configuration variables to the child.
Another important issue is process exit codes. env-vault run should return the exit code of the child process. This makes it act like a transparent wrapper. It should not hide failures. If the child fails, env-vault run must fail with the same code. This is critical for CI and scripts. If you add any preprocessing or validation that fails, use a different exit code so scripts can distinguish between “wrapper failure” and “child failure”.
You must also manage input and output. The child should inherit stdin, stdout, and stderr so it behaves normally. That means your wrapper should attach those streams directly. If you do not, users will see weird behavior like missing output or stuck prompts. The wrapper should be transparent.
How this fit on projects
This concept defines the run subcommand and ensures it behaves like a transparent process wrapper with secure environment injection.
Definitions & key terms
- Environment inheritance: Child process starts with parent’s env.
- Direct exec: Executing without a shell.
- Exit code propagation: Wrapper exits with child’s code.
- Env map: Collection of
KEY=VALUEpairs for a process.
Mental model diagram (ASCII)
vault secrets -> build env -> exec child -> child uses secrets
How it works (step-by-step)
- Load secrets from vault.
- Build a new env array from parent env + secrets.
- Execute child command directly with argv array.
- Attach stdin/stdout/stderr to child.
- Exit with child’s exit code.
Minimal concrete example
$ env-vault run -- ./deploy.sh
# deploy.sh sees injected env vars
Common misconceptions
- “Using
sh -cis simpler.” -> It adds injection risk and leaks. - “Exit codes can be normalized.” -> They must match the child for scripts.
- “Secrets can be logged for debugging.” -> This creates leaks.
Check-your-understanding questions
- Why should
env-vaultavoid invoking a shell? - How do you ensure child processes receive secrets?
- Why should wrapper exit code match child exit code?
Check-your-understanding answers
- To prevent injection and avoid exposing secrets.
- By passing a constructed env array to exec.
- Scripts depend on exact exit codes.
Real-world applications
direnvanddotenvwrappers use similar patterns.- Deployment tools inject env vars into child commands.
Where you will apply it
- See §3.2 Functional Requirements and §3.7 Real World Outcome.
- Also used in: Project 7: git-insight and Project 10: distro-flow.
References
- The Linux Programming Interface, process execution chapters
- Go
os/execdocumentation
Key insights
Safe execution is about avoiding the shell and propagating exit codes transparently.
Summary
A secure env runner must exec directly with an explicit environment and transparent I/O.
Homework/Exercises to practice the concept
- Wrap a command and pass a custom env var.
- Verify exit codes match the child.
- Run a child that prints its env and confirm injection.
Solutions to the homework/exercises
- Use
exec.Commandandcmd.Env. - Use
echo $?after run. - Run
envinside the child process.
2.3 Threat Modeling and Redaction
Fundamentals
Threat modeling is the discipline of identifying how secrets can leak and designing defenses. For a CLI, the main leak vectors are shell history, logs, temp files, and process listings. Redaction is the practice of masking sensitive values in logs and error messages. Without redaction, debugging output can expose secrets. The goal is to design a tool that is safe even when users run it with --debug.
Deep Dive into the concept
The first step is to list the assets: API keys, tokens, passwords. Then list the attack surfaces: command-line flags, environment variables, config files, and logs. Passing secrets as flags is the most dangerous because the shell stores them in history and they may appear in process listings (ps). That is why env-vault should reject secrets passed as flags and require stdin or prompt input.
Logs are another risk. Developers often print config for debugging, which can include secrets. The safe approach is to implement a redaction function that scans output and replaces secret values with ****. This should be applied to all log output, especially in --debug mode. Redaction should be deterministic and should not be optional. If a secret is present in the environment, it should be redacted before printing the environment.
Temporary files are a subtle risk. If you write secrets to a temp file, you must ensure it is encrypted or securely deleted. In most cases, avoid writing secrets to disk at all. If you must, use a secure temp directory and delete immediately after use. Another vector is crash dumps. In some systems, core dumps can contain environment variables. You can reduce this risk by unsetting secrets in the parent process after launching the child, but this is not always possible. Document this risk and provide guidance.
Threat modeling also helps define your exit code taxonomy. For example, exit code 2 for validation errors, 3 for encryption failures, 4 for missing vault. This makes automation safer because scripts can distinguish between invalid input and security failures.
Finally, redaction extends to user-facing output. The list command should list secret names only, never values. The show command should be optional and should require explicit confirmation. Even then, it should print to stdout only when the user requested it. Avoid accidental leaks in default workflows.
How this fit on projects
This concept defines how env-vault handles logs, errors, and secret exposure in all commands. It also shapes your error codes.
Definitions & key terms
- Threat model: List of assets and attack vectors.
- Redaction: Masking sensitive values.
- Process listing: OS view of running processes.
- Shell history: Stored command lines with arguments.
Mental model diagram (ASCII)
Secrets -> (stdin/prompt) -> vault
-> (never flags) -> no history leak
Logs -> redaction -> safe output
How it works (step-by-step)
- Identify where secrets could leak.
- Prevent secret input via flags.
- Redact secrets in all log output.
- Avoid writing secrets to disk in plaintext.
- Provide explicit show/export commands with warnings.
Minimal concrete example
Log: "Setting API_KEY=****" instead of actual value
Common misconceptions
- “Debug logs are safe.” -> They often leak secrets.
- “Env vars are invisible.” -> They can be read by child processes and dumps.
- “Process listings are harmless.” -> CLI args are visible to other users.
Check-your-understanding questions
- Why are CLI flags unsafe for secrets?
- What does redaction protect against?
- Why should
listnever show values?
Check-your-understanding answers
- They appear in shell history and process listings.
- It prevents accidental leaks in logs and debug output.
- It avoids accidental disclosure by default.
Real-world applications
- Vault clients and secret managers always redact logs.
awsCLI hides secret values in output.
Where you will apply it
- See §3.2 Functional Requirements and §7 Common Pitfalls.
- Also used in: Project 2: task-nexus and Project 8: api-forge.
References
- OWASP Secrets Management Cheat Sheet
- AWS CLI security guidelines
Key insights
Security is a UX feature: safe defaults prevent accidental leaks.
Summary
Threat modeling and redaction are essential for any CLI that handles secrets. Prevent leaks by design.
Homework/Exercises to practice the concept
- List all possible secret leak vectors for a CLI.
- Implement a redaction function and test it.
- Add a warning prompt before showing a secret.
Solutions to the homework/exercises
- Flags, logs, files, process listings, crash dumps.
- Replace known secret values with
****in output. - Require
--showplus a confirmation.
3. Project Specification
3.1 What You Will Build
A CLI named env-vault that stores secrets securely and runs commands with injected environment variables. It supports set, get, list, and run commands. It never prints secret values by default, and it uses encrypted storage or OS keychain. It propagates child process exit codes.
3.2 Functional Requirements
- Secret storage: encrypted vault or OS keychain.
- Commands:
set,get,list,run,reset. - Secret input: via prompt or stdin, not flags.
- Run wrapper: execute child with injected env.
- Redaction: logs and debug output never show secrets.
- Exit codes: clear taxonomy for errors.
3.3 Non-Functional Requirements
- Security: no plaintext secrets on disk.
- Reliability: atomic vault writes and safe error handling.
- Usability: clear prompts and help output.
3.4 Example Usage / Output
$ env-vault set API_KEY
Enter secret: ********
Saved secret for API_KEY
$ env-vault run -- ./deploy.sh
Running ./deploy.sh with 1 secret injected
3.5 Data Formats / Schemas / Protocols
{
"API_KEY": {
"nonce": "sQ8pR6o9mM0=",
"ciphertext": "p5m4nYlG3V6wK2rJxQ==",
"created_at": "2026-01-01T00:00:00Z"
}
}
3.6 Edge Cases
- Missing vault -> exit code 4 with clear message.
- Invalid passphrase -> exit code 3.
- Secret name not found -> exit code 2.
3.7 Real World Outcome
3.7.1 How to Run (Copy/Paste)
# Build
go build -o env-vault ./cmd/env-vault
# Initialize and set secret
./env-vault init
./env-vault set API_KEY
3.7.2 Golden Path Demo (Deterministic)
$ ENV_VAULT_NOW=2026-01-01T00:00:00Z ./env-vault set API_KEY
Enter secret: ********
Saved secret for API_KEY
$ ./env-vault list
API_KEY
$ ./env-vault run -- env | grep API_KEY
API_KEY=********
$ echo $?
0
3.7.3 Failure Demo (Deterministic)
$ ./env-vault get MISSING_KEY
env-vault: secret not found: MISSING_KEY
$ echo $?
2
3.7.4 Exit Codes
0: Success.2: Missing secret.3: Invalid passphrase or decryption failure.4: Vault not initialized.
4. Solution Architecture
4.1 High-Level Design
+------------------+
| CLI Parser |
+------------------+
|
v
+------------------+ +------------------+
| Vault Manager | --> | Crypto Engine |
+------------------+ +------------------+
| |
v v
+------------------+ +------------------+
| Env Runner | --> | Logger/Redactor |
+------------------+ +------------------+
4.2 Key Components
| Component | Responsibility | Key Decisions |
|---|---|---|
| Vault manager | load/store secrets | atomic file writes |
| Crypto engine | encrypt/decrypt | AEAD + KDF |
| Env runner | exec child process | direct exec with env |
| Redactor | hide secrets | always on in logs |
4.3 Data Structures (No Full Code)
type VaultEntry struct {
Nonce string
Ciphertext string
CreatedAt time.Time
}
4.4 Algorithm Overview
Key Algorithm: Secure Set
- Read secret from prompt or stdin.
- Derive key from passphrase.
- Encrypt secret and store metadata.
- Write vault atomically.
Complexity Analysis:
- Time: O(N) per secret where N is secret length.
- Space: O(N) for ciphertext.
5. Implementation Guide
5.1 Development Environment Setup
mkdir env-vault && cd env-vault
mkdir -p cmd/env-vault internal
5.2 Project Structure
cmd/env-vault/
main.go
internal/
vault/
crypto/
run/
redact/
5.3 The Core Question You’re Answering
“How can a CLI handle secrets without leaking them into history or logs?”
5.4 Concepts You Must Understand First
- Authenticated encryption and key management.
- Environment inheritance and safe exec.
- Threat modeling and redaction.
5.5 Questions to Guide Your Design
- Where should the vault file live?
- How do you avoid secrets in shell history?
- Should the CLI allow plaintext export?
5.6 Thinking Exercise
Design a run flow that injects secrets into a child process without printing them.
5.7 The Interview Questions They’ll Ask
- “Why are CLI flags unsafe for secrets?”
- “How do you avoid leaking secrets in logs?”
- “How do you pass env vars to a child process safely?”
5.8 Hints in Layers
Hint 1: Store secrets encrypted Use a vault file with AEAD.
Hint 2: Read from stdin Never accept secret values in flags.
Hint 3: Redact output Mask secrets in any log output.
Hint 4: Propagate exit codes
env-vault run should exit like the child.
5.9 Books That Will Help
| Topic | Book | Chapter |
|---|---|---|
| Secure coding | Effective C | Ch. 5 |
| Process execution | The Linux Programming Interface | process chapters |
5.10 Implementation Phases
Phase 1: Vault Basics (2-3 days)
Goals: init, set, list, get.
Phase 2: Encryption (2-3 days)
Goals: AEAD + KDF, encrypted file.
Phase 3: Run Wrapper (2 days)
Goals: exec child with env injection, exit codes.
5.11 Key Implementation Decisions
| Decision | Options | Recommendation | Rationale |
|---|---|---|---|
| Storage | keychain vs encrypted file | encrypted file + keychain optional | portability. |
| Input | stdin vs flags | stdin/prompt | avoids leaks. |
| Logging | verbose vs redacted | redacted always | safety. |
6. Testing Strategy
6.1 Test Categories
| Category | Purpose | Examples |
|---|---|---|
| Unit Tests | crypto | decrypt round trip |
| Integration Tests | CLI flow | set -> list -> run |
| Edge Case Tests | missing secret | exit code 2 |
6.2 Critical Test Cases
- Stored secret can be retrieved and decrypted.
runinjects env and propagates exit code.- Logs do not show secret values.
6.3 Test Data
fixtures/vault.json with one encrypted entry
7. Common Pitfalls and Debugging
7.1 Frequent Mistakes
| Pitfall | Symptom | Solution |
|---|---|---|
| Secrets in history | values leaked | read from stdin or prompt |
| Unencrypted vault | plaintext on disk | enforce encryption |
| Exit code mismatch | scripts fail | propagate child exit code |
7.2 Debugging Strategies
- Use a debug mode that redacts secrets.
- Add a test command that prints env keys only.
- Validate vault file format with a schema checker.
7.3 Performance Traps
- Re-deriving keys for every secret on list can be slow; cache derived key.
8. Extensions and Challenges
8.1 Beginner Extensions
- Add
env-vault unset KEY. - Add
env-vault rename OLD NEW.
8.2 Intermediate Extensions
- Add scoped secrets per project.
- Add
--dotenvexport mode with warnings.
8.3 Advanced Extensions
- Add key rotation support.
- Add audit log of secret access (redacted).
9. Real-World Connections
9.1 Industry Applications
- DevOps scripts that inject secrets into deployment commands.
9.2 Related Open Source Projects
pass,gopass,vaultCLI patterns.
9.3 Interview Relevance
- Encryption and safe process execution are common security topics.
10. Resources
10.1 Essential Reading
- OWASP Cryptographic Storage Cheat Sheet
- The Linux Programming Interface (process execution)
10.2 Video Resources
- “Secrets Management” talks
10.3 Tools and Documentation
- libsodium or OpenSSL docs
- OS keychain APIs
10.4 Related Projects in This Series
11. Self-Assessment Checklist
11.1 Understanding
- I can explain AEAD and why it is required.
- I can explain how env inheritance works.
- I can explain why flags are unsafe for secrets.
11.2 Implementation
- Secrets are encrypted at rest.
- Run wrapper propagates exit codes.
- Logs are redacted.
11.3 Growth
- I can reason about threat models and mitigations.
- I can add new secret commands safely.
- I can demo a safe run workflow.
12. Submission / Completion Criteria
Minimum Viable Completion:
- Vault init and set/list/get work.
- Secrets are not stored in plaintext.
runinjects env safely.
Full Completion:
- Redaction and error taxonomy implemented.
- Deterministic output options.
Excellence (Going Above and Beyond):
- Key rotation and audit logging.
- OS keychain integration.