Security Model
Threat model
Section titled “Threat model”LatchGate assumes the model or agent is compromised and designs the system so that it does not matter for protected actions. The core assumption is: the agent process is untrusted.
An untrusted agent can submit requests, but it cannot:
- bypass identity binding (DPoP sender constraint)
- replay previous requests (jti-based replay protection, fail-closed)
- execute actions outside its allowed set (ACL + scope enforcement)
- reach targets not in the allowlist (sink validation at host layer, with proper domain matching)
- access credentials (secrets never enter the sandbox)
- make network calls outside declared imports (capability-gated WASM)
- consume unbounded resources (fuel, memory, I/O budget, epoch-based wall-clock deadline)
- tamper with evidence (signed receipts, integrity chain, signed grants)
- diverge approved execution path from auto-allow path (shared execution tail)
What LatchGate protects
Section titled “What LatchGate protects”LatchGate protects declared protected actions. If an operation is registered as a protected action, its side effect will not occur outside LatchGate, and the organization gets a verifiable receipt for what happened.
This guarantee holds on a controlled backend (container, CI job, locked-down VM) where the agent’s only path to external systems is the LatchGate socket.
What LatchGate does not protect
Section titled “What LatchGate does not protect”LatchGate does not claim to be a full host sandbox on its own. Specifically, it does not protect against:
- Arbitrary local file writes by a process running outside protected actions (unless the agent sandbox is active — see below)
- Host compromise through unrelated code or vulnerabilities
- Prompt injection as a complete category (LatchGate controls what actions can do, not what the model decides to do)
- Governance of arbitrary MCP traffic — LatchGate gates its own registered actions; MCP tool calls routed to other MCP servers bypass LatchGate entirely
On a developer workstation without the agent sandbox, LatchGate controls external actions but does not contain the agent process itself. For containment on a workstation, use latchgate sandbox (Linux namespace isolation) or pair with a separate container/VM layer.
Agent sandbox
Section titled “Agent sandbox”The latchgate sandbox command provides process-level containment for the agent itself — not just the WASM providers, but the entire runtime. The agent runs in Linux user/network/mount/PID/UTS/IPC/cgroup namespaces where:
- The host filesystem is fully detached via
pivot_root. Only the workspace directory and explicit read-only mounts are visible. - The network stack is empty. The only connectivity is through two UDS endpoints: the gate socket (for protected actions) and an HTTPS CONNECT proxy (for LLM API traffic to explicitly allowed hosts).
- Environment variables are not inherited. Only explicitly listed variables are passed in.
- Host processes are not visible.
This closes the workstation gap: even if the agent process is fully compromised (prompt injection, supply chain attack, malicious plugin), it cannot reach anything outside the namespace boundary. The only side effects it can trigger are those gated through LatchGate.
The agent sandbox requires Linux ≥ 5.8 with unprivileged user namespaces. On other platforms, the gate runs normally — only the sandbox launcher is Linux-specific.
For configuration, namespace details, and platform requirements, see Agent Sandbox.
MCP adapter
Section titled “MCP adapter”The latchgate-mcp binary exposes registered actions as MCP tools to IDE agents (Cursor, Claude Desktop, Windsurf, Cline, Codex). Security properties:
- The adapter authenticates to LatchGate as a standard DPoP client — it holds a keypair and obtains a Lease. It does not bypass the pipeline.
- MCP tool calls are mapped to
POST /v1/actions/{id}/executeand go through the full enforcement pipeline: authentication, policy, WASM sandbox, signed receipt. - The adapter is a pass-through, not a trust boundary relaxation. A compromised IDE plugin that submits tool calls through the adapter still faces the same policy, budget, and approval gates as any other caller.
- The adapter does not expose admin or approval endpoints. Operator actions require direct access to the admin UDS.
Defense in depth
Section titled “Defense in depth”Identity layer
Section titled “Identity layer”Agent identity:
- Short-lived Lease JWT bound to caller key via DPoP (RFC 9449)
- Caller principal is server-determined (UID-based via
SO_PEERCRED, OIDC, or mTLS); never client-asserted - Scopes constrained to intersection of requested and identity-permitted (never escalate)
- jti-based replay protection (Redis SETNX when configured; in-memory bounded DashMap in embedded mode). Fail-closed on backend outage.
- IAT window validation with clock-skew tolerance (60s max age, 5s forward skew)
Operator identity:
- Admin and approval endpoints served exclusively on a separate UDS (
listen_admin_uds_path), unreachable from the agent-facing socket - Per-operator named credentials with DPoP sender binding — a stolen operator token without the private key is useless
- Production startup rejects shared operator keys; each operator has a distinct credential
- Operator JWK thumbprint (
operator_binding) is embedded in execution grants and signed into approvals for forensic attribution
Authorization layer
Section titled “Authorization layer”- Deterministic policy evaluation (external OPA or embedded regorus — default-deny)
- Per-session call budgets, atomically tracked (Redis Lua or SQLite
BEGIN IMMEDIATE) - Human-in-the-loop approval for high-risk actions, binding the exact execution plan
- Approval consumption is atomic and one-shot (Redis Lua
LUA_CLAIMor SQLite exclusive lock) + durable outcome marker - Durable SQLite outcome record guards against re-execution across backend restarts (double-layer idempotency)
Execution layer
Section titled “Execution layer”- Every provider in a fresh WASM sandbox (wasmtime, component model)
- Zero ambient capabilities: no filesystem, no network, no syscalls
- Capability-gated imports: only declared I/O interfaces linked, plus runtime
check_import_alloweddefense-in-depth - Module digest verified before loading (SHA-256)
- Fuel metering, memory cap, I/O budget, epoch-based wall-clock deadline (dedicated OS thread ticker — works on pure-compute loops)
- Concurrent-execution semaphore prevents resource exhaustion
I/O layer
Section titled “I/O layer”- Sink validation against grant allowlist with proper domain matching (
allowed = "api.com"does NOT matchevil-api.com) - Credential injection at host transport layer (secrets never in sandbox)
- SSRF protection: private IP blocking (loopback, RFC-1918, link-local, CGNAT, IPv6 loopback/ULA, IPv4-mapped IPv6), DNS pinning via
reqwest::resolve()(closes TOCTOU window while preserving TLS SNI), system proxy bypass (no_proxy()— prevents a hostHTTP_PROXYfrom routing traffic around SSRF checks), redirect blocking at transport - Per-call timeouts and response size limits (Content-Length check + streaming enforcement)
- SMTP: all recipient fields (To/Cc/Bcc) validated against
allowed_sinks— not just To - Filesystem:
openat2withRESOLVE_BENEATH | RESOLVE_NO_SYMLINKS | RESOLVE_NO_MAGICLINKSconfines all file access beneath the declared root — no path traversal, no symlink escape. Component-walk fallback withO_NOFOLLOWon older kernels and macOS. Path allowlists and denylists enforced before open.
Webhook layer
Section titled “Webhook layer”- HMAC-SHA256 signed payloads (
X-LatchGate-Signature) with per-endpoint secrets — receivers can verify authenticity independently - Independent SSRF protection: DNS pinning, private IP blocking, redirect blocking (
Policy::none()), HTTPS-only enforcement — same defenses as the provider I/O path, applied separately - Outbox-based delivery with retry (exponential backoff) — webhook failure does not block the action pipeline
- SSRF and signing failures are non-retryable; only transient network errors trigger retry
Containment layer (agent sandbox)
Section titled “Containment layer (agent sandbox)”- Linux user/network/mount/PID/UTS/IPC/cgroup namespaces — unprivileged, no root required
pivot_root— host filesystem fully detached from agent’s mount tree- seccomp-BPF filter — restricts available syscalls after namespace setup, applied as the last step before
exec - Isolated network stack — no external interfaces or routes; only loopback and UDS endpoints. The sole egress path is the proxy, reached over loopback
- HTTPS CONNECT proxy — TLS passthrough to allowed hosts only, port 443 only, denied destinations receive TCP RST
- Explicit environment variable passthrough — no blanket inheritance, reserved variables enforced
- Workspace isolation — single read-write mount; everything else read-only or absent
Evidence layer
Section titled “Evidence layer”- Pre-dispatch
ExecutionIntent— durable marker written before WASM dispatch; detects “dispatch occurred but evidence was not finalized” states by querying the ledger for intents without matching receipts - Ed25519-signed receipts with historical key rotation support (
kid-based JWKS) - Append-only, hash-chained SQLite ledger (each event’s
prev_hash = SHA-256(prev_event_json)) - Transactional receipt + final audit write — client success response gated on durable evidence
- JSONL export for SIEM/WORM integration
Grant integrity
Section titled “Grant integrity”ExecutionGrantis Ed25519-signed immediately after construction, verified before provider dispatch- Separate signing key from receipt signing (defense-in-depth)
- Signature covers
grant_id,subject,sender_binding,plan_hash,issued_at,expires_at,revocation_epoch,approval_hash— any mutation is detectable operator_binding(DPoP JWK thumbprint) cryptographically proves which operator key authorized the approval- Grant is short-lived with explicit expiry; revocation epoch invalidates all outstanding grants on
latchgate revoke
Revocation
Section titled “Revocation”- Revocation epoch — a monotonically increasing counter embedded in every grant’s signed payload.
latchgate revokeincrements the epoch; any grant signed with a stale epoch fails signature verification before dispatch. This provides instant, system-wide kill-switch semantics without needing to enumerate individual grants. - Lease revocation — individual leases can be revoked by ID. Revoked leases fail validation at the authentication step.
- Approval revocation — pending approvals can be revoked before an operator acts on them. Revocation is durable — a revoked approval cannot be claimed even after a backend restart.
Fail-closed behavior
Section titled “Fail-closed behavior”If any component is in an unknown state, the action is denied:
| Component failure | Result |
|---|---|
| State backend unreachable (Redis or SQLite) | Deny (replay check, budget, approval) — HTTP 503 |
| Policy engine unreachable (OPA) or error (regorus) | Deny (policy evaluation) — HTTP 503 |
| OPA timeout | Deny (not allow) — HTTP 503 |
Policy returns no result field | Deny — HTTP 503 |
| Receipt write failure | Side effect may have occurred; client receives evidence_persistence_failed — never false success |
| Intent write failure | Deny before dispatch (no side effect yet) |
| Provider timeout | Fail (epoch-based deadline; works on pure-compute loops) |
| Provider fuel exhaustion | Fail (deterministic termination) |
| Provider memory exhaustion | Fail |
| Provider import gating violation | Fail at instantiation |
| Grant signature verification fails | Fail before dispatch |
| Plan hash mismatch on approval | Fail (tampering detected) |
| Signing key unavailable at startup | Startup rejected |
sops binary missing when sops_secrets_file set | latchgate doctor fails |
There is no code path that produces ALLOW when a dependency is in an unknown state.
Operational security
Section titled “Operational security”- Admin and approval endpoints served on a dedicated UDS (
listen_admin_uds_path), network-isolated from the agent-facing socket — an agent cannot reach operator endpoints even with a valid lease - Insecure configurations require explicit
unsafe_flags recorded in audit - Production startup rejects dev-mode identity, ephemeral keys, shared operator credentials,
response_schema_enforcement = warn, missing JWKS path - Metrics and health endpoints do not leak secrets or sensitive internals
/healthzreturns only{"status":"ok"}/readyzreturns dependency status; 503 until all startup checks pass- Rate limiting (all token-bucket): 50 req/s on lease issuance, 20 req/s on operator write endpoints, 100 req/s on operator read endpoints, 20 req/s per session on the execute path (5 req/s for anonymous/unauthenticated peers). Execute-path limiting fires before DPoP verification — a misbehaving agent cannot burn CPU on cryptographic checks without hitting its bucket first.
- Transport-level body size limit (1 MB compile-time constant) rejects oversized payloads before buffering
For the system layer diagram, crate layout, and security invariants, see Architecture. For production hardening, see Deployment. For the broader agent-execution threat landscape — credential theft, prompt injection, uncontrolled execution, supply-chain risk, audit integrity — see the threat model on latchgate.ai.