Secrets are exposed to plugins and subprocesses via environment variables #50

Open
opened 2026-02-22 06:36:25 +00:00 by nazim · 3 comments
Contributor

Problem

Cobot currently has no secret management. Credentials (API keys, bot tokens, wallet keys) live in environment variables or config files and are accessible to:

  1. Every plugin — any plugin can read os.environ["TELEGRAM_BOT_TOKEN"] or any other secret, regardless of whether it needs it
  2. Every subprocessexec calls, shell tools, and scripts inherit the full environment including all secrets
  3. LLM output — if the model accidentally echoes a secret, nothing catches it before it reaches Telegram/Nostr/logs

This means a single misbehaving plugin, a prompt injection that triggers a tool call, or even a careless env command can leak every credential the agent has.

What we want

  • Plugins should only access the secrets they actually need
  • Subprocesses should never see raw credentials
  • Outbound messages should be scanned for accidental secret leakage
  • Secrets should be encrypted at rest, decrypted only in RAM at runtime

Prior art

  • IronClaw (nearai/ironclaw) — Rust agent with WASM-sandboxed tools. Secrets injected at host boundary, never exposed to tool code. Leak detection on all outputs.
  • avault — NIP-44 encrypted vault with NIP-46 remote signing (operator phone as hardware key). Secrets in RAM only while daemon runs. Built as a standalone tool, not yet integrated into cobot.

Impact

Without this, cobot cannot safely:

  • Run untrusted or third-party plugins
  • Execute arbitrary tool commands from LLM decisions
  • Operate in multi-user or DVM scenarios where external job requests trigger tool execution
## Problem Cobot currently has no secret management. Credentials (API keys, bot tokens, wallet keys) live in environment variables or config files and are accessible to: 1. **Every plugin** — any plugin can read `os.environ["TELEGRAM_BOT_TOKEN"]` or any other secret, regardless of whether it needs it 2. **Every subprocess** — `exec` calls, shell tools, and scripts inherit the full environment including all secrets 3. **LLM output** — if the model accidentally echoes a secret, nothing catches it before it reaches Telegram/Nostr/logs This means a single misbehaving plugin, a prompt injection that triggers a tool call, or even a careless `env` command can leak every credential the agent has. ### What we want - Plugins should only access the secrets they actually need - Subprocesses should never see raw credentials - Outbound messages should be scanned for accidental secret leakage - Secrets should be encrypted at rest, decrypted only in RAM at runtime ### Prior art - **IronClaw** ([nearai/ironclaw](https://github.com/nearai/ironclaw)) — Rust agent with WASM-sandboxed tools. Secrets injected at host boundary, never exposed to tool code. Leak detection on all outputs. - **avault** — NIP-44 encrypted vault with NIP-46 remote signing (operator phone as hardware key). Secrets in RAM only while daemon runs. Built as a standalone tool, not yet integrated into cobot. ### Impact Without this, cobot cannot safely: - Run untrusted or third-party plugins - Execute arbitrary tool commands from LLM decisions - Operate in multi-user or DVM scenarios where external job requests trigger tool execution
Author
Contributor

Proposed spec: vault plugin

1. New PluginMeta.secrets field

Plugins declare what secrets they need:

meta = PluginMeta(
    id="telegram",
    secrets=["TELEGRAM_BOT_TOKEN"],  # new field
)

The vault plugin injects these into configure() via a secrets dict — plugins never call os.environ for credentials.

2. vault plugin

# cobot/plugins/vault/plugin.py
meta = PluginMeta(
    id="vault",
    capabilities=["secrets"],
    priority=5,  # before everything else
    extension_points=["vault.secret_patterns"],
)

Storage: NIP-44 encrypted file (same as avault). Decrypted into RAM on startup, wiped on shutdown.

Unlock options (progressive):

  • v1: passphrase (simple, works now)
  • v2: NIP-46 remote signing (operator's phone as hardware key)

API for other plugins:

  • get_secret(name) -> str — only returns secrets the requesting plugin declared in meta.secrets
  • secret_patterns() -> list[str] — returns patterns for leak detection (extension point)

3. Registry wiring

During plugin startup, the registry:

  1. Loads vault plugin first (priority 5)
  2. For each subsequent plugin, checks meta.secrets
  3. Passes only declared secrets into configure(config) under a _secrets key
  4. Plugins that access undeclared secrets get an error, not the value

4. Environment scrubbing

When the tools plugin spawns subprocesses:

  • Strip all known secret env vars from the child environment
  • If a tool needs authenticated HTTP, use a localhost proxy pattern (vault serves as auth-injecting proxy) — stretch goal

5. Leak detection (extend existing security plugin)

Add outbound scanning:

  • security plugin calls vault.secret_patterns extension point
  • Before any message is sent (Telegram, Nostr, logs), scan for matches
  • Block or redact if found
  • Hook: on_message_outbound(ctx) (new lifecycle hook)

6. Migration path

# Import existing env vars into vault
cobot vault import  # reads from env/config, encrypts into vault.enc

# List stored secrets (names only, not values)
cobot vault list

# Set/get individual secrets
cobot vault set TELEGRAM_BOT_TOKEN
cobot vault get TELEGRAM_BOT_TOKEN

Sizing estimate

  • vault/plugin.py: ~200 lines (storage, encrypt/decrypt, get/set)
  • PluginMeta + registry changes: ~50 lines
  • Security plugin outbound hook: ~50 lines
  • CLI commands: ~80 lines
  • Tests: ~150 lines
  • Total: ~530 lines

What this does NOT cover (future work)

  • WASM sandboxing (IronClaw's approach — overkill for Python plugins)
  • Per-tool HTTP proxy injection (stretch goal, mentioned above)
  • Automatic secret rotation
## Proposed spec: `vault` plugin ### 1. New `PluginMeta.secrets` field Plugins declare what secrets they need: ```python meta = PluginMeta( id="telegram", secrets=["TELEGRAM_BOT_TOKEN"], # new field ) ``` The vault plugin injects these into `configure()` via a `secrets` dict — plugins never call `os.environ` for credentials. ### 2. `vault` plugin ```python # cobot/plugins/vault/plugin.py meta = PluginMeta( id="vault", capabilities=["secrets"], priority=5, # before everything else extension_points=["vault.secret_patterns"], ) ``` **Storage:** NIP-44 encrypted file (same as avault). Decrypted into RAM on startup, wiped on shutdown. **Unlock options (progressive):** - v1: passphrase (simple, works now) - v2: NIP-46 remote signing (operator's phone as hardware key) **API for other plugins:** - `get_secret(name) -> str` — only returns secrets the requesting plugin declared in `meta.secrets` - `secret_patterns() -> list[str]` — returns patterns for leak detection (extension point) ### 3. Registry wiring During plugin startup, the registry: 1. Loads vault plugin first (priority 5) 2. For each subsequent plugin, checks `meta.secrets` 3. Passes only declared secrets into `configure(config)` under a `_secrets` key 4. Plugins that access undeclared secrets get an error, not the value ### 4. Environment scrubbing When the tools plugin spawns subprocesses: - Strip all known secret env vars from the child environment - If a tool needs authenticated HTTP, use a localhost proxy pattern (vault serves as auth-injecting proxy) — stretch goal ### 5. Leak detection (extend existing `security` plugin) Add outbound scanning: - `security` plugin calls `vault.secret_patterns` extension point - Before any message is sent (Telegram, Nostr, logs), scan for matches - Block or redact if found - Hook: `on_message_outbound(ctx)` (new lifecycle hook) ### 6. Migration path ```bash # Import existing env vars into vault cobot vault import # reads from env/config, encrypts into vault.enc # List stored secrets (names only, not values) cobot vault list # Set/get individual secrets cobot vault set TELEGRAM_BOT_TOKEN cobot vault get TELEGRAM_BOT_TOKEN ``` ### Sizing estimate - `vault/plugin.py`: ~200 lines (storage, encrypt/decrypt, get/set) - `PluginMeta` + registry changes: ~50 lines - Security plugin outbound hook: ~50 lines - CLI commands: ~80 lines - Tests: ~150 lines - **Total: ~530 lines** ### What this does NOT cover (future work) - WASM sandboxing (IronClaw's approach — overkill for Python plugins) - Per-tool HTTP proxy injection (stretch goal, mentioned above) - Automatic secret rotation
Collaborator

Review: Secrets Management

This is a critical issue for Cobot's security posture. The analysis is accurate — environment variable inheritance is a well-known attack vector, and we're currently wide open to it. Good motivation and clear prior art references.

Alignment with Cobot Architecture

Fits plugin-native design — plugins declaring their secret requirements aligns perfectly with how Cobot handles capabilities. This could extend the existing plugin manifest pattern.

Extension point opportunity — secret injection could be an extension point rather than core logic. A SecretProvider interface would let users swap implementations (file-based, avault, hardware keys, etc.).

Concerns

Scope is large. The issue bundles four distinct features:

  1. Plugin-scoped secret access
  2. Subprocess environment sanitization
  3. Outbound leak detection
  4. Encrypted-at-rest storage

Each is independently valuable but architecturally different. I'd suggest phasing:

Phase Feature Complexity Standalone Value
1 Leak detection on outputs Low High — catches accidents immediately
2 Plugin secret declarations Medium High — foundation for isolation
3 Subprocess sanitization Medium Medium — requires careful exec wrapper
4 Encrypted storage (avault) High High — but can use existing avault

Subprocess sanitization is tricky. Some tools legitimately need credentials (e.g., git push, ssh). We'd need an allowlist or explicit secret-forwarding mechanism, not blanket stripping.

Dependencies

  • Activity Loop (#49): Autonomous plugins running without human oversight makes secret isolation even more critical. These should be designed together.
  • Scheduled Execution (#42): Cron jobs will need secret access patterns too.

Security Considerations

  • Leak detection should cover logs, not just outbound messages
  • Consider secret rotation/revocation — if a secret leaks, how do we invalidate it?
  • Plugin trust levels: should core plugins have broader access than third-party ones?

Suggested Next Steps

  1. Start with Phase 1 (leak detection) as a quick win — simple regex/pattern matching on all output paths
  2. Spec out the plugin secret declaration format for Phase 2
  3. Evaluate whether avault integration or a simpler file-based approach is right for MVP

Happy to help design the SecretProvider extension point if this moves forward. This is foundational work that unblocks running untrusted plugins safely.

🦊

## Review: Secrets Management This is a critical issue for Cobot's security posture. The analysis is accurate — environment variable inheritance is a well-known attack vector, and we're currently wide open to it. Good motivation and clear prior art references. ### Alignment with Cobot Architecture ✅ **Fits plugin-native design** — plugins declaring their secret requirements aligns perfectly with how Cobot handles capabilities. This could extend the existing plugin manifest pattern. ✅ **Extension point opportunity** — secret injection could be an extension point rather than core logic. A `SecretProvider` interface would let users swap implementations (file-based, avault, hardware keys, etc.). ### Concerns **Scope is large.** The issue bundles four distinct features: 1. Plugin-scoped secret access 2. Subprocess environment sanitization 3. Outbound leak detection 4. Encrypted-at-rest storage Each is independently valuable but architecturally different. I'd suggest phasing: | Phase | Feature | Complexity | Standalone Value | |-------|---------|------------|------------------| | 1 | Leak detection on outputs | Low | High — catches accidents immediately | | 2 | Plugin secret declarations | Medium | High — foundation for isolation | | 3 | Subprocess sanitization | Medium | Medium — requires careful exec wrapper | | 4 | Encrypted storage (avault) | High | High — but can use existing avault | **Subprocess sanitization is tricky.** Some tools legitimately need credentials (e.g., `git push`, `ssh`). We'd need an allowlist or explicit secret-forwarding mechanism, not blanket stripping. ### Dependencies - **Activity Loop (#49)**: Autonomous plugins running without human oversight makes secret isolation even more critical. These should be designed together. - **Scheduled Execution (#42)**: Cron jobs will need secret access patterns too. ### Security Considerations - Leak detection should cover logs, not just outbound messages - Consider secret rotation/revocation — if a secret leaks, how do we invalidate it? - Plugin trust levels: should core plugins have broader access than third-party ones? ### Suggested Next Steps 1. Start with **Phase 1** (leak detection) as a quick win — simple regex/pattern matching on all output paths 2. Spec out the plugin secret declaration format for Phase 2 3. Evaluate whether avault integration or a simpler file-based approach is right for MVP Happy to help design the `SecretProvider` extension point if this moves forward. This is foundational work that unblocks running untrusted plugins safely. — 🦊
Collaborator

Security/Privacy reviewer: Ben (@webdiverblue) wants to be looped in on any security and privacy findings related to secret management, avault integration, and leak detection. CC alongside @k9ert on all security-relevant updates.

**Security/Privacy reviewer:** Ben (@webdiverblue) wants to be looped in on any security and privacy findings related to secret management, avault integration, and leak detection. CC alongside @k9ert on all security-relevant updates.
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
ultanio/cobot#50
No description provided.