Skip to content

[Security][Feature]: Credential broker — separate secret storage from agent execution context #14411

Description

@secbear

Category: Security / Enhancement (relates to #11829, #11202, #9627)

Summary

TL;DR - API keys leak because the Gateway process both resolves credentials and runs the agent. No amount of field stripping (#11202) or output redaction (#10659) fixes this — the agent shares a trust boundary with every secret. This proposes a CredentialBroker that resolves and injects auth internally, so the agent execution path never sees raw credentials.

#11829 covers the leak vectors well. This issue names the structural cause: credential co-location.

The Gateway process is simultaneously:

  1. The thing that authenticates to APIs (needs real credentials)
  2. The thing that runs agent logic (processes untrusted input)

Current flow:

openclaw.json / env vars / credentials/
    → model-auth.ts resolves apiKey → plain string
    → models-config.ts writes to models.json
    → system prompt serializes it (#11202)
    → agent sees all provider API keys every turn

Same pattern for channels — channels.telegram.botToken etc. are plain strings in Gateway memory, readable by any tool the agent invokes.

This means fixing #11202 (strip from prompt context) and #10659 (output redaction) are necessary but insufficient. A prompt-injected agent can still:

  • Read ~/.openclaw/credentials/ via exec tool
  • Enumerate env vars (env | grep KEY)
  • Access config via the filesystem
  • Read Gateway process memory (plugins run in-process)

Proposed solution

One new module: src/agents/credential-broker.ts

Agent Execution (untrusted input)
    │
    │ "call anthropic/claude-opus-4-6 with this prompt"
    │ (no apiKey, no token, no secret)
    │
    ▼
Credential Broker (closure boundary)
    │
    │ resolves auth from profiles/env/files
    │ injects headers into outbound request
    │ strips secrets from model catalog
    │
    ▼
Authenticated HTTP request → api.anthropic.com

The broker is a function boundary, not a process boundary. The existing model-auth.ts resolution logic stays — the broker wraps it so that callers get authenticated requests back instead of raw credential strings.

export interface CredentialBroker {
  // Inject auth into outbound request. Caller never sees the raw key.
  injectAuth(request: OutboundRequest): Promise;

  // Model catalog for prompt context — credentials stripped.
  getSafeModelCatalog(): SafeModelInfo[];
}

What Changes

Phase 1 (~50 LOC, days): Strip apiKey from model catalog before prompt serialization. This is the #11829 Layer 1 fix, included as the first commit.

Phase 2 (~300-500 LOC, weeks): The broker itself.

  • model-auth.ts becomes an internal detail of the broker
  • agent-scope.ts calls broker.injectAuth() instead of resolving auth directly
  • models-config.ts uses broker.getSafeModelCatalog()

Phase 3 (future PR): Extend to channel adapters and plugins.

Config Extension: apiKeyFile

For deployments managing secrets externally (agenix, Docker secrets, Vault, SOPS), add optional file-path alternatives:

{
  "models": {
    "providers": {
      "anthropic": {
        "apiKeyFile": "/run/secrets/anthropic-api-key"
      }
    }
  }
}

Broker reads at runtime, warns if permissions aren't 0600. This also fixes #9627 (config writes resolving env vars to plaintext) — apiKeyFile references survive config serialization.

Same pattern as systemd EnvironmentFile, PostgreSQL password_file, Docker secrets.

What this doesn't cover

  • Network egress control (agent can still make outbound HTTP)
  • Prompt injection prevention (broker reduces blast radius, doesn't prevent injection)
  • Skill supply chain (malicious skills in-process still dangerous)

Additional context

@steipete, @theonejvo

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestsecuritySecurity documentation

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions