wardn 0.3.0

Credential isolation proxy — agents never see real API keys
Documentation
# 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)
```

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

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

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