Egress Proxy
For actions using egress.profile = "proxy_allowlist", LatchGate enforces two independent layers of egress control: the kernel’s sink validation at the host I/O layer, and a forward HTTP proxy (Squid) enforcing a domain allowlist at the network transport layer.
For production deployments with proxy_allowlist actions, the egress proxy is strongly recommended. Without it, the gate starts with a warning and uses kernel-only enforcement (Layer 1: sink validation + SSRF protection + manifest domain allowlists). The proxy adds an independent Layer 2 backstop — if a kernel-layer bug would ever let a request reach the network with an unauthorized target, the proxy catches it.
Why two layers
Section titled “Why two layers”The kernel’s validate_sink() is the primary authorization check. It runs before each host I/O call and rejects targets not in the grant’s allowed_sinks. But a single layer is a single point of failure:
- A regex or parser bug could accept a malformed URL that bypasses validation
- A future refactor could move network calls to a path that skips the check
- A compromised dependency could leak traffic to unauthorized domains
The proxy is a cheap, independent backstop. Squid has been hardened for decades; it has no knowledge of LatchGate internals. It only knows the allowlist of domains and refuses everything else. The proxy cannot be bypassed by kernel bugs because it sits at the network layer — it intercepts all HTTP egress regardless of which code path issued the request.
How the layers interact
Section titled “How the layers interact” Provider (WASM sandbox) │ │ io-http request (target URL) ▼ Host I/O layer ├─ validate_sink(url) ← layer 1: kernel │ rejects if url's host not in grant.allowed_sinks │ ├─ inject credentials (header or conn string) │ └─ send via reqwest through http_proxy │ ▼ Squid forward proxy ├─ ACL check against allowed_domains ← layer 2: network │ rejects if host not in allowlist │ └─ forwards to destination │ ▼ External systemBoth layers must pass for the request to succeed. If either rejects, the action fails closed.
Single source of truth
Section titled “Single source of truth”Egress allowlists are generated from action manifests — never hand-curated. The manifest is the source of truth; every downstream enforcement layer (Squid, CI env vars) consumes output from the same generator:
# Extract allowed domains from manifest YAML files to generate egress allowlists.# Use the deploy/pin-digests.sh script or parse manifests directly:grep -h 'allowed_domains' manifests/*.yaml # quick inspectionCI should fail if checked-in lists diverge from the domains declared in action manifests.
Two deployment modes
Section titled “Two deployment modes”LatchGate supports two egress proxy setups. Pick one based on how your allowlist changes.
Mode A — live sync (recommended for production)
Section titled “Mode A — live sync (recommended for production)”The gate atomically writes (manifest_domains ∪ learned_domains) ∩ runtime_allowlist to a file on every relevant event (startup, domains add/remove/clear, approval domain learning), then runs a reload command. Squid reads the same file via a shared volume. Adding a learned domain through the approval flow reflects in Squid within seconds — no redeploy.
egress_proxy_url = "http://squid:3128"egress_live_allowlist_path = "/var/run/latchgate/egress/allowlist.txt"egress_reload_command = "squid -k reconfigure"Docker Compose:
services: gate: volumes: - egress-allowlist:/var/run/latchgate/egress
squid: image: ghcr.io/latchgate-ai/latchgate-egress:latest volumes: - egress-allowlist:/var/run/latchgate/egress:rw networks: - toolnet - egress
volumes: egress-allowlist:
networks: toolnet: internal: true # providers have no direct internet egress: driver: bridge # Squid's outbound pathThe published image ghcr.io/latchgate-ai/latchgate-egress:<version> ships with a default-deny squid.conf and expects the allowlist on a shared volume. It is rebuilt by the release pipeline on every version.
Mode B — offline / CI (static allowlist)
Section titled “Mode B — offline / CI (static allowlist)”For environments without live sync — CI pipelines, airgapped deployments — generate the allowlist once and mount it read-only:
# Generate Squid allowlist from the allowed_domains in action manifests.# Extract domains for the actions you need:grep -A5 'allowed_domains' manifests/http_fetch.yaml manifests/github_read.yaml manifests/gmail_send.yaml \ | grep '^\s*-' | sed 's/.*- "//' | sed 's/"//' | sort -u > deploy/squid/allowlist.txtegress_proxy_url = "http://squid:3128"# No egress_live_allowlist_path — the file is pre-generated and shipped.Regenerate and redeploy whenever an action or manifest domain changes. A CI check comparing generator output to the checked-in file catches drift early:
# Compare manifest-declared domains against checked-in allowlistRuntime narrowing
Section titled “Runtime narrowing”In either mode, the operator can narrow the effective allowlist at runtime without touching manifests. Set egress_runtime_allowlist in latchgate.toml (this field is TOML-only and cannot be overridden via environment variables). The kernel and the live-synced file both compute:
effective = (manifest_domains ∪ learned_domains) ∩ egress_runtime_allowlistThis can only remove domains from the manifest set, never add — a domain absent from any manifest cannot become reachable through this field. Typical use: deploying a subset of actions and restricting network reach to only the domains those actions need.
SECURITY: operator-controlled only. Agent or caller input must never flow into this field.
Verify
Section titled “Verify”latchgate doctorExpected output includes:
Dependencies ✓ egress_proxy reachable at http://squid:3128In live-sync mode, confirm the gate wrote the allowlist:
docker exec <squid-container> cat /var/run/latchgate/egress/allowlist.txtAdd a learned domain and watch the file update.
In the TUI (Allowlists screen, 5, Domains sub-tab): use ←/→ to select the action, press a to add a domain, x to remove the selected one, and c to clear all learned domains for the action.
CLI equivalent
latchgate domains add web_read newapi.example.comdocker exec <squid-container> cat /var/run/latchgate/egress/allowlist.txt # newapi.example.com now presentIf egress_proxy is configured but status: false, the proxy is unreachable. Fix before proceeding — the gate’s readyz probe reports degraded and executions depending on proxy egress will fail.
Squid configuration
Section titled “Squid configuration”The published ghcr.io/latchgate-ai/latchgate-egress image and the reference deploy/squid/squid.conf enforce default-deny with SSRF protection at the network layer:
# Listenerhttp_port 3128
# Domain allowlist (live-synced by the gate in Mode A,# pre-generated in Mode B).acl allowed_domains dstdomain "/var/run/latchgate/egress/allowlist.txt"
# SSRF protection — a compromised kernel or WASM provider must not reach# internal infrastructure, cloud metadata, or loopback via the proxy.acl to_private dst 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16acl to_cgnat dst 100.64.0.0/10acl to_metadata dst 169.254.169.254
# Port restrictionsacl SSL_ports port 443acl Safe_ports port 80acl Safe_ports port 443acl CONNECT method CONNECT
# Access rules (first match wins)http_access deny !Safe_portshttp_access deny CONNECT !SSL_portshttp_access deny to_localhosthttp_access deny to_privatehttp_access deny to_cgnathttp_access deny to_metadatahttp_access allow allowed_domainshttp_access deny all
# Security hardeningforwarded_for deletecache deny all
# Logging to stdout/stderr for container log collectionaccess_log stdio:/dev/stdoutcache_log /dev/stderrThe config rejects all private IP ranges (RFC 1918), carrier-grade NAT, loopback, and the AWS/GCP metadata endpoint 169.254.169.254 regardless of what is in the allowlist — a second SSRF layer beyond the kernel’s DNS resolution check. Caching is disabled: this is a security proxy, not a performance one. forwarded_for delete strips the X-Forwarded-For header so internal client IPs never leak to upstream destinations.
Wildcard subdomains
Section titled “Wildcard subdomains”Squid’s dstdomain ACL supports leading-dot wildcards:
.example.commatches api.example.com, www.example.com, and example.com itself. Without the leading dot, only the exact hostname matches. The generator emits whatever form you configured in the manifest.
Troubleshooting
Section titled “Troubleshooting”All HTTP actions return 502 action_execution_failed
Section titled “All HTTP actions return 502 action_execution_failed”Check whether the proxy is reachable from the gate’s network:
curl -x http://squid:3128 https://api.github.com/If this fails, Squid is down or unreachable. Check container health, network policies, and the egress_proxy_url value.
Specific domain is rejected by Squid
Section titled “Specific domain is rejected by Squid”TCP_DENIED/403 | CONNECT api.new-service.com:443The domain is not in the effective allowlist. Either:
- Add it to the relevant manifest’s
egress.allowed_domainsand redeploy (Mode B) or let the gate live-sync (Mode A) - Approve an action with
latchgate approvals approve <id> --learn-domain api.new-service.com(Mode A only — learned domains) - In Mode B, regenerate the Squid allowlist from manifest YAML files and reload Squid
Never bypass the proxy ACL to unblock a domain — the point is defense-in-depth.
Squid returns 407 (Proxy Authentication Required)
Section titled “Squid returns 407 (Proxy Authentication Required)”LatchGate does not authenticate to the proxy. If you need authenticated proxy access, use a network-layer policy (firewall rule, iptables) to restrict which hosts can reach the proxy port — don’t enable Squid’s basic auth, which would require LatchGate to store proxy credentials.
Learned domain doesn’t appear in Squid
Section titled “Learned domain doesn’t appear in Squid”In live-sync mode, the gate runs egress_reload_command after writing the file. If the command fails (typo, missing binary, permission denied), the file updates but Squid keeps the old ACL in memory. Check the gate’s logs for reload errors and verify the command works from the gate container:
docker exec <gate-container> squid -k reconfigurePerformance considerations
Section titled “Performance considerations”The proxy adds one TCP hop. Typical overhead is <5 ms for same-network deployments, up to 50 ms over a WAN. For latency-sensitive use cases, colocate Squid with the gate (same host, same container network, or sidecar).
Squid holds one open connection per concurrent request. Size the proxy container to match the WASM concurrency limit (compile-time constant: 4).
Running without the proxy
Section titled “Running without the proxy”Without egress_proxy_url, the gate starts with a warning and uses kernel-only enforcement for proxy_allowlist actions. The kernel’s validate_sink() still checks every target URL against manifest domain allowlists, blocks SSRF targets, and pins DNS. Only the network-layer proxy backstop is absent.
This is the default in embedded mode (latchgate up without --infra). For single-instance deployments where the WASM sandbox has no network access and all egress goes through host I/O with validated sinks, kernel-only enforcement is sufficient. For multi-instance or high-assurance deployments, configure the proxy for defense-in-depth.
For the configuration reference, see Configuration. For production hardening checklist, see Deployment. For domain management, see latchgate domains.