
изоляция учетных данных для 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`. Скомпрометированный агент, вредоносный навык, обычный стилер или инъекция подсказок, извлекающие `Authorization: Bearer sk-...` из лога LLM, получают полный доступ к вашим учетным данным.```
~/.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)
## Демо
<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-cloud | Конфиденциальная вычислительная анклав (Nitro / SEV-SNP) + удаленная аттестация + поток шифрования к прокси. Реальное заявление «компрометация хоста ничего не раскрывает». |
Уровень самостоятельного хостинга является основным заявлением и доступен сегодня. Уровень хостинга — это строгий путь обновления: он требует денег и операционной сложности, и его дизайн описан в [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)** — таблица покрытия/непокрытия, явно указано, что «ни одно программное хранилище не устраняет компрометацию хоста», и путь обновления.
## Установка```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 использует MCP-сервер wardn для получения токенов-заполнителей вместо чтения реальных ключей из вашего окружения.
### Что происходит дальше
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**. Это доступная только для чтения, только локальная панель, содержащая:
- **Учётные данные** — каждое сохранённое учётное данное с его ACL (разрешённые агенты, разрешённые домены, значки лимитов и бюджета).
- **Недавняя активность** — последние 50 событий прокси с методом, доменом, путём, статусом, агентом, request_id и зафиксированной стоимостью (`request_completed`, `credential_injected`, `rate_limit`, `budget_exceeded`, `loop_detected`, `request_error`).
- **Бюджеты** — настроенный бюджет каждого учётного данного (макс., потрачено, осталось, окно, режим) с индикатором выполнения, который меняет цвет с предупреждающего на плохой при пересечении отметок 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
### Управление хранилищем```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 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
wardn setupwardn в вашей системеclaude mcp add с WARDN_PASSPHRASE в конфигурации окружения~/.cursor/mcp.json с кодовой фразой в envwardn serve --mcp как подпроцессПосле запуска настройки перезапустите вашу IDE и попробуйте следующие запросы:``` "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` / системной связки ключей). Существующие значения бесшумно перезаписываются — импортер работает только со значениями, метаданные (разрешенные агенты / домены / лимит скорости / бюджет) сохраняются.
### Автоматизация
Для 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 Сервер```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 | Описание |
|---|---|
get_credential_ref | Получить ваш токен-заполнитель для учетных данных |
list_credentials | Список учетных данных, к которым у вас есть доступ |
check_rate_limit | Проверить оставшуюся квоту |
Для более глубокого сравнения с ближайшими прямыми аналогами wardn (Infisical Agent Vault, 1Password for Agents, LiteLLM virtual keys) и того, как гарантии wardn соотносятся с Agentic Top 10 от OWASP и руководствами по безопасности спецификации MCP, см. docs/comparison.md.
Wardn концентрирует доверие в одном локальном процессе (прокси) вместо того, чтобы распределять его по каждому плагину, инструменту и контекстному окну LLM. Это меньшая, но не нулевая поверхность атаки:
localhost:7777, а не против реальных API, и может быть ограничен по частоте и отозван для каждого агентаКаждый доступ к учетным данным регистрируется с уникальным идентификатором запроса для отслеживания:``` 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
Установите `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_... — бесполезно |
| Вор нацелен на конфигурацию агента | Находит только токены-заполнители |
| Инъекция подсказки (Prompt injection) извлекает ключ | Ключ никогда не появляется в контекстном окне агента |
| Журналы агента содержат учетные данные | Журналы содержат только строки-заполнители |
| Полная компрометация агента | Атакующий имеет бесполезный заполнитель |
| Неуправляемые затраты от зациклившегося агента | Ограничение частоты для каждого учетного данного на агента |
| Инструмент | Что он делает | В чем отличие wardn |
|---|
| Менеджеры секретов (Vault, AWS SM, 1Password) | Безопасное хранение + получение | Агент всё равно получает настоящий ключ во время выполнения. Wardn гарантирует, что агент никогда его не коснётся. |
| Varlock | Валидация .env на основе схемы + безопасная для ИИ конфигурация | Сосредоточен на управлении конфигурацией и сканировании утечек. Wardn выполняет инъекцию учетных данных во время выполнения — ключ никогда не попадает в процесс агента. |
| OpenRouter | Маршрутизация API + управление ключами | Доверяет клиенту API-ключ. Wardn не доверяет — агент держит бесполезный заполнитель. |
| dotenv + .gitignore | Не допускать секреты в git | Ключи все еще в памяти, переменных окружения, журналах. Wardn удаляет их из всех трех. |
| Service meshes (Istio, Linkerd) | Аутентификация между сервисами | Решают mTLS на уровне инфраструктуры. Wardn решает аутентификацию агента к API, где сам агент не является доверенным. |