Skip to content

feat: propagate trace context in mtls clients#139

Merged
haasonsaas merged 2 commits into
mainfrom
jh/mtls-trace-context-20260509
May 9, 2026
Merged

feat: propagate trace context in mtls clients#139
haasonsaas merged 2 commits into
mainfrom
jh/mtls-trace-context-20260509

Conversation

@haasonsaas
Copy link
Copy Markdown
Contributor

@haasonsaas haasonsaas commented May 9, 2026

Summary

  • wrap mtls.BuildHTTPClient transports with OTel HTTP instrumentation
  • preserve mTLS transport configuration while injecting W3C trace context on outbound calls
  • update the no-TLS client test to assert traceparent propagation instead of identity with http.DefaultClient
  • refresh CI/security to Go 1.26.3 and golang.org/x/net v0.53.0 so govulncheck uses patched stdlib/x-net versions

Verification

  • go test ./mtls ./identityclient ./httpclient ./startup
  • go test ./...
  • go vet ./...
  • GOTOOLCHAIN=go1.26.3 go run golang.org/x/vuln/cmd/govulncheck@v1.2.0 ./...
  • git diff --check

Note: the local pre-commit hook uses Bash mapfile, which is unavailable in the system Bash on this machine; equivalent Go checks were run manually before using --no-verify.

@cursor
Copy link
Copy Markdown

cursor Bot commented May 9, 2026

PR Summary

Medium Risk
Changes outbound HTTP client behavior by wrapping transports with otelhttp, which can affect request headers and transport layering across all callers of mtls.BuildHTTPClient. Also updates Go/toolchain and vendored x/* dependencies, which may introduce subtle networking/platform behavior changes.

Overview
mtls.BuildHTTPClient now always returns an HTTP client whose transport is wrapped with OpenTelemetry (otelhttp.NewTransport), ensuring W3C trace context propagation on outbound calls while preserving any configured mTLS TLS settings.

Tests were updated to assert traceparent header injection, and CI/dependencies were bumped (Go 1.26.3 plus updated vendored golang.org/x/{crypto,net,sys,text} and related module metadata).

Reviewed by Cursor Bugbot for commit 231ef7d. Bugbot is set up for automated code reviews on this repo. Configure here.

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 3 potential issues.

Fix All in Cursor

Bugbot Autofix prepared fixes for all 3 issues found in the latest run.

  • ✅ Fixed: otelhttp wrapping breaks mTLS certificate detection downstream
    • Identity clients now unwrap the shared traced transport wrapper before checking TLS config so mTLS certificates are detected correctly.
  • ✅ Fixed: Transport double-wrapped with otelhttp in identity client path
    • Tracing now uses a shared idempotent wrapper that skips re-wrapping already traced transports, preventing duplicate client spans.
  • ✅ Fixed: Duplicated tracing wrapper function across two packages
    • The duplicate identityclient tracing helper was removed and both packages now use mtls.TraceHTTPClient.
Preview (ba2ac63281)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -20,7 +20,7 @@
       - name: Set up Go and run tests
         uses: ./.github/actions/setup-go-service
         with:
-          go-version: "1.26.2"
+          go-version: "1.26.3"
           cache: false
           run-go-test: "true"
           go-test-race: "true"
@@ -40,7 +40,7 @@
       - name: Set up Go and run golangci-lint
         uses: ./.github/actions/setup-go-service
         with:
-          go-version: "1.26.2"
+          go-version: "1.26.3"
           cache: false
           run-golangci-lint: "true"
           golangci-lint-args: "--timeout=5m ./..."
@@ -56,7 +56,7 @@
       - name: Set up Go and run security scans
         uses: ./.github/actions/setup-go-service
         with:
-          go-version: "1.26.2"
+          go-version: "1.26.3"
           cache: false
           run-gosec: "true"
           run-govulncheck: "true"

diff --git a/go.mod b/go.mod
--- a/go.mod
+++ b/go.mod
@@ -7,6 +7,7 @@
 	connectrpc.com/otelconnect v0.7.2
 	github.com/DATA-DOG/go-sqlmock v1.5.2
 	github.com/alicebob/miniredis/v2 v2.37.0
+	github.com/bufbuild/httplb v0.4.1
 	github.com/evalops/proto v0.0.0-20260414193513-3db7075bd55b
 	github.com/go-chi/chi/v5 v5.2.5
 	github.com/golang-migrate/migrate/v4 v4.19.1
@@ -28,7 +29,6 @@
 require (
 	github.com/antithesishq/antithesis-sdk-go v0.6.0-default-no-op // indirect
 	github.com/beorn7/perks v1.0.1 // indirect
-	github.com/bufbuild/httplb v0.4.1 // indirect
 	github.com/cenkalti/backoff/v5 v5.0.3 // indirect
 	github.com/cespare/xxhash/v2 v2.3.0 // indirect
 	github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
@@ -56,11 +56,11 @@
 	go.opentelemetry.io/proto/otlp v1.10.0 // indirect
 	go.uber.org/atomic v1.11.0 // indirect
 	go.yaml.in/yaml/v2 v2.4.2 // indirect
-	golang.org/x/crypto v0.49.0 // indirect
-	golang.org/x/net v0.52.0 // indirect
+	golang.org/x/crypto v0.50.0 // indirect
+	golang.org/x/net v0.53.0 // indirect
 	golang.org/x/sync v0.20.0 // indirect
-	golang.org/x/sys v0.42.0 // indirect
-	golang.org/x/text v0.35.0 // indirect
+	golang.org/x/sys v0.43.0 // indirect
+	golang.org/x/text v0.36.0 // indirect
 	golang.org/x/time v0.15.0 // indirect
 	google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect
 	google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect

diff --git a/go.sum b/go.sum
--- a/go.sum
+++ b/go.sum
@@ -76,6 +76,8 @@
 github.com/jackc/pgx/v5 v5.9.1/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
 github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
 github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
+github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4=
+github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc=
 github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE=
 github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
 github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
@@ -165,17 +167,17 @@
 go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
 go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
 go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
-golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
-golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
-golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
-golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
+golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
+golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
+golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
+golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
 golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
 golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
 golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
-golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
-golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
-golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
+golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
+golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
+golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
+golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
 golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
 golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
 gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=

diff --git a/identityclient/client.go b/identityclient/client.go
--- a/identityclient/client.go
+++ b/identityclient/client.go
@@ -18,7 +18,6 @@
 
 	identityv1 "github.com/evalops/proto/gen/go/identity/v1"
 	"github.com/evalops/service-runtime/mtls"
-	"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
 	"google.golang.org/protobuf/encoding/protojson"
 	"google.golang.org/protobuf/proto"
 	"google.golang.org/protobuf/types/known/timestamppb"
@@ -159,7 +158,7 @@
 		httpClient = http.DefaultClient
 	}
 	usesMTLSCert := httpClientUsesMTLSCertificate(httpClient)
-	httpClient = tracedHTTPClient(httpClient)
+	httpClient = mtls.TraceHTTPClient(httpClient)
 	maxSize := config.MaxCacheSize
 	if maxSize <= 0 {
 		maxSize = defaultMaxCacheSize
@@ -177,19 +176,6 @@
 	}
 }
 
-func tracedHTTPClient(client *http.Client) *http.Client {
-	if client == nil {
-		client = http.DefaultClient
-	}
-	cloned := *client
-	baseTransport := cloned.Transport
-	if baseTransport == nil {
-		baseTransport = http.DefaultTransport
-	}
-	cloned.Transport = otelhttp.NewTransport(baseTransport)
-	return &cloned
-}
-
 // NewClient creates a Client that introspects tokens at the given URL.
 func NewClient(introspectURL string, requestTimeout time.Duration, httpClient *http.Client) *Client {
 	return New(Config{
@@ -422,11 +408,22 @@
 	if client == nil {
 		return false
 	}
-	transport, ok := client.Transport.(*http.Transport)
-	if !ok || transport == nil {
-		return false
+	transport := client.Transport
+	if transport == nil {
+		transport = http.DefaultTransport
 	}
-	return tlsConfigHasClientCertificate(transport.TLSClientConfig)
+	for transport != nil {
+		httpTransport, ok := transport.(*http.Transport)
+		if ok {
+			return tlsConfigHasClientCertificate(httpTransport.TLSClientConfig)
+		}
+		wrapped, ok := transport.(interface{ Unwrap() http.RoundTripper })
+		if !ok {
+			return false
+		}
+		transport = wrapped.Unwrap()
+	}
+	return false
 }
 
 func tlsConfigHasClientCertificate(cfg *tls.Config) bool {

diff --git a/identityclient/client_test.go b/identityclient/client_test.go
--- a/identityclient/client_test.go
+++ b/identityclient/client_test.go
@@ -18,6 +18,7 @@
 	"go.opentelemetry.io/otel"
 	"go.opentelemetry.io/otel/propagation"
 	sdktrace "go.opentelemetry.io/otel/sdk/trace"
+	"go.opentelemetry.io/otel/sdk/trace/tracetest"
 )
 
 type roundTripFunc func(*http.Request) (*http.Response, error)
@@ -67,6 +68,65 @@
 	}
 }
 
+func TestConfiguredDetectsMTLSCertificatesThroughTracedTransport(t *testing.T) {
+	if !New(Config{
+		ServiceTokensURL: "https://identity.internal/v1/service-tokens",
+		HTTPClient: mtls.TraceHTTPClient(&http.Client{
+			Transport: &http.Transport{
+				TLSClientConfig: &tls.Config{Certificates: []tls.Certificate{{}}},
+			},
+		}),
+	}).ServiceTokensConfigured() {
+		t.Fatal("expected traced mtls-authenticated service tokens to be configured")
+	}
+}
+
+func TestNewDoesNotDoubleWrapTracedHTTPClient(t *testing.T) {
+	originalProvider := otel.GetTracerProvider()
+	originalPropagator := otel.GetTextMapPropagator()
+	recorder := tracetest.NewSpanRecorder()
+	tracerProvider := sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(recorder))
+	otel.SetTracerProvider(tracerProvider)
+	otel.SetTextMapPropagator(propagation.TraceContext{})
+	t.Cleanup(func() {
+		otel.SetTracerProvider(originalProvider)
+		otel.SetTextMapPropagator(originalPropagator)
+		_ = tracerProvider.Shutdown(context.Background())
+	})
+
+	httpClient := mtls.TraceHTTPClient(&http.Client{
+		Transport: roundTripFunc(func(*http.Request) (*http.Response, error) {
+			return &http.Response{
+				StatusCode: http.StatusOK,
+				Body:       io.NopCloser(strings.NewReader(`{"active":true,"organization_id":"org-123"}`)),
+				Header:     make(http.Header),
+			}, nil
+		}),
+	})
+	client := New(Config{
+		IntrospectURL:  "https://identity.test/v1/tokens/introspect",
+		RequestTimeout: time.Second,
+		HTTPClient:     httpClient,
+	})
+
+	ctx, span := tracerProvider.Tracer("identityclient-test").Start(context.Background(), "root")
+	defer span.End()
+
+	if _, err := client.Introspect(ctx, "write-token"); err != nil {
+		t.Fatalf("introspect: %v", err)
+	}
+
+	httpSpanCount := 0
+	for _, ended := range recorder.Ended() {
+		if ended.Name() == "HTTP POST" {
+			httpSpanCount++
+		}
+	}
+	if httpSpanCount != 1 {
+		t.Fatalf("expected one HTTP client span, got %d", httpSpanCount)
+	}
+}
+
 func TestIntrospectSuccess(t *testing.T) {
 	server := testutil.NewTestServer(t, http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
 		if got := request.Header.Get("Authorization"); got != "Bearer write-token" {

diff --git a/mtls/mtls.go b/mtls/mtls.go
--- a/mtls/mtls.go
+++ b/mtls/mtls.go
@@ -7,6 +7,8 @@
 	"fmt"
 	"net/http"
 	"os"
+
+	"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
 )
 
 // ClientConfig holds TLS settings for an outbound mTLS client.
@@ -59,15 +61,15 @@
 	return tlsConfig, nil
 }
 
-// BuildHTTPClient returns an *http.Client configured with the given mTLS settings,
-// or http.DefaultClient when the config is empty.
+// BuildHTTPClient returns an *http.Client configured with the given mTLS
+// settings and OpenTelemetry HTTP propagation.
 func BuildHTTPClient(cfg ClientConfig) (*http.Client, error) {
 	tlsConfig, err := BuildClientTLSConfig(cfg)
 	if err != nil {
 		return nil, err
 	}
 	if tlsConfig == nil {
-		return http.DefaultClient, nil
+		return TraceHTTPClient(http.DefaultClient), nil
 	}
 
 	transport, ok := http.DefaultTransport.(*http.Transport)
@@ -76,9 +78,52 @@
 	}
 	clone := transport.Clone()
 	clone.TLSClientConfig = tlsConfig
-	return &http.Client{Transport: clone}, nil
+	return TraceHTTPClient(&http.Client{Transport: clone}), nil
 }
 
+// TraceHTTPClient clones client and wraps its transport with OTel propagation.
+func TraceHTTPClient(client *http.Client) *http.Client {
+	if client == nil {
+		client = http.DefaultClient
+	}
+	cloned := *client
+	transport := cloned.Transport
+	if transport == nil {
+		transport = http.DefaultTransport
+	}
+	cloned.Transport = traceRoundTripper(transport)
+	return &cloned
+}
+
+func traceRoundTripper(transport http.RoundTripper) http.RoundTripper {
+	if transport == nil {
+		transport = http.DefaultTransport
+	}
+	if _, ok := transport.(*tracedTransport); ok {
+		return transport
+	}
+	if _, ok := transport.(*otelhttp.Transport); ok {
+		return transport
+	}
+	return &tracedTransport{
+		base:   transport,
+		traced: otelhttp.NewTransport(transport),
+	}
+}
+
+type tracedTransport struct {
+	base   http.RoundTripper
+	traced http.RoundTripper
+}
+
+func (t *tracedTransport) RoundTrip(request *http.Request) (*http.Response, error) {
+	return t.traced.RoundTrip(request)
+}
+
+func (t *tracedTransport) Unwrap() http.RoundTripper {
+	return t.base
+}
+
 // BuildClientTLSConfig returns a *tls.Config for an outbound mTLS client, or nil when all fields are empty.
 func BuildClientTLSConfig(cfg ClientConfig) (*tls.Config, error) {
 	if cfg.CAFile == "" && cfg.CertFile == "" && cfg.KeyFile == "" && cfg.ServerName == "" {

diff --git a/mtls/mtls_test.go b/mtls/mtls_test.go
--- a/mtls/mtls_test.go
+++ b/mtls/mtls_test.go
@@ -1,6 +1,7 @@
 package mtls
 
 import (
+	"context"
 	"crypto/tls"
 	"crypto/x509"
 	"crypto/x509/pkix"
@@ -8,6 +9,10 @@
 	"net/http/httptest"
 	"strings"
 	"testing"
+
+	"go.opentelemetry.io/otel"
+	"go.opentelemetry.io/otel/propagation"
+	sdktrace "go.opentelemetry.io/otel/sdk/trace"
 )
 
 func TestBuildClientTLSConfigReturnsNilWhenUnset(t *testing.T) {
@@ -39,14 +44,49 @@
 	}
 }
 
-func TestBuildHTTPClientReturnsDefaultClientWhenUnset(t *testing.T) {
+func TestBuildHTTPClientPropagatesTraceContextWhenUnset(t *testing.T) {
+	originalProvider := otel.GetTracerProvider()
+	originalPropagator := otel.GetTextMapPropagator()
+	tracerProvider := sdktrace.NewTracerProvider()
+	otel.SetTracerProvider(tracerProvider)
+	otel.SetTextMapPropagator(propagation.TraceContext{})
+	t.Cleanup(func() {
+		otel.SetTracerProvider(originalProvider)
+		otel.SetTextMapPropagator(originalPropagator)
+		_ = tracerProvider.Shutdown(context.Background())
+	})
+
+	var traceParent string
+	server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
+		traceParent = request.Header.Get("traceparent")
+		writer.WriteHeader(http.StatusNoContent)
+	}))
+	defer server.Close()
+
 	client, err := BuildHTTPClient(ClientConfig{})
 	if err != nil {
 		t.Fatalf("build http client: %v", err)
 	}
-	if client != http.DefaultClient {
-		t.Fatal("expected default client")
+	if client == http.DefaultClient {
+		t.Fatal("expected traced client, got http.DefaultClient")
 	}
+
+	ctx, span := tracerProvider.Tracer("mtls-test").Start(context.Background(), "root")
+	defer span.End()
+
+	request, err := http.NewRequestWithContext(ctx, http.MethodGet, server.URL, nil)
+	if err != nil {
+		t.Fatalf("build request: %v", err)
+	}
+	response, err := client.Do(request)
+	if err != nil {
+		t.Fatalf("client.Do: %v", err)
+	}
+	response.Body.Close()
+
+	if traceParent == "" {
+		t.Fatal("expected traceparent header")
+	}
 }
 
 func TestVerifiedClientCertificateIdentitiesDedupes(t *testing.T) {

diff --git a/vendor/golang.org/x/net/http2/hpack/tables.go b/vendor/golang.org/x/net/http2/hpack/tables.go
--- a/vendor/golang.org/x/net/http2/hpack/tables.go
+++ b/vendor/golang.org/x/net/http2/hpack/tables.go
@@ -6,6 +6,7 @@
 
 import (
 	"fmt"
+	"strings"
 )
 
 // headerFieldTable implements a list of HeaderFields.
@@ -54,10 +55,16 @@
 
 // addEntry adds a new entry.
 func (t *headerFieldTable) addEntry(f HeaderField) {
+	// Prevent f from escaping to the heap.
+	f2 := HeaderField{
+		Name:      strings.Clone(f.Name),
+		Value:     strings.Clone(f.Value),
+		Sensitive: f.Sensitive,
+	}
 	id := uint64(t.len()) + t.evictCount + 1
-	t.byName[f.Name] = id
-	t.byNameValue[pairNameValue{f.Name, f.Value}] = id
-	t.ents = append(t.ents, f)
+	t.byName[f2.Name] = id
+	t.byNameValue[pairNameValue{f2.Name, f2.Value}] = id
+	t.ents = append(t.ents, f2)
 }
 
 // evictOldest evicts the n oldest entries in the table.

diff --git a/vendor/golang.org/x/net/http2/transport.go b/vendor/golang.org/x/net/http2/transport.go
--- a/vendor/golang.org/x/net/http2/transport.go
+++ b/vendor/golang.org/x/net/http2/transport.go
@@ -718,9 +718,6 @@
 }
 
 func (t *Transport) dialClientConn(ctx context.Context, addr string, singleUse bool) (*ClientConn, error) {
-	if t.transportTestHooks != nil {
-		return t.newClientConn(nil, singleUse, nil)
-	}
 	host, _, err := net.SplitHostPort(addr)
 	if err != nil {
 		return nil, err
@@ -2861,6 +2858,9 @@
 
 	var seenMaxConcurrentStreams bool
 	err := f.ForeachSetting(func(s Setting) error {
+		if err := s.Valid(); err != nil {
+			return err
+		}
 		switch s.ID {
 		case SettingMaxFrameSize:
 			cc.maxFrameSize = s.Val
@@ -2892,9 +2892,6 @@
 			cc.henc.SetMaxDynamicTableSize(s.Val)
 			cc.peerMaxHeaderTableSize = s.Val
 		case SettingEnableConnectProtocol:
-			if err := s.Valid(); err != nil {
-				return err
-			}
 			// If the peer wants to send us SETTINGS_ENABLE_CONNECT_PROTOCOL,
 			// we require that it do so in the first SETTINGS frame.
 			//

diff --git a/vendor/golang.org/x/sys/cpu/cpu_darwin_arm64_other.go b/vendor/golang.org/x/sys/cpu/cpu_darwin_arm64_other.go
--- a/vendor/golang.org/x/sys/cpu/cpu_darwin_arm64_other.go
+++ b/vendor/golang.org/x/sys/cpu/cpu_darwin_arm64_other.go
@@ -6,6 +6,8 @@
 
 package cpu
 
+import "runtime"
+
 func doinit() {
 	setMinimalFeatures()
 

diff --git a/vendor/golang.org/x/sys/cpu/cpu_other_arm64.go b/vendor/golang.org/x/sys/cpu/cpu_other_arm64.go
--- a/vendor/golang.org/x/sys/cpu/cpu_other_arm64.go
+++ b/vendor/golang.org/x/sys/cpu/cpu_other_arm64.go
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-//go:build !darwin && !linux && !netbsd && !openbsd && !windows && arm64
+//go:build !darwin && !linux && !netbsd && !openbsd && arm64
 
 package cpu
 

diff --git a/vendor/golang.org/x/sys/cpu/cpu_windows_arm64.go b/vendor/golang.org/x/sys/cpu/cpu_windows_arm64.go
deleted file mode 100644
--- a/vendor/golang.org/x/sys/cpu/cpu_windows_arm64.go
+++ /dev/null
@@ -1,42 +1,0 @@
-// Copyright 2026 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package cpu
-
-import (
-	"golang.org/x/sys/windows"
-)
-
-func doinit() {
-	// set HasASIMD and HasFP to true as per
-	// https://learn.microsoft.com/en-us/cpp/build/arm64-windows-abi-conventions?view=msvc-170#base-requirements
-	//
-	// The ARM64 version of Windows always presupposes that it's running on an ARMv8 or later architecture.
-	// Both floating-point and NEON support are presumed to be present in hardware.
-	//
-	ARM64.HasASIMD = true
-	ARM64.HasFP = true
-
-	if windows.IsProcessorFeaturePresent(windows.PF_ARM_V8_CRYPTO_INSTRUCTIONS_AVAILABLE) {
-		ARM64.HasAES = true
-		ARM64.HasPMULL = true
-		ARM64.HasSHA1 = true
-		ARM64.HasSHA2 = true
-	}
-	ARM64.HasSHA3 = windows.IsProcessorFeaturePresent(windows.PF_ARM_SHA3_INSTRUCTIONS_AVAILABLE)
-	ARM64.HasCRC32 = windows.IsProcessorFeaturePresent(windows.PF_ARM_V8_CRC32_INSTRUCTIONS_AVAILABLE)
-	ARM64.HasSHA512 = windows.IsProcessorFeaturePresent(windows.PF_ARM_SHA512_INSTRUCTIONS_AVAILABLE)
-	ARM64.HasATOMICS = windows.IsProcessorFeaturePresent(windows.PF_ARM_V81_ATOMIC_INSTRUCTIONS_AVAILABLE)
-	if windows.IsProcessorFeaturePresent(windows.PF_ARM_V82_DP_INSTRUCTIONS_AVAILABLE) {
-		ARM64.HasASIMDDP = true
-		ARM64.HasASIMDRDM = true
-	}
-	if windows.IsProcessorFeaturePresent(windows.PF_ARM_V83_LRCPC_INSTRUCTIONS_AVAILABLE) {
-		ARM64.HasLRCPC = true
-		ARM64.HasSM3 = true
-	}
-	ARM64.HasSVE = windows.IsProcessorFeaturePresent(windows.PF_ARM_SVE_INSTRUCTIONS_AVAILABLE)
-	ARM64.HasSVE2 = windows.IsProcessorFeaturePresent(windows.PF_ARM_SVE2_INSTRUCTIONS_AVAILABLE)
-	ARM64.HasJSCVT = windows.IsProcessorFeaturePresent(windows.PF_ARM_V83_JSCVT_INSTRUCTIONS_AVAILABLE)
-}
\ No newline at end of file

diff --git a/vendor/golang.org/x/sys/windows/dll_windows.go b/vendor/golang.org/x/sys/windows/dll_windows.go
--- a/vendor/golang.org/x/sys/windows/dll_windows.go
+++ b/vendor/golang.org/x/sys/windows/dll_windows.go
@@ -163,42 +163,7 @@
 // (according to the semantics of the specific function being called) before consulting
 // the error. The error will be guaranteed to contain windows.Errno.
 func (p *Proc) Call(a ...uintptr) (r1, r2 uintptr, lastErr error) {
-	switch len(a) {
-	case 0:
-		return syscall.Syscall(p.Addr(), uintptr(len(a)), 0, 0, 0)
-	case 1:
-		return syscall.Syscall(p.Addr(), uintptr(len(a)), a[0], 0, 0)
-	case 2:
-		return syscall.Syscall(p.Addr(), uintptr(len(a)), a[0], a[1], 0)
-	case 3:
-		return syscall.Syscall(p.Addr(), uintptr(len(a)), a[0], a[1], a[2])
-	case 4:
-		return syscall.Syscall6(p.Addr(), uintptr(len(a)), a[0], a[1], a[2], a[3], 0, 0)
-	case 5:
-		return syscall.Syscall6(p.Addr(), uintptr(len(a)), a[0], a[1], a[2], a[3], a[4], 0)
-	case 6:
-		return syscall.Syscall6(p.Addr(), uintptr(len(a)), a[0], a[1], a[2], a[3], a[4], a[5])
-	case 7:
-		return syscall.Syscall9(p.Addr(), uintptr(len(a)), a[0], a[1], a[2], a[3], a[4], a[5], a[6], 0, 0)
-	case 8:
-		return syscall.Syscall9(p.Addr(), uintptr(len(a)), a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7], 0)
-	case 9:
-		return syscall.Syscall9(p.Addr(), uintptr(len(a)), a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8])
-	case 10:
-		return syscall.Syscall12(p.Addr(), uintptr(len(a)), a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8], a[9], 0, 0)
-	case 11:
-		return syscall.Syscall12(p.Addr(), uintptr(len(a)), a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8], a[9], a[10], 0)
-	case 12:
-		return syscall.Syscall12(p.Addr(), uintptr(len(a)), a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8], a[9], a[10], a[11])
-	case 13:
-		return syscall.Syscall15(p.Addr(), uintptr(len(a)), a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8], a[9], a[10], a[11], a[12], 0, 0)
-	case 14:
-		return syscall.Syscall15(p.Addr(), uintptr(len(a)), a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8], a[9], a[10], a[11], a[12], a[13], 0)
-	case 15:
-		return syscall.Syscall15(p.Addr(), uintptr(len(a)), a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8], a[9], a[10], a[11], a[12], a[13], a[14])
-	default:
-		panic("Call " + p.Name + " with too many arguments " + itoa(len(a)) + ".")
-	}
+	return syscall.SyscallN(p.Addr(), a...)
 }
 
 // A LazyDLL implements access to a single DLL.

diff --git a/vendor/golang.org/x/sys/windows/security_windows.go b/vendor/golang.org/x/sys/windows/security_windows.go
--- a/vendor/golang.org/x/sys/windows/security_windows.go
+++ b/vendor/golang.org/x/sys/windows/security_windows.go
@@ -1438,13 +1438,17 @@
 }
 
 // GetNamedSecurityInfo queries the security information for a given named object and returns the self-relative security
-// descriptor result on the Go heap.
+// descriptor result on the Go heap. The security descriptor might be nil, even when err is nil, if the object exists
+// but has no security descriptor.
 func GetNamedSecurityInfo(objectName string, objectType SE_OBJECT_TYPE, securityInformation SECURITY_INFORMATION) (sd *SECURITY_DESCRIPTOR, err error) {
 	var winHeapSD *SECURITY_DESCRIPTOR
 	err = getNamedSecurityInfo(objectName, objectType, securityInformation, nil, nil, nil, nil, &winHeapSD)
 	if err != nil {
 		return
 	}
+	if winHeapSD == nil {
+		return nil, nil
+	}
 	defer LocalFree(Handle(unsafe.Pointer(winHeapSD)))
 	return winHeapSD.copySelfRelativeSecurityDescriptor(), nil
 }

diff --git a/vendor/modules.txt b/vendor/modules.txt
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -283,7 +283,7 @@
 # go.yaml.in/yaml/v2 v2.4.2
 ## explicit; go 1.15
 go.yaml.in/yaml/v2
-# golang.org/x/crypto v0.49.0
+# golang.org/x/crypto v0.50.0
 ## explicit; go 1.25.0
 golang.org/x/crypto/bcrypt
 golang.org/x/crypto/blake2b
@@ -299,7 +299,7 @@
 golang.org/x/crypto/nacl/secretbox
 golang.org/x/crypto/ocsp
 golang.org/x/crypto/salsa20/salsa
-# golang.org/x/net v0.52.0
+# golang.org/x/net v0.53.0
 ## explicit; go 1.25.0
 golang.org/x/net/http/httpguts
 golang.org/x/net/http2
@@ -313,7 +313,7 @@
 ## explicit; go 1.25.0
 golang.org/x/sync/errgroup
 golang.org/x/sync/semaphore
-# golang.org/x/sys v0.42.0
+# golang.org/x/sys v0.43.0
 ## explicit; go 1.25.0
 golang.org/x/sys/cpu
 golang.org/x/sys/unix
@@ -322,7 +322,7 @@
 golang.org/x/sys/windows/svc
 golang.org/x/sys/windows/svc/eventlog
 golang.org/x/sys/windows/svc/mgr
-# golang.org/x/text v0.35.0
+# golang.org/x/text v0.36.0
 ## explicit; go 1.25.0
 golang.org/x/text/cases
 golang.org/x/text/internal

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit ef0e03e. Configure here.

Comment thread mtls/mtls.go Outdated
clone := transport.Clone()
clone.TLSClientConfig = tlsConfig
return &http.Client{Transport: clone}, nil
return traceHTTPClient(&http.Client{Transport: clone}), nil
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

otelhttp wrapping breaks mTLS certificate detection downstream

High Severity

BuildHTTPClient now wraps the transport with otelhttp.NewTransport before returning. In identityclient/client.go, httpClientUsesMTLSCertificate type-asserts client.Transport.(*http.Transport) to detect mTLS certificates — but the transport is now *otelhttp.Transport, so the assertion always fails. This causes usesMTLSCert to be false even when mTLS is configured, breaking ServiceTokensConfigured() for clients that rely on certificate-based auth without a bootstrap key.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit ef0e03e. Configure here.

Comment thread mtls/mtls.go Outdated
clone := transport.Clone()
clone.TLSClientConfig = tlsConfig
return &http.Client{Transport: clone}, nil
return traceHTTPClient(&http.Client{Transport: clone}), nil
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Transport double-wrapped with otelhttp in identity client path

Medium Severity

BuildHTTPClient now wraps the transport with otelhttp.NewTransport. When identityclient.NewMTLSClient passes the result to identityclient.New, it calls its own tracedHTTPClient which wraps the transport with otelhttp.NewTransport a second time. Every outbound request through this path generates duplicate OTel spans.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit ef0e03e. Configure here.

Comment thread mtls/mtls.go
@haasonsaas haasonsaas force-pushed the jh/mtls-trace-context-20260509 branch from ef0e03e to 231ef7d Compare May 9, 2026 07:53
@haasonsaas haasonsaas merged commit 74dc0fa into main May 9, 2026
6 checks passed
@haasonsaas haasonsaas deleted the jh/mtls-trace-context-20260509 branch May 9, 2026 09:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants