
isolamento de credenciais para agentes de IA. Os agentes nunca veem chaves de API reais - garantia estrutural, não política.
Um firewall de credenciais para agentes de IA.
A alegação principal é estrutural, não política: os agentes recebem tokens de espaço reservado, nunca chaves de API reais. A chave real cruza apenas uma costura de rede — dentro do proxy wardn, a caminho da API upstream — e é removida das respostas antes que cheguem ao agente. Logs, ambiente, janelas de contexto de LLM, arquivos temporários e histórico de shell contêm apenas espaços reservados.```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)
Esta é a afirmação principal e é defensável hoje contra comprometimento de agente, injeção de prompt, roubo de logs e exfiltração de habilidades.
Leia [docs/THREAT-MODEL.md](https://github.com/rohansx/wardn/blob/HEAD/docs/THREAT-MODEL.md) para a divisão honesta entre o que é coberto e o que não é — incluindo o nível onde a afirmação mais forte de "comprometimento do host não vaza nada" se torna alcançável.
O próprio cofre (criptografado em repouso, chave derivada de senha) é um componente real e a razão pela qual o firewall pode ser executado em uma única máquina. O próximo nível [docs/HOSTED-TIER.md](https://github.com/rohansx/wardn/blob/HEAD/docs/HOSTED-TIER.md) adicionalmente envolve o proxy em um enclave de computação confidencial para que mesmo um VPS totalmente comprometido não possa ler a chave.
[](https://crates.io/crates/wardn)
[](LICENSE)
## O Problema
Todo framework de agente de IA hoje armazena chaves de API em variáveis de ambiente ou arquivos `.env`. Um agente comprometido, habilidade maliciosa, ladrão comum ou injeção de prompt exfiltrado `Authorization: Bearer sk-...` de um log de LLM obtém acesso total às suas credenciais.```
~/.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 entrega aos agentes uma string de placeholder inútil e remove a chave real de toda superfície que pode alcançar. As chaves reais são injetadas na camada de rede — uma única costura — e removidas das respostas antes de chegarem ao agente.``` 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)
## Arquitetura```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)
## Demonstração
<p align="center">
<img src="https://assets.kitploit.com/production/public/readmes/12823/1fa6109ffd855ec98c173c5edd2d7ee77f6b0c918a3cdecb1ea5fbfe8326161d.gif" alt="wardn demonstração" width="800">
</p>
## Níveis de Confiança, Honestos
| Nível | Onde | O que garante |
|---|---|---|
| **Auto-hospedado (hoje)** | seu laptop, seu VPS, CI | Cofre criptografado em repouso, alegação de firewall contra agentes. **Não** defende contra root no host. |
| **Hospedado (em breve)** | gerenciado pela wardn ou BYO-cloud | Enclave de computação confidencial (Nitro / SEV-SNP) + atestação remota + fluxo de criptografia para o proxy. Alegação real de "comprometimento do host não vaza nada". |
O nível auto-hospedado é a alegação principal e é enviado hoje. O nível hospedado é o caminho de atualização estrito: custa dinheiro e complexidade operacional, e seu design está em [docs/HOSTED-TIER.md](https://github.com/rohansx/wardn/blob/HEAD/docs/HOSTED-TIER.md). Inventário completo e honesto do que é e não é coberto:
👉 **[docs/THREAT-MODEL.md](https://github.com/rohansx/wardn/blob/HEAD/docs/THREAT-MODEL.md)** — tabela de cobertura / não cobertura, "nenhum cofre de software elimina comprometimento do host" explicitamente destacado, e o caminho de atualização.
## Instalação```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
Assim é. O Claude Code agora utiliza o servidor MCP do wardn para obter tokens de espaço reservado em vez de ler as chaves reais do seu ambiente.
### O que acontece a seguir
1. O Claude Code chama `get_credential_ref` → obtém `wdn_placeholder_a1b2...` (não a chave real)
2. O agente envia a requisição com o espaço reservado através do proxy wardn
3. O proxy troca o espaço reservado pela chave real, encaminha para a API
4. O proxy remove a chave real da resposta antes de devolvê-la ao agente
A chave real nunca entra na memória, logs ou contexto do LLM do agente.
## Painel Local
Assim que o daemon estiver ativo (`wardn serve`, ou iniciado por `wardn run`), abra
**http://127.0.0.1:7777/ui** num navegador. Uma visualização somente leitura e local de:
- **Credenciais** — cada credencial armazenada com suas ACLs (agentes permitidos,
domínios permitidos, limites de taxa e distintivos de orçamento).
- **Atividade Recente** — os últimos 50 eventos de proxy com método, domínio,
caminho, status, agente, request_id e custo registado (`request_completed`,
`credential_injected`, `rate_limit`, `budget_exceeded`, `loop_detected`,
`request_error`).
- **Orçamentos** — o orçamento configurado de cada credencial (máximo, gasto,
restante, janela, modo) com uma barra de progresso que fica amarela → vermelha à medida que
ultrapassa 50% / 80%.
Atualizado automaticamente a cada 2 s. Sem endpoints de mutação — a única forma
de sair do painel é pela própria 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
## Referência da CLI
### Gerenciamento de 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 / Integração com 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 fazwardn no seu sistemaclaude mcp add com WARDN_PASSPHRASE na configuração de ambiente~/.cursor/mcp.json com a frase secreta em envwardn serve --mcpApós executar a configuração, reinicie seu IDE e tente estes comandos:``` "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
#### Ferramentas MCP disponíveis
| Ferramenta | O que retorna | Segurança |
|------|----------------|----------|
| `get_credential_ref` | Token temporário (`wdn_placeholder_...`) | Nunca o valor real |
| `list_credentials` | Nomes de credenciais + metadados | Filtrado pelo acesso do agente |
| `check_rate_limit` | Cota restante, informações de nova tentativa | Somente leitura |
### Migração de Credenciais```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
Cada importador solicita a frase secreta do cofre no primeiro uso (ou a lê a partir de `WARDN_PASSPHRASE` / do chaveiro do SO). Os valores existentes são sobrescritos silenciosamente — o importador é apenas de valor, os metadados (agentes permitidos / domínios / limite de taxa / orçamento) são preservados.
### Automation
Para CI/scripts, defina as variáveis de ambiente `WARDN_PASSPHRASE` e `WARDN_VALUE` para ignorar prompts interativos:```bash
WARDN_PASSPHRASE=my-pass wardn vault list
WARDN_PASSPHRASE=my-pass WARDN_VALUE=sk-proj-xxx wardn vault set OPENAI_KEY
Adicione ao seu Cargo.toml:```toml
[dependencies]
wardn = "0.4"
### Operações do Cofre```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?;
Ferramentas MCP expostas (somente leitura, nenhum valor de credencial é retornado):
| Tool | Descrição |
|---|---|
get_credential_ref | Obter seu token de espaço reservado para uma credencial |
list_credentials | Listar credenciais que você está autorizado a acessar |
check_rate_limit | Verificar sua cota restante |
Para uma comparação mais aprofundada contra os pares diretos mais próximos do wardn (Infisical Agent Vault, 1Password for Agents, LiteLLM virtual keys) e como as garantias do wardn se mapeiam para o OWASP's Agentic Top 10 e as diretrizes de segurança da especificação MCP, consulte docs/comparison.md.
O Wardn concentra a confiança em um único processo local (o proxy) em vez de espalhá-la por todos os plugins, ferramentas e janelas de contexto do LLM. Isso é uma superfície de ataque menor, não uma superfície de ataque zero:
localhost:7777, não contra APIs reais, e pode ser limitado por taxa e revogado por agenteCada acesso a credencial é registrado com um ID de requisição único para rastreabilidade:``` 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
Defina `RUST_LOG=wardn=info` (ou `debug`/`trace`) para controlar a verbosidade. Os logs vão para stderr, nunca para stdout.
## Configuração```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
## Desenvolvimento```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
### Formato do Arquivo```
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 é a camada de isolamento de credenciais do VibeGuard — um daemon de segurança para agentes de IA. Outros módulos planejados:
MIT
| Propriedade | Garantia |
|---|
| Nenhuma credencial na memória do agente | O processo do agente contém apenas strings de espaço reservado |
| Nenhuma credencial em texto simples no disco | Cofre criptografado AES-256-GCM com KDF Argon2id |
| Nenhuma credencial nos logs | Apenas espaços reservados aparecem em qualquer saída de log |
| Nenhuma credencial no contexto do LLM | Espaço reservado injetado no env, chave real na camada de rede |
| Exposição de custo limitada | Limites de taxa de bucket de token por credencial por agente |
| Proteção de eco de credencial | Chaves reais removidas das respostas da API antes de chegar ao agente |
| Segurança de memória | SensitiveString/SensitiveBytes zerados ao descartar |
| Persistência atômica | Escrever-tmp-depois-renomear evita corrupção do cofre |
| Ataque | Como o wardn o impede |
|---|
Roubo de credencial .env | Sem arquivos .env. Chaves apenas no cofre criptografado |
Skill maliciosa lê $OPENAI_KEY | Obtém wdn_placeholder_... — inútil |
| Stealer ataca a configuração do agente | Encontra apenas tokens de espaço reservado |
| Injeção de prompt exfiltra chave | Chave nunca na janela de contexto do agente |
| Logs do agente contêm credenciais | Logs contêm apenas strings de espaço reservado |
| Comprometimento total do agente | Atacante tem um espaço reservado inútil |
| Fuga de custo de agente em loop | Limite de taxa por credencial por agente |
| Ferramenta | O que faz | Como o wardn difere |
|---|
| Gerenciadores de segredos (Vault, AWS SM, 1Password) | Armazenamento + recuperação seguros | O agente ainda obtém a chave real em tempo de execução. O Wardn garante que o agente nunca a toque. |
| Varlock | Validação de .env baseada em esquema + configuração segura para IA | Foca em gerenciamento de configuração e varredura de vazamentos. O Wardn faz injeção de credencial em tempo de execução — a chave nunca entra no processo do agente. |
| OpenRouter | Roteamento de API + gerenciamento de chaves | Confia no cliente com uma chave de API. O Wardn não — o agente detém um espaço reservado inútil. |
| dotenv + .gitignore | Manter segredos fora do git | Chaves ainda na memória, variáveis de ambiente, logs. O Wardn as remove de todos os três. |
| Malhas de serviço (Istio, Linkerd) | Autenticação serviço a serviço | Resolvem mTLS em nível de infraestrutura. O Wardn resolve autenticação agente-para-API onde o próprio agente não é confiável. |