wardn
Credential isolation for AI agents. Agents never see real API keys — structural guarantee, not policy.
The Problem
Every AI agent framework today stores API keys in environment variables or .env files. A compromised agent, malicious skill, or commodity stealer gets full access to your credentials.
~/.env → OPENAI_KEY=sk-proj-real-key # plaintext, readable by anyone
agent context → "Use OPENAI_KEY=sk-proj-real-key" # leaked into LLM context window
agent logs → Authorization: Bearer sk-proj-... # sitting in log files
The Fix
Wardn vaults credentials with AES-256-GCM encryption and gives agents useless placeholder tokens. Real keys are injected at the network layer — agents never touch them.
agent environment → OPENAI_KEY=wdn_placeholder_a1b2c3d4e5f6g7h8 (useless)
wardn vault → OPENAI_KEY=sk-proj-real-key (encrypted)
agent logs → Authorization: Bearer wdn_placeholder_a1b2... (useless)
LLM context window → wdn_placeholder_a1b2c3d4e5f6g7h8 (useless)
Architecture
flowchart TB
subgraph Agent["AI Agent Process"]
A1["Agent Code"]
A2["ENV: OPENAI_KEY=wdn_placeholder_a1b2..."]
end
subgraph Wardn["wardn daemon · localhost:7777"]
direction TB
P["HTTP Proxy"]
MCP["MCP Server\n(stdio)"]
subgraph Pipeline["Request Pipeline"]
direction LR
S1["Identify\nAgent"] --> S2["Resolve\nPlaceholder"] --> S3["Check\nAuth"] --> S4["Rate\nLimit"] --> S5["Inject\nReal Key"]
end
subgraph ResponsePipeline["Response Pipeline"]
direction RL
R1["Strip Real\nKeys"] --> R2["Replace with\nPlaceholders"]
end
subgraph Vault["Encrypted Vault"]
V1["AES-256-GCM"]
V2["Argon2id KDF"]
V3["Placeholder Map\nper agent × credential"]
end
end
subgraph External["External APIs"]
E1["api.openai.com"]
E2["api.anthropic.com"]
E3["..."]
end
A1 -- "placeholder token\nin headers/body" --> P
A1 -. "MCP: get_credential_ref\nlist_credentials\ncheck_rate_limit" .-> MCP
MCP -. "placeholder token\n(never real keys)" .-> A1
P --> Pipeline
Pipeline --> External
External --> ResponsePipeline
ResponsePipeline -- "response with\nplaceholders only" --> A1
Pipeline <--> Vault
ResponsePipeline <--> Vault
style Agent fill:#1a1a2e,stroke:#e94560,color:#fff
style Wardn fill:#0f3460,stroke:#16213e,color:#fff
style Pipeline fill:#16213e,stroke:#e94560,color:#fff
style ResponsePipeline fill:#16213e,stroke:#e94560,color:#fff
style Vault fill:#1a1a2e,stroke:#00d2ff,color:#fff
style External fill:#0a0a0a,stroke:#533483,color:#fff
How It Works
Agent sends request with placeholder in Authorization header
│
▼
┌─────────────────────────┐
│ wardn proxy │
│ localhost:7777 │
│ │
│ 1. Identify agent │
│ 2. Resolve placeholder │
│ 3. Check authorization │
│ 4. Check rate limit │
│ 5. Inject real key │
│ 6. Forward request │
│ 7. Strip key from resp │
│ 8. Return to agent │
└─────────────────────────┘
│
▼
External API (only place real key exists in transit)
Demo
Install
Quick Start
# Create an encrypted vault and store your keys
# Set up Claude Code integration (one command)
That's it. Claude Code now uses wardn's MCP server to get placeholder tokens instead of reading real keys from your environment.
What happens next
- Claude Code calls
get_credential_ref→ getswdn_placeholder_a1b2...(not the real key) - Agent sends request with placeholder through wardn proxy
- Proxy swaps placeholder for real key, forwards to API
- Proxy strips real key from response before returning to agent
The real key never enters the agent's memory, logs, or LLM context window.
Manual setup
# Get a placeholder token (never the real key)
# → wdn_placeholder_a1b2c3d4e5f6g7h8
# List stored credentials (names only, no values)
# Start the proxy
# Start proxy + MCP server for Claude Code / Cursor
CLI Reference
Vault Management
# Custom vault path
Proxy Server
Claude Code / Cursor Integration
# Or manually:
What wardn setup does
- Prompts for your vault passphrase and verifies it can open the vault
- Finds the
wardnbinary path on your system - Registers wardn as an MCP server:
- Claude Code: runs
claude mcp addwithWARDN_PASSPHRASEin the env config - Cursor: writes to
~/.cursor/mcp.jsonwith the passphrase inenv
- Claude Code: runs
- On next launch, the IDE spawns
wardn serve --mcpas a subprocess
Verifying it works
After running setup, restart your IDE and try these prompts:
"List my wardn credentials"
→ Claude calls list_credentials, shows credential names (never values)
"Get me a reference to OPENAI_KEY"
→ Claude calls get_credential_ref, gets wdn_placeholder_... (not the real key)
"Check my rate limit for OPENAI_KEY"
→ Claude calls check_rate_limit, shows remaining quota
MCP tools available
| Tool | What it returns | Security |
|---|---|---|
get_credential_ref |
Placeholder token (wdn_placeholder_...) |
Never the real value |
list_credentials |
Credential names + metadata | Filtered by agent's access |
check_rate_limit |
Remaining quota, retry info | Read-only |
Credential Migration
Automation
For CI/scripts, set WARDN_PASSPHRASE and WARDN_VALUE env vars to skip interactive prompts:
WARDN_PASSPHRASE=my-pass
WARDN_PASSPHRASE=my-pass WARDN_VALUE=sk-proj-xxx
Library API
Add to your Cargo.toml:
[]
= "0.3"
Vault Operations
use ;
// Create an encrypted vault
let vault = create?;
// Store a credential
vault.set_with_config?;
// Agent gets a placeholder (not the real key)
let placeholder = vault.get_placeholder?;
// → "wdn_placeholder_a1b2c3d4e5f6g7h8"
// Rotate the real key — all placeholders keep working
vault.rotate?;
HTTP Proxy
use ;
let daemon = new;
daemon.serve_proxy.await?;
MCP Server
use WardenMcpServer;
// Serve over stdio (for Claude Code, Cursor, etc.)
serve_stdio.await?;
MCP tools exposed (read-only, no credential values ever returned):
| Tool | Description |
|---|---|
get_credential_ref |
Get your placeholder token for a credential |
list_credentials |
List credentials you're authorized to access |
check_rate_limit |
Check your remaining quota |
Security Properties
| Property | Guarantee |
|---|---|
| No credential in agent memory | Agent process only holds placeholder strings |
| No credential on disk in plaintext | AES-256-GCM encrypted vault with Argon2id KDF |
| No credential in logs | Only placeholders appear in any log output |
| No credential in LLM context | Placeholder injected into env, real key at network layer |
| Bounded cost exposure | Token bucket rate limits per credential per agent |
| Credential echo protection | Real keys stripped from API responses before reaching agent |
| Memory safety | SensitiveString/SensitiveBytes zeroed on drop |
| Atomic persistence | Write-tmp-then-rename prevents vault corruption |
What This Defeats
| Attack | How wardn stops it |
|---|---|
.env credential theft |
No .env files. Keys only in encrypted vault |
Malicious skill reads $OPENAI_KEY |
Gets wdn_placeholder_... — useless |
| Stealer targets agent config | Finds only placeholder tokens |
| Prompt injection exfiltrates key | Key never in agent context window |
| Agent logs contain credentials | Logs contain only placeholder strings |
| Full agent compromise | Attacker has a useless placeholder |
| Cost runaway from looping agent | Rate limit per credential per agent |
How Is This Different From...
| Tool | What it does | How wardn differs |
|---|---|---|
| Secrets managers (Vault, AWS SM, 1Password) | Secure storage + retrieval | Agent still gets the real key at runtime. Wardn ensures the agent never touches it. |
| Varlock | Schema-based .env validation + AI-safe config |
Focuses on config management and leak scanning. Wardn does runtime credential injection — the key never enters the agent process. |
| OpenRouter | API routing + key management | Trusts the client with an API key. Wardn doesn't — agent holds a useless placeholder. |
| dotenv + .gitignore | Keep secrets out of git | Keys still in memory, env vars, logs. Wardn removes them from all three. |
| Service meshes (Istio, Linkerd) | Service-to-service auth | Solve infra-level mTLS. Wardn solves agent-to-API auth where the agent itself is untrusted. |
Trust Boundary
Wardn concentrates trust in a single local process (the proxy) instead of spreading it across every plugin, tool, and LLM context window. This is a smaller attack surface, not zero attack surface:
- The proxy runs locally as a subprocess spawned by your IDE or shell — same trust level as your kernel
- The vault is encrypted at rest and only decrypted in-memory with your passphrase
- If your local machine is fully compromised, wardn can't help (nothing can)
- The placeholder token is a bearer token to the proxy — but it only works via
localhost:7777, not against real APIs, and can be rate-limited and revoked per-agent
Audit Logging
Every credential access is logged with a unique request ID for traceability:
INFO request_id=a1b2c3 agent=claude-code method=POST domain=api.openai.com path=/v1/chat/completions proxy request received
INFO request_id=a1b2c3 agent=claude-code credential=OPENAI_KEY domain=api.openai.com credential injected
INFO request_id=a1b2c3 agent=claude-code upstream_status=200 credentials_injected=1 credentials_stripped=0 proxy request completed
Set RUST_LOG=wardn=info (or debug/trace) to control verbosity. Logs go to stderr, never stdout.
Configuration
[]
= "~/.vibeguard/vault.enc"
[]
= { = 200, = "hour" }
= ["researcher", "writer"]
= ["api.openai.com"]
[]
= { = 100, = "hour" }
= ["researcher"]
= ["api.anthropic.com"]
Project Structure
wardn/
├── src/
│ ├── main.rs # CLI entry point (clap + tokio)
│ ├── cli/
│ │ ├── mod.rs # Clap argument definitions
│ │ ├── vault_cmd.rs # Vault subcommand handlers
│ │ ├── serve_cmd.rs # Serve subcommand handler
│ │ ├── setup_cmd.rs # Claude Code / Cursor MCP setup
│ │ └── migrate_cmd.rs # Migrate subcommand handler
│ ├── lib.rs # Public API, WardenError
│ ├── config.rs # TOML configuration parsing
│ ├── vault/
│ │ ├── mod.rs # Vault CRUD operations
│ │ ├── encryption.rs # AES-256-GCM + Argon2id + zeroize types
│ │ ├── storage.rs # On-disk format (WDNV), atomic writes
│ │ └── placeholder.rs # Token generation, per-agent isolation
│ ├── proxy/
│ │ ├── mod.rs # HTTP proxy server (axum)
│ │ ├── inject.rs # Credential injection into requests
│ │ ├── strip.rs # Credential stripping from responses
│ │ └── rate_limit.rs # Token bucket rate limiter
│ ├── mcp/
│ │ ├── mod.rs # MCP server (rmcp, stdio transport)
│ │ └── tools.rs # Tool parameter/response types
│ ├── migrate/
│ │ ├── mod.rs # Migration orchestrator + risk scoring
│ │ └── scanners/
│ │ └── credentials.rs # API key pattern scanner
│ └── daemon/
│ └── mod.rs # Daemon (proxy + MCP in single process)
└── tests/
├── cli_tests.rs # CLI integration tests
├── vault_tests.rs # Vault integration tests
└── proxy_tests.rs # Proxy integration tests
Vault Encryption
flowchart LR
subgraph Input
Pass["Passphrase"]
Salt["Random Salt\n(16 bytes)"]
Creds["Credentials\n(JSON)"]
end
subgraph KDF["Key Derivation"]
Argon["Argon2id\nm=19456 t=2 p=1"]
end
subgraph Encrypt["Encryption"]
AES["AES-256-GCM"]
Nonce["Random Nonce\n(12 bytes)"]
end
subgraph Output["WDNV File"]
direction TB
Magic["WDNV (4B)"]
Ver["Version (2B)"]
SaltOut["Salt (16B)"]
Payload["Nonce ‖ Ciphertext ‖ Tag"]
end
Pass --> Argon
Salt --> Argon
Argon -- "256-bit key" --> AES
Creds --> AES
Nonce --> AES
AES --> Payload
style Input fill:#1a1a2e,stroke:#e94560,color:#fff
style KDF fill:#16213e,stroke:#00d2ff,color:#fff
style Encrypt fill:#16213e,stroke:#00d2ff,color:#fff
style Output fill:#0f3460,stroke:#533483,color:#fff
File Format
Bytes 0-3: Magic "WDNV"
Bytes 4-5: Version (u16 LE)
Bytes 6-21: Argon2id salt (16 bytes)
Bytes 22+: AES-256-GCM encrypted payload (nonce ‖ ciphertext ‖ tag)
Part of VibeGuard
Wardn is the credential isolation layer of VibeGuard — a security daemon for AI agents. Other planned modules:
- Sentinel — prompt injection firewall
- CloakPipe — PII redaction middleware
- Watcher — audit log + dashboard
License
MIT