Granting Plugins Configuration and Secrets

This page answers one task: a host runs third-party or tenant-provided WebAssembly plugins — integrations, transformers, webhooks — and each plugin needs its own settings (an endpoint URL, a threshold, a mapping) and sometimes credentials (an API key for a service it calls). You want each plugin to see exactly its own configuration and secrets, nothing of the host’s or other plugins’, with a design that limits damage if a plugin misbehaves.

Prerequisites

  • [ ] A plugin host (Wasmtime, Extism, a Component Model runtime, or a browser host).
  • [ ] A per-plugin record of configuration values and secret references.
  • [ ] A secret store on the host side (a vault service, environment-managed secrets, or encrypted storage).

The principle: plugins get capabilities, not ambient access

A WebAssembly plugin starts with nothing: no environment variables, no files, no network — only the imports the host provides. That is a strong foundation. The mistake to avoid is undoing it by handing every plugin the host’s entire environment or configuration file “for convenience”. Instead, give each plugin a narrow, explicit set of inputs: its own configuration values, and for secrets, preferably not the secret itself but a capability to use it — a host function that performs the authenticated operation on the plugin’s behalf.

That distinction matters. A plugin that receives a raw API key can leak it — log it, send it to a server it controls, store it — and the key must then be rotated. A plugin that can only call http_request with the host attaching credentials for an allowed destination never sees the key at all.

Handing plugins raw secrets versus capabilities Giving a plugin the raw API key lets it use the key anywhere, leak it in logs or exfiltrate it, so any compromise requires rotation. Giving it a host function that attaches credentials for allowed destinations lets it use the secret without ever seeing it, and the host can audit and revoke access. raw secret in config/env plugin holds the key can leak or exfiltrate it rotation on any doubt avoid where possible capability via host function host attaches credentials destinations allow-listed audited, revocable preferred

Step 1 — define a configuration schema per plugin

Each plugin declares the configuration it accepts — names, types, defaults, which fields are secrets — in its manifest. The host validates administrators’ values against it before the plugin ever runs:

{
  "name": "crm-sync",
  "config": {
    "endpoint": { "type": "string", "format": "uri", "required": true },
    "batch_size": { "type": "integer", "default": 100, "maximum": 1000 },
    "api_token": { "type": "secret", "required": true }
  },
  "allowed_hosts": ["api.example-crm.com"]
}

Validating up front means plugins receive well-formed values and fail at configuration time, not at 3 a.m. during a sync.

Step 2 — pass plain configuration at initialisation

Non-secret values can be passed directly. With Extism, configuration is a key-value map the plugin reads through its PDK; with WASI, environment variables or a config file in a preopened directory; with the Component Model, an exported init(config: config-record) function generated from WIT:

// host side (Extism)
let manifest = Manifest::new([Wasm::file("crm_sync.wasm")])
    .with_config_key("endpoint", &cfg.endpoint)
    .with_config_key("batch_size", &cfg.batch_size.to_string())
    .with_allowed_host("api.example-crm.com");
let plugin = Plugin::new(&manifest, [http_with_credentials_fn], true)?;
// plugin side (Extism PDK)
let endpoint = config::get("endpoint")?.ok_or(Error::msg("missing endpoint"))?;

The plugin sees only the keys the host set for it. Never pass the host’s whole environment.

Step 3 — expose secrets through host functions

For credentials, give the plugin a host function that performs the authenticated operation:

// host function: perform an HTTP request with the plugin's credentials attached
host_fn!(http_with_credentials(user_data: PluginCtx; req: Json<PluginRequest>) -> Json<PluginResponse> {
    let ctx = user_data.get()?;
    let ctx = ctx.lock().unwrap();
    let req = req.into_inner();
    ensure_allowed(&ctx.allowed_hosts, &req.url)?;              // destination allow-list
    let token = ctx.secrets.get("api_token")?;                  // fetched from the vault, never returned to the plugin
    let resp = ctx.http.request(req).bearer_auth(token).send()?;
    ctx.audit.log(&ctx.plugin_id, &req.url, resp.status());
    Ok(Json(PluginResponse::from(resp)))
});

The plugin asks for “POST to the CRM endpoint with this body” and gets the response; the token stays in the host. The host enforces the destination allow-list, rate limits and auditing in one place.

A plugin using a secret it never sees The plugin calls the host function with a request to an allowed destination. The host checks the destination against the plugin's allow-list, fetches the plugin's token from the secret store, attaches it, performs the request, records an audit entry, and returns only the response to the plugin. plugin calls host fn POST /contacts check allow-list api.example-crm.com fetch secret vault, per plugin attach + send audit entry return response only token never exposed

Step 4 — when a plugin must hold a secret

Some protocols require the plugin to compute something with the secret itself — an HMAC signature, a custom handshake. Provide narrow host functions for those operations too (sign_hmac(key_name, message)), so the key still stays in the host. Only if that is impossible, pass the raw secret, scoped to that plugin, short-lived where the service supports it (a token valid for minutes), and never logged. Remember that a plugin’s linear memory can end up in crash dumps or debug snapshots; treat such dumps as sensitive.

Step 5 — rotate, revoke and audit

Because secrets live in the host’s store and plugins access them through functions, rotation is a store update; plugins pick up new values on the next call. Revoking a plugin’s access means removing its secret reference or allow-listed host — no redeploy of the plugin needed. Keep an audit log of which plugin used which capability when, and alert on unusual patterns (a sudden burst of requests, calls to unexpected endpoints that the allow-list rejected).

Browser-hosted plugins

In browser hosts, the same pattern applies with different storage: secrets belong on your server, not in the page. Plugins in the browser call host functions that proxy authenticated requests through your backend, which attaches credentials server-side. Never put long-lived API keys in browser storage for plugins to read.

Keeping data from leaking through outputs

Even without secrets, plugins see data — the records they transform, the events they handle — and could leak it through their outputs. The destination allow-list is the main control: a plugin that may only talk to its own service cannot send data elsewhere through the network. Outputs returned to the host are a second channel: a transformation plugin could smuggle data into fields the host then forwards. Validate outputs against a schema, cap their size, and for sensitive pipelines restrict which fields a plugin may add or change. Logs are a third channel: a log host function lets plugins write anything, so rate-limit it, cap message length, and store plugin logs separately with their own retention. None of these replace choosing trustworthy plugins, but together they make accidental and deliberate leaks harder and easier to detect.

Configuration changes and versioning

Plugin manifests evolve: new options appear, defaults change, a field becomes required. Version the configuration schema with the plugin, and when a new plugin version arrives with a changed schema, validate existing values against it before switching — migrate values where a mapping is clear, and block the upgrade with an explanation where an administrator must decide. Keep the previous plugin version and its configuration available for rollback. Store configuration separately from the plugin binary, so updating a plugin never silently resets its settings, and record who changed which value when.

Testing the boundary

Write tests from the plugin’s point of view: a test plugin that tries to read configuration keys it was not given, call a host function for a destination not on its allow-list, and request another plugin’s secret by name, asserting that each attempt fails. Run them whenever host functions change.

Expected output

Each plugin declares a configuration schema validated at setup; plain settings arrive through the PDK’s config map; the CRM plugin performs authenticated requests through http_with_credentials without ever seeing its token; requests to non-allow-listed hosts are rejected and audited; rotating the token is a vault update; and disabling a plugin’s access requires no redeploy.

Gotchas

  • Passing the host environment to plugins. Leaks unrelated secrets. Pass explicit keys only.
  • Raw API keys in plugin config. Easy to exfiltrate. Prefer capability host functions.
  • No destination allow-list. A plugin can send data anywhere. Enforce one in the host.
  • Secrets in logs or memory dumps. Treat dumps as sensitive and never log secret values.
  • Secrets in browser storage. Proxy through your server instead.
  • Plugin updates resetting settings. Store configuration separately and validate it against the new schema.

Performance note

Routing requests through a host function added about 0.1 ms of overhead per call compared with a plugin making a direct request — negligible next to network latency, and the price of keeping credentials out of plugin memory.

Overhead of credentialed requests through the host Milliseconds per HTTP request for a plugin calling an external API directly with a raw token and through a host function that attaches credentials and audits, excluding network time. ms overhead per request direct request with raw token 0.1 ms host function + audit 0.1 ms

Frequently Asked Questions

Can WASI environment variables hold secrets? They can, but the plugin then holds the raw value; prefer host functions for credentials.

How do plugins get secrets in the Component Model? Through imported interfaces the host implements, such as a credentials or http interface with host-attached auth.

Should configuration be mutable at runtime? Re-initialise the plugin or provide a host function to read current values; avoid mutating plugin memory directly.

What about per-tenant secrets? Key the secret store by tenant and plugin, and resolve inside host functions using the calling plugin’s identity.

Can a plugin leak data through its outputs? Yes — validate outputs against a schema, cap sizes, and rate-limit plugin logging in addition to restricting network destinations.

How do I test that plugins cannot reach other plugins’ secrets? Use a hostile test plugin that requests unknown keys, other plugins’ secrets and disallowed destinations, and assert every attempt fails.

Who should be able to change plugin settings? Administrators with an audit trail; record who changed which value and when.

← Back to Plugin Systems & Extensibility