Back to Paperclip

Plugin Secret Refs: Company Scope Reintroduction Plan

doc/plans/2026-04-26-plugin-secret-ref-company-scope.md

2026.512.04.6 KB
Original Source

Plugin Secret Refs: Company Scope Reintroduction Plan

Date: 2026-04-26 Status: follow-up after fail-closed mitigation Related issue: PAP-2394

Current state

PAP-2394 now fails closed:

  • POST /api/plugins/:pluginId/config rejects any config containing plugin secret refs.
  • ctx.secrets.resolve() is disabled for plugin workers.

This removes the release-blocking cross-company exposure path, but it also disables plugin secret-ref support until the runtime carries company scope end to end.

Vulnerability summary

The original design mixed an instance-global config store with company-scoped secret bindings:

This violated Least Privilege, Complete Mediation, and Secure Defaults.

Re-enable plugin secret refs only after both of these are true:

  1. Plugin config reads/writes are company-scoped.
  2. Runtime secret resolution carries explicit company context and enforces it at resolution time.

Implementation plan

1. Make plugin config company-scoped

  • Add company_id to plugin_config, with a unique index on (plugin_id, company_id).
  • Update registry helpers to require companyId for getConfig, upsertConfig, patchConfig, and deleteConfig.
  • Update plugin config routes to require companyId and call assertCompanyAccess(req, companyId).
  • Keep instance-global plugin lifecycle state separate from company-scoped plugin config.

2. Propagate company context through the worker runtime

  • Extend the SDK so ctx.config.get() and ctx.secrets.resolve() can receive or derive companyId.
  • Introduce worker request context storage for handlers that already run with company scope:
    • getData
    • performAction
    • scoped API routes
    • tool executions
    • environment driver calls
  • Fail closed when plugin code tries to read company-scoped config or secrets outside an active company context.

3. Rebind secrets by (companyId, pluginId, configPath)

  • On config save, validate every referenced secret belongs to the authorized company.
  • Store bindings only for that company.
  • Resolve secrets only by the current company-scoped binding, never by bare plugin ID plus UUID.
  • Treat stale bindings as invalid and remove them on config replacement.

4. Prevent cross-company config disclosure

  • When returning config to the UI, only materialize the selected company's secret refs.
  • Never expose another company's secret UUIDs through the global plugin config surface.

Required regression coverage

  • Company A board user cannot save plugin config that references a Company B secret.
  • Company A plugin execution cannot resolve a Company B secret even if the same plugin is configured for Company B.
  • Company-scoped config reads only return the selected company's secret bindings.
  • Config replacement removes stale bindings for the same (companyId, pluginId) target.
  • Runtime calls without company context fail closed.

Migration notes

  • Existing plugin_config rows need a migration strategy before re-enable.
  • Safest default: do not auto-assume a company for historical secret refs.
  • Prefer one of:
    • explicit admin migration per company, or
    • import existing rows as non-secret config only and require re-entry of secret refs.

Release posture

  • Keep plugin secret refs disabled until all steps above land.
  • Do not restore the feature behind a soft warning; the insecure path must remain unavailable by default.