
AIエージェントのためのクレデンシャル分離。エージェントは実際のAPIキーを一切見ることがありません - 構造的保証であり、ポリシーではありません。
AIエージェントのためのクレデンシャルファイアウォール。
この冒頭の主張は構造的なものであり、ポリシーではありません。エージェントはプレースホルダートークンを受け取り、実際のAPIキーは決して受け取りません。実際のキーは、wardnプロキシ内部からアップストリームAPIに至る一つのネットワークシームのみを通過し、エージェントに到達する前にレスポンスから除去されます。ログ、環境、LLMコンテキストウィンドウ、スクラッチファイル、シェル履歴にはプレースホルダーのみが保持されます。```text agent process OPENAI_KEY=wdn_placeholder_a1b2c3d4e5f6g7h8 (useless) agent logs Authorization: Bearer wdn_placeholder_a1b2... (useless) LLM context wdn_placeholder_a1b2c3d4e5f6g7h8 (useless) wardn proxy injects the real key in-flight, single seam (deleted on response) ~/.vibeguard/vault.enc AES-256-GCM(Argon2id(passphrase)) (encrypted at rest)
これは負荷に耐える主張であり、現在のエージェント侵害、プロンプトインジェクション、ログの窃取、スキルの外部流出に対して防御可能です。
詳細については [docs/THREAT-MODEL.md](https://github.com/rohansx/wardn/blob/HEAD/docs/THREAT-MODEL.md) をご覧ください。カバーされている範囲とされていない範囲の正直な内訳が書かれています。より強力な「ホストが侵害されても何も漏洩しない」という主張が達成可能になるティアも含まれています。
保管庫自体(保存時に暗号化、パスフレーズ由来の鍵)は実際のコンポーネントであり、ファイアウォールを単一のマシンで実行できる理由です。今後の [docs/HOSTED-TIER.md](https://github.com/rohansx/wardn/blob/HEAD/docs/HOSTED-TIER.md) ティアでは、さらにプロキシを機密コンピューティングエンクレーブでラップするため、完全に侵害されたVPSでも鍵を読み取ることはできません。
[](https://crates.io/crates/wardn)
[](LICENSE)
## 問題点
現在のすべてのAIエージェントフレームワークは、APIキーを環境変数や `.env` ファイルに保存しています。侵害されたエージェント、悪意のあるスキル、コモディティスティーラー、またはLLMログから `Authorization: Bearer sk-...` を外部流出させるプロンプトインジェクションによって、あなたの認証情報が完全に取得されます。```
~/.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
wardnはエージェントに無価値なプレースホルダ文字列を渡し、到達可能なすべての表面から実際のキーを削除します。実際のキーはネットワーク層(単一の継ぎ目)で注入され、エージェントに到達する前にレスポンスから除去されます。``` agent environment → OPENAI_KEY=wdn_placeholder_a1b2c3d4e5f6g7h8 (useless) wardn vault → OPENAI_KEY=sk-proj-real-key (encrypted at rest) upstream request → Authorization: Bearer sk-proj-real-key (network transit only) upstream response → ...real keys stripped, placeholders returned... (re-injected on the way back) agent logs → Authorization: Bearer wdn_placeholder_a1b2... (useless) LLM context window → wdn_placeholder_a1b2c3d4e5f6g7h8 (useless)
## アーキテクチャ```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
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="https://assets.kitploit.com/production/public/readmes/12823/1fa6109ffd855ec98c173c5edd2d7ee77f6b0c918a3cdecb1ea5fbfe8326161d.gif" alt="wardn デモ" width="800">
</p>
## 信頼レベル(正直に)
| 階層 | 場所 | 保証内容 |
|---|---|---|
| **セルフホスト(現在)** | あなたのラップトップ、VPS、CI | 保存時暗号化されたボールト、エージェントに対するファイアウォールの主張。ホスト上のrootからの防御は**行いません**。 |
| **ホステッド(近日対応予定)** | wardn管理またはBYOクラウド | 機密コンピューティングエンクレーブ(Nitro / SEV-SNP)+ リモートアテステーション + プロキシへの暗号化フロー。本当の「host compromise leaks nothing」という主張。 |
セルフホスト階層は現在の主要な主張であり、今日提供されています。ホステッド階層は厳格なアップグレードパスです。費用と運用の複雑さがかかり、その設計は[docs/HOSTED-TIER.md](https://github.com/rohansx/wardn/blob/HEAD/docs/HOSTED-TIER.md)にあります。カバーされるものとされないものの正直な完全な目録:
👉 **[docs/THREAT-MODEL.md](https://github.com/rohansx/wardn/blob/HEAD/docs/THREAT-MODEL.md)** — カバー/非カバーの表、「no software vault eliminates host compromise」を明確に指摘し、アップグレードパスも記載。
## Install```bash
# Prebuilt binary (Linux/macOS, amd64/arm64), checksum-verified
curl -sSf https://raw.githubusercontent.com/rohansx/wardn/main/install.sh | sh
# or from crates.io
cargo install wardn
# or Homebrew, once the tap is published (see Formula/wardn.rb)
brew install rohansx/wardn/wardn
wardn vault create wardn vault set OPENAI_KEY wardn vault set ANTHROPIC_KEY
wardn setup claude-code
以上です。Claude Code は、環境から実際のキーを読み取る代わりに、wardn の MCP サーバーを使用してプレースホルダートークンを取得するようになりました。
### その後の流れ
1. Claude Code が `get_credential_ref` を呼び出す → `wdn_placeholder_a1b2...`(実際のキーではない)を取得
2. エージェントがプレースホルダーを含むリクエストを wardn プロキシ経由で送信
3. プロキシがプレースホルダーを実際のキーに置き換え、API に転送
4. レスポンスがエージェントに返される前に、プロキシが実際のキーを削除
実際のキーがエージェントのメモリ、ログ、LLM コンテキストウィンドウに入ることはありません。
## ローカルダッシュボード
デーモンが起動したら(`wardn serve`、または `wardn run` で起動)、ブラウザで
**http://127.0.0.1:7777/ui** を開いてください。読み取り専用、ローカルのみのビューです。
- **認証情報 (Credentials)** — 保存されたすべての認証情報とその ACL(許可されたエージェント、
許可されたドメイン、レート制限 + 予算バッジ)を表示。
- **最近のアクティビティ (Recent Activity)** — 直近 50 件のプロキシイベントを表示。メソッド、ドメイン、
パス、ステータス、エージェント、リクエスト ID、記録されたコスト(`request_completed`、
`credential_injected`、`rate_limit`、`budget_exceeded`、`loop_detected`、
`request_error`)を含む。
- **予算 (Budgets)** — 各認証情報の設定済み予算(最大、使用済み、
残り、期間、モード)を、50% / 80% を超えると警告 → 不良に変わる進捗バー付きで表示。
2 秒ごとに自動ポーリングされます。変更用のエンドポイントはありません。ダッシュボードから操作する唯一の方法は API そのもの(`/api/summary`、`/api/credentials`、`/api/audit?limit=N`、`/api/budgets`)です。```bash
# Static, anonymous, never sees real keys
curl http://127.0.0.1:7777/api/summary | jq
wardn vault get OPENAI_KEY
wardn vault list
wardn serve
wardn serve --mcp --agent my-agent
## CLIリファレンス
### Vault管理```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
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 統合```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
wardn setup doeswardn binary path on your systemclaude mcp add with WARDN_PASSPHRASE in the env config~/.cursor/mcp.json with the passphrase in envwardn serve --mcp as a subprocessAfter 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ツール
| ツール | 返却内容 | セキュリティ |
|------|----------------|----------|
| `get_credential_ref` | プレースホルダートークン(`wdn_placeholder_...`) | 実際の値は含まれません |
| `list_credentials` | 認証情報名+メタデータ | エージェントのアクセス権限でフィルタリング |
| `check_rate_limit` | 残りクォータ、リトライ情報 | 読み取り専用 |
### 認証情報の移行```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
export, quoted valueswardn import dotenv ./.env
wardn import file ./creds.json wardn import file ./creds.yaml
op session. Default name iswardn import one-password op://Personal/openai/api_key wardn import one-password op://Work/anthropic/token --name ANTHROPIC_KEY
echo 'OPENAI_KEY=sk-...' | wardn import stdin
各インポーターは初回使用時にボールトのパスフレーズを要求します(または
`WARDN_PASSPHRASE` / OSキーチェーンから読み取ります)。既存の値は静かに
上書きされます — インポーターは値のみを扱い、メタデータ(許可エージェント /
ドメイン / レート制限 / 予算)は保持されます。
### 自動化
CI/スクリプトでは、`WARDN_PASSPHRASE` と `WARDN_VALUE` の環境変数を設定してインタラクティブなプロンプトをスキップします。```bash
WARDN_PASSPHRASE=my-pass wardn vault list
WARDN_PASSPHRASE=my-pass WARDN_VALUE=sk-proj-xxx wardn vault set OPENAI_KEY
Cargo.toml に追加してください:```toml
[dependencies]
wardn = "0.4"
### Vault 操作```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")?;
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 | 認証情報のプレースホルダトークンを取得します |
list_credentials | アクセスが許可されている認証情報を一覧表示します |
check_rate_limit | 残りのクォータを確認します |
wardnの最も近い直接的なピア(Infisical Agent Vault、1Password for Agents、LiteLLMの仮想キー)との詳細な比較、およびwardnの保証がOWASPのAgentic Top 10とMCP仕様のセキュリティガイダンスにどのように対応するかについては、docs/comparison.mdを参照してください。
Wardnは信頼を単一のローカルプロセス(プロキシ)に集中させ、すべてのプラグイン、ツール、LLMコンテキストウィンドウに分散させません。これは攻撃対象領域を小さくするもので、ゼロではありません。
localhost:7777経由でのみ機能し、実際のAPIには使用できない。エージェントごとにレート制限および取り消しが可能すべての認証情報アクセスは、トレーサビリティのために一意のリクエストIDとともに記録されます。``` 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`(または `debug`/`trace`)を設定して、冗長性を制御します。ログは stderr に出力され、stdout には出力されません。
## 設定```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"]
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
│ │ ├── run_cmd.rs # wardn run — lazy-starts the daemon, wires
│ │ │ # agent env vars, execs the child
│ │ ├── setup_cmd.rs # Claude Code / Cursor MCP setup (+ shell alias)
│ │ └── migrate_cmd.rs # Migrate subcommand handler
│ ├── lib.rs # Public API, WardenError
│ ├── config.rs # TOML configuration parsing, [upstreams] map
│ ├── 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
│ │ └── keyring_store.rs # OS keychain passphrase storage
│ ├── proxy/
│ │ ├── mod.rs # HTTP proxy server (axum)
│ │ ├── route.rs # Provider-prefix vs Host-header upstream routing
│ │ ├── inject.rs # Credential injection into requests
│ │ ├── strip.rs # Credential stripping (shared pair-building)
│ │ ├── stream.rs # Streaming (SSE/chunked) credential stripper
│ │ └── 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 tests without a real upstream
├── proxy_e2e_tests.rs # Real upstream via wiremock (header/body/SSE)
└── run_cmd_tests.rs # Real end-to-end wardn run
## 開発```bash
# Integration tests use a fast (insecure) KDF so the suite runs in
# milliseconds instead of paying the real Argon2id cost per test —
# always pass this feature flag when running tests locally or in CI:
cargo test --features test-fast-kdf
cargo build
cargo clippy --all-targets --features test-fast-kdf
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
### ファイル形式```
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)
WardnはVibeGuardの認証情報分離層です — AIエージェント向けセキュリティデーモンです。他の計画済みモジュール:
MIT
| プロパティ | 保証 |
|---|
| エージェントメモリに認証情報なし | エージェントプロセスはプレースホルダ文字列のみ保持 |
| ディスクに平文で認証情報を保存しない | AES-256-GCM暗号化ボールトとArgon2id KDF |
| ログに認証情報なし | ログ出力にはプレースホルダのみ表示 |
| LLMコンテキストに認証情報なし | プレースホルダが環境変数に注入され、実際のキーはネットワーク層に保持 |
| コスト露出の制限 | トークンバケットレート制限(認証情報ごと、エージェントごと) |
| 認証情報エコー保護 | 実際のキーはAPIレスポンスから削除され、エージェントに到達しない |
| メモリ安全性 | SensitiveString/SensitiveBytesはドロップ時にゼロ化 |
| アトミックな永続性 | 一時ファイルに書き込んでリネームする方式でボールトの破損を防止 |
| 攻撃 | wardnがどのように防ぐか |
|---|
.env認証情報の窃取 | .envファイルなし。キーは暗号化ボールトのみ |
悪意のあるスキルが$OPENAI_KEYを読み取る | wdn_placeholder_...を取得 — 無意味 |
| スティーラーがエージェント設定を標的にする | プレースホルダトークンのみを見つける |
| プロンプトインジェクションによるキーの漏洩 | キーはエージェントコンテキストウィンドウに決して存在しない |
| エージェントログに認証情報が含まれる | ログにはプレースホルダ文字列のみ含まれる |
| エージェントの完全な侵害 | 攻撃者は役に立たないプレースホルダを入手 |
| ループするエージェントによるコスト暴走 | 認証情報ごと、エージェントごとのレート制限 |
| ツール | 機能 | wardnとの違い |
|---|
| シークレットマネージャー (Vault, AWS SM, 1Password) | 安全な保存と取得 | エージェントは実行時に依然として実際のキーを取得する。Wardnはエージェントがキーに触れないようにする。 |
| Varlock | スキーマベースの.env検証 + AIセーフな設定 | 設定管理と漏洩スキャンに焦点。Wardnはランタイムでの認証情報注入を行い、キーはエージェントプロセスに決して入らない。 |
| OpenRouter | APIルーティング + キー管理 | クライアントにAPIキーを信頼させる。Wardnは違う — エージェントは役に立たないプレースホルダを保持する。 |
| dotenv + .gitignore | シークレットをgitから守る | キーは依然としてメモリ、環境変数、ログに存在する。Wardnはそれらすべてから削除する。 |
| サービスメッシュ (Istio, Linkerd) | サービス間認証 | インフラレベルのmTLSを解決。Wardnはエージェント自体が信頼できない場合のエージェント対API認証を解決する。 |