08 — Security & Isolation

Security model for the bext platform, covering app isolation, plugin sandboxing, and tenant boundaries.

Isolation Layers

┌─────────────────────────────────────────────┐
│                  Platform                    │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  │
│  │  App A   │  │  App B   │  │  App C   │  │  ← App isolation
│  │ ┌──────┐ │  │ ┌──────┐ │  │          │  │
│  │ │Plugin│ │  │ │Plugin│ │  │  (none)  │  │  ← Plugin sandbox
│  │ └──────┘ │  │ └──────┘ │  │          │  │
│  │ ┌──────┐ │  │ ┌──────┐ │  │ ┌──────┐ │  │
│  │ │Isolat│ │  │ │Isolat│ │  │ │Static│ │  │  ← JS isolation
│  │ └──────┘ │  │ └──────┘ │  │ └──────┘ │  │
│  └──────────┘  └──────────┘  └──────────┘  │
│  ┌─────────────────────────────────────┐    │
│  │        Shared Infrastructure         │    │  ← Shared (rate limit, TLS, logging)
│  └─────────────────────────────────────┘    │
└─────────────────────────────────────────────┘

App Isolation

What's isolated per app

Resource Isolation level Implementation
JS context Separate JSC context per app IsolateManager
Memory Per-isolate limit (configurable) JSC memory limits
CPU Fuel budgeting + timeout Epoch interruption
Cache Namespace-prefixed cache keys a:{app_id}:... prefix
Rate limits Per-app RPM Per-app rate limiter
Plugins Per-app plugin set Scoped plugin registry
Static files Separate directories Per-app static_dir
Config Per-app config object AppConfig struct
Domains Exclusive domain ownership No domain overlap validation
Logs Per-app log level Structured log labels

What's shared

Resource Shared between Protection
Process memory All apps Per-isolate limits
Database pool Multi-tenant apps tenantId filtering
Worker threads All isolates Work-stealing scheduler
Network stack All apps Per-app rate limiting
Disk (data_dir) All apps Directory scoping
Plugin WASM engine All WASM plugins wasmtime isolation

Threat Model

Threat Mitigation
App A reads App B's cache Cache key prefixed with app_id
App A exhausts memory Per-isolate memory limit, LRU eviction
App A infinite loops Execution timeout (epoch interruption)
App A floods requests Per-app rate limiting
Malicious SSR bundle JSC sandbox (no filesystem, no network from JS)
App A reads App B's env vars Env vars scoped per app in config
Domain hijacking Validation: no overlapping domains

Plugin Sandbox

WASM Security Model

wasmtime provides hardware-level isolation:

  • Memory: Linear memory, bounded by store limits (64MB default)
  • CPU: Fuel consumption tracking, epoch-based wall-clock timeout
  • No filesystem: Only scoped storage via host functions
  • No network: Only via bext.http_fetch with URL allowlist
  • No system calls: WASM has no syscall interface

Permission Enforcement

pub struct WasmPluginPermissions {
    // Network
    pub allowed_urls: Vec,       // Glob patterns
    pub max_fetch_per_minute: u32,       // Rate limit

    // Storage
    pub storage_quota_kb: u64,           // Disk quota

    // KV store
    pub kv_namespaces: Vec,      // Allowed namespaces

    // Cache
    pub cache_read: bool,
    pub cache_write: bool,

    // Queue
    pub queue_names: Vec,

    // Secrets
    pub secrets: Vec,            // Allowed secret names

    // Cron
    pub max_cron_jobs: u32,

    // Resource limits
    pub fuel_per_request: u64,           // CPU budget
    pub memory_limit_mb: usize,          // Memory cap
    pub execution_timeout_ms: u64,       // Wall clock limit
}

Every host function checks permissions before executing:

// Example: http_fetch permission check
fn http_fetch(caller: &mut Caller, req: FetchRequest) -> FetchResponse {
    // 1. URL allowlist check
    if !caller.data().sandbox.is_url_allowed(&req.url) {
        return error("URL not in allowlist");
    }
    // 2. Rate limit check
    if !caller.data().sandbox.try_fetch() {
        return error("Rate limit exceeded");
    }
    // 3. SSRF prevention
    if is_private_ip(&req.url) {
        return error("Private IP not allowed");
    }
    // Execute fetch...
}

Plugin Crash Containment

If a WASM plugin crashes:

  1. wasmtime trap is caught (not a process crash)
  2. Error logged with plugin name and context
  3. Plugin call returns error, request continues
  4. Circuit breaker: after N failures, plugin disabled for cooldown period
  5. Platform admin notified

Multi-Tenant Security

For apps that are themselves multi-tenant (like Company Manager):

Tenant Boundary Enforcement

Request → App Router → App Middleware → Tenant Resolution
                                              │
                                              ▼
                                    ┌──────────────────┐
                                    │  Tenant Context   │
                                    │  tenantId: "..."  │
                                    │  siteId: "..."    │
                                    └──────────────────┘
                                              │
                                    ┌─────────┴─────────┐
                                    ▼                   ▼
                              Cache key            DB queries
                           t:{tenantId}:...     WHERE tenantId = ...
  • Cache keys include tenantId — no cross-tenant cache leakage
  • Database queries MUST include tenantId (enforced by service layer)
  • Tenant context set in middleware, immutable for request lifetime
  • Tenant resolution: hostname → (tenantId, siteId) with DashMap cache

Authentication

Platform Admin Auth

For platform management APIs (/api/platform/*):

  • Bearer token authentication
  • Token stored in platform config or environment variable
  • Required for: deploy, rollback, app management, plugin management, cache purge
[platform]
admin_token_env = "BEXT_ADMIN_TOKEN"

Per-App Auth

Each app configures its own auth:

[apps.dashboard.auth]
required = true
jwt_secret_env = "DASHBOARD_JWT_SECRET"
cookie_name = "session"

# JWT verification
issuer = "https://auth.example.com"
audience = "dashboard"

# JWKS endpoint (fetches public keys)
# jwks_url = "https://auth.example.com/.well-known/jwks.json"

Auth Bypass

Development mode:

[auth]
bypass_localhost = true    # Skip auth for 127.0.0.1 / ::1

Network Security

Rate Limiting

Three levels:

  1. Global: Platform-wide RPM (DDoS protection)
  2. Per-app: App-specific RPM
  3. Per-user: Plugin-implemented (via KV store)

SSRF Prevention

Applied to:

  • Plugin http_fetch calls
  • Image optimization pipeline (fetch external images)
  • Data client (hybrid mode proxy)

Blocked:

  • Private IP ranges (10.x, 172.16-31.x, 192.168.x)
  • Loopback (127.x, ::1)
  • Link-local (169.254.x, fe80::)
  • Metadata endpoints (169.254.169.254)

TLS

[platform]
tls_cert = "/path/to/cert.pem"
tls_key = "/path/to/key.pem"

# OR auto-provision via ACME (future)
# tls_auto = true
# tls_email = "admin@example.com"

Implementation Tasks

SC-1: App Isolation Enforcement

Tasks:

  • Cache key prefixing with app_id (modify ISR cache key generation)
  • Per-isolate memory limits (JSC API or RSS monitoring)
  • Execution timeout per isolate
  • Env var scoping per app (no leakage between apps)
  • Domain overlap validation at startup
  • Tests: verify app A can't read app B's cache

SC-2: Plugin Permission Enforcement

Tasks:

  • Extend WasmPluginPermissions with new fields (kv, cache, queue, secrets, cron)
  • Permission check in every new host function
  • Permission validation at plugin load time (reject if requesting unavailable permissions)
  • Circuit breaker for failing plugins
  • Plugin error containment tests

SC-3: Platform Admin Auth

Tasks:

  • Admin token middleware for /api/platform/* routes
  • Token from config or environment variable
  • Token rotation support (accept old + new during rotation)
  • Audit log for admin operations

SC-4: SSRF Prevention

Tasks:

  • Create bext-core/src/security/ssrf.rs (implemented in bext-plugin-wasm/src/sandbox.rs as is_private_url)
  • IP classification (private, loopback, link-local, metadata)
  • DNS resolution check (resolve hostname, verify IP isn't private)
  • Applied to: http_fetch host function, image pipeline, data client
  • Tests with various bypass attempts

SC-5: Secrets Management

Tasks:

  • Encrypted secret storage in SQLite
  • Encryption key from platform config or environment
  • bext secrets set <name> <value> CLI command
  • bext secrets list (names only, no values)
  • bext.secret_get host function with permission check
  • Secrets scoped per app or per plugin
  • Secret rotation: update value, plugin gets new value on next call