Skip to content

MITM Proxy

The MITM proxy is Capsem’s HTTPS inspection layer. It terminates TLS from the guest, normalizes protocol details into SecurityEvent, evaluates the shared security rule rail, forwards allowed requests to the real upstream, and logs telemetry plus matched rule rows to the session database.

Each guest HTTPS connection flows through this pipeline:

graph TD
    A["Guest connection<br/>vsock:5002"] --> B["Read metadata prefix<br/>(optional process name)"]
    B --> C["TLS handshake<br/>MitmCertResolver captures SNI"]
    C --> D["Read HTTP request<br/>method, path, headers, body"]
    D --> E["Build SecurityEvent<br/>http + optional model roots"]
    E --> P["Preprocess plugins<br/>credential broker, scanners"]
    P --> F{"Security rules<br/>CEL over SecurityEvent"}
    F -->|Block or unresolved ask| G["403 Forbidden<br/>+ ledger projection"]
    F -->|Allow| Q["Postprocess plugins"]
    Q --> I["Runtime materialization<br/>upstream-safe bytes"]
    I --> J["Upstream TLS connection<br/>(cached per-connection)"]
    J --> K["Forward request"]
    K --> L["Stream response to guest<br/>(inline SSE parsing for AI traffic)"]
    L --> M["Logging plugins<br/>ledger-safe projection"]
    M --> N["Emit telemetry<br/>primary row + security_rule_events"]

The proxy uses hyper for HTTP parsing and tokio-rustls for TLS. Each vsock connection can carry multiple HTTP requests via keep-alive — upstream connections are cached per-connection to avoid re-establishing TLS for each request.

graph LR
    CA["CertAuthority<br/>(static CA keypair)"]
    POL["Network mechanics<br/>(hot-swappable via RwLock)"]
    DB["DbWriter<br/>(async telemetry)"]
    TLS["Upstream TLS config<br/>(webpki roots)"]
    PRICE["PricingTable<br/>(embedded JSON)"]
    TRACE["TraceState<br/>(multi-turn linking)"]

    CA --> CFG["MitmProxyConfig"]
    POL --> CFG
    DB --> CFG
    TLS --> CFG
    PRICE --> CFG
    TRACE --> CFG
FieldTypePurpose
caArc<CertAuthority>Static Capsem CA for leaf cert minting
policyArc<RwLock<Arc<NetworkPolicy>>>Hot-swappable network mechanics such as body capture and upstream port handling
dbArc<DbWriter>Async telemetry writer to session.db
upstream_tlsArc<rustls::ClientConfig>Shared TLS config with webpki root CAs
pricingPricingTableEmbedded model pricing for cost estimation
trace_stateMutex<TraceState>Links multi-turn tool-use conversations by trace_id
security rulesArc<RwLock<Arc<SecurityRuleSet>>>Hot-swappable CEL rules over SecurityEvent roots

The proxy mints per-domain TLS certificates on-the-fly, signed by a static Capsem CA.

sequenceDiagram
    participant G as Guest
    participant R as MitmCertResolver
    participant CA as CertAuthority
    participant C as Cache

    G->>R: TLS ClientHello (SNI: github.com)
    R->>C: Lookup github.com
    alt Cache hit
        C-->>R: Arc<CertifiedKey>
    else Cache miss
        R->>CA: mint_leaf("github.com")
        CA-->>R: CertifiedKey [leaf, ca]
        R->>C: Store in cache
    end
    R-->>G: TLS ServerHello + cert chain
ParameterValue
AlgorithmECDSA P-256
Validity24 hours
Back-dating1 hour (clock skew tolerance)
SANDNS name of the target domain
Extended key usageServerAuth
Chain[leaf, CA] (2 certificates)
CA key sourcesecurity/keys/capsem-ca.key (committed, compile-time include_str!)

The cache uses double-checked locking: read lock for hits, write lock only on miss with a second check after acquiring the write lock. Concurrent requests for the same domain never mint duplicate certs.

The MITM proxy CA private key is committed to the repository. This is intentional — the CA is only trusted inside Capsem’s own air-gapped VMs and has zero trust outside them. A public key provides transparency: anyone can verify there is no hidden interception. Per-installation key generation would reduce auditability.

See Network Isolation for the full security rule reference. Key properties:

PropertyBehavior
Network mechanicsPort routing, body capture, decompression, provider metadata, and cache behavior
Security authoritySecurityRuleSet over normalized SecurityEvent fields
Default behaviorProfile defaults compile into normal late-priority rules
Conflict resolutionEarlier/lower priority enforcement wins; block is absolute once effective

Network mechanics are hot-swappable via RwLock. Each HTTP request snapshots the Arc<NetworkPolicy> for mechanical settings, then builds a normalized SecurityEvent. The shared SecurityRuleSet and plugin rail are the only security decision path.

Runtime and ledger materialization are intentionally separate. Runtime materialization preserves allowed protocol bytes for upstream, including resolving broker refs when a real credential is required. Ledger materialization runs through logging plugins and writes only broker refs, hashes, bounded previews, typed detections, and plugin execution evidence to session.db, structured logs, service routes, and UI stats.

The MITM proxy creates a normalized SecurityEvent and evaluates the shared rule rail. HTTP rules use first-party fields such as http.host, http.method, http.path, http.status, and http.body. They can also match other roots attached to the same event, such as model.provider, without creating a second callback-specific rule.

Example:

[profiles.rules.block_openai_github]
name = "block_openai_github"
action = "block"
reason = "Block OpenAI organization GitHub writes"
match = 'http.host == "github.com" && http.method == "POST" && http.path.matches("^/openai(/|$)")'

Plugin behavior is configured through profile/corp plugin descriptors, not by calling plugins from CEL rules. Rules decide enforcement and detection over the typed SecurityEvent; plugins run at their declared stages, own their private filtering/scope, and may mutate the event or ledger payload according to their contract. For example, credential brokering can capture and materialize credential:blake3:* references without exposing raw credential fields as CEL roots.

For AI provider domains, the proxy parses SSE response streams inline to extract structured telemetry. The parser preserves response bytes for the guest and emits typed model facts into the same security-event rail used by HTTP, DNS, MCP, file, and process events.

DomainProviderAPI paths
api.anthropic.comAnthropic/v1/messages
api.openai.comOpenAI/v1/responses, /v1/chat/completions
generativelanguage.googleapis.comGoogle/v1beta/*
graph LR
    A["HTTP response body<br/>(chunked)"] --> B["AiResponseBody<br/>(hyper Body wrapper)"]
    B --> C["SseParser<br/>(stateful wire format)"]
    C --> D["ProviderStreamParser<br/>(Anthropic/OpenAI/Google)"]
    D --> E["Vec&lt;LlmEvent&gt;<br/>(accumulated)"]
    E --> F["collect_summary()<br/>(pure function)"]
    F --> G["StreamSummary<br/>(text, tools, tokens, cost)"]

Parsing runs inline during poll_frame() — response bytes pass through unchanged to the guest with zero added latency.

EventFieldsDescription
MessageStartmessage_id, modelStream began
TextDeltaindex, textIncremental text output
ThinkingDeltaindex, textReasoning/chain-of-thought output
ToolCallStartindex, call_id, nameModel invoked a tool
ToolCallArgumentDeltaindex, deltaIncremental tool call JSON arguments
ToolCallEndindexTool call arguments complete
ContentBlockEndindexContent block finished
Usageinput_tokens, output_tokens, detailsToken usage update (details: cache_read, thinking, etc.)
MessageEndstop_reasonStream finished (EndTurn, ToolUse, MaxTokens, ContentFilter)
Unknownevent_type, rawUnrecognized SSE event (logged, not parsed)
OriginCriteriaExample
nativeDefault for tool names without __write_file, bash
localMatches is_builtin_tool()fetch_http, grep_http, http_headers
mcp_proxyName contains __ (MCP namespace separator)github__list_repos

Model pricing is loaded from the compact Capsem runtime ledger at config/data/genai-prices.json (embedded at compile time via include_str!). The ledger is transformed from pydantic/genai-prices with just update-prices, and model lookup uses the upstream match clauses without fuzzy fallback.

The TraceState tracks multi-turn agent conversations across request/response cycles:

sequenceDiagram
    participant Agent
    participant Proxy
    participant State as TraceState

    Agent->>Proxy: Request (no tool_responses)
    Note over Proxy: New trace_id = UUID
    Proxy->>State: Register tool call_ids
    Proxy-->>Agent: Response (stop: ToolUse, calls: [A, B])

    Agent->>Proxy: Request (tool_responses for A, B)
    Proxy->>State: Lookup call_ids [A, B]
    Note over State: Found trace_id from previous turn
    Proxy->>State: Register new call_ids [C]
    Proxy-->>Agent: Response (stop: ToolUse, calls: [C])

    Agent->>Proxy: Request (tool_responses for C)
    Proxy->>State: Lookup call_id [C]
    Proxy-->>Agent: Response (stop: EndTurn)
    Proxy->>State: Complete trace (cleanup)

All model_calls rows in the same trace share a trace_id, enabling per-turn cost and token aggregation.

Telemetry is emitted asynchronously after the response body completes (not during streaming):

Event typeWhenData
NetEventEvery HTTP requestDomain, method, path, status, bytes, latency, decision, compact display fields, body blob references
ModelCallAI provider requests onlyProvider, model, tokens, cost, tool calls, text content, trace_id

The TelemetryBody wrapper around the hyper response body triggers tokio::spawn(emitter.emit()) when the body stream reaches EOF.

OptimizationMechanism
Connection reuseUpstream reqwest sender cached per-connection for keep-alive
TLS session reuseShared rustls::ClientConfig with webpki roots
Cert cachingDouble-checked locking; each domain minted once
Inline parsingSSE parsing runs in poll_frame(), zero-copy passthrough
Async telemetryDB writes happen on a dedicated thread; never blocks the proxy
Policy snapshotsArc clone per request avoids holding the RwLock during I/O
FilePurpose
capsem-core/src/net/mitm_proxy/Connection handling, HTTP forwarding, telemetry hooks, and proxy pipeline
capsem-core/src/net/cert_authority.rsCA loading, leaf cert minting, cache
capsem-core/src/net/policy.rsNetwork mechanics: ports, capture, decompression, routing, cache settings
capsem-core/src/net/policy_config/Profile/corp config parsing into network mechanics and SecurityRuleSet
capsem-core/src/security_engine/SecurityEvent, SecurityRuleSet/CEL evaluation, plugins, endpoint DTOs
capsem-core/src/net/ai_traffic/SSE parsing, provider parsers, events, pricing
capsem-core/src/net/ai_traffic/mod.rsTraceState for multi-turn linking
security/keys/capsem-ca.key, security/keys/capsem-ca.crtStatic ECDSA P-256 CA keypair