Skip to content

[EPIC]: Configurable and Portable WASM-Based Sandboxing for Plugins #24

@monshri

Description

@monshri

Summary

This document proposes a comprehensive design for implementing configurable, portable WASM-based sandboxing for plugins across multiple programming languages (Python, Go, Rust) and operating systems. The solution provides fine-grained security controls while maintaining performance and developer experience.

Key Features:

  • Configurable per-plugin security policies via YAML
  • Cross-language portability (Python, Go, Rust)
  • Capability-based security with zero-trust model
  • Near-native performance with minimal overhead
  • Support for both WASM Component Model and Core WASM

Motivation

Problem Statement

Modern plugin systems face critical security challenges:

  • Native plugins run with full process privileges, accessing gateway internals without restrictions
  • External plugins in Docker containers are isolated but heavyweight and have deployment complexity
  • Lack of granular control over individual plugin capabilities (filesystem, network, memory)
  • No portable solution that works consistently across languages and operating systems

Why WASM Sandboxing?

WebAssembly provides:

  • Capability-based security: Explicit permission grants for resources
  • Memory isolation: Linear memory model prevents buffer overflows
  • Platform independence: Same sandbox works on Linux, macOS, Windows
  • Near-native performance: Minimal overhead compared to containers
  • Language agnostic: Compile from any language to WASM

Design

Design Goals & Requirements

Functional Requirements

FR1: Policy Configuration

  • Define security policies in YAML per plugin
  • Support filesystem, network, environment, and compute policies
  • Allow policy inheritance and composition

FR2: Multi-Language Support

  • Host support: Rust
  • Guest support: Python, Go and Rust

FR3: Sandbox Management

  • Load and execute plugins in isolated WASM instances
  • Handle plugin dependencies and recursive calls
  • Manage sandbox lifecycle (create, execute, destroy)

FR4: Resource Control

  • Enforce memory limits
  • Implement execution timeouts
  • Control CPU usage

FR5: Capability-Based Security

  • Explicit grants for filesystem paths
  • Network endpoint allowlisting
  • Environment variable filtering

Design Decisions

WASM Runtime Selection

Component Model vs Core WASM Decision Matrix

Criteria Core WASM Component Model Our Choice
Performance ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ Hybrid
Type Safety ⭐⭐ ⭐⭐⭐⭐⭐ Component
Composition ⭐⭐⭐⭐⭐ Component
Simplicity ⭐⭐⭐⭐ ⭐⭐⭐ Core
Maturity ⭐⭐⭐⭐⭐ ⭐⭐⭐ Core
Future-Ready ⭐⭐ ⭐⭐⭐⭐⭐ Component
Multi-Language ⭐⭐ ⭐⭐⭐⭐⭐ Component

Use Component Model When:

  1. Complex Plugin Systems - Multiple plugins need to communicate
  2. Rich Data Types - Passing complex structures
  3. Language Interoperability - Plugins in different languages
  4. Future-Proofing - Want WASI Preview 2 features
  5. Better Developer Experience - Type-safe interfaces

Use Core WASM When:

  1. Maximum Performance - Compute-intensive tasks
  2. Simple, Isolated Plugins - Single-purpose functions
  3. Existing Ecosystem - Large Core WASM codebase
  4. Minimal Dependencies - Simple I/O requirements
  5. Embedded Systems - Resource-constrained environments

Support for both is chosen because Component Model is still maturing—core WASM provides stable, production-ready compatibility today while Component Model offers a future-proof migration path as tooling and ecosystem adoption improve.

Architecture Overview

High-Level Architecture

┌──────────────────────────────────────────────────────────────────────────────┐
│                               Gateway                              │
│                                                                              │
│  ┌────────────────────────────────────────────────────────────────────────┐  │
│  │                    Plugin Manager (Host Language)                     │  │
│  │  - Load plugin configurations                                         │  │
│  │  - Invoke plugin hooks                                                │  │
│  │  - Manage plugin lifecycle                                            │  │
│  └───────────────────────────────┬────────────────────────────────────────┘  │
│                                  │                                           │
│  ┌───────────────────────────────▼────────────────────────────────────────┐  │
│  │                     Sandbox Manager (Rust Core)                       │  │
│  │  - Parse security policies                                            │  │
│  │  - Create WASM instances with defined policies                        │  │
│  │  - Enforce resource limits                                            │  │
│  │  - Handle recursive plugin calls or dependencies                      │  │
│  └───────────────────────────────┬────────────────────────────────────────┘  │
│                                  │                                           │
│  ┌───────────────────────────────▼────────────────────────────────────────┐  │
│  │                   WASM Runtime (Wasmtime)                      │  │
│  │  - Execute WASM bytecode                                              │  │
│  │  - Provide WASI interface                                             │  │
│  │  - Enforce memory isolation                                           │  │
│  └───────────────────────────────┬────────────────────────────────────────┘  │
│                                  │                                           │
│  ┌───────────────────────────────▼────────────────────────────────────────┐  │
│  │                    Capability Provider Layer                           │  │
│  │  ┌────────────┐  ┌────────────┐  ┌────────────┐                       │  │
│  │  │ Filesystem │  │  Network   │  │    Env     │                       │  │
│  │  │  Provider  │  │  Provider  │  │  Provider  │                       │  │
│  │  └────────────┘  └────────────┘  └────────────┘                       │  │
│  └────────────────────────────────────────────────────────────────────────┘  │
│                                                                              │
│  ┌────────────────────────────────────────────────────────────────────────┐  │
│  │                        Plugin Instances (WASM)                        │  │
│  │  ┌──────────┐     ┌──────────┐     ┌──────────┐                       │  │
│  │  │ Plugin A │     │ Plugin B │     │ Plugin C │                       │  │
│  │  │  (Auth)  │     │  (Data)  │     │   (UI)   │                       │  │
│  │  └──────────┘     └──────────┘     └──────────┘                       │  │
│  └────────────────────────────────────────────────────────────────────────┘  │
└──────────────────────────────────────────────────────────────────────────────┘

Configuration

YAML Schema Example

name: "DenyListPlugin"
kind: "plugins.deny_filter.deny.DenyListPlugin"
hooks: ["prompt_pre_fetch"]
mode: "enforce"
priority: 100

config:
  words: ["innovative", "groundbreaking", "revolutionary"]

sandbox:
  type: wasm
  
  wasm:
    format: component  # or "core"
    
    # Filesystem access permissions
    filesystem:
      # Directories that can be read from
      read_only:
        - path: "./data/input"
          guest_path: "/input"
          description: "Input data directory (read-only)"
      
      # Directories that can be written to
      read_write:
        - path: "./data/output"
          guest_path: "/output"
          description: "Output data directory (read-write)"
    
    # Network access permissions
    network:
      allowed_hosts:
        - "api.example.com"
        - "httpbin.org"
        - "localhost"
      # Allowed ports
      allowed_ports:
        - 80
        - 443
        - 8080
    
    # Environment variables accessible to the child process
    environment:
      # Inherit from parent
      inherit:
        - "PATH"
        - "HOME"
    
    # Resource limits
    resources:
      # Maximum memory allocation (in MB)
      max_memory_mb: 128
      # Maximum execution time (in seconds)
      max_execution_time_sec: 30
      # Maximum number of file descriptors
      max_file_descriptors: 100
    
    # Capabilities and permissions
    capabilities:
      # Allow access to system clock
      clock_access: true
      # Allow random number generation
      random_access: true
      # Allow command-line arguments
      args_access: true
      # Allow stdin/stdout/stderr
      stdio_access: true
    
    # Logging configuration
    logging:
      enabled: true
      level: "info"
      log_file: "./sandbox.log"

The configuration for sandbox could be defined by user inside the key sandbox where the type of sandboxing, in this case wasm could be specified. Within the wasm key the format which could either be core or component could be specified, and then policies within the keys filesystem, network, environment, resources and capabilities could be specified.

The plugin manager communicates with the sandbox manager, which is responsible for creating and managing isolated environments for each plugin instance based on the policies defined in the plugin sandbox configuration. The sandbox manager interprets these policies, initializes WebAssembly (Wasm) instances accordingly, and enforces resource limits using the WASI context.

It sets up a Wasmtime engine with resource constraints and backtrace support, and configures the WASI interface with features like standard I/O inheritance, environment variables, and preopened directories using use wasmtime_wasi::WasiCtxBuilder. The manager then loads the plugin.wasm file, instantiates it within the sandbox, executes it under the defined restrictions, monitors resource (fuel) usage, and reports whether execution succeeds or fails.

The plugin.wasm is pre-compiled using wasm32-wasip1 (core) or wasm32-wasip2 (component). If using core, then direct compilation with rustc to WASI Preview 1. However, if using the component setup then it is compiled using the component model using cargo-component.

📝 NOTE: WASI Preview 1 (wasm32-wasip1) has limited networking support. WASI Preview 2 (Wasm component model) or custom host functions is needed.

Interaction Flow

  1. Plugin Configuration Load
    Plugin Manager → Parse YAML → Extract Policies

  2. Sandbox Creation
    Plugin Manager → Sandbox Manager → Create WASM Instance
    → Configure Capabilities → Return Sandbox Handle

  3. Plugin Execution
    Hook Trigger → Plugin Manager → Sandbox Manager
    → WASM Runtime → Execute Plugin Code → Return Result

  4. Capability Access
    Plugin Code → WASI Call → Capability Provider
    → Policy Check → Grant/Deny → Return Result

  5. Recursive Plugin Call
    Plugin A → Sandbox Manager → Check Dependency Graph
    → Create/Reuse Sandbox B → Execute → Return to A

📝 NOTE: For supporting recursive plugin calls (plugins calling other plugins), the Component Model is required. Core WASM does not support this capability.

Sandbox Manager

Runs natively on your system with full privileges, while the plugin (plugin.wasm) runs in a restricted WASM sandbox with only explicitly granted capabilities.

Responsibilities:

  • Parse and validate security policies
  • Create and configure WASM instances
  • Manage sandbox lifecycle
  • Handle inter-plugin communication
  • Enforce resource limits
  • Provide audit logging
fn main() -> Result<()> {
    // 1. Load security configuration
    let config = load_config("sandbox_config.yaml")?;
    
    // 2. Create WASM engine with limits
    let engine = Engine::new(&engine_config)?;
    
    // 3. Load the plugin module
    let module = Module::from_file(&engine, "plugin.wasm")?;
    
    // 4. Build sandboxed WASI context
    let wasi_ctx = build_wasi_context(&config, &child_args)?;
    
    // 5. Create store with fuel limits
    let mut store = Store::new(&engine, wasi_ctx);
    store.set_fuel(config.resources.max_execution_time_sec * 1_000_000)?;
    
    // 6. Execute the plugin
    let instance = linker.instantiate(&mut store, &module)?;
    start.call(&mut store, ())?;
}

Policy

Defines sandbox constraints:

pub struct SandboxPolicy {
    pub name: String,
    pub compute: Compute,        // CPU fuel allocation
    pub ram: Option<u64>,         // Memory limit in bytes
    pub timeout: Option<String>,  // Max execution time
    pub max_retries: u64,         // Retry attempts on failure
    pub allowed_files: Vec<...>,  // Filesystem access rules
    pub allowed_hosts: Vec<...>,  // Network access whitelist
    pub env_variables: Vec<...>,  // Environment variable access
    pub mounts: Vec<...>,         // Dynamic directory mappings
}

Sandbox manager to binary compilation

cargo build --release --bin sandboxmanager

Plugin to Wasm Compilation

Makefile that compiles plugin.rs with both Core WASM and Component Model WASM:

  • Core WASM: Compiles using rustc --target wasm32-wasip1
  • Component Model: Compiles using cargo component build --target wasm32-wasip2
  • Automatic dependency checking and installation
  • Builds sandbox manager binaries for both implementations

Main Commands:

  • make all - Build both WASM formats
  • make core - Build Core WASM only
  • make component - Build Component Model only

Phased implementation

Phase 1: Core Wasm (Rust): Implement the proposed architecture for sandbox manager with end-to-end functionality from plugin manager to sandboxed plugin execution, including robust error handling and test cases using Rust and Core Wasm. Some limitations might be encountered due to compatibility issues or limited functionality of wasm32-wasip1, but most of it could be addressed using wasm32-wasip2 in phase 2.
Phase 2: Component Wasm (Rust): Implement the WIT interfaces and add component model support to the sandbox manager.
Phase 3: Recursive Composition (Rust): Design and implement plugin dependency resolution, leveraging the Wasm component model to support recursive composition.
Phase 4: Extend plugin wasm compilation support beyond Rust to include Go and Python.

Dependencies

wasmtime - WebAssembly runtime
wasmtime-wasi - WebAssembly System Interface
wasmtime-wasi-http WASI HTTP interface for the Wasmtime WebAssembly runtime

Risks

The Rust-based ecosystem for WebAssembly is mature across both the core and component models, with well-established tooling such as wasmtime, wasm-tools, and cargo component providing a reliable path from source to sandboxed execution. However, compiling plugins written in Python or Go to Wasm introduces significant friction, as the tooling for these languages is considerably less mature and imposes constraints that can be difficult to work around in practice.

  1. Preliminary experiments were conducted with componentize-py to compile Python-based plugins into Wasm components. While basic compilation succeeded, we encountered compatibility issues with pydantic — a dependency commonly used across the plugin ecosystem for data validation and schema definition. These issues stem from pydantic's reliance on native C extensions and runtime type introspection, which do not translate cleanly to the Wasm component model's static interface constraints defined via WIT. Similar limitations are likely to surface with other Python libraries that depend on native code or dynamic features not well-supported by the current componentize-py toolchain.

  2. For Go, compilation to Wasm components is possible via TinyGo, but it comes with its own set of restrictions — limited standard library coverage, constraints on reflection and goroutines, and divergence from upstream Go semantics — which would need to be evaluated before committing to it as a supported path.

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Projects

Status

In progress

Relationships

None yet

Development

No branches or pull requests

Issue actions