Skip to main content
OpenLegion was designed assuming agents will be compromised. The security model is defense-in-depth: trust zones gate what each component can touch, agent containers are hardened, credentials are isolated, every cross-boundary call is permission-checked, and the browser service has its own egress filter. Default deny. Fail-closed.

Trust Zones

Four zones plus a 2.5 operator-or-internal tier (mirrors the table in Architecture):
LevelZoneNotes
0UntrustedExternal input. Webhook bodies 1MB cap, optional HMAC-SHA256, all messages sanitized via sanitize_for_prompt().
1Sandboxed (agents)Hardened agent containers (see below). Hold no LLM keys — proxy through mesh.
2Trusted (mesh)Mesh host. Holds credentials, manages containers, routes messages.
2.5Operator-or-internal_require_operator_or_internal gate on fleet-wide metrics, per-agent metrics, stale tasks, audit/archive endpoints.
3Loopback-onlyRequires both x-mesh-internal: 1 header and loopback IP. Caddy strips the header from inbound public traffic on managed VPSes.

Defense-in-Depth Layers

LayerMechanismWhat it prevents
Runtime isolationDocker containers (default) or Docker Sandbox microVMs (opt-in via --sandbox)Agent escape, kernel exploits
Container hardeningUID 1000, cap_drop=ALL, no-new-privileges, read-only root FS, tmpfs=/tmp 100m noexec/nosuid, pids_limit=256, 384MB / 0.15 CPU (worker)Privilege escalation, resource abuse
Browser-container egressiptables OUTPUT REJECT for RFC1918, loopback, link-local, CGNAT, IANA-reserved IPv4+IPv6; allows in-container loopback; fail-closedSSRF/data exfil from the browser tier
Credential separationTwo-tier vault: OPENLEGION_SYSTEM_* mesh-only; OPENLEGION_CRED_* agent-tier gated by glob. Opaque $CRED{name} handle substitution at call time.Key leakage, unauthorized API use
Permission enforcementPer-agent ACLs (default deny) for messaging, pub/sub, blackboard globs, allowed_apis, browser_actions, wallet, control-plane flagsUnauthorized data access, capability escalation
Input validationPath traversal (4-stage check), AST validation for create_skill, SSRF protection, safe condition eval (no eval()), bounded execution capsInjection, runaway loops
Unicode sanitizationInvisible-character stripping at 56 call sites across user input, tool results, and system-prompt contextPrompt injection via hidden Unicode

Dual Runtime Backend

Docker Containers (default)Docker Sandbox microVMs
IsolationShared kernel, namespace separationOwn kernel per agent (hypervisor)
Escape riskKernel exploit could escapeHypervisor boundary — much harder
PerformanceNative speedNear-native (Rosetta 2 on Apple Silicon)
RequirementsAny Docker installDocker Desktop 4.58+
Enableopenlegion startopenlegion start --sandbox
SandboxBackend initialization can fail (Docker Desktop too old, hypervisor disabled, etc.). On failure it falls back to DockerBackend automatically and logs the reason.

Container Hardening (full set)

Worker containers run with the following enforced flags (engine/src/host/runtime.py):
  • UID 1000 (non-root).
  • 384MB RAM, 0.15 CPU quota.
  • pids_limit=256.
  • cap_drop=ALL, no-new-privileges.
  • read_only=True root filesystem.
  • tmpfs=/tmp 100MB, noexec/nosuid.
Operator containers run with the same hardening but 128MB RAM / 0.05 CPU. The browser service container is hardened the same way except it must add NET_ADMIN, SETUID, SETGID capabilities to run iptables and gosu to UID 1000. Its egress is locked down by the iptables filter described above.

Credential Vault

Agents never hold API keys. The vault uses a two-tier prefix system:
  • OPENLEGION_SYSTEM_* — LLM provider keys, channel tokens, master secrets. Never agent-accessible.
  • OPENLEGION_CRED_* — agent-tier tool/service keys. Access controlled per-agent via allowed_credentials (fnmatch glob, case-insensitive; ["*"] = all agent-tier; system creds always blocked).
How it works:
  1. The vault loads credentials from both prefix sets at startup.
  2. Agents call APIs through the mesh proxy. Their tool args may reference $CRED{name} — an opaque handle, not the secret value.
  3. The mesh substitutes the real value server-side before forwarding to the provider.
  4. Budget limits are checked before dispatching LLM calls; usage is recorded after.
A fully compromised agent never sees the raw key — only the handle. CAPTCHA solver credentials (CAPTCHA_SOLVER_KEY, _SECONDARY, CAPTCHA_SOLVER_PROXY_LOGIN, _PASSWORD) intentionally bypass the standard vault. They live in env only and are stripped from config/settings.json at load.

Permission Matrix

Per-agent ACLs in config/permissions.json. Default policy: deny; missing file means deny-all. Permissions cover:
  • Messaging: can_message (list of target agent IDs).
  • Pub/Sub: can_publish, can_subscribe (topic lists).
  • Blackboard: blackboard_read, blackboard_write (glob patterns).
  • APIs: allowed_apis, allowed_credentials (glob over agent-tier creds).
  • Browser: can_use_browser, browser_actions (None/["*"] = all known actions; [] = deny; specific list = allowlist).
  • Cron: can_manage_cron.
  • Spawning: can_spawn.
  • Wallet: can_use_wallet, wallet_allowed_chains, wallet_spend_limit_per_tx_usd, wallet_spend_limit_daily_usd, wallet_rate_limit_per_hour, wallet_allowed_contracts.
  • Control-plane (six flags, operator-default true, workers false): can_manage_fleet, can_manage_projects, can_edit_agent_config, can_view_fleet_metrics, can_route_tasks, can_request_user_credentials.
Operator ceiling: the operator can adjust most permissions but cannot grant can_spawn=true or can_use_wallet=true. Those capabilities require a human operator outside the chat surface.

Input Validation

  • Path traversal protection — agent file operations are confined to /data via a 4-stage check (strip /data/ prefix, reject .. lexically, lstat() symlink-safe walk, final is_relative_to("/data")).
  • AST validation for skill self-authoringcreate_skill runs the candidate through an AST validator with 23 forbidden imports, 16 forbidden calls, 11 forbidden attribute accesses, plus a 10,000-char size cap (_MAX_SKILL_SIZE).
  • Safe condition evaluation — workflow/condition parsing uses a regex-based safe parser, never eval().
  • Bounded execution — task loops cap at MAX_ITERATIONS=20; chat at CHAT_MAX_TOOL_ROUNDS=30 per turn / CHAT_MAX_TOTAL_ROUNDS=200 per session / _MAX_SESSION_CONTINUES=5; heartbeats at HEARTBEAT_MAX_ITERATIONS=12.
  • Token budgets — per-agent daily (0.010.01–1000) and monthly (0.100.10–30000) caps enforced before dispatch.
  • Tool-loop detection — warn @ 2 repeats, block @ 4, terminate @ 9.

SSRF Protection

The HTTP tool runs every outbound request through a multi-layer SSRF filter:
  • DNS pinning — resolve once, reuse the same IP for the actual connection.
  • Blocklist — RFC1918, loopback, link-local, CGNAT (100.64.0.0/10), 6to4 (2002::/16), Teredo, IPv4-mapped IPv6.
  • Fail-closed on DNS error — any resolution failure rejects the request.
  • Max 5 redirects, re-validated at each hop.
  • Cross-origin auth strip — Authorization headers are removed when redirecting to a new origin.
The browser service container enforces its own iptables egress filter on top of this.

Dashboard & VNC

  • CSRF — state-changing dashboard endpoints require an X-Requested-With header.
  • CSP — the dashboard sets a CSP, but it allows unsafe-inline and unsafe-eval (Alpine.js requirement). Jinja autoescape=True is the primary XSS defense, not CSP.
  • VNC proxy at /agent-vnc/{agent_id}/{path}rejects agent Bearer tokens and requires the ol_session cookie on both HTTP and WebSocket upgrade. Agent credentials cannot leak through a browser session.
  • Authentication — dev/self-hosted is open if /opt/openlegion/.access_token is absent. Hosted requires an ol_session cookie verified via HMAC (24h max age + 5-min skew). The SSO callback /__auth/callback and HMAC verification live in the upstream Caddy auth-gate sidecar — the engine itself only consumes the cookie.

Wallet Seed Reveal

The master mnemonic is generated by POST /api/wallet/init and returned once with Cache-Control: no-store. Every subsequent call to GET /api/wallet/seed returns HTTP 410 Gone — there is no second-chance reveal. Private keys derived from the seed (BIP-44 for EVM, HMAC-SHA512 over PBKDF2 for Solana) never leave the mesh process.

What This Does NOT Do

OpenLegion is small and honest about its limits. The following are not part of the threat model:
  • No “zero-trust” claim. We use trust zones + defense-in-depth.
  • No compliance certifications (SOC 2, ISO 27001, HIPAA). The engine is v0.1.0.
  • Engine SQLite databases are not encrypted at rest. Only .env is chmod 0o600; the provisioner separately encrypts SSH keys at rest with Fernet.
  • JWT sessions are non-revocable (NextAuth JWT strategy is stateless).
  • App rate limiter is in-memory, single-region — no global rate limits.
  • _blackboard_xproject_count and tool_denials_24h are observability-only, not enforcement.
  • No air-gapped mode. All agents need mesh egress for LLM proxy.
  • The engine does not implement SSO end-to-end. HMAC verification, replay protection, and cookie issuance live in the auth-gate sidecar shipped via cloud-init on managed VPSes — not in engine code.