Credential proxy — technical
How the proxy works.
What it protects against. What it doesn't.
This page is for security reviewers, penetration testers, and engineers evaluating the proxy's threat model. It describes what the proxy does at the protocol level, where credentials exist in memory, and what attack surfaces remain.
Architecture
The proxy is a CONNECT-based HTTPS MITM proxy that runs on 127.0.0.1. An AI agent sets HTTPS_PROXY to point at it. When the agent makes an HTTPS request, the proxy intercepts the TLS connection, inspects request headers for credential references, resolves them against the Clavitor vault, and forwards the request with injected credentials to the upstream API.
Agent Proxy (127.0.0.1:1983) Upstream API | | | |--- CONNECT host:443 ----> | | |<-- 200 OK --------------- | | | | | |== TLS (MITM cert) ======= | | | | | |--- GET /v1/models ------> | | | Authorization: | | | Bearer clavitor://X/key | | | |--- GET /v1/models ------------>| | | Authorization: | | | Bearer sk-proj-abc123... | | | | | |<-- 200 OK --------------------| |<-- 200 OK --------------- | |
The proxy is a standalone Go binary. No CGO. All Clavitor protocol cryptography routes through a canonical Rust implementation compiled to WebAssembly and loaded via wazero at startup.
TLS handling
The proxy generates a self-signed ECDSA P-256 root CA on first run, persisted at the binary's directory with mode 0600. For each upstream host, a leaf certificate is minted on-demand, signed by this CA, and cached in memory with bounded eviction (1,000 hosts). Leaf certificates are valid for 24 hours and regenerated transparently at the 23-hour mark to prevent mid-session expiry.
The agent must trust the proxy's CA certificate. Export it with clavitor-proxy ca.
Upstream connections use TLS 1.3 minimum with ALPN negotiation for HTTP/2 and HTTP/1.1. The system certificate pool is used for upstream verification. No certificate pinning — the proxy trusts whatever the OS trusts.
Credential lifecycle
Credentials are never cached, never written to disk, and never held longer than one HTTP request.
| Phase | Where the credential exists | Duration |
|---|---|---|
| At rest in vault | AES-GCM ciphertext in the vault database | Until deleted |
| In transit to proxy | TLS-encrypted JSON response from vault API | One HTTP round-trip |
| Decrypted in proxy | Process memory (Go string on heap) | One HTTP request |
| Injected into upstream request | TLS-encrypted bytes on the wire to upstream | One HTTP request |
The proxy holds the agent's L2 key (16 bytes) in memory for its entire runtime. L2 is the decryption key for tier-2 vault fields. It is loaded from the encrypted sidecar config (CLV1 format) at startup and cleared on graceful shutdown. L2 never leaves the process.
The sidecar config is encrypted with AES-128-GCM and HMAC-SHA256 using deterministic keys derived from a static seed. This is obfuscation, not confidentiality — the security boundary is file permissions (0600) and possession of the file. The CLV1 format is shared between the proxy, the CLI, and the browser extension.
Resolution modes
Mode 1 — explicit placeholder
The agent includes a clavitor://Entry/field reference in a request header. The proxy searches the vault for the entry by name, fetches it, decrypts the named field, and substitutes the placeholder with the real value.
If the search returns zero or more than one result, the proxy returns 502 with a stable error code. The placeholder is never stripped and forwarded as-is.
Mode 2 — URL match
When no placeholder is present, the proxy asks the vault for entries whose URL field matches the upstream host. If exactly one match exists with a recognised field shape, the proxy injects credentials automatically.
Zero matches → passthrough (no credential expectation). Multiple matches → 502 with disambiguation guidance. Unknown field shape → 502.
The decision tree is deterministic: placeholder present → resolve or fail. No placeholder → URL match or passthrough. There is no silent fallback path where a failed resolution results in a request going upstream without credentials.
Agent identity
By default, the vault sees the proxy's own agent ID on every request. Rate limits, scope checks, and audit entries are attributed to the proxy.
When multiple agents share one proxy instance, the placeholder can include an agent ID: clavitor://agentid@Entry/field. The proxy sends this agent ID to the vault, which applies that agent's scopes and rate limits and logs the access against it. The agent ID is the 32-char hex value shown on the agent detail page in the vault UI.
| Deployment | Identity model | Isolation |
|---|---|---|
| One proxy per agent | Proxy ID = agent ID (default) | Full — separate binary, config, scope, rate limits |
| Shared proxy, no agent ID in URL | All agents share proxy's ID | Shared scope and rate limits |
Shared proxy + agentid@ in URL | Per-agent identity | Per-agent scope, rate limits, and audit |
The agent ID in the URL is not an authentication mechanism — the proxy's CVT token authenticates the connection. The agent ID determines attribution: whose scopes apply, whose rate limits count, whose audit trail records the access. The vault rejects unknown agent IDs with a loud failure.
Network security
SSRF protection
By default, the proxy blocks upstream connections to private networks (RFC 1918), cloud instance metadata (169.254.169.254), loopback, link-local, and carrier-grade NAT ranges. DNS is resolved first; all returned IPs are validated before the TCP connection is made, closing the DNS rebinding TOCTOU window.
Override with CLAVITOR_PROXY_ALLOW_PRIVATE=true for agents that legitimately reach private APIs.
Target pinning
The CONNECT target host is captured at tunnel establishment and used for the entire tunnel lifetime. Subsequent requests within the tunnel cannot redirect to a different host by manipulating the Host header. A mismatch results in 502.
This prevents an agent from establishing a tunnel to api.openai.com and then sending requests to internal-service.corp.
Header handling
Hop-by-hop headers are stripped from both requests and responses per RFC 7230 §6.1: Connection, Keep-Alive, Proxy-Authenticate, Proxy-Authorization, Proxy-Connection, TE, Trailers, Transfer-Encoding, Upgrade.
Set-Cookie is stripped from upstream responses to prevent upstreams from planting cookies in the agent's HTTP client.
Request and response bodies stream through without buffering. Request bodies are capped at 64 MB by default (CLAVITOR_PROXY_MAX_BODY_MB). Response bodies stream without a hard cap; a log warning is emitted when Content-Length exceeds 100 MB.
Field-to-header mapping
In URL-match mode, the proxy maps vault field labels to HTTP headers:
| Field label | Injected header |
|---|---|
key, apikey, api_key, token, secret, bearer, access_token | Authorization: Bearer <value> |
x-api-key, api-key | X-API-Key: <value> |
username + password (paired) | Authorization: Basic base64(user:pass) |
| Anything else | Rejected — ERR-PROXY-052 |
In placeholder mode, the agent controls which field is resolved and where it goes. The mapping above only applies to URL-match mode.
Error codes
Every failure produces a stable ERR-PROXY-NNN code. These codes are part of the proxy's public interface — agents and operators can match on them for alerting and debugging.
| Range | Category |
|---|---|
001–019 | Setup (config, init, CA generation, WASM) |
020–029 | Daemon lifecycle |
030–049 | Placeholder resolution (clavitor:// URIs) |
050–069 | URL-match injection |
070–089 | Upstream / TLS |
Tier-3 fields are unreachable
Vault entries support three encryption tiers. Tier-1 fields are plaintext metadata. Tier-2 fields are encrypted with L2 (the agent's key). Tier-3 fields (identity fields) are encrypted with L3 — a key the server and the proxy have never seen. Only the vault owner, via their hardware security key, can decrypt tier-3 fields.
If a placeholder references a tier-3 field, the proxy returns ERR-PROXY-035. No fallback, no partial result. The field is architecturally unreachable from the proxy.
What the proxy does not protect against
A compromised host
The proxy runs on the same machine as the agent. An attacker with root access can read process memory, attach a debugger, or intercept loopback traffic. The proxy is a credential-injection layer, not a hardware security boundary.
Credential exfiltration via the API response
If the upstream API echoes the credential back in its response (e.g., a "whoami" endpoint), the agent sees it. The proxy injects credentials into requests, not responses. It does not filter what comes back.
The proxy's threat model is a compromised skill or prompt injection that makes an authenticated agent harvest credentials. The vault's per-agent rate limits, unique-entries quotas, and two-strike lockdown are the primary defenses. The proxy adds a network-layer enforcement point where credentials are resolved per-request and never held by the agent.
Logging
The proxy logs one line per accepted CONNECT and emits error lines for failures. It never logs:
- Decrypted credential values
- Full request URLs (query strings may carry secrets — only scheme + host + path are logged)
- Request or response bodies
- The L2 key or sidecar config contents
When the proxy detects that a 400 response from upstream contains auth-related keywords (unauthorized, invalid token, etc.), it logs a diagnostic hint suggesting the injected credential may be stale. The response is forwarded unchanged.
Cryptography
All Clavitor protocol cryptography — AES-GCM field decryption, HKDF key derivation, base62 encoding, CVT token minting, CLV1 config pack/unpack — executes inside a single WebAssembly module (clavis_crypto.wasm) loaded via wazero, a pure-Go WASM runtime. No CGO. No Go reimplementation of Clavitor primitives.
The WASM module is compiled from the same Rust crate (clavis-crypto) used by the browser, the CLI, and the browser extensions. One source of truth, one binary, one audit surface.
The proxy uses Go's crypto/tls for the TLS wire and crypto/ecdsa for MITM certificate generation. These are transport concerns, not Clavitor protocol operations.
Configuration
Secrets (L2 key, agent ID, device ID, vault URL) live in the encrypted CLV1 sidecar config, written once during clavitor-proxy init. Operational knobs live in environment variables:
| Variable | Default | Purpose |
|---|---|---|
CLAVITOR_PROXY_PORT | 1983 | Listen port |
CLAVITOR_PROXY_ALLOW_PRIVATE | false | Allow connections to RFC-1918 / private networks |
CLAVITOR_PROXY_MAX_BODY_MB | 64 | Request body size cap |
CLAVITOR_PROXY_WRITE_TIMEOUT | 300 | Response write timeout in seconds |
CLAVITOR_CONFIG | (exe dir) | Override sidecar config path |
Operational knobs are not secrets. They don't belong in the encrypted config. They belong where deployment tools already manage them — the environment.
Review this yourself.
The cryptography is a single auditable WASM artifact. The threat model is documented. If you find something we missed, we want to hear about it.