Skip to content

Security: Synapse-Run/cell

SECURITY.md

Synapse Cell — Security Standards

Status: non-negotiable. Source of truth: this file. Owner: Mike Mazur (Mike Mazur).

This file consolidates the Red Team Standard from .agents/workflows/cell_commercialization.md Part 2 plus operational rules accumulated since. If a script, test, or benchmark fails because of any rule below — fix the script, never weaken the rule.


Part 1 — The Threat Model

Cell exposes FFI calls from sandboxed guest Wasm code into the Rust host process. The guest is untrusted by definition (it's executing customer-supplied code). Every FFI is a potential escape hatch. The host runs on bare-metal Hetzner with full filesystem and network access — a successful escape is catastrophic.

We defend against:

  1. Sandbox escape via path traversal (e.g., ../../../etc/passwd)
  2. SSRF via outbound HTTP to cloud metadata or internal networks
  3. Denial of service via OOM panic or exhaustion of host resources
  4. Information disclosure via timing / side channels (out of scope for v1)
  5. Lateral movement between tenants in multi-tenant Hub deployments

We do NOT defend (yet) against:

  • Side-channel timing attacks (Spectre-class) — wasmtime's standard defenses apply
  • Adversarial Wasm bytecode itself — we trust wasmtime's verifier (industry-standard hardened)
  • Physical access to the Hetzner host — that's an Ops concern, not a Cell concern

Part 2 — The Rules

Rule 1: No Path Traversal (sandbox escape)

ANY path generated by host_fs_* calls MUST:

  1. Be resolved through std::fs::canonicalize()
  2. Verify .starts_with(/sandbox_volumes/) (or the configured root)
  3. Reject (return error code) on any failure

Implementation: cell/gateway/src/main.rs::host_fs_write and host_fs_read. The pattern looks like:

let root_volumes = resolve_synapse_root().join("sandbox_volumes");
let base_path = std::fs::canonicalize(&root_volumes).unwrap_or(root_volumes);
let target_path = base_path.join(path_str);

if let Ok(canon_target) = std::fs::canonicalize(&target_path) {
    if !canon_target.starts_with(&base_path) {
        return -1; // Block path traversal
    }
}

Test coverage: run_test.py Phase 2 tries ../../../../../../etc/passwd and expects rejection.

Rule 2: SSRF Shields (outbound network filtering)

host_fetch MUST preemptively block outbound HTTP to:

Range What it is Why blocked
169.254.169.254 AWS / Hetzner cloud metadata service Exposes IAM credentials, instance secrets
127.0.0.0/8 Loopback Reaches host services not meant for guests
10.0.0.0/8 RFC1918 private networks Lateral movement to internal infrastructure
172.16.0.0/12 RFC1918 private Same as above (NEW — was missing in v1)
192.168.0.0/16 RFC1918 private Same as above (NEW — was missing in v1)
localhost Loopback alias Same as 127.x
*.local mDNS / Bonjour Local network discovery

Current implementation in main.rs::host_fetch:

if url.contains("169.254.169.254") || url.contains("127.0.")
   || url.contains("localhost") || url.contains("10.") {
    return -1;
}

Known gap: the current substring check is too permissive (catches 10.something.com legit domains) and too narrow (misses 172.16.x.x and 192.168.x.x). Pending: replace string contains with proper IP parse + CIDR check. Tracked as Horizon 1 hardening.

Test coverage: run_test.py Phase 3 attempts SSRF to 169.254.169.254 and expects rejection.

Rule 3: OOM Caps (resource exhaustion)

All FFIs accepting max_len parameters MUST enforce a hard ceiling. Currently 10 MB across host_fetch, host_fs_read, host_fs_write:

let max_len = std::cmp::min(max_len, 10_000_000); // 10MB Host Memory Protection

Wasm linear memory itself is bounded by wasmtime's per-instance configuration (default 4 GB for 32-bit Wasm, configurable per cell). We rely on wasmtime's bound enforcement for guest memory.

Rule 4: Rate Limiting (DoS resistance)

Per-IP rate limiter is implemented in main.rs::GatewayState::rate_limits. Default: 20 requests / second / IP. Configurable per deployment via env var SYNAPSE_RATE_LIMIT_RPS. Above the limit, the gateway returns 429 Too Many Requests.

Hub tier: rate limits differ per license tier. Pending implementation alongside the Ed25519 license validator (Plan Part 10.3, Horizon 2).

Rule 5: License-Gated FFIs (commercial defense)

Pending implementation. Pro-tier FFIs (load_weights, host_mcp_call, GPU operations) MUST refuse to load if no valid Ed25519 license certificate is present in /etc/synapse/license.json or $SYNAPSE_LICENSE. Free tier (EdgeCell) gets only the safe-subset Wasm sandbox without these FFIs.

See plan Part 10.3 sub-layers A–D for the full architecture: Authority key, certificate format, gateway verification, receipt binding.

Rule 6: No Strip-Down of Security to Make a Test Pass

If run_test.py fails because of any rule above, fix the test or the calling code — never delete the security guard. Specifically:

  • ❌ Don't comment out the canonicalize check to make a path test pass
  • ❌ Don't widen the SSRF allowlist for a "convenience" feature
  • ❌ Don't raise max_len ceilings without a documented threat model exception in this file
  • ✅ Do refactor the test to use a sandbox-safe path
  • ✅ Do propose new host_* FFIs with explicit security review

Rule 7: No New host_* FFIs Without Security Review

Adding a new host_* FFI requires:

  1. A proposal in a cell/journals/JC-NNN_<topic>.md entry covering:
    • What guest code can do with the FFI
    • What host resources it touches
    • What the sandbox escape attack vectors are
    • How those attacks are mitigated
  2. Code review by a separate session / agent (or Mike, for substantial new surfaces)
  3. Test coverage added to run_test.py for at least one attack vector
  4. This file (SECURITY.md) updated with the new FFI and its threat model

Rule 8: Deterministic Receipt Hashing

Every Cell execution emits a SHA-256 receipt. The hash MUST include (in the canonical order):

SHA-256(execution_id|code_hash|result_hash|template|timestamp)

Future fields such as compiler fingerprint, license identity, or verifier status must extend the receipt under an explicitly versioned receipt schema. Any change to the hash composition is a breaking change and requires a major version bump plus auditor-facing release notes.

Rule 9: Air-Gap Friendliness

Cell MUST run with no outbound network access required. License verification is local (Ed25519 signature against an embedded public key). Telemetry is opt-in and defaults to OFF in air-gap mode. The Synapse Hub trust registry can be served from a local mirror for air-gapped customers.

Rule 10: No Hardcoded Secrets in Source

grep -rE "sk_live_[a-zA-Z0-9]{8,}|e2b_[a-f0-9]{40}|password\s*=\s*\"" MUST return zero results in committed code. Test credentials live in .env.example and are sourced from environment variables. GitHub secret-scanning + push protection enforces this on every push.


Part 3 — The Test Discipline

Every release of Cell must pass:

  1. python3 run_test.py — Military Audit: latency + path traversal + SSRF
  2. cd cell/sdk && python3 tests/stress_test.py --quick — concurrent cells, throughput, persistence
  3. cd cell/sdk && python3 tests/red_team.py — broader red-team coverage
  4. python3 sdk/tests/benchmark_e2b.py — E2B parity benchmark

A failure in any of these blocks the release. The cell:ship-release skill (when it exists) checks all four.

Part 4 — Incident Response (when something gets through)

If a sandbox escape, SSRF, or OOM is discovered in production:

  1. Take the affected node out of rotation immediately (DNS failover or systemd disable)
  2. Snapshot the host state (tarball of /var/log/, sandbox volumes, gateway logs)
  3. File a JC-NNN entry under SEVERITY: SECURITY with timeline, root cause, blast radius
  4. Fix at the root — the canonicalize / SSRF / cap rule that should have prevented it
  5. Add a test that reproduces the attack to run_test.py so it can never regress
  6. Notify affected enterprise customers within 48 hours (Hub tier SLA commitment)
  7. Disclose publicly within 14 days (CVE if applicable) — see Manifesto Rule 4 below

Part 5 — Disclosure & Trust

Aligned with the Sovereign Compute Manifesto: we are accountable to the commons. Security vulnerabilities found by external researchers must be:

  • Acknowledged within 48 hours of report (security@synapserun.dev — to be set up)
  • Patched within 14 days for high-severity, 30 days for medium
  • Disclosed publicly with credit to the researcher (and a bounty if material)
  • Backported to all supported Cell versions (currently: latest only)

We do NOT issue silent patches. We do NOT NDA security researchers. We do NOT delay disclosure for marketing reasons.


Part 6 — Pointers

  • Threat model details: this file
  • Code that enforces the rules: cell/gateway/src/main.rs (search for host_fetch, host_fs_read, host_fs_write)
  • Test coverage: run_test.py, sdk/tests/red_team.py, sdk/tests/stress_test.py
  • License system: cell/docs/2026-04-15-cell-commercial-setup-plan.md Part 10.3
  • Original framing: .agents/workflows/cell_commercialization.md Part 2

There aren't any published security advisories