# wardn
Credential isolation for AI agents. Agents never see real API keys — structural guarantee, not policy.
[](https://crates.io/crates/wardn)
[](LICENSE)
## 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
```mermaid
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)
```
## Install
```bash
cargo install wardn
```
## Quick Start
```bash
# Create an encrypted vault
wardn vault create
# Store credentials
wardn vault set OPENAI_KEY
wardn vault set ANTHROPIC_KEY
# Get a placeholder token (never the real key)
wardn vault get OPENAI_KEY
# → wdn_placeholder_a1b2c3d4e5f6g7h8
# List stored credentials (names only, no values)
wardn vault list
# Start the proxy
wardn serve
# Start proxy + MCP server for Claude Code / Cursor
wardn serve --mcp --agent my-agent
```
## CLI Reference
### Vault Management
```bash
wardn vault create # create encrypted vault
wardn vault set OPENAI_KEY # store credential (prompts for value, no echo)
wardn vault get OPENAI_KEY # get placeholder token (never the real value)
wardn vault get OPENAI_KEY --agent bot # get placeholder for specific agent
wardn vault list # list all credentials
wardn vault rotate OPENAI_KEY # rotate value, placeholders unchanged
wardn vault remove OPENAI_KEY # remove credential
# Custom vault path
wardn --vault /path/to/vault.enc vault list
```
### Proxy Server
```bash
wardn serve # HTTP proxy on 127.0.0.1:7777
wardn serve --host 0.0.0.0 --port 8080 # custom bind address
wardn serve --config wardn.toml # load config with rate limits + ACLs
wardn serve --mcp --agent my-agent # proxy + MCP server (stdio)
```
### Credential Migration
```bash
wardn migrate --dry-run # audit Claude Code dir for exposed keys
wardn migrate --source claude-code # scan + migrate to vault
wardn migrate --source open-claw # scan OpenClaw config
wardn migrate --source directory --path ./my-proj # scan any directory
```
### Automation
For CI/scripts, set `WARDN_PASSPHRASE` and `WARDN_VALUE` env vars to skip interactive prompts:
```bash
WARDN_PASSPHRASE=my-pass wardn vault list
WARDN_PASSPHRASE=my-pass WARDN_VALUE=sk-proj-xxx wardn vault set OPENAI_KEY
```
## Library API
Add to your `Cargo.toml`:
```toml
[dependencies]
wardn = "0.3"
```
### Vault Operations
```rust
use wardn::{Vault, config::CredentialConfig};
// Create an encrypted vault
let vault = Vault::create("vault.enc", "my-passphrase")?;
// Store a credential
vault.set_with_config("OPENAI_KEY", "sk-proj-real-key-123", &CredentialConfig {
allowed_agents: vec!["researcher".into(), "writer".into()],
allowed_domains: vec!["api.openai.com".into()],
rate_limit: Some(RateLimitConfig { max_calls: 200, per: TimePeriod::Hour }),
})?;
// Agent gets a placeholder (not the real key)
let placeholder = vault.get_placeholder("OPENAI_KEY", "researcher")?;
// → "wdn_placeholder_a1b2c3d4e5f6g7h8"
// Rotate the real key — all placeholders keep working
vault.rotate("OPENAI_KEY", "sk-proj-new-key-456")?;
```
### HTTP Proxy
```rust
use wardn::daemon::{Daemon, DaemonConfig};
let daemon = Daemon::new(vault, DaemonConfig::default());
daemon.serve_proxy().await?;
```
### MCP Server
```rust
use wardn::mcp::WardenMcpServer;
// Serve over stdio (for Claude Code, Cursor, etc.)
WardenMcpServer::serve_stdio(vault, rate_limiter, "agent-id".into()).await?;
```
MCP tools exposed (read-only, no credential values ever returned):
| `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
| 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
| `.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 |
## Configuration
```toml
[warden]
vault_path = "~/.vibeguard/vault.enc"
[warden.credentials.OPENAI_KEY]
rate_limit = { max_calls = 200, per = "hour" }
allowed_agents = ["researcher", "writer"]
allowed_domains = ["api.openai.com"]
[warden.credentials.ANTHROPIC_KEY]
rate_limit = { max_calls = 100, per = "hour" }
allowed_agents = ["researcher"]
allowed_domains = ["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
│ │ └── 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
```mermaid
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