You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The last slice of Lote S. Adds operator-facing diagnostics, the fourth plugin-surface
Isthmus guard, and the published trust-boundary threat model.
- guard.plugin_extension_identifier: the branded-name minters (sdk/brands.ts) now
validate with the non-emitting isSafeIdentifier predicate and emit their OWN
plugin-surface guard on a reject (distinct from the tenant-DDL guard.tenant_identifier),
so a hostile plugin identifier reads apart from a bad tenant id. Same accept/reject
set. Registry entry + matrix recipe + fuzz-spec assertion updated.
- plugin:doctor command + pure PluginDoctorService: diagnoses the deployed platform
posture from the discovered manifests + trust/firewall config — Satellite ABI drift
(error/warn), native-addon sandbox risk, dead TRUSTED_SATELLITES entries, a missing
read-only firewall while untrusted plugins are installed, and a disclosure of every
declared (consent-gated) permission. It does NOT introspect specs (that is the
check-plugin-permissions CI guard). Full command ceremony (commands.json, index,
commands.md); the service is unit-tested per check.
- Threat model: docs/guides/security.md gains the five-layer plugin trust-boundary
matrix (S1..S5 + the in-process-sandbox non-goal, each verdict real/friction) plus
hardening-checklist items; stability.md gets a plugin-platform row (Experimental);
.github/SECURITY.md states the sandbox-escape non-goal.
- Coverage floors ratcheted lines/statements 64 -> 65.
Verified via a 4-lens adversarial review (bypass/tests/ceremony/honesty); 3 low findings
folded in (severity assertions for 3 doctor checks, disclose ALL declared permission
kinds not a subset, corrected a stale plugin_env comment).
Gates green: core unit 1575, tsc 0 all workspaces, check 43/43, api-report 3/3, lint 0,
Isthmus audit-coverage 100% (23 registered guards), coverage green at the new floors.
Lote S (S-0..S-6) is complete. Merge order stays A -> S -> B -> C.
Copy file name to clipboardExpand all lines: .github/SECURITY.md
+6Lines changed: 6 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -31,3 +31,9 @@ Reports against the core isolation guarantees (cross-tenant read or write
31
31
leakage, resolver hijack, fail-open under dependency outage) are highest
32
32
priority. See the [stability matrix](https://arcoders.github.io/Adonisjs-lasagna-saas-tenancy/reference/stability)
33
33
for the per-feature labels.
34
+
35
+
An installed plugin/satellite runs in-process with full privilege by design, so an
36
+
**in-process plugin sandbox escape is not a goal** — the in-process trust controls are
37
+
friction, and the hard boundary for an untrusted plugin's write is the read-only
38
+
Postgres role (S3). The [plugin platform trust boundary](https://arcoders.github.io/Adonisjs-lasagna-saas-tenancy/guides/security#plugin-platform-trust-boundary)
39
+
in the security guide states the five-layer model and what each layer does and does not contain.
| Cross-tenant IDOR via tenant-id swap |[`tests/@guarantees/security/integration/security_tenant_guard_authorize.spec.ts`](https://github.com/Arcoders/Adonisjs-lasagna-saas-tenancy/blob/master/packages/core/tests/@guarantees/security/integration/security_tenant_guard_authorize.spec.ts): with `authorizeTenantAccess` wired, a caller whose principal belongs to tenant A gets 403 when resolving to tenant B. |
71
71
72
+
## Plugin platform trust boundary
73
+
74
+
A satellite installed as a provider runs **in-process with full privilege**. The
75
+
plugin platform does not pretend otherwise: an installed plugin can `import` any
76
+
module and reach any singleton, so nothing implemented in JavaScript is a sandbox.
77
+
The controls below raise the cost of a careless or opportunistic plugin and give an
78
+
operator an honest, consented install, but only two of them are hard boundaries, and
79
+
both are enforced outside the JavaScript VM.
80
+
81
+
| Layer | Control | Enforced by | Verdict |
82
+
|---|---|---|---|
83
+
|**S1 — Install consent**| The plugin declares its sensitive `permissions`; `configure` shows them and refuses a non-interactive install without `--accept-permissions`. | A human at install time | Real gate (human) |
84
+
|**S2 — Supply chain**| Native-addon install gate (`--allow-native`), plus `lasagna:health-check` (`npm audit` over the tree, install-script and native-addon flags). | Install / CI | Real (raises the bar; not runtime) |
85
+
|**S3 — Read-only DB role**| Untrusted plugin code is routed to a per-tenant connection authenticated as a `NOSUPERUSER NOBYPASSRLS` role with `default_transaction_read_only = on`. | PostgreSQL |**Hard boundary** (Postgres denies the write) |
86
+
|**S4 — Out-of-process worker**| Background plugin code (scheduler / data-change, once Lote B/C land) runs in a dedicated worker under the Node Permission Model (`--permission`, fs/child_process narrowed). A native-addon plugin fails boot without `--allow-addons`. | Node runtime + orchestrator | Real when configured (native addons opt out — treat as trusted) |
87
+
|**S5 — In-process friction**| Trusted-list proxies over the tenant repository and the db handle; an allowlist over sensitive capability provide/consume. | JavaScript |**Friction, not a boundary** (a direct import reaches around it) |
88
+
| In-process sandbox escape | — | — |**Non-goal** — an installed plugin has full in-process reach by design |
89
+
90
+
The one control that actually denies an untrusted plugin's write is **S3, the
91
+
read-only Postgres role**. Configure it (`plugins.readOnly`) and put untrusted
92
+
third-party plugins outside `TRUSTED_SATELLITES`. `plugin:doctor` reports the standing
93
+
posture: ABI drift, native-addon risk, a stale trust allowlist, and a missing read-only
94
+
firewall. See the [plugins guide](/guides/plugins) for authoring against these controls.
95
+
72
96
## Hardening checklist for production
73
97
74
98
Before going live, work through this list; every item is a host
@@ -87,6 +111,7 @@ responsibility (the package gives you the primitives).
87
111
-[ ] OIDC `client_secret`, encryption keys, and S3 credentials live in a secrets manager, not `.env` checked into git.
88
112
-[ ]`tenant:secrets:reencrypt` has been run **before upgrading** (it brings every stored webhook/SSO secret to the current `APP_KEY` and its per-class context). Reads now fail closed, so any secret left under the legacy shared context or in plaintext stops working until migrated. Run with `OLD_APP_KEY` set for a key rotation, or unset for a context-only migration; it is idempotent and resumable.
89
113
-[ ]`tenant:doctor` runs on a cron in production (the [doctor command](/reference/commands#tenant-doctor)) and pages on `error`-level findings.
114
+
-[ ] If you install third-party satellites: `plugins.readOnly` is configured (the S3 Postgres read-only role), untrusted plugins are kept off `TRUSTED_SATELLITES`, and `plugin:doctor` runs alongside `tenant:doctor` (see the [plugin platform trust boundary](#plugin-platform-trust-boundary)). Native-addon plugins are treated as fully trusted — they cannot be sandboxed by the worker Permission Model.
90
115
-[ ] Health probes wired (`/livez`, `/readyz`, `/healthz`, `/metrics`); see [Health & metrics](/guides/health).
Copy file name to clipboardExpand all lines: docs/reference/stability.md
+1Lines changed: 1 addition & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -98,6 +98,7 @@ The isolation substrate. Everything here is **release candidate** unless noted.
98
98
| Health probes (`/livez`, `/readyz`, `/healthz`) | Release candidate ||
99
99
| Doctor (base checks) | Release candidate ||
100
100
| Plans and quotas (`enforceQuota`) | Experimental | Opt-in feature, not part of the isolation guarantee. |
101
+
| Plugin platform (`definePlugin`, request-path seams, capability registry, trust controls) | Experimental | Opt-in extension surface for third-party satellites. The in-process trust controls are friction, not a sandbox; the read-only role is the boundary. See [plugins](/guides/plugins) and the [security guide](/guides/security). |
101
102
| Read-replica routing | Experimental | No automatic failover by design; use the retry-on-primary pattern in [read replicas](/guides/read-replicas). |
102
103
| Audit logs | Experimental | Satellite (in core). |
103
104
| Webhooks | Experimental | Satellite (in core). |
Copy file name to clipboardExpand all lines: packages/core/src/commands/commands.json
+13Lines changed: 13 additions & 0 deletions
Original file line number
Diff line number
Diff line change
@@ -532,6 +532,19 @@
532
532
"args": [],
533
533
"options": { "startApp": false },
534
534
"filePath": "health_check.js"
535
+
},
536
+
{
537
+
"commandName": "plugin:doctor",
538
+
"description": "Diagnose the installed plugin/satellite platform posture — ABI compatibility, native-addon sandbox risk, the TRUSTED_SATELLITES allowlist, and the read-only firewall",
539
+
"help": "",
540
+
"namespace": "plugin",
541
+
"aliases": [],
542
+
"flags": [
543
+
{ "name": "json", "flagName": "json", "required": false, "type": "boolean", "description": "Emit a JSON report on stdout instead of the table" }
ref: 'a plugin name, authorizer, capability key, or model name is minted from author input and interpolated into a Redis key, a Symbol, or DDL; a homoglyph or a colon must be rejected at the plugin surface, not folded onto an existing id',
0 commit comments