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_fetchwith 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:
- wasmtime trap is caught (not a process crash)
- Error logged with plugin name and context
- Plugin call returns error, request continues
- Circuit breaker: after N failures, plugin disabled for cooldown period
- 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:
- Global: Platform-wide RPM (DDoS protection)
- Per-app: App-specific RPM
- Per-user: Plugin-implemented (via KV store)
SSRF Prevention
Applied to:
- Plugin
http_fetchcalls - 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
WasmPluginPermissionswith 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 inbext-plugin-wasm/src/sandbox.rsasis_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_gethost function with permission check - Secrets scoped per app or per plugin
- Secret rotation: update value, plugin gets new value on next call