Skip to content

Feat: Outbound TLS bridge (Phase 1) — decrypt agent egress HTTPS into the pipeline#522

Merged
huang195 merged 24 commits into
rossoctl:mainfrom
huang195:feat/tlsbridge-phase1
Jun 18, 2026
Merged

Feat: Outbound TLS bridge (Phase 1) — decrypt agent egress HTTPS into the pipeline#522
huang195 merged 24 commits into
rossoctl:mainfrom
huang195:feat/tlsbridge-phase1

Conversation

@huang195

@huang195 huang195 commented Jun 18, 2026

Copy link
Copy Markdown
Member

Summary

Adds an outbound TLS bridge to AuthBridge's Go forward proxy (proxy-sidecar/lite): it terminates the agent's egress TLS by forging a per-origin leaf signed by an in-process CA, runs the existing, unchanged outbound pipeline on the decrypted request, and re-originates a separately-verified TLS connection to the real origin. This closes the gap where every content plugin (token-exchange, MCP/A2A parsing, IBAC, guardrails) silently did nothing on HTTPS egress because the proxy blind-tunneled TLS.

The feature was previously prototyped under the name "MITM"; it is now the TLS bridge (terminate one TLS connection, run logic on plaintext, originate a fresh verified TLS — a bridge between two TLS connections). Package tlsbridge, config block tls_bridge:.

Phase 1 is AuthBridge-only and test-only. It has no operator dependency and is off by default (tls_bridge.enabled: false). A real operator-deployed agent does not yet trust the bridge CA, so its egress safely tunnels — the decrypt→pipeline→re-originate loop is proven by in-process integration tests. Live-agent trust (mounting ca.crt + trust env via the operator's cert-manager CA) is Phase 2 (sketched in the plan); Phase 1 is built so that path drops in via the CASource seam with no changes to the bridge core.

Design: authbridge/docs/superpowers/specs/2026-06-12-authbridge-tlsbridge-design.md
Plan: authbridge/docs/superpowers/plans/2026-06-17-authbridge-tlsbridge-phase1.md

What's included

  • New package authlib/tlsbridge/: CASource (ephemeral self-signed + file, PKCS#8/PKCS#1/SEC1 key parsing for cert-manager compatibility); Minter (per-host leaf, one shared P-256 leaf key, LRU+TTL cache); Decision (5-byte TLS-record-header validation, port/scope/CIDR/skip gates) + SkipSet; UpstreamClient (system + injected roots, never InsecureSkipVerify); Terminator (ALPN h2+http/1.1); ServeConn (h2 + HTTP/1.1 keep-alive, blocks for the connection lifetime); Engine facade + best-effort trust self-check.
  • Forward-proxy integration: extracted serveOutbound (re-originates via the dedicated upstream client when bridging — never the mesh-mTLS dialer that presents the agent SVID); bridgeServe (verify upstream before forging — reversibility); reversible bridge branch on both the transparent and CONNECT capture paths; peekedConn.Peek.
  • Config + wiring: tls_bridge: config block with validation; main.go constructs the engine when enabled and runs the trust self-check.

No-broken-calls guarantee

The headline safety property is that no bridge configuration ever turns a working agent call into a hard failure. Encoded as integration tests through the real capture path:

  • Unverifiable upstream → upstream-verify fails before forging → call is tunneled (agent's own end-to-end TLS reaches the origin); pipeline not run.
  • Pinned client (rejects the minted leaf) → host auto-skipped → retry tunnels and succeeds.
  • Non-TLS bytes on an eligible port → passthrough tunnel.

Test plan

  • go test ./... green in authbridge/authlib (14 tlsbridge unit tests + 6 forward-proxy integration tests).
  • go test ./tlsbridge/ ./listener/forwardproxy/ -race clean.
  • authbridge-proxy binary builds.
  • go vet clean; no new golangci-lint findings; gofmt clean.
  • Happy path: agent HTTPS (h1.1) decrypted → pipeline runs (probe records the plaintext /secret) → real verified origin → response byte-intact, on both transparent and CONNECT paths.
  • Custom-root origin verifies via injected upstream_ca_bundle; system-roots-only rejects.

Follow-ups (not blocking; tracked for Phase 2)

  • Bound the runtime SkipSet (LRU/TTL) — currently unbounded.
  • Tests: CONNECT-path fall-open (re-dial) path; h2 end-to-end through the bridge; relay-fails-after-handshake behavior.
  • Phase 2: operator-coordinated per-agent cert-manager CA (FileSource), agent-side ca.crt hard-mount + trust env, :9094 session-API hardening when bridging is enabled, CA rotation.

Assisted-By: Claude (Anthropic AI) noreply@anthropic.com

Summary by CodeRabbit

  • New Features
    • Added an optional outbound TLS Bridge (tls_bridge) that can terminate and re-originate traffic for eligible HTTPS/TLS ports on both transparent and CONNECT flows.
  • Configuration
    • Added tls_bridge mode enablement, required CA directory, port allow-list validation, passthrough/skip hosts, and optional upstream CA bundle injection.
    • When enabled, session API binding is hardened to loopback.
  • Bug Fixes
    • Improved fail-open behavior by verifying upstream first and reliably falling back to tunneling when bridging can’t be performed.
  • Tests
    • Added unit and integration coverage for config parsing/validation, TLS bridging decisions, certificate minting, and connection handling.
  • Dependencies
    • Updated golang.org/x/* module versions.

huang195 added 15 commits June 17, 2026 13:54
Self-contained design + implementation plan for the AuthBridge outbound
TLS bridge (terminate agent egress TLS, run the existing pipeline on
plaintext, re-originate verified TLS). Phase 1 is AuthBridge-only and
test-only; Phase 2 (cert-manager CA) drops in via the CASource seam.
Includes the empty tlsbridge package (doc.go).

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Hai Huang <huang195@gmail.com>
Signed-off-by: Hai Huang <huang195@gmail.com>
… cache

Signed-off-by: Hai Huang <huang195@gmail.com>
Signed-off-by: Hai Huang <huang195@gmail.com>
Signed-off-by: Hai Huang <huang195@gmail.com>
…cision branches

Signed-off-by: Hai Huang <huang195@gmail.com>
Signed-off-by: Hai Huang <huang195@gmail.com>
Signed-off-by: Hai Huang <huang195@gmail.com>
…acade + sniff Peek

Signed-off-by: Hai Huang <huang195@gmail.com>
http.Server.Serve dispatches the accepted conn to a goroutine and calls
Accept again immediately; the old oneConnListener returned net.ErrClosed
on that second Accept, so Serve — and ServeConn — returned before the
request was handled, tearing the conn down mid-response. Make the second
Accept block until the served conn is closed (via a close-notifying conn
wrapper), so ServeConn serves the whole connection synchronously like a
tunnel. The T5 keep-alive unit test masked this by running ServeConn in a
goroutine; the synchronous transparent-bridge path (T7) needs it to block.

Signed-off-by: Hai Huang <huang195@gmail.com>
Add the bridge branch to HandleTransparentConn: peek the sniffed
ClientHello, classify, and on Terminate verify the upstream origin first
(reversibility) then forge a leaf, terminate the agent TLS, and run the
unchanged outbound pipeline via serveOutbound, relaying over the verified
upstream client. Falls open to a re-dialed tunnel when upstream verify
fails; auto-skips a host whose minted leaf the client rejects. Integration
test drives the full decrypt -> pipeline -> re-originate loop end to end.

Signed-off-by: Hai Huang <huang195@gmail.com>
Signed-off-by: Hai Huang <huang195@gmail.com>
Signed-off-by: Hai Huang <huang195@gmail.com>
Signed-off-by: Hai Huang <huang195@gmail.com>
…lidation

An unknown ca_source (e.g. typo'd 'vault') previously fell through to the
ephemeral CA silently; fail fast so a misconfigured file-CA deployment is a
loud startup error rather than silent ephemeral mode.

Signed-off-by: Hai Huang <huang195@gmail.com>
@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@huang195, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 34 minutes and 53 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 1f992a67-3d0c-4b6f-aab8-4fd12fcc2075

📥 Commits

Reviewing files that changed from the base of the PR and between 4acd038 and 82ded4c.

📒 Files selected for processing (3)
  • authbridge/authlib/tlsbridge/decision.go
  • authbridge/authlib/tlsbridge/decision_test.go
  • authbridge/cmd/authbridge-proxy/main.go
📝 Walkthrough

Walkthrough

Implements a complete outbound TLS bridge (MITM) feature for AuthBridge: adds a new authlib/tlsbridge package with CA sourcing (ephemeral and file-backed), per-host leaf certificate minting with LRU/TTL cache, TLS eligibility routing decisions, verified upstream re-origination, TLS termination with ALPN negotiation, and single-connection HTTP serving. Integrates into forward proxy's transparent and CONNECT paths with verify-first/fail-open semantics, wired via configuration block and main.go engine construction, covered by end-to-end integration tests and comprehensive design documentation.

Changes

AuthBridge Phase 1 TLS Bridge

Layer / File(s) Summary
CA sources (ephemeral and file-backed)
authbridge/authlib/tlsbridge/doc.go, ca.go, ca_test.go
Introduces CASource interface supplying issuer keypair (crypto.Signer + parsed x509.Certificate) and PEM-encoded CA cert. NewEphemeralSource generates in-memory self-signed ECDSA P-256 CA; NewFileSource loads disk-backed CA with multi-format key parsing (PKCS#8, PKCS#1, SEC1) and validation (IsCA, KeyUsageCertSign, cert/key matching).
Per-host certificate minting with LRU+TTL caching
minter.go, minter_test.go
Minter uses shared P-256 leaf key and mutex-protected LRU+TTL cache to forge and cache per-host TLS leaf certificates signed by CA, with automatic eviction of least-recently-used entries and refresh on TTL expiry. Tests validate CA chaining, SAN presence, IP literals, cache reuse, TTL expiry re-minting, and LRU eviction.
TLS eligibility decision and runtime skip set
decision.go, decision_test.go
Decision.Classify gates bridging by port eligibility, TLS record header detection (5-byte pattern validation for content-type 22), static/runtime skip hosts, and optional CIDR-based internal IP gating. Concurrency-safe SkipSet provides auto-skip on pinned-client handshake failures. Default ports: 443, 8443.
Verified upstream HTTP client
upstream.go, upstream_test.go
NewUpstreamClient constructs verified TLS HTTP client using system root certificates plus optional injected PEM roots, enforcing minimum TLS 1.2, disabling redirects, and validating appended PEM at construction time.
TLS termination and single-connection HTTP serving
terminator.go, terminator_test.go, serve.go
Terminator.Terminate wraps net.Conn into tls.Conn using Minter with h2/http/1.1 ALPN and bounded handshake timeout. ServeConn drives terminated connection through http.Handler via http2.Server.ServeConn for h2 or custom oneConnListener for HTTP/1.1 keep-alive across multiple requests.
Engine facade bundling bridge components
engine.go
Aggregates Decision, Terminator, SkipSet, upstream *http.Client, and CA PEM bytes into Engine struct. Implements RunTrustSelfCheck scanning SSL_CERT_FILE, NODE_EXTRA_CA_CERTS, REQUESTS_CA_BUNDLE for bridge CA presence and logging discovery status.
TLS bridge configuration and validation
authbridge/authlib/config/config.go, config_test.go
Adds TLSBridgeConfig with Validate() enforcing scope/ca_source enum values, requiring cert/key paths for file mode, and validating port ranges (1–65535) and CIDR ranges. Implements forceLocalhost() to rewrite session API bind address to 127.0.0.1 when bridge is enabled.
Forward proxy refactoring: serveOutbound and bridge helpers
authbridge/authlib/listener/forwardproxy/sniff.go, server.go
Adds peekedConn.Peek() for lookahead without consuming buffered bytes. Extracts serveOutbound(w, r, isBridge) centralizing outbound pipeline and selecting upstream client (TLSBridge.Upstream when isBridge=true, else default Client). Implements bridgeServe (bounded HEAD probe → TLS termination → serveOutbound replay → fallback tunnel). Adds hostOnly/portOf helpers.
Bridge integration into transparent handler
authbridge/authlib/listener/forwardproxy/transparent.go
Extends HandleTransparentConn with conditional TLS bridge classification: peeks initial bytes, consults skip set, calls Decision.Classify, and on Terminate closes pre-dial upstream and attempts bridgeServe; re-dials and falls back to tunnel on bridging failure.
End-to-end bridge and fallback tests
authbridge/authlib/listener/forwardproxy/tlsbridge_integration_test.go
Five integration tests: transparent bridge happy path (decrypt → pipeline → re-originate), CONNECT bridge path (hijack → TLS over raw conn), unverifiable upstream (fallback to tunnel), pinned client (auto-skip then tunnel), non-TLS passthrough. Uses bridgeProbePlugin (records decrypted request metadata), bufferedConn, listenSniffable.
Engine construction and server wiring
authbridge/cmd/authbridge-proxy/main.go
Conditionally constructs tlsbridge.Engine: selects CA source (file-backed or ephemeral), loads optional upstream CA bundle, creates upstream client, configures Decision/Minter/SkipSet from TLSBridgeConfig, runs RunTrustSelfCheck, logs enablement. Attaches engine to fpSrv.TLSBridge.
Dependency updates and documentation
authbridge/authlib/go.mod, cmd/*/go.mod, docs/superpowers/specs/..., docs/superpowers/plans/...
Updates golang.org/x/{crypto,net,sync,sys,text} versions across all go.mod files. Adds comprehensive design specification (architecture, trust distribution, fail-open semantics, upstream-first verification, security hardening). Includes Phase 1 implementation plan (10 tasks with detailed checklists) and Phase 2 compatibility approach.

Sequence Diagram(s)

sequenceDiagram
  participant Agent as TLS Agent
  participant Proxy as Forward Proxy
  participant Decision
  participant BridgeServe
  participant Upstream
  participant Terminator
  participant Handler as Outbound Pipeline
  participant Origin
  
  rect rgba(100, 180, 100, 0.5)
    note over Agent,Origin: Bridge-eligible TLS (port 443/8443, ClientHello detected)
    Agent->>Proxy: raw TLS bytes (CONNECT or transparent)
    Proxy->>Proxy: peekedConn.Peek(5 bytes)
    Proxy->>Decision: Classify(host, port, first 5 bytes)
    Decision-->>Proxy: Terminate verdict
    Proxy->>Proxy: Check TLSBridge.Skip.Contains(host)
    Proxy->>BridgeServe: verify and bridge
    BridgeServe->>Upstream: HEAD https://authority (bounded timeout)
    Upstream-->>BridgeServe: 200 OK (upstream TLS verified)
    BridgeServe->>Terminator: Terminate(conn, host)
    Terminator-->>BridgeServe: tls.Conn (forged leaf cert via Minter)
    Agent->>Proxy: decrypted HTTP request (over tls.Conn)
    Proxy->>Handler: serveOutbound(isBridge=true)
    Handler->>Handler: plugins, auth normalization, session events
    Handler->>Upstream: re-originate plaintext request
    Upstream->>Origin: GET https://origin (verified TLS, system+injected roots)
    Origin-->>Upstream: HTTP response
    Handler-->>Agent: response
  end
  
  rect rgba(255, 150, 0, 0.5)
    note over Proxy,Origin: Unverifiable upstream or pinned client → fail-open
    alt Upstream TLS verification fails OR client rejects forged leaf
      BridgeServe-->>Proxy: error
      Proxy->>Proxy: close pre-dial, re-dial dst
      Proxy->>Proxy: SkipSet.Add(host) if pinned
      Proxy->>Agent: bidirectional raw tunnel (transparent or CONNECT)
    end
  end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • kagenti/kagenti-extensions#485: Introduced HandleTransparentConn for transparent egress tunneling; this PR extends that same function with TLS-bridge classification, termination, and fall-open branching.
  • kagenti/kagenti-extensions#500: Modifies forward proxy's skip-host bypass logic in forwardproxy/server.go; overlaps with this PR's serveOutbound refactor and skip-set integration in the same code regions.

Suggested reviewers

  • pdettori
  • mrsabath

🐇 I peeked through your TLS today,
Forged a leaf cert to lead you astray,
Upstream verified, the pipeline runs free,
Pinned clients auto-skip with glee!
No InsecureSkipVerify here—safe tunneling, hooray! 🔐

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly summarizes the main change: introducing an outbound TLS bridge feature that decrypts agent egress HTTPS traffic into the processing pipeline.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

The tlsbridge work promoted golang.org/x/net to a direct authlib dep and
bumped it v0.51.0 -> v0.56.0. go.work masks the resulting cmd-module go.mod
staleness locally, but CI builds each cmd binary with GOWORK=off, where
'go fmt/vet ./...' aborts with 'updates to go.mod needed'. Tidy the three
cmd modules (proxy/envoy/lite) so their go.mod/go.sum reflect x/net v0.56.0.

Signed-off-by: Hai Huang <huang195@gmail.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🧹 Nitpick comments (3)
authbridge/authlib/config/config_test.go (1)

733-751: ⚡ Quick win

Expand TLS bridge tests to pin validation failures.

Line 733 currently validates only the happy decode path. Please add negative cases for invalid scope, invalid ca_source, and ca_source=file without ca_cert_path/ca_key_path so Lines 60-68 in config.go stay regression-proof.

🧪 Suggested test shape
+func TestConfig_TLSBridgeValidationErrors(t *testing.T) {
+	cases := []struct {
+		name string
+		yaml string
+	}{
+		{
+			name: "invalid scope",
+			yaml: "mode: proxy-sidecar\ntls_bridge:\n  enabled: true\n  scope: internet\n",
+		},
+		{
+			name: "invalid ca_source",
+			yaml: "mode: proxy-sidecar\ntls_bridge:\n  enabled: true\n  ca_source: vault\n",
+		},
+		{
+			name: "file source missing paths",
+			yaml: "mode: proxy-sidecar\ntls_bridge:\n  enabled: true\n  ca_source: file\n",
+		},
+	}
+	for _, tc := range cases {
+		t.Run(tc.name, func(t *testing.T) {
+			p := filepath.Join(t.TempDir(), "cfg.yaml")
+			if err := os.WriteFile(p, []byte(tc.yaml), 0o600); err != nil {
+				t.Fatal(err)
+			}
+			if _, err := Load(p); err == nil {
+				t.Fatalf("expected Load error for %s", tc.name)
+			}
+		})
+	}
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@authbridge/authlib/config/config_test.go` around lines 733 - 751, The test
TestConfig_TLSBridgeBlockDecodes currently only validates the happy path for TLS
bridge configuration decoding. Add three additional test functions to cover
negative validation cases: create TestConfig_TLSBridgeInvalidScope to test
rejected scope values, TestConfig_TLSBridgeInvalidCASource to test rejected
ca_source values, and TestConfig_TLSBridgeFileMissingPaths to test ca_source set
to file without providing ca_cert_path and ca_key_path. Each test should follow
the same pattern as the existing test by writing invalid YAML configurations to
a temporary file, calling Load() on the config path, and using t.Fatalf to
assert that Load() returns an error (not nil) to ensure the validation logic in
config.go lines 60-68 properly rejects these invalid configurations.
authbridge/authlib/listener/forwardproxy/tlsbridge_integration_test.go (1)

71-80: 💤 Low value

Consider using the listenSniffable helper.

This inline port-binding logic duplicates the listenSniffable() helper defined at lines 406-417. Refactoring to use the helper would improve consistency with the other tests.

♻️ Suggested refactor
-	var ln net.Listener
-	for _, p := range []string{"8443", "8080"} {
-		if l, e := net.Listen("tcp", "127.0.0.1:"+p); e == nil {
-			ln = l
-			break
-		}
-	}
-	if ln == nil {
+	ln := listenSniffable()
+	if ln == nil {
 		t.Skip("transparent bridge test needs a free sniffable port (8443 or 8080)")
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@authbridge/authlib/listener/forwardproxy/tlsbridge_integration_test.go`
around lines 71 - 80, The inline port-binding logic in the test that iterates
through ports 8443 and 8080 to find a free listener duplicates functionality
already provided by the listenSniffable() helper function. Replace the entire
for loop and conditional check (the loop over ports and the ln == nil check)
with a single call to listenSniffable(), which will handle finding a free
sniffable port and skipping the test appropriately if none is available. This
eliminates code duplication and improves consistency with other tests in the
file.
authbridge/authlib/tlsbridge/ca_test.go (1)

42-79: ⚡ Quick win

Add an RSA PKCS#1 case to cover the advertised key-format support.

Lines 42-79 validate EC PKCS#8 and SEC1, but not RSA PKCS#1. Since parsePrivateKey claims PKCS#1 support, a dedicated case here would prevent regressions in that path.

🧪 Example test extension
 func TestFileSource_LoadsPKCS1AndPKCS8(t *testing.T) {
@@
 	}
+
+	t.Run("rsa-pkcs1", func(t *testing.T) {
+		key, err := rsa.GenerateKey(rand.Reader, 2048)
+		if err != nil {
+			t.Fatalf("rsa.GenerateKey: %v", err)
+		}
+		tmpl := &x509.Certificate{
+			SerialNumber: big.NewInt(2), Subject: pkix.Name{CommonName: "t-rsa"},
+			NotBefore: time.Now().Add(-time.Minute), NotAfter: time.Now().Add(time.Hour),
+			IsCA: true, KeyUsage: x509.KeyUsageCertSign, BasicConstraintsValid: true,
+		}
+		der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, key.Public(), key)
+		if err != nil {
+			t.Fatalf("CreateCertificate: %v", err)
+		}
+		dir := t.TempDir()
+		certP := filepath.Join(dir, "tls.crt")
+		keyP := filepath.Join(dir, "tls.key")
+		if err := os.WriteFile(certP, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}), 0o600); err != nil {
+			t.Fatalf("write cert: %v", err)
+		}
+		keyDER := x509.MarshalPKCS1PrivateKey(key)
+		if err := os.WriteFile(keyP, pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: keyDER}), 0o600); err != nil {
+			t.Fatalf("write key: %v", err)
+		}
+		if _, err := NewFileSource(certP, keyP); err != nil {
+			t.Fatalf("NewFileSource(rsa-pkcs1): %v", err)
+		}
+	})
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@authbridge/authlib/tlsbridge/ca_test.go` around lines 42 - 79, The
table-driven test TestFileSource_LoadsPKCS1AndPKCS8 currently only tests EC key
formats (PKCS#8 and SEC1) but does not test RSA PKCS#1 format despite
parsePrivateKey claiming to support it. Add a new test case to the test table
for RSA PKCS#1 format by generating an RSA private key using rsa.GenerateKey
instead of ecdsa.GenerateKey, marshaling it using x509.MarshalPKCS1PrivateKey,
and using "RSA PRIVATE KEY" as the PEM block type to ensure the RSA PKCS#1 code
path is properly tested and prevent regressions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@authbridge/authlib/tlsbridge/ca.go`:
- Around line 3-15: The NewFileSource function parses certificate and private
key files without validating that the certificate is suitable for CA operations
or that the keys match, which allows invalid configurations to pass
initialization silently. Add validation checks in the NewFileSource function
after parsing the certificate to verify that cert.IsCA is true and that
cert.KeyUsage includes x509.KeyUsageCertSign, then use the public key's Equal()
method (the idiomatic Go approach) to verify that the public key embedded in the
certificate matches the provided private key. These checks should return an
error immediately if any validation fails, catching configuration errors at
startup rather than during certificate signing operations.

In `@authbridge/authlib/tlsbridge/decision.go`:
- Around line 95-110: The SkipSet struct's map field m can grow unbounded as
hosts are added via the Add method, leading to memory accumulation in long-lived
processes. Implement a size cap mechanism by adding a maximum capacity field to
the SkipSet struct and checking the map size in the Add method before inserting
new entries. When the map reaches its capacity limit, either reject new
additions, evict the least recently used entry, or implement a TTL-based
eviction policy by tracking entry timestamps and periodically removing stale
entries. Update the NewSkipSet constructor to initialize the capacity limit.

In `@authbridge/authlib/tlsbridge/engine.go`:
- Around line 26-35: The code does not validate that caPEM is non-empty before
performing substring matching in the for loop. If caPEM is empty or contains
only whitespace, strings.Contains(string(b), want) will always return true since
an empty string is contained in all strings, causing a false positive "OK" trust
result to be logged. Add an early non-empty validation check on the want
variable immediately after the line want := strings.TrimSpace(string(caPEM)) and
before the for loop that iterates over the SSL_CERT_FILE, NODE_EXTRA_CA_CERTS,
and REQUESTS_CA_BUNDLE environment variables to ensure want has actual content
before proceeding with the substring matching logic.

In `@authbridge/authlib/tlsbridge/minter.go`:
- Around line 48-50: The ecdsa.GenerateKey call in the Minter constructor is
discarding the error return value, which allows nil to be assigned to the
leafKey field if key generation fails. This causes a panic later when
leafKey.Public() is called. Instead of ignoring the error with the underscore
operator, capture both the key and error from ecdsa.GenerateKey, check if an
error occurred, and return that error early from the constructor. This requires
adding an error return type to the constructor function if it doesn't already
have one, and returning an error value when key generation fails to fail fast at
construction time.

In `@authbridge/authlib/tlsbridge/terminator.go`:
- Around line 28-31: The conn.Handshake() call in the TLS Server handshake block
can block indefinitely if the client stalls during the handshake process. Before
calling conn.Handshake(), set a read deadline on the conn object using
SetReadDeadline() with a short timeout value. After the handshake completes
successfully (after the error check passes), clear the deadline by calling
SetReadDeadline() again with a zero time value to allow normal operation without
the timeout constraint.

In `@authbridge/authlib/tlsbridge/upstream.go`:
- Around line 25-35: The http.Client returned from this function is missing
timeout protections for upstream verification calls. Add a ResponseHeaderTimeout
field to the http.Transport configuration to prevent blocking when an upstream
server completes the TLS handshake but stalls on sending response headers, and
also add a Timeout field to the http.Client itself to ensure overall request
completion bounds. These timeouts should be set to reasonable values (e.g.,
ResponseHeaderTimeout around 10 seconds similar to TLSHandshakeTimeout, and
client Timeout slightly higher to accommodate the handshake and header
timeouts).

In `@authbridge/docs/superpowers/plans/2026-06-17-authbridge-tlsbridge-phase1.md`:
- Around line 28-29: Correct the logging guidance in the plan document at lines
28-29. The current text states that main.go uses slog and os.Exit rather than
log.Fatalf, but this conflicts with the actual current convention where
log.Fatal* is intentionally used in cmd-entrypoint bootstrap paths
(authbridge-cpex, authbridge-proxy, authbridge-envoy, authbridge-lite). Update
the documentation to accurately reflect that log.Fatal* is used in the main
wiring section, and add a note that this convention is intentional and will be
refactored during the shared Run() extraction phase mentioned in Task 6.

In `@authbridge/docs/superpowers/specs/2026-06-12-authbridge-tlsbridge-design.md`:
- Around line 68-76: Update the specification document to align package and
configuration names with the actual implementation. Replace all occurrences of
"authlib/mitm/" with "authlib/tlsbridge/" throughout the document. Additionally,
replace all instances of "mitm:" with "tls_bridge" and rename "MITMConfig" and
"isMITM" to "TLSBridgeConfig" to match the shipped interface. These changes need
to be applied across the table starting at line 68 and any other sections that
reference these identifiers (including lines 207 and 252) to maintain
consistency between the specification and the actual implementation.

---

Nitpick comments:
In `@authbridge/authlib/config/config_test.go`:
- Around line 733-751: The test TestConfig_TLSBridgeBlockDecodes currently only
validates the happy path for TLS bridge configuration decoding. Add three
additional test functions to cover negative validation cases: create
TestConfig_TLSBridgeInvalidScope to test rejected scope values,
TestConfig_TLSBridgeInvalidCASource to test rejected ca_source values, and
TestConfig_TLSBridgeFileMissingPaths to test ca_source set to file without
providing ca_cert_path and ca_key_path. Each test should follow the same pattern
as the existing test by writing invalid YAML configurations to a temporary file,
calling Load() on the config path, and using t.Fatalf to assert that Load()
returns an error (not nil) to ensure the validation logic in config.go lines
60-68 properly rejects these invalid configurations.

In `@authbridge/authlib/listener/forwardproxy/tlsbridge_integration_test.go`:
- Around line 71-80: The inline port-binding logic in the test that iterates
through ports 8443 and 8080 to find a free listener duplicates functionality
already provided by the listenSniffable() helper function. Replace the entire
for loop and conditional check (the loop over ports and the ln == nil check)
with a single call to listenSniffable(), which will handle finding a free
sniffable port and skipping the test appropriately if none is available. This
eliminates code duplication and improves consistency with other tests in the
file.

In `@authbridge/authlib/tlsbridge/ca_test.go`:
- Around line 42-79: The table-driven test TestFileSource_LoadsPKCS1AndPKCS8
currently only tests EC key formats (PKCS#8 and SEC1) but does not test RSA
PKCS#1 format despite parsePrivateKey claiming to support it. Add a new test
case to the test table for RSA PKCS#1 format by generating an RSA private key
using rsa.GenerateKey instead of ecdsa.GenerateKey, marshaling it using
x509.MarshalPKCS1PrivateKey, and using "RSA PRIVATE KEY" as the PEM block type
to ensure the RSA PKCS#1 code path is properly tested and prevent regressions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 79ac147c-25e7-4feb-a981-2209d8186763

📥 Commits

Reviewing files that changed from the base of the PR and between c94b132 and e76eb80.

⛔ Files ignored due to path filters (1)
  • authbridge/authlib/go.sum is excluded by !**/*.sum
📒 Files selected for processing (23)
  • authbridge/authlib/config/config.go
  • authbridge/authlib/config/config_test.go
  • authbridge/authlib/go.mod
  • authbridge/authlib/listener/forwardproxy/server.go
  • authbridge/authlib/listener/forwardproxy/sniff.go
  • authbridge/authlib/listener/forwardproxy/tlsbridge_integration_test.go
  • authbridge/authlib/listener/forwardproxy/transparent.go
  • authbridge/authlib/tlsbridge/ca.go
  • authbridge/authlib/tlsbridge/ca_test.go
  • authbridge/authlib/tlsbridge/decision.go
  • authbridge/authlib/tlsbridge/decision_test.go
  • authbridge/authlib/tlsbridge/doc.go
  • authbridge/authlib/tlsbridge/engine.go
  • authbridge/authlib/tlsbridge/minter.go
  • authbridge/authlib/tlsbridge/minter_test.go
  • authbridge/authlib/tlsbridge/serve.go
  • authbridge/authlib/tlsbridge/terminator.go
  • authbridge/authlib/tlsbridge/terminator_test.go
  • authbridge/authlib/tlsbridge/upstream.go
  • authbridge/authlib/tlsbridge/upstream_test.go
  • authbridge/cmd/authbridge-proxy/main.go
  • authbridge/docs/superpowers/plans/2026-06-17-authbridge-tlsbridge-phase1.md
  • authbridge/docs/superpowers/specs/2026-06-12-authbridge-tlsbridge-design.md

Comment thread authbridge/authlib/tlsbridge/ca.go
Comment thread authbridge/authlib/tlsbridge/decision.go
Comment thread authbridge/authlib/tlsbridge/engine.go
Comment thread authbridge/authlib/tlsbridge/minter.go Outdated
Comment thread authbridge/authlib/tlsbridge/terminator.go
Comment thread authbridge/authlib/tlsbridge/upstream.go
Comment thread authbridge/docs/superpowers/plans/2026-06-17-authbridge-tlsbridge-phase1.md Outdated
huang195 added 2 commits June 17, 2026 22:35
…eview rossoctl#522)

- TLSBridgeConfig.Validate now rejects malformed internal_cidrs entries so an
  operator CIDR typo fails loudly at load instead of being silently dropped by
  Decision (which would stop excluding an intended in-cluster range under
  scope=external). Adds TestTLSBridgeConfig_Validate covering scope/ca_source/
  file-paths/CIDR cases.
- Update the TLSBridgeConfig doc comment: 'MITM termination' -> 'TLS termination
  (formerly MITM)' for grep-consistency with the rename.

Signed-off-by: Hai Huang <huang195@gmail.com>
…f-check; fail-fast keygen (review rossoctl#522)

Address CodeRabbit findings on PR rossoctl#522:
- Terminator.Terminate now sets a 10s handshake deadline (cleared on success
  so it doesn't bound the kept-alive served conn). Nothing upstream bounded the
  client conn — sniffHost clears its read deadline and the CONNECT peek has none
  — so a stalled client could pin the serving goroutine indefinitely.
- bridgeServe bounds the pre-forge HEAD reachability/cert probe with a 10s
  context timeout (on the probe ONLY — NOT the relay, which must stay unbounded
  for streaming responses; adding ResponseHeaderTimeout to the shared upstream
  client would reintroduce the streaming-MCP 502 the main client's comment warns
  against). CodeRabbit's 'Critical' on upstream.go was not a TLS-bypass — RootCAs/
  MinVersion/no-skip-verify are correct; the real issue was an unbounded verify.
- RunTrustSelfCheck returns early on empty CA PEM (strings.Contains(b,"") would
  otherwise log a false 'trust OK').
- NewMinter no longer discards the ecdsa.GenerateKey error (latent nil-deref in
  mint); panics at construction on the effectively-impossible failure.
- Plan doc: correct the main-logging guidance (cmd entrypoint uses log.Fatalf,
  not slog+os.Exit).

Deferred (per reviewer / Phase 2): bound SkipSet + per-host verify cache (LRU/TTL);
NewFileSource CA IsCA/key-match validation (cert-manager path). Spec mitm-identifier
drift is covered by the spec's existing terminology disclaimer.

Signed-off-by: Hai Huang <huang195@gmail.com>

@pdettori pdettori left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Solid Phase 1 implementation of the outbound TLS bridge. The security posture is sound: crypto/rand throughout, P-256 keys, upstream TLS verification never disabled, fall-open design thoroughly tested, and no secrets logged. The package decomposition is clean with well-defined boundaries.

Three hardening suggestions for Phase 2 consideration below. No blockers.

Areas reviewed: Go (security, concurrency, resource management), Docs
Commits: 18 commits, all signed-off
CI status: all passing


// SkipSet is the runtime auto-skip set (hosts whose minted leaf the client
// rejected). Concurrent-safe; augments the static skip list.
type SkipSet struct {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: SkipSet grows unboundedly as hosts are auto-skipped on handshake failure. Under adversarial conditions (many unique hosts failing verification), this map could exhaust memory. Consider adding a max-size cap or TTL-based expiration for Phase 2.

// Terminate completes the server-side TLS handshake against the agent. host is
// the dialed name/IP, used to mint when the ClientHello carries no SNI.
func (t *Terminator) Terminate(client net.Conn, host string) (*tls.Conn, error) {
cfg := &tls.Config{

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: The server-side tls.Config doesn't explicitly set MinVersion. Go defaults to TLS 1.2 since 1.18, but an explicit MinVersion: tls.VersionTLS12 would make the security posture visible and guard against future Go default changes.

// keep-alive, negotiating h2 when ALPN selected it. It blocks until the
// connection is closed (like a tunnel), so callers can serve it synchronously.
func ServeConn(tconn *tls.Conn, handler http.Handler) {
srv := &http.Server{Handler: handler}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: The per-conn http.Server has no IdleTimeout or ReadHeaderTimeout. If an agent holds a bridged connection open without sending data, the goroutine is pinned indefinitely. Consider IdleTimeout: 5*time.Minute for the h1.1 keep-alive path to prevent resource accumulation.

huang195 added 6 commits June 18, 2026 08:42
Add an explicit 'Protocol & port coverage' section to the design spec: the
bridge is HTTPS-inspection (TLS + configured port + trusted CA), not general
egress inspection. Tabulates the no-visibility categories (non-TLS TCP like
SSH/DBs, TLS on non-standard ports, STARTTLS, non-TCP/QUIC, un-MITM-able
pinned/mTLS/ECH), documents why the port gate is load-bearing (ServeConn
assumes HTTP; bridging non-HTTP TLS like LDAPS/SMTPS would break the conn),
and names connection-level egress policy as the backstop. Records envoy-sidecar
bridging and non-HTTP protocols as non-goals.

Signed-off-by: Hai Huang <huang195@gmail.com>
…,8443)

Adds tls_bridge.ports to TLSBridgeConfig (empty => {443,8443}), threads it
into Decision via main.go, and unifies the transparent listener's sniff gate
with the bridge's port set: shouldSniff() keeps its host-recovery heuristic
{80,443,8080,8443}, but the transparent path now ALSO sniffs whatever ports
the bridge intercepts (Decision.HandlesPort), so a configured non-standard
port (e.g. 9443) gets the peekable conn the bridge needs — no drift between
the two lists. Validation rejects out-of-range ports. New
TestTransparentBridge_CustomPort proves the bridge engages on a random
ephemeral port (outside shouldSniff's set) when configured.

Keep ports HTTP(S)-only: the bridge serves the decrypted stream as HTTP/1.1
or h2, so non-HTTP TLS (LDAPS/SMTPS/DB-over-TLS) must NOT be added here.

Signed-off-by: Hai Huang <huang195@gmail.com>
The tls_bridge schema is new/unreleased — tighten it now, before the operator
renders it and an alpha pins it:

- Collapse enabled+scope -> mode: disabled|enabled (matches operator
  tlsBridgeMode 1:1). Drop the external/all distinction and the whole
  Scope/isInternal/internal_cidrs machinery in Decision — the bridge
  intercepts everything eligible on the configured ports, period. Classify
  loses its now-unused ip param.
- Drop ca_source + ca_cert_path + ca_key_path -> single ca_dir (cert-manager
  Secret key conventions tls.crt/tls.key/ca.crt). ephemeral was never usable
  in production (a real agent can't trust an unexported in-memory CA); it
  survives only as NewEphemeralSource for in-process tests, not a config knob.
  Production CA is always the operator-mounted file.
- Rename skip_hosts -> passthrough_hosts to disambiguate from the existing
  listener.skip_hosts (full pipeline bypass vs bridge tunnel).

Net: 6 fields -> mode + ca_dir + passthrough_hosts (+ existing ports,
upstream_ca_bundle). No migration — nothing consumes the schema yet.

Signed-off-by: Hai Huang <huang195@gmail.com>
As-built note + corrected YAML: mode|ca_dir|passthrough_hosts|ports|
upstream_ca_bundle (dropped enabled/scope/internal_cidrs/ca_source/cert+key
paths/Name-Constraints; ephemeral is test-only).

Signed-off-by: Hai Huang <huang195@gmail.com>
…Secret

Two operator-path hardenings, both load-time:
- When tls_bridge.mode=enabled, Load() rewrites listener.session_api_addr to
  127.0.0.1 (preserving the port) so the session API — which now carries
  decrypted bodies — isn't reachable by other pods. kubectl port-forward
  (abctl) still works (it targets the pod's loopback). No auth, no redaction
  yet; this just shrinks the reachable surface.
- NewFileSource now fails loud on a misissued cert-manager Secret: rejects a
  non-CA cert (IsCA=false / missing KeyUsageCertSign) and a cert/key public-key
  mismatch. Previously these loaded 'fine' and silently failed at signing time
  → every call tunnels with no error. Turns a misconfigured Secret into a clear
  startup failure.

Tests: TestForceLocalhost, TestLoad_TLSBridgeHardensSessionAPI,
TestFileSource_RejectsNonCAOrMismatch.

Signed-off-by: Hai Huang <huang195@gmail.com>
…idual MITM comment

- SkipSet was an unbounded map. With scope/CIDR removed, all eligible egress
  flows through the bridge, so a flood of distinct SNIs each rejecting the
  forged leaf could grow it without bound. Add a 10m TTL (self-healing: a
  transiently-pinned/rotated client gets re-attempted) and a 4096 cap with
  oldest-expiry eviction on the cold Add path. Contains stays an RLock read
  (expired entries read as absent; Add reclaims them). Tests cover TTL expiry
  and the size bound.
- main.go: drop a residual '(MITM)' in a comment (rename completed elsewhere).

Defers (per reviewer): caching upstream-verify success per host is a pure
optimization that trades verify-freshness for fewer probes — left for Phase 2.

Signed-off-by: Hai Huang <huang195@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

3 participants