Context
The zc_env_read host function exposed to WASM plugins (added in #5913 for Phase 2 D2) lets any plugin with the env_read permission call std::env::var(name) for any variable by name. A plugin granted env_read to read its own API key (e.g. FAL_API_KEY) can, with the same permission, read:
OPENAI_API_KEY, ANTHROPIC_API_KEY, AWS_SECRET_ACCESS_KEY, CARGO_REGISTRY_TOKEN, DATABASE_URL
- Session cookies, SSH agent sockets, any other secret in the process environment
An env_read permission grant "I need my own API key" escalates to "read every secret in the process."
WASM plugins are third-party binaries. The permission grant in the manifest should describe which env vars the plugin can read, not just that it can read env vars.
Surfaced in PR #5913 review (advisory, not blocking).
Target
Three candidate designs, not mutually exclusive:
-
Per-plugin manifest allowlist (preferred — most explicit):
permissions = [\"env_read\"]
env_read_vars = [\"FAL_API_KEY\"]
Enforced in handle_env_read: if var_name not in the allowlist, return permission-denied.
-
Namespace convention (simplest):
Plugins can only read variables with a prefix like ZC_PLUGIN_<name>_* or ZC_PLUGIN_SHARED_*. Users set ZC_PLUGIN_IMAGE_GEN_FAL_API_KEY instead of FAL_API_KEY. Host rejects anything outside the prefix.
-
Host-provided key store indirection (most robust, biggest scope):
Plugins don't read env at all. Instead, the host exposes zc_secret_get(handle) where handle is pre-bound to a specific secret by the operator in [plugins.<name>] config. Plugins never see raw env var names.
Option 1 has the lowest scope-cost and the clearest migration story: existing manifests keep working (empty env_read_vars → deny-all if env_read permission is present), and plugins opt into specific names.
Why it matters
- Secret exfiltration — a well-intentioned plugin can be modified by an attacker (pre-signature, or via a compromised build) to harvest every secret in the environment the moment it's installed.
- Trust boundary alignment — the manifest is the operator's trust contract. Right now it lies by omission: "env_read" reads like "reads its own config"; it actually means "reads any process-global secret."
- Signature verification default is disabled — until
signature_mode = strict is the default (which it cannot reasonably be during bootstrap), the manifest is the operator's primary tool. It needs to be precise.
Current state
zc_env_read host function calls std::env::var(&var_name) unconditionally after checking the env_read permission bit.
PluginManifest has no field for describing which env vars a plugin needs.
- ADR-003 §Threat model calls out this gap explicitly.
- The only plugin that exists so far (
image-gen-fal) reads exactly one var (FAL_API_KEY), so retrofitting option 1 is trivial.
Scope out
OAuth flows, secret rotation, encrypted secret storage — separate larger scope. This issue is just: bound the name space that env_read can read from.
Referenced from ADR-003. Parent RFC: #5574. Introduced by #5913.
Context
The
zc_env_readhost function exposed to WASM plugins (added in #5913 for Phase 2 D2) lets any plugin with theenv_readpermission callstd::env::var(name)for any variable by name. A plugin grantedenv_readto read its own API key (e.g.FAL_API_KEY) can, with the same permission, read:OPENAI_API_KEY,ANTHROPIC_API_KEY,AWS_SECRET_ACCESS_KEY,CARGO_REGISTRY_TOKEN,DATABASE_URLAn
env_readpermission grant "I need my own API key" escalates to "read every secret in the process."WASM plugins are third-party binaries. The permission grant in the manifest should describe which env vars the plugin can read, not just that it can read env vars.
Surfaced in PR #5913 review (advisory, not blocking).
Target
Three candidate designs, not mutually exclusive:
Per-plugin manifest allowlist (preferred — most explicit):
Enforced in
handle_env_read: ifvar_namenot in the allowlist, return permission-denied.Namespace convention (simplest):
Plugins can only read variables with a prefix like
ZC_PLUGIN_<name>_*orZC_PLUGIN_SHARED_*. Users setZC_PLUGIN_IMAGE_GEN_FAL_API_KEYinstead ofFAL_API_KEY. Host rejects anything outside the prefix.Host-provided key store indirection (most robust, biggest scope):
Plugins don't read env at all. Instead, the host exposes
zc_secret_get(handle)wherehandleis pre-bound to a specific secret by the operator in[plugins.<name>]config. Plugins never see raw env var names.Option 1 has the lowest scope-cost and the clearest migration story: existing manifests keep working (empty
env_read_vars→ deny-all ifenv_readpermission is present), and plugins opt into specific names.Why it matters
signature_mode = strictis the default (which it cannot reasonably be during bootstrap), the manifest is the operator's primary tool. It needs to be precise.Current state
zc_env_readhost function callsstd::env::var(&var_name)unconditionally after checking theenv_readpermission bit.PluginManifesthas no field for describing which env vars a plugin needs.image-gen-fal) reads exactly one var (FAL_API_KEY), so retrofitting option 1 is trivial.Scope out
OAuth flows, secret rotation, encrypted secret storage — separate larger scope. This issue is just: bound the name space that
env_readcan read from.Referenced from ADR-003. Parent RFC: #5574. Introduced by #5913.