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

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

cargo install wardn

Quick Start

# 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

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

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

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:

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:

[dependencies]
wardn = "0.3"

Vault Operations

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

use wardn::daemon::{Daemon, DaemonConfig};

let daemon = Daemon::new(vault, DaemonConfig::default());
daemon.serve_proxy().await?;

MCP Server

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

[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

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