Plugin Sandbox Threat Model (STRIDE)
Audience: Security Guild, Plugin/Release-Orchestrator engineers, operators hardening plugin hosts. Scope: A STRIDE analysis of the host-side plugin execution boundary across the
BuiltIn,Trusted, andUntrustedsandbox modes, with verified controls and tracked residual risk.Sprint 20260501-054 PLG-OS-CONF-06. Companion to the implementation in Plugin module dossier and the operator runbook Plugin sandbox on Linux. Covers
BuiltIn(InProcessHardened),Trusted(OutOfProcessOverlay), andUntrusted(OutOfProcessConfined) execution modes shipped by sprints 20260501-003 / 051 / 052 / 053 / 054.The host-side implementation lives in
src/Plugin/— chieflyStellaOps.Plugin.Abstractions(PluginTrustLevel),StellaOps.Plugin.Host(AssemblyPluginLoader,SandboxedPluginProxy,PluginTrustDecisionMaker), andStellaOps.Plugin.Sandbox(SandboxModeMatrix,ProcessSandbox,DefaultPluginSandboxWorkspaceFactory,LinuxAppArmorAttacher,WindowsResourceLimiter,NetworkPolicyEnforcer,LinuxStaleMountSweeper, and thePluginSandboxEvent*diagnostics surface). TheReleaseOrchestratormodule consumes this runtime; it does not own the sandbox code.
1. Scope & Method
- Methodology: STRIDE applied to the host-side plugin execution surfaces (load, manifest verification, process boundary, IPC bridge, filesystem workspace, OS confinement, observability), and to the plugin authoring surfaces only insofar as a malicious or buggy plugin can influence host outcomes.
- Assets in scope: plugin install directories (
lower), per-sandbox scratch state (upper/work/merged), the gRPC plugin bridge, plugin host process privileges, host kernel resources reachable via capabilities the plugin process inherits, structured plugin audit events, and the trust derivation that selects sandbox mode from signature trust. - Out of scope: signature trust derivation itself (covered by sprint 20260501-001 and the Authority threat model), plugin author code quality, supply-chain compromise of the plugin binary before signature verification (governed by SBOM/attestation policy), and integration connector IPC (a separate per-operation stdio surface — see
IntegrationPluginProcessIpc).
2. Trust Levels & Sandbox Modes
| Trust level | Sandbox mode | Process boundary | Filesystem | OS confinement | Default for |
|---|---|---|---|---|---|
BuiltIn | InProcessHardened | None — runs in host ALC | Host filesystem (SDK policy seam only) | None | First-party packs shipped with the platform |
Trusted | OutOfProcessOverlay | ProcessSandbox (gRPC bridge) | Overlay workspace (kernel overlayfs / fuse-overlayfs / Windows junction) | None by default | Verified/signed third-party plugins from a trusted issuer |
Untrusted | OutOfProcessConfined | ProcessSandbox (gRPC bridge) | Overlay workspace + lower-immutability proof | Linux: AppArmor (or SELinux where supported); Windows: Job Object with memory + active-process limits | Plugins lacking strong signature trust |
Trust level is derived from signature verification in PluginTrustDecisionMaker.DecideAsync: the signature must verify (IPluginSignatureValidator.ValidateAsync), then the verified leaf thumbprint is mapped to BuiltIn / Trusted / Untrusted against the PluginHostOptions.BuiltInSignerThumbprints / TrustedSignerThumbprints signer allowlists; a verifying signature whose thumbprint is on neither list resolves to Untrusted. A signature that does not verify is rejected outright (PluginSignatureRejectedException). Plugin identity allowlists are not a trust input — only the cryptographic signer thumbprint is. SandboxModeMatrix.FromTrust then maps the trust verdict to the runtime sandbox mode, and AssemblyPluginLoader consumes that mode (it rejects in-process Trusted loads outright; see TB2). A dev-only escape hatch (AllowUnsignedPluginsInDevelopment, honoured only when IHostEnvironment.IsDevelopment()) loads unsigned plugins as Untrusted with a structured warning; it is ignored in Production.
3. Trust Boundaries
| Boundary | Rationale | Controls |
|---|---|---|
TB1 — Plugin install dir ↔️ Sandbox lower | The verified plugin contents must remain immutable for the lifetime of any sandbox. | Read-only attribute on Windows install files; overlayfs lower immutability on Linux; workspace factory rejects overlap and reparse escapes. |
| TB2 — Host process ↔️ Plugin process | Trusted/Untrusted plugins run in a separate process; host code must never load Trusted plugin types in-process. | AssemblyPluginLoader rejects direct Trusted in-process loads; SandboxedPluginProxy routes operation invocation through IPluginOperationInvoker over the gRPC bridge. |
| TB3 — Plugin process ↔️ Host kernel | Plugin process inherits the service account’s effective capabilities. For Untrusted, OS confinement narrows them. | AppArmor profile, SELinux sample policy, Windows Job Object with kill-on-close + memory + active-process limits + no breakaway. |
| TB4 — Sandbox workspace ↔️ Host filesystem | Plugin-visible writes must land in upper, never above the sandbox root. | Workspace factory absolute-path + non-traversal + non-overlap validation; cleanup constrained to the configured sandbox root. |
| TB5 — gRPC bridge ↔️ Other host services | The plugin bridge is the only sanctioned IPC channel; named pipes / Unix sockets are local-only. | Per-sandbox correlation IDs, JSON payload bounds, deterministic error mapping (plugin.sandbox.* / plugin.operation.*). |
| TB6 — Plugin process ↔️ Other plugins | Two plugins must not share a workspace, mount point, or Job Object. | Per-sandbox unique workspace under <sandbox-root>/<sandbox-id>/; per-process Windows Job Object handle; per-sandbox AppArmor profile attach. |
| TB7 — Plugin process ↔️ Network egress | A confined plugin must not exfiltrate context or reach lateral hosts/services beyond its declared egress policy. | NetworkPolicyEnforcer.ApplyPolicyAsync installs per-sandbox firewall rules (Linux iptables chain STELLAOPS_<id>; Windows netsh advfirewall rules). Untrusted default (SandboxConfiguration.Default/trust-level config) sets AllowAllHosts=false with an empty allowlist → block-all-outbound, plus a blocked-port set (e.g. 22/3389/5432/27017/6379). Trusted (SandboxConfiguration.Trusted) sets AllowAllHosts=true. Rules are torn down on stop (RemovePolicyAsync). |
4. Data Flow Diagram (Untrusted on Linux)
flowchart LR
subgraph Host
H[Plugin host runtime
StellaOps.Plugin.Host]
WSF[IPluginSandboxWorkspaceFactory]
AA[AppArmor profile
stellaops-plugin-sandbox]
NPE[INetworkPolicyEnforcer
iptables chain]
BR[gRPC bridge
IGrpcPluginBridge]
end
subgraph SandboxRoot["sandbox-root/<id>"]
L[lower
verified install]
U[upper
scratch]
W[work
overlay state]
M[merged
plugin CWD]
end
subgraph Plugin["Plugin process (confined)"]
P[plugin.dll loaded by host]
end
H -->|prepare| WSF
WSF -->|mount overlay| M
L -.read.-> M
U -.write.-> M
W -.work.-> M
H -->|fork+exec| Plugin
AA -.attach via aa_change_onexec.-> Plugin
NPE -.egress filter (block-all-outbound).-> Plugin
P <-->|gRPC over UDS| BR
BR <--> H
P -.reads.-> M
P -.writes.-> M
5. STRIDE Analysis
Two distinct identifier families. Start/operation error codes carried by
PluginSandboxException/PluginOperationExceptionuse theplugin.sandbox.*andplugin.operation.*/plugin.workspace.*prefixes (e.g.plugin.sandbox.os_confinement_failed,plugin.sandbox.process_exited,plugin.workspace.cleanup_failed). The structured diagnostic event names emitted throughIPluginSandboxEventEmitterare a separate, namespaced set of stable constants inPluginSandboxEventName(plugin.sandbox.confinement.*,plugin.sandbox.workspace.*,plugin.sandbox.resource_limiter.*,plugin.sandbox.lifecycle.start_denied). They overlap conceptually but are not interchangeable strings.
| # | Threat | STRIDE | Boundary | Risk (L×I) | Existing controls | Residual / follow-up |
|---|---|---|---|---|---|---|
| T01 | ALC reflection escape (legacy Trusted) — Trusted plugin uses Type.GetType / private reflection to call host internals or Authority APIs from inside the host ALC. | E | TB2 | Med×High | Sprint 051 routes Trusted lifecycle through ProcessSandbox. AssemblyPluginLoader rejects direct in-process Trusted loads. Regression test fails if Trusted operation execution instantiates plugin types in the host process. | None for new releases. Operators must complete the migration off the legacy ALC path; release notes call out the deprecation. |
| T02 | Process boundary bypass — Trusted/Untrusted plugin escapes the gRPC bridge by spawning a sibling process that exits the sandbox tree before kill. | T | TB2 / TB3 | Low×High (Untrusted), Med×Med (Trusted) | Untrusted: Job Object on Windows with LIMIT_ACTIVE_PROCESS=1 + KILL_ON_JOB_CLOSE + no breakaway; AppArmor deny ptrace, deny mount, deny mknod. Trusted: process-tree kill on graceful-stop fallback. | Trusted process-tree kill is best-effort — a fast-forking grandchild may escape briefly. Acceptable for Trusted; not acceptable for Untrusted, where LIMIT_ACTIVE_PROCESS=1 is the contract. Linux ptrace-deny is enforced by AppArmor only when the profile attaches; if attach fails, sandbox start fails closed. |
| T03 | Kernel overlayfs lower mutation from host — A privileged host actor (operator, sibling service, badly written init script) writes into lower. The kernel overlay does not detect host-level lower writes; subsequent reads through merged will see the mutation, breaking install-dir immutability. | T | TB1 | Low×High | Workspace layout documents lower as immutable for sandbox lifetime. Operator runbook forbids lower mutation. Plugin install signature is verified before lower is exposed. | Residual. No filesystem-level lock prevents a privileged sibling from writing to lower. Mitigation: keep the install directory on a read-only bind mount (operator runbook); the runtime cannot enforce this without taking ownership of host filesystem layout. |
| T04 | fuse-overlayfs FUSE-layer escape — A bug or non-default mount option in fuse-overlayfs exposes upper writes to other users on the host, or leaks host paths through xattr passthrough. | I | TB4 | Low×Med | Strategy is opt-in only when kernel overlay is unavailable. Pin minimum version fuse-overlayfs ≥ 1.10 for xattr passthrough fixes. Mount options come from a typed builder, never shell concatenation. fuse module access is the host’s responsibility. | Residual. FUSE userspace driver bugs are upstream risk. Mitigation: prefer kernel overlay where possible; treat fuse-overlayfs as the rootless/CI fallback. |
| T05 | Stale mount cleanup escapes the sandbox root — The stale-mount sweep (PLG-LINUX-OVL-04) walks above <sandbox-root> and unmounts host paths it should not touch. | T | TB4 | Low×High | Implemented: LinuxStaleMountSweeper.FindStaleSandboxMounts filters parsed /proc/self/mountinfo to overlay / fuse-overlayfs mounts that are strictly contained under the configured <sandbox-root> (IsStrictlyContainedIn, with a separator-boundary check so …/stellaops-runner-evil cannot match …/stellaops-runner, plus .. normalization). ILinuxMountActionExecutor.TryRemoveScratchAsync re-enforces the same strict-containment as a last gate. Per-entry failures are accumulated, never thrown, and surfaced as plugin.sandbox.workspace.cleanup_failed events. | Residual closed for the prefix-matching threat. Covered by LinuxStaleMountSweeperTests against a fixture mount table that asserts <sandbox-root>-prefix-only matching (/home/operator/sandbox/merged is excluded). Symlink-target escape is handled by the action executor at sweep time, not parse time. The live unmount/scratch-removal path (SweepAsync) is Linux-only and runner-gated. |
| T06 | AppArmor false positives — Production plugin denied a legitimate path because the profile is too narrow. Operator removes the profile, regressing to no-confinement. | DoS / E | TB3 | Med×Med | Profile authored from the workspace contract (read lower+merged, write upper; deny ptrace/deny mount/deny mknod and a denied capability bag in devops/linux/apparmor/stellaops-plugin-sandbox.profile). When AppArmorOptions.EnableAuditLogScraping is on, ApparmorAuditLogScraper tails the kernel audit sink (default /var/log/audit/audit.log) and converts apparmor="DENIED" AVC lines into structured plugin.sandbox.confinement.profile_denied events (Sprint 20260504-002 KENF-AA-02). SELinux sample policy mirrors the same intent. | Residual. Profile false-positive risk is paid forward to denial-event monitoring. The scraper is opt-in and no-ops (with one startup warning, never throwing) on hosts without a readable audit log, so denial visibility is not guaranteed on every host. Operators must investigate denials before relaxing the profile; the threat model recommends a “complain mode” rollout window before “enforce” in production. |
| T07 | AppArmor / SELinux attach failure silently downgrades — Profile install or aa_change_onexec fails and the sandbox starts without confinement. | E | TB3 | Med×High | Untrusted attach is mandatory and is verified in ProcessSandbox.PreflightAppArmorLinuxAsync before the plugin process is ever spawned (step 3a, ahead of IPluginProcessManager.StartAsync). Any of profile-not-loadable / required-but-unavailable / aa_change_onexec failure raises SandboxStartupRefusedException, surfaced as the start-error code plugin.sandbox.os_confinement_failed (and the structured plugin.sandbox.confinement.fail_closed / …profile_missing events). Untrusted forces AppArmorOptions.RequireAppArmor=true on Linux. Trusted does not require attach (it runs the best-effort warn-and-degrade PreflightAppArmorTrustedWarnLinuxAsync path). | Residual closed for Untrusted. Trusted intentionally has no OS confinement attach by default; operators who enable Trusted profile attachment opt in to the same fail-closed semantics. |
| T08 | SELinux sample-only limitation — Released SELinux .te is advisory; running on RHEL with enforcing mode but without the profile loaded means Untrusted runs without SELinux enforcement. | E | TB3 | Med×High on SELinux-first distros | Sprint 054 explicitly marks SELinux sample-only until a validation runner exists; release notes call out that operators on SELinux-first hosts must load the policy or run with AppArmor. | Residual. Until PLG-OS-CONF-03 has a runner, SELinux remains “operator-loaded”; the threat model treats SELinux Untrusted hosts as “AppArmor-equivalent intent, operator-verified”. |
| T09 | Windows junction reparse-point escape — A plugin abuses merged (junction target) to traverse out of the sandbox root via ..\ segments, or uses NTFS extended reparse semantics to redirect a path lookup. | E / T | TB1 / TB4 | Low×High | Workspace factory uses Win32 reparse-point APIs (not cmd/mklink), validates absolute paths, rejects parent traversal, rejects source/destination overlap, rejects existing reparse points at the destination. Existing install files are marked read-only while the workspace is active. | Residual. Junctions follow target on traversal; a plugin reading merged\..\..\Windows\System32\config\SAM is constrained only by NTFS ACLs of the host service account. Mitigation: run the plugin host service under a least-privilege account; full ACL enforcement awaits future driver-level COW work (PSB follow-up). |
| T10 | Policy-level COW residual on Windows — OverlayFilesystemPolicy redirects writes via SDK/policy interposition, not a filesystem driver. A native plugin that bypasses the SDK and calls CreateFileW directly on a lower path can observe the read-only ACL but not a write redirection. | T / I | TB1 | Med×High for native plugins | Windows install files are read-only while the workspace is active; native writes hit ERROR_ACCESS_DENIED. ALC-managed plugins are rerouted by SDK. | Residual. Native plugins on Windows must opt in to writing through merged/upper paths; the threat model marks this as a known posture and tracks driver-level COW (WinFsp) as a future hardening sprint. |
| T11 | Job Object breakaway / child-process fan-out — Untrusted Windows plugin spawns child processes to exhaust active-process limits or escape the job. | DoS / E | TB3 | Low×High | Sprint 054 PLG-OS-CONF-04: WindowsResourceLimiter.ApplyLimitsAsync sets the Untrusted Job Object to JOB_OBJECT_LIMIT_ACTIVE_PROCESS with ActiveProcessLimit=1 (from ResourceLimits.MaxProcesses, overridable only by explicit operator config), KILL_ON_JOB_CLOSE, and no breakaway flags. VerifyAppliedLimits re-reads the applied limits via QueryInformationJobObject and asserts IsProcessInJob, kill-on-close, no breakaway, and the configured memory/process limits — at step 5, before the gRPC bridge connects (step 6). Successful attach emits plugin.sandbox.confinement.job_object_attached; attach failure throws and emits plugin.sandbox.confinement.fail_closed. | Residual. A child-process spawn past the active-process limit is rejected by the OS Job Object (CreateProcess fails); the rejection is an OS-level enforcement, not a per-attempt Stella event. Memory-pressure termination is verified locally but awaits dedicated Windows runner evidence before release sign-off. |
| T12 | Job Object memory-limit bypass — Untrusted plugin exhausts host memory below the per-job limit but above shared host headroom (e.g., balloons file-cache pages). | DoS | TB3 | Low×High | WindowsResourceLimiter configures LIMIT_PROCESS_MEMORY and LIMIT_JOB_MEMORY from ResourceLimits.MaxMemoryMb. The kernel terminates the plugin at or above the limit. | Residual. Page-cache pressure outside the job is a host-capacity concern, not a confinement bypass. Operators sizing hosts must budget plugin memory ceilings against total host RAM. |
| T13 | gRPC bridge correlation collision — A misbehaving plugin reuses correlation IDs to confuse host operation routing. | T / Spoofing | TB5 | Low×Low | GrpcPluginBridge.InvokeAsync / InvokeStreamingAsync generate a fresh per-call Guid.NewGuid().ToString("N") CorrelationId on the request; the plugin never supplies it. Operation routing is request/response correlated by the gRPC call itself (one outstanding InvokeAsync per call), bounded by the per-operation timeout. | Residual closed. Correlation IDs are host-generated and opaque to plugins; the host does not re-read/validate the plugin’s echo of the ID, so the ID is a diagnostic correlation key rather than an integrity check — response binding relies on the synchronous gRPC call, not on echo matching. |
| T14 | gRPC payload exhaustion — Plugin returns an enormous JSON payload to OOM the host bridge serializer. | DoS | TB5 | Low×Med | The host bound is the gRPC channel’s default maximum receive message size (Grpc.Net.Client default ~4 MB); GrpcChannelOptions in GrpcPluginBridge.ConnectAsync does not raise it, so oversized responses are rejected at the channel before the JSON deserializer (JsonSerializer.Deserialize) runs. Operation timeouts terminate stalled invocations. | Residual. The cap is the implicit gRPC channel default, not an explicit, operator-configurable MaxReceiveMessageSize; making it an explicit setting (and adding a JsonSerializerOptions.MaxDepth guard) is tracked as future hardening. |
| T15 | BuiltIn residual risk — BuiltIn runs in the host ALC with full host privileges. A compromised first-party pack has full host process access. | E | TB2 | Low×Critical | BuiltIn is reserved for first-party packs shipped with the platform; the trust derivation cannot promote a third-party plugin to BuiltIn. SBOM/attestation governs the supply chain of first-party packs. | Residual. This is the inherent cost of running first-party packs in-process; the threat model documents it as “supply chain governs first-party safety”. |
| T16 | STELLAOPS_SANDBOX_WORKSPACE_* env-var spoofing — A plugin reads its own workspace env vars and trusts them as ground truth, even though the plugin process can mutate its own environment. | I | TB5 | Low×Low | Env vars are diagnostics, not a security control. The host confirms workspace state through workspace metadata returned from IPluginSandboxWorkspaceFactory. | Residual closed. Documented in dossier — env vars are diagnostics only. |
| T17 | Stale workspace from crash leaks signed install bytes — After a host crash, leftover upper/merged may contain partial copies of lower (materialized-copy strategy) accessible by other host users. | I | TB1 | Low×Med | Sandbox root is created with restrictive permissions (0700-equivalent) at host start. On Linux, the LinuxStaleMountSweeper (PLG-LINUX-OVL-04, now shipped) unmounts and best-effort-removes orphaned sandbox scratch under <sandbox-root> at host start. | Residual. The sweeper’s live SweepAsync is Linux/runner-gated, and there is no equivalent automated crash-recovery sweep on Windows yet, so operators still rely on the runbook for crash cleanup on hosts where the sweeper does not run. |
| T18 | Plugin signature replay across trust transitions — Operator downgrades a plugin from Untrusted to Trusted by re-issuing a signature with a different issuer, retaining cached state from the Untrusted run. | E | TB2 | Low×Med | Sandbox state is per-invocation; upper/work/merged are deleted on stop. Signature trust is recomputed on every load; cached ALC instances are not reused across trust transitions. | Residual closed. Operator policy decisions on issuer trust are governed separately. |
| T19 | Confinement event log forging — A plugin writes log lines that mimic confinement audit events to confuse SOC. | Spoofing / Repudiation | TB5 | Low×Med | Plugin stdout/stderr is captured by the host; structured confinement events are emitted host-side only, through IPluginSandboxEventEmitter (default LoggerPluginSandboxEventEmitter) as typed PluginSandboxEvent records. Event names are stable constants in PluginSandboxEventName (validated by PluginSandboxEvent.Create); a plugin cannot inject a record into this channel because it never holds the emitter. | Residual closed. The typed event subsystem (PLG-OS-CONF-05) is implemented: PluginSandboxEvent, PluginSandboxEventName, PluginSandboxEventReason/Severity/StrategyName, and the IPluginSandboxEventEmitter seam. Audit consumers match on the host-emitted event-name constants and ignore plugin stdout/stderr by category. |
| T20 | Cleanup-failure DoS — A buggy plugin holds a workspace mount across stop; cleanup fails; subsequent sandbox starts with the same id are blocked. | DoS | TB4 | Low×Med | New sandbox starts use a fresh per-invocation id, so a stale workspace cannot block new starts directly. Operator runbook covers manual cleanup. | Residual. The blocked path is operator-visible (workspace exception code plugin.workspace.cleanup_failed / event plugin.sandbox.workspace.cleanup_failed); operator action is documented. |
| T21 | Plugin network egress / data exfiltration — A confined plugin opens outbound connections to exfiltrate host context or reach lateral services. | I / E | TB7 | Low×High (Untrusted), Med×Med (Trusted) | NetworkPolicyEnforcer.ApplyPolicyAsync runs before ExecuteAsync and installs per-sandbox egress rules: Linux iptables custom chain STELLAOPS_<id> jumped from OUTPUT; Windows netsh advfirewall out rules. Untrusted defaults to block-all-outbound (AllowAllHosts=false, empty allowlist) plus a blocked-port set; allowlisted hosts are resolved to IPs and ACCEPT-ed before a trailing DROP. Rules are removed on stop. | Residual. Enforcement depends on the host firewall stack (iptables/Windows Firewall) being present and the service account holding the privilege to program it; on hosts where the command fails the failure is logged (Linux apply rethrows; Windows logs) but rule installation is not independently re-verified the way Job Object / AppArmor attachment is. Trusted runs with AllowAllHosts=true by design. Egress controls are not part of the in-process BuiltIn posture. |
| T22 | Untrusted in-process compatibility ALC — AssemblyPluginLoader still has a collectible-ALC path for Untrusted loads (used until the confined-process runner fully owns that route), so an Untrusted plugin loaded through the loader (rather than ProcessSandbox) runs in-process behind ALC isolation only. | E | TB2 | Low×High | The loader hard-rejects in-process Trusted loads (SandboxMode.OutOfProcessOverlay → PluginLoadException). For Untrusted it creates a collectible PluginAssemblyLoadContext (EnableAssemblyIsolation), not an OS-confined process; OS confinement (AppArmor/Job Object) only applies on the ProcessSandbox route. | Residual. ALC isolation is escapable by reflection (the same class of weakness as legacy Trusted in T01). The intended runtime route for Untrusted is ProcessSandbox (SandboxMode.OutOfProcessConfined); the loader’s Untrusted ALC branch is a documented transitional compatibility path. Operators must drive Untrusted plugins through the process sandbox, not the in-process loader. |
6. Open Items
| Item | Sprint | Status | Owner |
|---|---|---|---|
| Kernel overlayfs and fuse-overlayfs workspace paths | 052 PLG-LINUX-OVL-02/03 | DONE in code (DefaultPluginSandboxWorkspaceFactory + LinuxOverlayMountCommand; sprint archived). Live kernel-enforcement still runner-gated — LinuxKernelOverlayIntegrationTests / LinuxFuseOverlayIntegrationTests skip without a Linux runner. | Plugin + DevOps |
Stale-mount sweep with <sandbox-root>-prefix constraint | 052 PLG-LINUX-OVL-04 | DONE (LinuxStaleMountSweeper + LinuxStaleMountSweeperTests fixture-table proof; sprint archived). Live SweepAsync is Linux/runner-gated. | Plugin |
| AppArmor profile + attach strategy | 054 PLG-OS-CONF-02 | DONE (2026-05-02; devops/linux/apparmor/stellaops-plugin-sandbox.profile + libapparmor aa_change_onexec attach + WSL2 unit/integration tests shipped) | Plugin + DevOps |
| AppArmor kernel-enforcement runner provisioning | 054 PLG-OS-CONF-02 residual | OPEN (need a Linux runner with apparmor LSM compiled into kernel; WSL2 lacks it; multipass-on-Hyper-V networking blocked in current Windows 11 environment) | Plugin + DevOps |
| SELinux sample policy + manual validation | 054 PLG-OS-CONF-03 | DONE (2026-05-02; sample devops/linux/selinux/stellaops-plugin-sandbox.te committed, checkmodule + semodule_package syntax-check green; rollout posture sample-only/advisory) | DevOps + Docs |
| Windows Job Object memory-pressure runner evidence | 054 PLG-OS-CONF-04 | OPEN — Job Object attach/limit code (WindowsResourceLimiter) is DONE and unit-tested; live memory-pressure termination still needs a pinned Windows runner (sprint archived) | Plugin |
| Structured Plugin sandbox confinement event subsystem | 054 PLG-OS-CONF-05 | DONE (PluginSandboxEvent + PluginSandboxEventName/Reason/Severity/StrategyName + IPluginSandboxEventEmitter; contract in docs/modules/plugin/sandbox-confinement-events.md; sprint archived) | Plugin + Docs |
| Driver-level Windows COW (WinFsp follow-up) | post-053 | Future hardening | Plugin |
Explicit, operator-configurable gRPC MaxReceiveMessageSize (+ JsonSerializerOptions.MaxDepth) | future | Backlog | Plugin |
7. Release Notes (sprint 054 confinement posture)
The following items are added to docs/releases/RELEASE_PROCESS.md checklist “Plugin sandbox posture” and should appear in release notes for any release that ships the sprint 054 changes:
- Untrusted plugins are now OS-confined by default. Linux hosts must load the AppArmor profile shipped at
devops/linux/apparmor/stellaops-plugin-sandbox.profile(profile namestellaops-plugin-sandbox) before Untrusted plugins start; missing/failing profile attach makes Untrusted sandbox start fail closed (start-error codeplugin.sandbox.os_confinement_failed; structured eventsplugin.sandbox.confinement.fail_closed/plugin.sandbox.confinement.profile_missing). - SELinux is sample-only this release. Operators on SELinux-first distros (RHEL/CentOS/Fedora with
enforcing) must load thestellaops-plugin-sandbox.tesample underdevops/linux/selinux/and treat it as advisory until a validation runner exists. Switch to AppArmor where possible, or run Trusted-only plugins. - Windows Untrusted plugins are confined to a Job Object. Active process count is capped at 1 unless an operator/fixture explicitly raises it; memory limits are enforced; breakaway is denied; missing Job Object attachment fails closed.
- Trusted plugins remain process-isolated but are not OS-confined by default. Operators who want Trusted profile attachment must opt in explicitly; the same fail-closed semantics apply when they do.
- Untrusted egress is firewalled per sandbox.
NetworkPolicyEnforcerinstalls per-sandboxiptables(Linux) /netsh advfirewall(Windows) rules; the Untrusted default is block-all-outbound. This requires the host firewall stack to be present and the plugin host service account to hold the privilege to program it (e.g.CAP_NET_ADMINforiptables). Where that privilege is absent, egress rule installation logs a failure and the network confinement degrades — size the deployment accordingly. - Host prerequisites (Linux): kernel ≥ 4.18 with
overlayin/proc/filesystems,CAP_SYS_ADMINin the service account’s effective set, AppArmor enabled with the Stella profile loaded. For rootless/fuse-overlayfs deployments:fuse-overlayfs ≥ 1.10inPATHandfusekernel module loaded. - Host prerequisites (Windows): Windows Server 2019+ with reparse point support on the sandbox root volume; the plugin host service account should be least-privilege (does not require admin to attach a Job Object to its own children).
- What did not change: plugin-author manifests, SDK helpers, health checks, operation invocation semantics. Only the host execution boundary changed.
8. Review Cadence
- Re-validate this threat model when any of the following ship: Linux runner-gated kernel/fuse-overlay + AppArmor enforcement evidence (PLG-LINUX-OVL-02/03, PLG-OS-CONF-02 residual), Windows memory-pressure runner evidence (PLG-OS-CONF-04 residual), Windows driver-level COW (WinFsp), or an explicit operator-configurable gRPC payload-size cap.
- PLG-LINUX-OVL-04 (stale-mount sweep) and PLG-OS-CONF-05 (structured confinement event subsystem) have shipped; re-validate the corresponding rows (T05, T19) if their contracts change.
- Annual review by the Security Guild regardless of changes.
