Approval Flow
The approval flow is the core of env.cat's zero-trust design. Secrets are never sent to the CLI until you explicitly approve the request.
How It Works
Step 1: CLI Requests Secrets
Run the CLI command:
envcat get --bundle dev/api --api-base https://env.cat
What happens:
- CLI generates ephemeral keypair (Ed25519)
- Sends public key to server via
POST /api/v1/requests - Server stores request in Redis (TTL: 5 minutes)
- Server returns approval URL
Important: At this stage, NO secrets have been accessed yet.
Step 2: Approval Prompt
The CLI displays:
┌─────────────────────────────────────────┐
│ Approval Required │
│ │
│ Open this URL in your browser: │
│ https://env.cat/approve/abc123xyz │
│ │
│ Or scan this QR code: │
│ ████ ▄▄▄▄▄ █▀█ █▄▀▀▀█▄█ ▄▄▄▄▄ ████ │
│ ████ █ █ █▀▀ █ ▀▀▀ █ █ █ ████ │
│ ████ █▄▄▄█ ▀ █▄▀█▄█▄▀█ █▄▄▄█ ████ │
│ ████▄▄▄▄▄▄▄█ ▀ █ ▀ █ ▀▄▄▄▄▄▄▄████ │
│ │
│ Waiting for approval... │
│ (expires in 4:32) │
└─────────────────────────────────────────┘
Desktop: Click the URL
Mobile: Scan the QR code with your camera
What happens:
- CLI polls
GET /api/v1/requests/:id/waitevery 3 seconds - Waits up to 5 minutes for approval
- Shows countdown timer
Step 3: Approve in Browser
Open the approval URL. You'll see:
Bundle Selector:
- If CLI specified
--bundle, it's pre-selected - Otherwise, choose from your bundles
Key Selector:
- All keys in selected bundle shown
- Values masked (
DATABASE_URL: ••••••••) - Checkboxes to select which keys to approve
- Filter/search if bundle has many keys
Approve Button:
- Click "Approve & Encrypt"
- Server fetches key values from database
- Server decrypts values (in memory only)
- Server encrypts to CLI's public key (NaCl sealed box)
- Encrypted payload stored in Redis
What happens:
- Browser calls
POST /api/v1/requests/:id/encrypt-from-bundle - Server loads selected keys from bundle
- Server decrypts key values (AES-256-GCM)
- Server encrypts to CLI public key (sealed box)
- Server stores ciphertext in Redis
- Server marks request as "ready"
Security note: Server never logs plaintext values. Decryption happens in memory only.
Step 4: CLI Receives Secrets
CLI polling detects "ready" status:
What happens:
- CLI receives ciphertext from server
- CLI deletes request (single-use) via server
- CLI decrypts with private key (only CLI can decrypt)
- CLI parses JSON:
{"DATABASE_URL": "postgres://...", ...} - CLI outputs shell exports or writes .env
Output (shell format):
✓ Approved! Received 3 variable(s)
export DATABASE_URL='postgres://user:pass@localhost:5432/db'
export API_KEY='sk_live_abc123xyz'
export PORT='3000'
Step 5: Inject into Shell
Method 1: Eval (Recommended)
eval "$(envcat get --bundle dev/api --api-base https://env.cat)"
Variables now available in current shell:
echo $DATABASE_URL
# postgres://user:pass@localhost:5432/db
echo $API_KEY
# sk_live_abc123xyz
Method 2: Write to .env
envcat get --bundle dev/api --api-base https://env.cat --file .env
Then source it:
set -a && source .env && set +a
Security Model
Zero-Trust Principles
1. No implicit trust
- Every secret request requires explicit approval
- No automatic approvals
- No standing access tokens
2. Principle of least privilege
- Approve only the keys you need
- Request specific keys with
--keysflag - Don't approve "all secrets" by default
3. End-to-end encryption
- Secrets encrypted in database (at-rest)
- Encrypted to CLI public key (end-to-end)
- Only CLI can decrypt (sealed box)
4. Single-use requests
- Each request deleted after use
- Cannot replay approval
- New request for each CLI invocation
Encryption Layers
Layer 1: At-Rest (Database)
- Algorithm: AES-256-GCM
- Key:
BUNDLE_KMS_KEYenvironment variable - Scope: All key values in database
Layer 2: In-Transit (HTTPS)
- Protocol: TLS 1.3
- Certificates: Let's Encrypt (production)
- Scope: All API requests
Layer 3: End-to-End (Sealed Box)
- Algorithm: NaCl sealed box (X25519 + XSalsa20-Poly1305)
- Keypair: Ephemeral (generated per CLI request)
- Scope: Approval payload (server → CLI)
Threat Model
What env.cat protects against:
✅ Server compromise (secrets encrypted at-rest)
✅ Network eavesdropping (TLS + E2E encryption)
✅ Database leaks (values encrypted, keys rotatable)
✅ Logs exposure (no plaintext in logs)
✅ Accidental commits (CLI-only, no .env in repos)
What env.cat does NOT protect against:
❌ Compromised CLI host (malware can read decrypted secrets from memory)
❌ Shoulder surfing (approval UI shows bundle names)
❌ Social engineering (attacker tricks you into approving)
❌ Post-injection attacks (secrets visible in shell history, env vars)
Advanced Scenarios
Mobile Approval
Use case: Approve from your phone while working on desktop
How:
- CLI on desktop shows QR code
- Scan with phone camera
- Approve on phone browser
- Desktop CLI receives secrets
Benefits:
- No copy-paste of approval URLs
- Quick approval via biometric auth (Face ID/Touch ID)
- Works across devices on same network
Partial Approval
Use case: Bundle has 20 keys, you only need 3
How:
envcat get --bundle prod/api --keys DATABASE_URL,REDIS_URL,API_KEY --api-base https://env.cat
Approval UI shows only requested keys (pre-filtered).
Benefits:
- Reduce secrets exposure
- Faster approval (less to review)
- Clearer intent
Expiration & Timeouts
Request TTL: 5 minutes (300 seconds)
If not approved within 5 minutes:
- Request deleted from Redis
- CLI shows: "Request expired after 300 seconds"
- Must create new request
Polling interval: 3 seconds
CLI checks server every 3 seconds. Server may return:
{status: "pending"}- Still waiting{status: "ready", ciphertext: "..."}- Approved!404 Not Found- Expired or invalid request ID
Rate Limiting
Protection against abuse:
- Max 10 requests per minute per user
- Max 100 approval checks per minute
- Exponential backoff on repeated failures
If rate limited:
Error: Too many requests. Please wait 60 seconds and try again.
Troubleshooting
Request Expired
Issue: CLI shows "Request expired after 300 seconds"
Solution:
- Run
envcat getagain (creates new request) - Approve faster next time (5-minute window)
Approval URL 404
Issue: Browser shows "Request not found"
Solution:
- Request may have expired (check CLI)
- URL may be incomplete (copy full URL)
- Run
envcat getagain
Can't Decrypt Payload
Issue: CLI shows "Failed to decrypt payload"
Solution:
- Server may have used wrong public key (retry request)
- Ciphertext corrupted in transit (check network)
- Report as bug (should never happen)
No Variables Received
Issue: CLI shows "Received 0 variable(s)"
Solution:
- You approved but didn't select any keys (rerun and select keys)
- Bundle is empty (attach keys to bundle first)
- Network error during approval (check server logs)
Best Practices
Security
1. Approve only what you need
# Good: Specific keys
envcat get --bundle prod/api --keys DATABASE_URL,API_KEY
# Bad: All keys when you only need one
envcat get --bundle prod/api
2. Review before approving
- Check bundle name matches your intent
- Verify environment (dev vs prod)
- Select only required keys
3. Use ephemeral sessions
# Good: Eval in subshell
(eval "$(envcat get --bundle dev/api)" && npm start)
# Bad: Export to parent shell and leave
eval "$(envcat get --bundle dev/api)"
Workflow
1. Request from trusted device
- Use CLI on your work laptop, not shared machines
- Don't approve from public/shared computers
2. Approve quickly
- Don't let approval URLs sit in browser history
- Complete flow within 5 minutes
3. Use .env for long-running processes
# Good: Write to .env with restrictive permissions
envcat get --bundle dev/api --file .env
chmod 600 .env
docker-compose up
# Bad: Eval in long-running shell
eval "$(envcat get --bundle dev/api)"
# (secrets remain in shell for hours/days)
Next Steps
Now that you understand the approval flow:
- Try It Out - Test the flow in your browser
- CLI Reference - Master CLI commands
- Security Model - Deep dive into encryption
- API Reference - Approval API endpoints
Need Help?
- Quick Start - Get up and running fast
- Troubleshooting - Common issues
- GitHub Discussions - Community help