From f4083f89c0b0744f0c8d896b225ac5670d6e5caf Mon Sep 17 00:00:00 2001 From: Aleksander Slominski Date: Fri, 17 Jul 2026 18:18:02 -0400 Subject: [PATCH] Feat: Add tls_bridge.upstream_insecure to skip origin cert verification The TLS bridge re-originates to the real origin via NewUpstreamClient, which verified against system roots + upstream_ca_bundle and never skipped verification. For internal/self-signed upstreams (e.g. a private LiteLLM endpoint) where the origin CA is unavailable, add an opt-in escape hatch: - NewUpstreamClient(extraRootsPEM, insecure): sets InsecureSkipVerify when insecure is true. - TLSBridgeConfig.UpstreamInsecure (yaml: upstream_insecure): plumbs the flag from config; authbridge-proxy logs a warning when enabled. - upstream_test.go: cover insecure=true accepting a self-signed origin. Prefer upstream_ca_bundle when the origin CA is available. Assisted-By: Claude (Anthropic AI) Signed-off-by: Aleksander Slominski --- authbridge/authlib/config/config.go | 4 ++++ authbridge/authlib/tlsbridge/upstream.go | 10 ++++++---- authbridge/authlib/tlsbridge/upstream_test.go | 10 ++++++++-- authbridge/cmd/authbridge-proxy/main.go | 5 ++++- 4 files changed, 22 insertions(+), 7 deletions(-) diff --git a/authbridge/authlib/config/config.go b/authbridge/authlib/config/config.go index 1a83d91c4..a688cd006 100644 --- a/authbridge/authlib/config/config.go +++ b/authbridge/authlib/config/config.go @@ -58,6 +58,10 @@ type TLSBridgeConfig struct { // UpstreamCABundle is an extra-roots PEM file for re-origination (private-CA // origins the agent trusts); empty == system roots only. UpstreamCABundle string `yaml:"upstream_ca_bundle" json:"upstream_ca_bundle"` + // UpstreamInsecure disables origin cert verification on re-origination + // (InsecureSkipVerify). For internal/self-signed upstreams when the origin CA + // is unavailable. Prefer UpstreamCABundle when possible. + UpstreamInsecure bool `yaml:"upstream_insecure" json:"upstream_insecure"` // PassthroughHosts are hosts to tunnel (never intercept). Distinct from // listener.skip_hosts, which bypasses the whole pipeline; these still run the // egress gate, they just aren't TLS-terminated. diff --git a/authbridge/authlib/tlsbridge/upstream.go b/authbridge/authlib/tlsbridge/upstream.go index b090ce0fb..b2cfa0217 100644 --- a/authbridge/authlib/tlsbridge/upstream.go +++ b/authbridge/authlib/tlsbridge/upstream.go @@ -10,9 +10,11 @@ import ( // NewUpstreamClient builds the HTTP client the TLS bridge uses to re-originate // to the real origin. RootCAs = system roots + extraRootsPEM (the agent's -// injected upstream trust). It NEVER sets InsecureSkipVerify and never uses the -// mesh-mTLS dialer — re-origination must verify the origin the way the agent would. -func NewUpstreamClient(extraRootsPEM []byte) (*http.Client, error) { +// injected upstream trust). When insecure is true it sets InsecureSkipVerify so +// re-origination does NOT verify the origin cert — for internal/self-signed +// upstreams (e.g. a private LiteLLM endpoint). Prefer extraRootsPEM +// (upstream_ca_bundle) over insecure whenever the origin CA is available. +func NewUpstreamClient(extraRootsPEM []byte, insecure bool) (*http.Client, error) { pool, err := x509.SystemCertPool() if err != nil || pool == nil { pool = x509.NewCertPool() @@ -24,7 +26,7 @@ func NewUpstreamClient(extraRootsPEM []byte) (*http.Client, error) { } return &http.Client{ Transport: &http.Transport{ - TLSClientConfig: &tls.Config{RootCAs: pool, MinVersion: tls.VersionTLS12}, + TLSClientConfig: &tls.Config{RootCAs: pool, MinVersion: tls.VersionTLS12, InsecureSkipVerify: insecure}, //nolint:gosec // opt-in via upstream_insecure for internal/self-signed upstreams ForceAttemptHTTP2: true, MaxIdleConns: 100, IdleConnTimeout: 90 * time.Second, diff --git a/authbridge/authlib/tlsbridge/upstream_test.go b/authbridge/authlib/tlsbridge/upstream_test.go index 0115882aa..0512085a7 100644 --- a/authbridge/authlib/tlsbridge/upstream_test.go +++ b/authbridge/authlib/tlsbridge/upstream_test.go @@ -15,7 +15,7 @@ func TestUpstreamClient_InjectedRootVerifies(t *testing.T) { caPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: srv.Certificate().Raw}) // With the origin's CA injected → verifies. - good, err := NewUpstreamClient(caPEM) + good, err := NewUpstreamClient(caPEM, false) if err != nil { t.Fatalf("NewUpstreamClient: %v", err) } @@ -26,8 +26,14 @@ func TestUpstreamClient_InjectedRootVerifies(t *testing.T) { _ = resp.Body.Close() // Without it (system roots only) → the self-signed httptest cert is rejected. - bare, _ := NewUpstreamClient(nil) + bare, _ := NewUpstreamClient(nil, false) if _, err := bare.Get(srv.URL); err == nil { t.Errorf("expected verification failure with system roots only") } + + // insecure=true → self-signed origin is accepted without any injected root. + insecure, _ := NewUpstreamClient(nil, true) + if _, err := insecure.Get(srv.URL); err != nil { + t.Errorf("expected success with insecure=true, got %v", err) + } } diff --git a/authbridge/cmd/authbridge-proxy/main.go b/authbridge/cmd/authbridge-proxy/main.go index 0ad6f76ca..9f83495b7 100644 --- a/authbridge/cmd/authbridge-proxy/main.go +++ b/authbridge/cmd/authbridge-proxy/main.go @@ -329,10 +329,13 @@ func main() { log.Fatalf("tls-bridge upstream_ca_bundle read failed: %v", err) } } - up, uerr := tlsbridge.NewUpstreamClient(extra) + up, uerr := tlsbridge.NewUpstreamClient(extra, cfg.TLSBridge.UpstreamInsecure) if uerr != nil { log.Fatalf("tls-bridge upstream client failed: %v", uerr) } + if cfg.TLSBridge.UpstreamInsecure { + log.Printf("tls-bridge: upstream_insecure=true — re-origination does NOT verify the upstream TLS cert") + } minter := tlsbridge.NewMinter(src, tlsbridge.MinterOpts{}) var ports map[int]bool // nil => NewDecision defaults to {443, 8443} if len(cfg.TLSBridge.Ports) > 0 {