Skip to content

feat(vault): add vault proxy mode for credential isolation #12839

Closed
sfo2001 wants to merge 7 commits into
openclaw:mainfrom
sfo2001:feat/vault-proxy
Closed

feat(vault): add vault proxy mode for credential isolation #12839
sfo2001 wants to merge 7 commits into
openclaw:mainfrom
sfo2001:feat/vault-proxy

Conversation

@sfo2001

@sfo2001 sfo2001 commented Feb 9, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add vault proxy infrastructure that keeps model API keys out of the gateway process by routing requests through an nginx reverse proxy sidecar that injects credentials from an age-encrypted vault file
  • Add openclaw vault CLI (init, status, add, remove, list, migrate) for managing age-encrypted secrets with a provider registry mapping providers to proxy ports
  • Extend vault proxy support to web search providers (Brave, Perplexity, Grok/xAI) and add Groq, Mistral, xAI, Brave, Perplexity to the provider registry

Details

Architecture: Two Docker networks enforce isolation — the gateway has no route to the internet and no access to plaintext keys. The vault sidecar decrypts vault.age at startup, renders an nginx config with credentials, and proxies
requests to upstream APIs.

Config: vault.enabled + vault.proxies in openclaw.json. When active, normalizeProviders() rewrites baseUrl to the proxy URL and substitutes a placeholder API key.

CLI: openclaw vault init generates an age keypair; vault add/remove manage individual secrets with automatic proxy configuration for known providers; vault migrate moves plaintext apiKey values from config into the encrypted
vault.

Security:

  • --proxy-host validated against hostname regex to prevent SSRF
  • --json --reveal risk documented in vault.md
  • Vault file written with mode 0600, atomic rename

Test plan

  • 143 tests pass across 8 test files
  • Proxy resolution, baseUrl rewriting, schema validation (commit 1)
  • CLI subcommands: init, add, remove, list, migrate, status (commit 2)
  • Web search execute-path tests: Brave, Perplexity, Grok route through vault proxy with auth headers omitted (commit 3)
  • Hostname validation rejects SSRF payloads (attacker.com/path#, -bad-host, empty)
  • buildDefaultProxyMap() and findProviderBySecretName() helper coverage
  • Manual: deploy with docker-compose.vault.yml, verify gateway cannot reach upstream APIs directly

Use of AI

  • Idea / Architecture myself ; code / tests and documentation generated with Claude Code

Greptile Overview

Greptile Summary

This PR introduces a “vault proxy” mode intended to keep provider API keys out of the gateway process. It adds:

  • A new vault config section (vault.enabled, vault.proxies, vault.file, vault.publicKey) and schema validation.
  • Vault proxy integration in model auth and provider normalization: when a proxy mapping exists, provider baseUrl is rewritten to the proxy URL and auth resolution returns a fixed placeholder key.
  • Web-search provider support for routing through the vault proxy, omitting auth headers when proxied.
  • A new openclaw vault CLI for initializing an age-encrypted vault file and managing secrets / migrating plaintext apiKey values.

The changes primarily touch the agent configuration pipeline (ensureOpenClawModelsJsonnormalizeProviders), auth resolution (resolveApiKeyForProvider, resolveModelAuthMode), and tool execution paths for web search. The CLI and vault operations live under src/cli/vault-cli.ts and src/vault/operations.ts and are registered via the sub-CLI registry.

Confidence Score: 3/5

  • This PR is close to mergeable but has a likely CLI registration bug that can break the openclaw CLI entrypoint when the vault subcommand is loaded.
  • Main logic paths look coherent and are covered by tests, but registerVaultCli() appears to register a vault command that is already declared in the sub-CLI registry. That duplication commonly causes Commander to throw or behave unexpectedly in real runs, and current tests don’t exercise the full CLI registration flow.
  • src/cli/vault-cli.ts, src/cli/program/register.subclis.ts, src/agents/tools/web-search.ts

@openclaw-barnacle openclaw-barnacle Bot added docs Improvements or additions to documentation gateway Gateway runtime cli CLI command changes agents Agent runtime and tooling labels Feb 9, 2026

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

Comment thread src/cli/vault-cli.ts
Comment thread src/agents/tools/web-search.ts
@HenryLoenwind

Copy link
Copy Markdown
Contributor

This seems extremely limited. It handles only API keys in headers. The other one, #9271, handles any kind of secret, API keys, Oauth, bot tokens in the request body, and so on.

@sfo2001

sfo2001 commented Feb 14, 2026

Copy link
Copy Markdown
Contributor Author

This seems extremely limited. It handles only API keys in headers. The other one, #9271, handles any kind of secret, API keys, Oauth, bot tokens in the request body, and so on.

@HenryLoenwind Valid concern -- the base PR is deliberately scoped to API key injection via HTTP headers, which covers all LLM and web search providers. Follow-up PRs extend the vault proxy to additional secret types:

  • Channel token isolation (feat/vault-channel-tokens): Telegram, Discord, Slack, and WhatsApp bot tokens stored in the vault and resolved at runtime via an HTTP endpoint on the sidecar. Includes vault migrate support for channel tokens and automatic detection of channel secret names (TELEGRAM_BOT_TOKEN, DISCORD_BOT_TOKEN, etc.).
  • Home Assistant proxy (feat/vault-ha-proxy): HA long-lived access token proxied through a dedicated nginx server block (port 8091).
  • Dynamic entrypoint (feat/vault-dynamic-entrypoint): Removes the hardcoded provider list from the sidecar. Secret names and port mappings are derived from the vault file at startup, so adding a new provider requires zero changes to the container.

On domain allowlisting: the nginx sidecar already has hardcoded proxy_pass targets with a return 403 default per server block, so unauthorized paths are blocked per-provider. Full outbound isolation (Docker --internal network + explicit allowlist) is a natural follow-up and doesn't require architectural changes.

On OAuth: short-lived OAuth tokens (1h expiry) are a different risk profile than long-lived API keys. They can't be statically encrypted into a vault file. The channel token HTTP endpoint pattern (sidecar resolves tokens on demand) could be extended to OAuth in a future PR.

On cross-platform: the nginx sidecar is fully containerized and works identically on Linux, macOS, and Windows (via Docker Desktop) with no platform-specific code paths.

@openclaw-barnacle

Copy link
Copy Markdown

This pull request has been automatically marked as stale due to inactivity.
Please add updates or it will be closed.

@openclaw-barnacle openclaw-barnacle Bot added the stale Marked as stale due to inactivity label Feb 21, 2026
@sfo2001

sfo2001 commented Feb 21, 2026

Copy link
Copy Markdown
Contributor Author

This pull request has been automatically marked as stale due to inactivity. Please add updates or it will be closed.

Again , another one I will now rebase. Note that the maintainers have already closed another PR in favour of this one. So it would be great if a real human has a look at it and decides either to merge or close.

Status update: the vault proxy has been running in production (my own self-hosted Unraid deployment ) for ~2 weeks. I use it with Google, Brave, Nebius Token Factory in a Docker.

This addresses several open security issues: #11829, #14411, #9627, #11202, #20912, #7916, #10173. Happy to split the PR if that helps review.

sfo2001 and others added 7 commits February 26, 2026 19:37
Add vault proxy infrastructure that keeps API keys out of the gateway
process. When vault.enabled is true and vault.proxies maps a provider to
a proxy URL, the gateway uses a placeholder key and rewrites the baseUrl
to route through the vault sidecar (nginx), which injects the real
credential.

- Add VaultConfig type, zod schema, and config plumbing
- Add resolveVaultProxyUrl() + VAULT_PROXY_PLACEHOLDER_KEY in model-auth
- Rewrite baseUrl in normalizeProviders() when vault proxy is configured
- Add tests for proxy resolution, baseUrl rewriting, schema validation

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Add `openclaw vault` CLI subcommands for managing age-encrypted secrets
and a provider registry mapping providers to proxy ports/secret names.

Subcommands: init, status, add, remove, list, migrate.
Includes hostname validation for --proxy-host, extracted helpers
(buildDefaultProxyMap, findProviderBySecretName), and docs with
--json --reveal security note.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Extend vault proxy support to web search providers (Brave, Perplexity,
Grok/xAI) and add Groq, Mistral entries to the provider registry.

When vault proxy is active, auth headers are omitted and requests are
routed through the vault sidecar. SEARCH_VAULT_KEYS hoisted to module
scope; import path corrected.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Adds the runtime counterpart to the vault CLI and TS operations:
- Dockerfile: nginx:alpine base with age for secret decryption
- entrypoint.sh: decrypts vault.age, renders nginx config, starts proxy
- nginx.conf.template: reverse proxy blocks for all supported providers
- secrets.example.txt: documentation example for secret file format

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Providers like ollama and autorouter have no vault proxy mapping.
Migrating their keys strips them from config with no way to inject
them back, breaking those providers at runtime. Guard the migrate
loop with a providerProxyUrl() check to leave local keys in place.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
The Google SDK sets apiVersion="" when baseUrl is overridden, sending
requests to /models/{model}:method instead of /v1beta/models/{model}.
Add nginx location /models/ that rewrites to /v1beta/models/ upstream.

Also add dummy apiKey for local providers (ollama, autorouter) to
prevent SDK ModelRegistry validation from silently rejecting the
entire models.json — which dropped vault proxy baseUrl overrides.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Add all 10 supported providers to the table. Document Google SDK's
apiVersion="" behavior that requires a /models/ nginx location block,
and the no-key-required fix for local providers that prevents SDK
validation from silently dropping all models.json overrides.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@sfo2001

sfo2001 commented Feb 28, 2026

Copy link
Copy Markdown
Contributor Author

Closing this in favor of the architectural direction that landed in #26155.

The maintainers chose in-process secret resolution (env/file/exec providers) over the sidecar proxy approach. That's the right call for the general case: no Docker dependency, cross-platform, and the resolved values never touch config files on disk.
I think, the security tradeoff (secrets in gateway process memory vs. not) is acceptable for most deployments.

The vault-proxy approach offers stronger isolation (the gateway process never holds credential values at all; injection happens at the network layer), but that stronger guarantee requires a containerized sidecar and is inherently Docker-specific.
It's running in production on a self-hosted deployment, so the approach is validated, but it's propably not the right default for a general-purpose tool.

Thanks for the review and the clear direction.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling cli CLI command changes docs Improvements or additions to documentation gateway Gateway runtime size: XL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants