Skip to main content
The security proxy is a Go binary that runs inside every Firecracker VM when PII scanning, injection defense, or transformation rules are enabled. It acts as a transparent man-in-the-middle (MITM) for all outbound HTTPS traffic from the sandbox workload.

Role in the architecture

The security proxy sits between the sandbox workload and the internet:

Per-sandbox CA certificates

Each sandbox gets a unique CA certificate generated at creation time using ECDSA P-256.
// From infra/orchestrator/internal/tcpfirewall/security.go
func GenerateSandboxCA(sandboxID string) (*x509.Certificate, *ecdsa.PrivateKey, []byte, error) {
    key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
    template := &x509.Certificate{
        Subject: pkix.Name{
            Organization: []string{"Declaw Sandbox CA"},
            CommonName:   "Declaw CA - " + sandboxID,
        },
        NotBefore:    time.Now().Add(-1 * time.Hour),
        NotAfter:     time.Now().Add(24 * time.Hour),
        IsCA:         true,
        KeyUsage:     x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
    }
    // ... create and parse cert
}
The CA cert is:
  1. Written to /opt/declaw/run/<sandbox_id>/ca.pem
  2. Injected into the Firecracker VM’s trust store at boot (before envd starts)
  3. Used by the security proxy to sign leaf certificates on-the-fly for each destination hostname
When the agent code makes an HTTPS request to api.openai.com, the proxy presents a certificate signed by the sandbox CA (which the VM trusts), terminates the TLS connection, reads the plaintext body, runs the scanning pipeline, then establishes a new TLS connection to the real api.openai.com and forwards the (possibly modified) request.

Leaf certificate generation

For each new HTTPS destination, the proxy generates a short-lived leaf certificate signed by the sandbox CA:
func generateCertForHost(hostname string, caCert *x509.Certificate, caKey *ecdsa.PrivateKey) (*tls.Certificate, error) {
    key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
    template := &x509.Certificate{
        SerialNumber: randomSerial(),
        Subject:      pkix.Name{CommonName: hostname},
        DNSNames:     []string{hostname},
        NotBefore:    time.Now().Add(-1 * time.Hour),
        NotAfter:     time.Now().Add(1 * time.Hour),
        KeyUsage:     x509.KeyUsageDigitalSignature,
        ExtKeyUsage:  []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
    }
    certDER, _ := x509.CreateCertificate(rand.Reader, template, caCert, &key.PublicKey, caKey)
    return &tls.Certificate{...}, nil
}

TLS passthrough mode

When only network policies are configured (no PII scanning, no transformations), the proxy operates in passthrough mode:
  1. Peek at the TLS ClientHello to extract the SNI hostname (without decrypting)
  2. Check the SNI against the domain allowlist/denylist
  3. If allowed, forward the raw TCP stream directly — no TLS termination, no decryption
This means pure network policies have zero TLS overhead. TLS interception only activates when body inspection is required.

Scanning pipeline execution

When TLS interception is active, the proxy:
  1. Terminates TLS from the client
  2. Reads the HTTP request headers and body
  3. Runs the pipeline stages in order:
    • Transform rules (outbound) → body may be modified
    • PII scanner → PII tokens substituted, session map updated
    • Injection defense → body checked; blocked if injection detected
  4. Establishes TLS to the real destination
  5. Forwards the modified request
  6. Receives the response
  7. Runs pipeline on response (in reverse — injection scan, transforms, PII rehydration)
  8. Returns modified response to the workload

NamespaceProxy

The NamespaceProxy is the main struct that manages the TCP listener for a sandbox’s network namespace. It is created in the host’s root namespace but listens on a socket bound inside the sandbox namespace using ip netns exec.
Host namespace: Orchestrator <-> NamespaceProxy
Sandbox namespace: iptables REDIRECT -> proxy listening socket
Firecracker VM: Workload -> (via TAP) -> iptables -> proxy socket
The proxy binds separate listeners for HTTP (port 80) and HTTPS (port 443), with a third port for other TCP traffic that only performs domain-level filtering without MITM.

Domain matching

Domain matching supports three formats:
FormatExampleBehavior
Exactapi.openai.comMatches only the exact hostname
Wildcard*.openai.comMatches any direct subdomain
Regex (prefix ~)~.*\.openai\.comFull RE2 regex match
func isDomainMatch(hostname string, domains []string) bool {
    h := strings.ToLower(hostname)
    for _, d := range domains {
        if strings.HasPrefix(d, "~") {
            // regex match
        } else if d == h {
            return true  // exact
        } else if strings.HasPrefix(d, "*.") {
            suffix := d[1:]  // ".openai.com"
            if strings.HasSuffix(h, suffix) {
                return true  // wildcard subdomain
            }
        }
    }
    return false
}

Guardrails Service integration

The proxy sends PII scan requests and injection scan requests to the Guardrails Service HTTP API at GUARDRAILS_URL. Each request is a JSON POST with the text to scan and the scanner types to use. If the Guardrails Service is unreachable (timeout of 10 seconds), the proxy falls back to the built-in regex scanner transparently. No error is surfaced to the workload.

Audit event streaming

The proxy writes AuditEntry structs to a channel after each request/response cycle. The envd daemon reads from this channel and streams audit events to the orchestrator via the ConnectRPC connection, where they are stored in memory and exposed through the API.