wardn 0.4.1

Credential isolation proxy — agents never see real API keys
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
# wardn

Credential isolation for AI agents. Agents never see real API keys — structural guarantee, not policy.

[![Crates.io](https://img.shields.io/crates/v/wardn.svg)](https://crates.io/crates/wardn)
[![License](https://img.shields.io/crates/l/wardn.svg)](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)
```

## Demo

<p align="center">
  <img src="demo/wardn-demo.gif" alt="wardn demo" width="800">
</p>

## Install

```bash
cargo install wardn
```

## Quick Start

```bash
# Create an encrypted vault and store your keys
wardn vault create
wardn vault set OPENAI_KEY
wardn vault set ANTHROPIC_KEY

# Set up Claude Code integration (one command)
wardn setup claude-code
```

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

1. Claude Code calls `get_credential_ref` → gets `wdn_placeholder_a1b2...` (not the real key)
2. Agent sends request with placeholder through wardn proxy
3. Proxy swaps placeholder for real key, forwards to API
4. 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

```bash
# 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)
```

### Claude Code / Cursor Integration

```bash
wardn setup claude-code                   # register wardn as MCP server in Claude Code
wardn setup cursor                        # register wardn as MCP server in Cursor

# Or manually:
claude mcp add --transport stdio --scope user wardn -- wardn serve --mcp --agent claude-code
```

#### What `wardn setup` does

1. Prompts for your vault passphrase and verifies it can open the vault
2. Finds the `wardn` binary path on your system
3. Registers wardn as an MCP server:
   - **Claude Code**: runs `claude mcp add` with `WARDN_PASSPHRASE` in the env config
   - **Cursor**: writes to `~/.cursor/mcp.json` with the passphrase in `env`
4. On next launch, the IDE spawns `wardn serve --mcp` as 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

```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):

| 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

```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
│   │   ├── 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

```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