Documentation
¶
Overview ¶
Package egressproxy implements the transparent sandbox egress filter: a userspace proxy that all sandbox TCP is iptables-REDIRECT'd through, plus the redirect installer and the on-disk policy contract the control plane writes.
The enforcement model mirrors e2b-dev/infra's tcpfirewall, adapted for a Kubernetes Pod (init container installs the redirect, a sidecar runs the proxy) and hardened for the eval anti-cheat use case: the default action is deny (allowlist), not allow.
Index ¶
- Constants
- func GenerateCA(commonName string, ttl time.Duration) (certPEM, keyPEM string, err error)
- func InstallRedirect(cfg RedirectConfig) error
- func RemoveSecrets(path string) error
- func WritePolicy(path string, p Policy) error
- func WriteSecrets(path string, s Secrets) error
- type ApplyOutcome
- type Decision
- type InjectHeader
- type InjectRule
- type MatchType
- type Policy
- type Proxy
- type RedirectConfig
- type Secrets
- type ServeConfig
Constants ¶
const ( // DefaultProxyUID is the uid the sidecar runs as; the redirect exempts it so // the proxy's own upstream connections are not looped back. DefaultProxyUID = 1337 DefaultHTTPPort = 15001 DefaultTLSPort = 15002 DefaultOtherPort = 15003 // DefaultHealthPort serves /healthz. Separate from the three data-plane // ports on purpose: a probe aimed at those arrives indistinguishable from a // redirected sandbox connection, so it would be policy-evaluated, logged as // a denial on every interval, and — with private ranges allowed — dialed // straight back into this listener. DefaultHealthPort = 15004 // DefaultPolicyPath is the emptyDir file the control plane writes (exec) and // the proxy reads (fsnotify). Mounted only in the sidecar. DefaultPolicyPath = "/var/run/egress/policy.json" // DefaultSecretsPath is the injection config, written 0600 next to the // policy. It holds live credential material and the sandbox's CA key, so it // lives on the same sidecar-only tmpfs and is removed on release. DefaultSecretsPath = "/var/run/egress/secrets.json" )
Fixed wiring shared by the redirect installer, the proxy, and the control plane. These are container-internal (the sidecar owns the whole netns view), so constants rather than config keep init and sidecar in lockstep.
const ( ModeOverride = "Override" ModeIfAbsent = "IfAbsent" )
Injection mode strings, mirroring the CRD enum.
Variables ¶
This section is empty.
Functions ¶
func GenerateCA ¶ added in v0.0.8
GenerateCA mints a self-signed CA for one sandbox and returns the PEM pair. The control plane calls this at claim time: it holds both delivery channels (exec to the sidecar for the key, envd /init for the certificate), so generating here avoids a round-trip to read the certificate back out of the sidecar.
func InstallRedirect ¶
func InstallRedirect(cfg RedirectConfig) error
InstallRedirect programs nat OUTPUT so all sandbox TCP is transparently redirected to the local proxy, while the proxy's own traffic, DNS, and loopback are exempted. Idempotent: the chain is flushed and rebuilt, and the OUTPUT jump is added only if absent.
Runs in the Pod netns (shared by all containers); requires CAP_NET_ADMIN.
func RemoveSecrets ¶ added in v0.0.8
RemoveSecrets deletes the secrets file. Called on release so a recycled pod cannot carry the previous sandbox's credentials, CA key, or placeholder map into the next claim. A missing file is success.
func WritePolicy ¶
WritePolicy atomically writes the policy to path (write temp + rename) so a reader (fsnotify reload) never observes a half-written file.
func WriteSecrets ¶ added in v0.0.8
WriteSecrets atomically writes the injection config with owner-only permissions. Same temp+rename discipline as WritePolicy so a concurrent reader never sees a half-written file.
Types ¶
type ApplyOutcome ¶ added in v0.0.8
type ApplyOutcome struct {
Skipped bool // rule matched the host but not this request's path/method
HeadersSet int
Substituted int
SubstitutedK []string // placeholder-bearing header names, for debug logging
}
ApplyOutcome reports what a single Apply did, for metrics. It deliberately carries no header values or credential material.
type InjectHeader ¶ added in v0.0.8
type InjectHeader struct {
Name string `json:"name"`
// Value is the final header value; templates were expanded by the operator.
Value string `json:"value"`
// Mode is "Override" (default) or "IfAbsent".
Mode string `json:"mode,omitempty"`
}
InjectHeader is one header to write onto a matching request.
type InjectRule ¶ added in v0.0.8
type InjectRule struct {
// Host is an exact hostname, lowercase. Wildcards are rejected upstream.
Host string `json:"host"`
// Ports the rule covers. Empty means the defaults, 80 and 443.
Ports []int `json:"ports,omitempty"`
// Headers to inject.
Headers []InjectHeader `json:"headers,omitempty"`
// SubstitutePlaceholders lists the placeholders that may be swapped for
// their real value on this host.
SubstitutePlaceholders []string `json:"substitutePlaceholders,omitempty"`
// PathPrefixes / Methods narrow the rule. Empty means no narrowing.
PathPrefixes []string `json:"pathPrefixes,omitempty"`
Methods []string `json:"methods,omitempty"`
}
InjectRule is the per-host injection action.
type MatchType ¶
type MatchType string
MatchType records why a connection was allowed/denied, for metrics and logs.
type Policy ¶
type Policy struct {
// SandboxID records which sandbox this policy was pushed for. Informational
// (surfaced in logs/metrics); the proxy enforces whatever is present.
SandboxID string `json:"sandboxId,omitempty"`
// Enforce gates the whole filter. When false the policy is treated as
// fail-closed (deny all). The control plane sets it true only after a
// concrete ruleset has been resolved for a claimed sandbox.
Enforce bool `json:"enforce"`
// DisableEgress blocks all outbound traffic regardless of the allow lists
// (DNS is still permitted so name resolution does not hang). Equivalent to
// an empty allowlist.
DisableEgress bool `json:"disableEgress,omitempty"`
// AllowedDomains permits egress to matching hostnames (exact, "*", or
// "*.example.com"). Domains only ever allow; there is no domain deny.
AllowedDomains []string `json:"allowedDomains,omitempty"`
// AllowedCIDRs / DeniedCIDRs are IP/CIDR allow/deny for traffic without a
// usable hostname (non-80/443 TCP, or bare-IP connections).
AllowedCIDRs []string `json:"allowedCIDRs,omitempty"`
DeniedCIDRs []string `json:"deniedCIDRs,omitempty"`
// AllowPrivateNetworks disables the built-in anti-SSRF deny of private /
// link-local / cloud-metadata ranges. Default false (baseline stays on).
AllowPrivateNetworks bool `json:"allowPrivateNetworks,omitempty"`
}
Policy is the on-disk egress ruleset the control plane pushes to the proxy. It is the single source of truth read by the sidecar; an absent, empty, or unparseable file means fail-closed (deny all egress except DNS).
func FailClosed ¶
func FailClosed() Policy
FailClosed is the policy applied when no valid policy file is present: deny everything. DNS egress is permitted separately by the iptables rules, not by this policy, so resolution keeps working even while fail-closed.
func LoadPolicy ¶
LoadPolicy reads and parses the policy file. A missing file yields FailClosed with no error (that is the expected steady state before the first push). A present-but-corrupt file yields FailClosed WITH an error so the caller can log it — but enforcement still fails closed.
func (Policy) Evaluate ¶
Evaluate applies the policy to a connection. hostname is the peeked HTTP Host or TLS SNI ("" when unavailable); ip is the original destination IP.
Semantics (allowlist / default-deny, hardened vs e2b's default-allow):
- Not enforcing -> allow (feature effectively off).
- Anti-SSRF baseline -> deny private ranges unless AllowPrivateNetworks.
- DisableEgress -> deny (allowlist empty by construction).
- Allowed domain match -> allow.
- Allowed CIDR match -> allow.
- Denied CIDR match -> deny.
- Default -> DENY (this is the key hardening).
type Proxy ¶
type Proxy struct {
// contains filtered or unexported fields
}
Proxy is the running egress filter.
func NewProxy ¶
func NewProxy(cfg ServeConfig) *Proxy
NewProxy builds a Proxy, loading the initial policy (fail-closed if absent) and injection config (empty if absent).
type RedirectConfig ¶
type RedirectConfig struct {
ProxyUID int // sidecar runs as this uid; its own egress is exempted (no loop)
HTTPPort int
TLSPort int
OtherPort int
}
RedirectConfig parameterizes the iptables nat rules the init container installs in the Pod network namespace.
type Secrets ¶ added in v0.0.8
type Secrets struct {
// SandboxID records which sandbox this config was pushed for. Informational.
SandboxID string `json:"sandboxId,omitempty"`
// CACertPEM / CAKeyPEM are the per-sandbox CA used to mint leaf
// certificates for the hosts being intercepted. Absent means TLS
// interception is impossible and only plaintext :80 rules can apply.
CACertPEM string `json:"caCertPem,omitempty"`
CAKeyPEM string `json:"caKeyPem,omitempty"`
// Rules is the per-host injection table.
Rules []InjectRule `json:"rules,omitempty"`
// Substitutions maps a placeholder handed to the sandbox to the real
// credential. A placeholder is only ever swapped on a host whose rule
// lists it, so the real value cannot be steered to another destination.
Substitutions map[string]string `json:"substitutions,omitempty"`
}
Secrets is the injection config the control plane pushes over the exec channel. Unlike Policy it carries live credential material, so it is written only to the sidecar's own tmpfs (mode 0600) and is never persisted in a Pod annotation, an env var, or a log line.
Header values arrive already resolved: the operator expands the CRD's "{{ cred }}" templates before pushing, so credential *names* never reach the sidecar and the data plane needs no template engine.
func LoadSecrets ¶ added in v0.0.8
LoadSecrets reads and parses the secrets file. A missing or empty file yields an empty config with no error: that is the steady state before the first push and simply means "inject nothing", which is the safe default — the sandbox's request then goes out without a credential and the upstream rejects it.
func (*Secrets) Apply ¶ added in v0.0.8
func (s *Secrets) Apply(req *http.Request, rules []*InjectRule) ApplyOutcome
Apply rewrites req's headers according to the rule: placeholders are substituted first, then declared headers are injected. The order matters and is deliberate — an Override header wins over whatever substitution produced, while IfAbsent leaves it alone, which is what makes "placeholder first, header as fallback" expressible.
Substitution only touches header *values*, never the body or the query string: scanning a body would mean buffering the whole request, and a credential in a query string ends up in the upstream's access log.
func (*Secrets) InterceptHosts ¶ added in v0.0.8
InterceptHosts returns the lowercase hosts that require TLS termination — used to decide, from the SNI alone, whether a connection takes the MITM path or stays a byte-for-byte splice.
func (*Secrets) Intercepts ¶ added in v0.0.8
Intercepts reports whether host:port has any injection rule, i.e. whether the connection must take the TLS-terminating path instead of the plain splice.
func (*Secrets) MatchAll ¶ added in v0.0.8
func (s *Secrets) MatchAll(host string, port int) []*InjectRule
MatchAll returns every rule covering host:port, in declaration order.
A host may carry several rules (mirroring E2B's host -> rule-array wire shape), and all of them apply: stopping at the first match would silently drop the rest. Host comparison is exact and case-insensitive — a wildcard would hand the credential to whoever controls a matching subdomain, so the API layer refuses to write one and the data plane never interprets one.
type ServeConfig ¶
type ServeConfig struct {
PolicyPath string
SecretsPath string
HTTPPort int
TLSPort int
OtherPort int
// HealthPort serves a plain-text liveness/readiness endpoint on /healthz.
// Deliberately separate from the three data-plane ports: a probe aimed at
// those is indistinguishable from a redirected sandbox connection, so it
// gets policy-evaluated, logged, and (before the self-dial guard) could be
// dialed back into this same listener. Zero disables it.
HealthPort int
Logger *slog.Logger
}
ServeConfig configures the proxy listeners and policy source.