proxy

package
v0.4.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 20, 2026 License: Apache-2.0 Imports: 33 Imported by: 0

Documentation

Overview

Package proxy implements the L2 per-workspace egress sidecar (wardyn-proxy): an HTTP forward proxy that enforces the internal/egress decision model (default-deny domain allowlist, method rules, first-use approval), streams decision logs, and injects credentials proxy-side.

SECURITY INVARIANTS (mirror ARCHITECTURE.md and internal/egress):

  • Default deny: an empty allowlist allows nothing.
  • DeniedDomains always beats AllowedDomains.
  • Private/loopback/link-local/metadata IP ranges are unconditionally denied BEFORE any dial, regardless of policy (DNS-rebinding / SSRF guard). The vetted IP is dialed explicitly — the transport never re-resolves the hostname (no TOCTOU).
  • Fail closed on every error path.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func NewBuiltinEvaluator

func NewBuiltinEvaluator(spec types.RunPolicySpec) egress.Evaluator

NewBuiltinEvaluator returns the builtin egress.Evaluator for a compiled spec. It is exported so the conformance suite (and operators wiring the builtin explicitly) can construct it.

func ValidDomainEntry added in v0.4.0

func ValidDomainEntry(d string) error

ValidDomainEntry reports whether d is an allowlist/denylist entry the matcher above can ever match, and is the ONE shape check every operator-supplied policy ingest point runs (validatePolicySpec, internal/api). classifyDomain accepts anything — a mid-label pattern like "oidc.*.amazonaws.com" compiles to an exact hostname no real request can equal, so it silently protects nothing. Operator input fails closed instead: reject the dead entry at write time rather than ship a policy the operator believes is guarding them.

Valid: a bare exact host ("api.anthropic.com"), a leading-"*." wildcard ("*.example.com"), and either with a valid ":port" qualifier.

Types

type Config

type Config struct {
	// RunID is the governed run this sidecar serves.
	RunID uuid.UUID `json:"run_id"`
	// ControlPlaneURL is the base URL of wardynd (e.g. "http://wardynd:8080").
	ControlPlaneURL string `json:"control_plane_url"`
	// RunToken authenticates internal calls (Authorization: Bearer <token>).
	RunToken string `json:"run_token"`
	// Policy is the compiled egress allowlist / method rules / first-use flag.
	Policy types.RunPolicySpec `json:"policy"`
	// Injection rules drive proxy-side credential injection (plain HTTP only).
	// Each rule carries the grant_id the secret is minted from at startup.
	Injection []InjectionConfig `json:"injection,omitempty"`
	// Listen is the proxy listen address (default ":3128").
	Listen string `json:"listen,omitempty"`
	// DecisionBufferSize caps the async decision-log buffer (default 1024).
	DecisionBufferSize int `json:"decision_buffer_size,omitempty"`
	// MITMCACertPEM / MITMCAKeyPEM are the OPTIONAL TLS-MITM certificate authority
	// (PEM). When both are set AND content inspection is enabled, the proxy
	// TLS-terminates opaque CONNECT tunnels to known LLM hosts (Anthropic/OpenAI)
	// to inspect the subscription-OAuth path. The CA PRIVATE KEY stays in proxy
	// memory only; the sandbox trusts only the CA's public cert (delivered to its
	// trust store by the driver). Empty => opaque passthrough (no MITM).
	MITMCACertPEM string `json:"mitm_ca_cert_pem,omitempty"`
	MITMCAKeyPEM  string `json:"mitm_ca_key_pem,omitempty"`
	// MITMHosts are OPERATOR-CONFIGURED corp artifact hosts (exact hostnames) this
	// proxy is permitted to TLS-MITM in addition to the built-in LLM hosts, so a
	// corporate registry token can be injected on the wire. See isMITMHost's trust
	// boundary: this is a tight per-host operator allowlist compiled at dispatch
	// from the site-config artifact overrides — NOT attacker-reachable (the sandbox
	// cannot set it), NEVER a wildcard, and only meaningful with a paired injection
	// rule that supplies the operator's OWN token. Empty => LLM hosts only.
	MITMHosts []string `json:"mitm_hosts,omitempty"`
	// GitGrants is the git-broker per-repo allowlist: canonical "<org>/<repo>" ->
	// the github_token grant id to mint from, backing the /wardyn/gh/ route so the
	// sandbox reaches only its granted repos (never all of github.com) and the token
	// stays proxy-side. Compiled at dispatch from the run's github grants. Empty =>
	// the git-broker route always 403s (no repo brokered).
	GitGrants map[string]uuid.UUID `json:"git_grants,omitempty"`
	// MITMLLM reports whether TLS-MITM of the BUILT-IN LLM hosts (Anthropic/OpenAI)
	// is actually intended for this run — i.e. subscription credential injection OR
	// intercept_tls content inspection. Dispatch also mints the per-run CA for
	// artifact token injection (MITMHosts), so "a CA exists" no longer implies "LLM
	// MITM was wanted"; without this flag an artifact-only run would TLS-terminate a
	// direct CONNECT to Anthropic/OpenAI it was never asked to. Empty/false => the
	// LLM hosts stay opaque passthrough even when a CA is present for artifact hosts.
	MITMLLM bool `json:"mitm_llm,omitempty"`
	// HoldForReviewTimeoutSec bounds how long the proxy holds a connection open in
	// wait_for_review mode before failing closed (403, with the approval left
	// pending so a later retry can still be approved). Deliberately under common
	// client timeouts. <=0 uses the default (30s).
	HoldForReviewTimeoutSec int `json:"hold_for_review_timeout_sec,omitempty"`
	// MaxConcurrentHolds caps simultaneous wait_for_review holds for this run; past
	// the cap a new unknown host falls back to fail-fast pending (deny_with_review)
	// instead of parking another goroutine — a bound on held-connection resource
	// exhaustion. <=0 uses the default (16).
	MaxConcurrentHolds int `json:"max_concurrent_holds,omitempty"`
	// UpstreamProxyURL is the OPTIONAL corporate parent/upstream proxy that this
	// sidecar chains its egress through (http[s]://[user:pass@]host[:port]). In a
	// locked-down corporate network the sandbox host has NO direct internet route
	// — the org's HTTP CONNECT proxy is the only way out (and is frequently a
	// PRIVATE address). When set, forward-egress dials are issued as
	// CONNECT <real-host> to this proxy; control-plane calls to wardynd never
	// traverse it. Any embedded credential is held proxy-memory-only (like
	// RunToken) and masked from all decision-log/stdout output. Empty => direct
	// dial (backward-compatible).
	//
	// TODO(site-config): sourced operator-wide from the persisted site-config
	// (upstream_proxy_secret_ref) once that store lands; today it is threaded
	// from the run's ProxyConfig at dispatch.
	UpstreamProxyURL string `json:"upstream_proxy_url,omitempty"`
}

Config is the wardyn-proxy sidecar configuration, loaded from a JSON file via the -config flag. The run token authenticates the sidecar to the control plane's internal endpoints (verified via identity.Provider.Verify with audience "wardyn-internal"); it is NOT a secret usable outside the platform. No third-party secrets ever appear here — injected credentials are minted at startup from the broker and held only in proxy memory.

func LoadConfig

func LoadConfig(path string) (*Config, error)

LoadConfig reads and validates a Config from path.

func LoadConfigBytes

func LoadConfigBytes(b []byte) (*Config, error)

LoadConfigBytes parses and validates a Config from raw JSON. Used by the sidecar's env-var config path (WARDYN_PROXY_CONFIG_JSON), which is how the docker driver delivers the run's policy without managing host files.

type IPGuardResult

type IPGuardResult struct {
	// IP is the single vetted address to dial (no further DNS resolution).
	IP net.IP
	// Denied is true when no usable, policy-safe address exists.
	Denied bool
	// Reason explains a denial (for the decision log rule_source).
	Reason string
}

IPGuardResult records the outcome of resolving + vetting a target host.

func VetHost

func VetHost(host string, res resolver) IPGuardResult

VetHost resolves host and returns the first address that is NOT in a blocked range. If host is already a literal IP, it is vetted directly. On any failure or if every address is blocked, the result is Denied (fail closed). The returned IP MUST be the one dialed — callers must not re-resolve the hostname (TOCTOU / DNS-rebinding guard).

type InjectionConfig

type InjectionConfig struct {
	egress.InjectionRule
	// GrantID identifies the credential_grant to mint the secret value from
	// at startup via POST /api/v1/internal/credentials/mint.
	GrantID uuid.UUID `json:"grant_id"`
}

InjectionConfig pairs an egress.InjectionRule with the credential grant the proxy mints from at startup. The minted secret lives ONLY in proxy memory: it is never exposed to the sandbox (no env, no disk, no args). CONNECT tunnels cannot be injected into — the proxy has hostname-only visibility on TLS and never sees the encrypted request headers — so injection applies to plain-HTTP requests only.

type Options

type Options struct {
	RunID  uuid.UUID
	Policy *Policy
	// Evaluator overrides the pluggable host/method policy-verdict engine. Nil =>
	// the builtin evaluator wrapping Policy (default; unchanged behavior).
	Evaluator egress.Evaluator
	Approval  *approvalClient
	Injector  *injector
	Sink      *decisionSink
	Resolver  resolver
	// Scanner is the optional outbound content-inspection engine (nil == off).
	Scanner *contentscan.Engine
	// CA is the optional TLS-MITM certificate authority (nil == opaque CONNECT).
	CA *certAuthority
	// MITMHosts are operator-configured corp artifact hosts eligible for TLS-MITM
	// beyond the built-in LLM hosts (exact hostnames). See Proxy.mitmHosts.
	MITMHosts []string
	// MITMLLM gates TLS-MITM of the built-in LLM hosts on actual intent. See
	// Proxy.mitmLLM.
	MITMLLM bool
	// GitGrants is the git-broker per-repo allowlist ("<org>/<repo>" -> grant id)
	// backing the /wardyn/gh/ route. See Proxy.gitGrants.
	GitGrants map[string]uuid.UUID
	// ControlPlaneURL and RunToken back the local brokered routes. The run
	// token is injected only toward the control plane and never reaches the
	// sandbox or any LLM upstream.
	ControlPlaneURL string
	RunToken        *tokenSource
	// Upstream is the OPTIONAL corporate parent proxy (parsed form). Nil ==
	// direct dial. NewServer parses it from Config.UpstreamProxyURL; tests may
	// build one via parseUpstreamProxy.
	Upstream *upstreamProxy
	// Dial overrides the connection dialer (tests). Production leaves it nil
	// and a net.Dialer is used.
	Dial func(ctx context.Context, network, addr string) (net.Conn, error)
	// TLSClientConfig overrides the forwarding transport's TLS config. Tests
	// use this to trust an httptest TLS server standing in for an HTTPS LLM
	// upstream; production leaves it nil (system roots, ServerName from URL).
	TLSClientConfig *tls.Config
	Now             func() time.Time
}

Options configures a Proxy. Nil fields fall back to production defaults.

type Policy

type Policy struct {
	// contains filtered or unexported fields
}

Policy is a compiled, immutable view of types.RunPolicySpec optimized for per-request evaluation.

func CompilePolicy

func CompilePolicy(spec types.RunPolicySpec) *Policy

CompilePolicy builds a Policy from a RunPolicySpec. Domains are normalized to lowercase; trailing dots are stripped. Methods are uppercased.

func (*Policy) AllowedExactHost

func (p *Policy) AllowedExactHost(host string) bool

AllowedExactHost reports whether host is allowed via an EXACT allowlist entry (not a wildcard, not approval). Credential injection requires this stricter match so an injection rule can never widen egress nor leak a secret to a wildcard-matched host.

func (*Policy) FirstUseMode

func (p *Policy) FirstUseMode() types.FirstUseMode

FirstUseMode reports how unknown domains are handled (always_deny / deny_with_review / wait_for_review), normalized (never empty).

type Proxy

type Proxy struct {
	// contains filtered or unexported fields
}

Proxy is the L2 forward proxy. It serves both absolute-URI plain HTTP requests and CONNECT tunnels for TLS (hostname-only visibility; no interception in v0). All egress flows through the decision pipeline:

policy (deny>allow, default-deny) -> first-use approval (if unknown &&
  first_use_approval) -> method check -> IP vetting (block private/
  loopback/link-local/metadata) -> dial the vetted IP explicitly.

Every decision emits an egress.DecisionLog (async to the control plane, mirrored to stdout).

func (*Proxy) ServeHTTP

func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP routes between CONNECT tunneling, brokered LOCAL routes, and plain-HTTP forwarding.

SECURITY: the local brokered routes (/wardyn/...) are reachable ONLY via origin-form requests addressed to the proxy listener itself — i.e. the request-target is path-only (r.URL.Host == ""). An absolute-URI forward request for http://wardyn-proxy:3128/wardyn/v1/... carries a non-empty r.URL.Host and is handled by the normal forward-proxy path, where the proxy host is not allowlisted and is denied by policy. CONNECT is never a local route. This makes the run token unreachable by anything the sandbox can route through the forward proxy.

type Server

type Server struct {
	// contains filtered or unexported fields
}

Server bundles a Proxy with its http.Server and async decision sink for lifecycle management.

func NewServer

func NewServer(ctx context.Context, cfg *Config, client *http.Client, stdout io.Writer) (*Server, error)

NewServer wires a Proxy from a validated Config and dependencies. It mints injection credentials once (fail-closed on error) before returning.

func (*Server) Addr

func (s *Server) Addr() string

Addr returns the configured listen address.

func (*Server) ListenAndServe

func (s *Server) ListenAndServe() error

ListenAndServe starts serving and blocks until the server stops.

func (*Server) Shutdown

func (s *Server) Shutdown(ctx context.Context) error

Shutdown gracefully stops the HTTP server, stops the token renewer, and drains the decision sink.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL