From d3d1828d84da0a84e95d699371915907dac92956 Mon Sep 17 00:00:00 2001 From: MohammadHasan Akbari Date: Sun, 28 Jun 2026 15:24:28 +0400 Subject: [PATCH 1/2] remote: use endpoint address for buildkit client authority The remote driver created the buildkit client with an empty address: client.New(ctx, "", opts...) With an empty address the buildkit client falls back to the system default address (the local unix socket) and derives the gRPC ":authority" pseudo-header from it, which ends up being "localhost". The actual connection was still correct because the remote driver provides its own dialer, but the wrong authority broke HTTP/2 reverse proxies (such as Envoy) that route based on ":authority". Pass the configured endpoint address to client.New so the authority is derived from the remote endpoint hostname (e.g. my-buildkit.example.com:443). The custom dialer is preserved, so the dial target and TLS/SNI behavior are unchanged. Fixes #3880 Signed-off-by: MohammadHasan Akbari --- driver/remote/driver.go | 8 ++++- driver/remote/driver_test.go | 70 ++++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 driver/remote/driver_test.go diff --git a/driver/remote/driver.go b/driver/remote/driver.go index 495a487e04d1..9db78ef403f6 100644 --- a/driver/remote/driver.go +++ b/driver/remote/driver.go @@ -93,7 +93,13 @@ func (d *Driver) Client(ctx context.Context, opts ...client.ClientOpt) (*client. }), client.WithTracerDelegate(delegated.DefaultExporter), }, opts...) - c, err := client.New(ctx, "", opts...) + // Pass the configured endpoint address (rather than an empty string) so + // the buildkit client derives the gRPC ":authority" pseudo-header from + // the remote endpoint hostname. An empty address falls back to the + // system-default buildkit address, which makes the authority resolve to + // "localhost". The connection itself still goes through the custom + // dialer above, so the actual dial target is unaffected. + c, err := client.New(ctx, d.EndpointAddr, opts...) d.client = c d.err = err }) diff --git a/driver/remote/driver_test.go b/driver/remote/driver_test.go new file mode 100644 index 000000000000..c01a06934fae --- /dev/null +++ b/driver/remote/driver_test.go @@ -0,0 +1,70 @@ +package remote + +import ( + "context" + "net" + "testing" + "time" + + "github.com/docker/buildx/driver" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" +) + +// TestClientAuthority verifies that the remote driver derives the gRPC +// ":authority" pseudo-header from the configured endpoint address instead of +// defaulting to "localhost" (see docker/buildx#3880). It stands up an +// in-process gRPC server on a loopback listener and asserts the authority of +// the request it receives matches the endpoint host. +func TestClientAuthority(t *testing.T) { + ctx, cancel := context.WithTimeoutCause(context.Background(), 10*time.Second, context.DeadlineExceeded) + defer cancel() + + lc := net.ListenConfig{} + lis, err := lc.Listen(ctx, "tcp", "127.0.0.1:0") + require.NoError(t, err) + defer lis.Close() + + authorityCh := make(chan string, 1) + srv := grpc.NewServer(grpc.UnknownServiceHandler(func(_ any, stream grpc.ServerStream) error { + authority := "" + if md, ok := metadata.FromIncomingContext(stream.Context()); ok { + if a := md.Get(":authority"); len(a) > 0 { + authority = a[0] + } + } + select { + case authorityCh <- authority: + default: + } + return status.Error(codes.Unimplemented, "unimplemented") + })) + go func() { + _ = srv.Serve(lis) + }() + defer srv.Stop() + + d := &Driver{ + InitConfig: driver.InitConfig{ + EndpointAddr: "tcp://" + lis.Addr().String(), + }, + } + + c, err := d.Client(ctx) + require.NoError(t, err) + defer c.Close() + + // Any RPC will do: it fails server-side with Unimplemented, but the server + // records the ":authority" it received from the client before responding. + _, _ = c.ListWorkers(ctx) + + select { + case authority := <-authorityCh: + require.Equal(t, lis.Addr().String(), authority) + case <-ctx.Done(): + t.Fatal("timed out waiting for request to reach the server") + } +} From 877de7edf25c7e6028955b020c69d22790e3c63b Mon Sep 17 00:00:00 2001 From: MohammadHasan Akbari Date: Sat, 11 Jul 2026 10:17:12 +0400 Subject: [PATCH 2/2] remote: prefer servername for grpc authority When the servername driver-opt is set it is also used for TLS SNI and certificate validation, so use it for the gRPC ":authority" pseudo-header as well, falling back to the endpoint host otherwise. This matches how the buildkit client derives the authority from the server name when TLS credentials are supplied. Since the driver terminates TLS in its own dialer, the authority is set explicitly via client.WithGRPCDialOption(grpc.WithAuthority(...)). Signed-off-by: MohammadHasan Akbari --- driver/remote/driver.go | 35 +++++++++++++++++++++++----- driver/remote/driver_test.go | 44 +++++++++++++++++++++++++++++++++--- 2 files changed, 70 insertions(+), 9 deletions(-) diff --git a/driver/remote/driver.go b/driver/remote/driver.go index 9db78ef403f6..87aac15e892a 100644 --- a/driver/remote/driver.go +++ b/driver/remote/driver.go @@ -5,6 +5,7 @@ import ( "crypto/tls" "crypto/x509" "net" + "net/url" "os" "strings" "sync" @@ -17,6 +18,7 @@ import ( "github.com/moby/buildkit/client/connhelper" "github.com/moby/buildkit/util/tracing/delegated" "github.com/pkg/errors" + "google.golang.org/grpc" ) type Driver struct { @@ -93,12 +95,16 @@ func (d *Driver) Client(ctx context.Context, opts ...client.ClientOpt) (*client. }), client.WithTracerDelegate(delegated.DefaultExporter), }, opts...) - // Pass the configured endpoint address (rather than an empty string) so - // the buildkit client derives the gRPC ":authority" pseudo-header from - // the remote endpoint hostname. An empty address falls back to the - // system-default buildkit address, which makes the authority resolve to - // "localhost". The connection itself still goes through the custom - // dialer above, so the actual dial target is unaffected. + // The remote driver establishes the connection itself through a custom + // dialer (including TLS), so the buildkit client cannot derive the gRPC + // ":authority" pseudo-header from the connection and would fall back to + // "localhost". Set it explicitly from the configured endpoint so HTTP/2 + // reverse proxies (e.g. Envoy) can route on it. Passing the endpoint + // address also keeps the gRPC dial target meaningful; the actual dial + // target is unaffected as it still goes through the dialer above. + if authority := d.clientAuthority(); authority != "" { + opts = append(opts, client.WithGRPCDialOption(grpc.WithAuthority(authority))) + } c, err := client.New(ctx, d.EndpointAddr, opts...) d.client = c d.err = err @@ -106,6 +112,23 @@ func (d *Driver) Client(ctx context.Context, opts ...client.ClientOpt) (*client. return d.client, d.err } +// clientAuthority returns the value to use for the gRPC ":authority" +// pseudo-header when connecting to the remote endpoint. A configured +// servername takes precedence, since it is also used for TLS SNI and +// certificate validation; otherwise the authority is the endpoint host. This +// mirrors how the buildkit client derives the authority when TLS credentials +// are supplied. Endpoints without a host (e.g. unix sockets) have no authority. +func (d *Driver) clientAuthority() string { + if d.tlsOpts != nil && d.serverName != "" { + return d.serverName + } + u, err := url.Parse(d.EndpointAddr) + if err != nil { + return "" + } + return u.Host +} + func (d *Driver) Dial(ctx context.Context) (net.Conn, error) { addr := d.EndpointAddr ch, err := connhelper.GetConnectionHelper(addr) diff --git a/driver/remote/driver_test.go b/driver/remote/driver_test.go index c01a06934fae..7e54bae871f4 100644 --- a/driver/remote/driver_test.go +++ b/driver/remote/driver_test.go @@ -14,9 +14,47 @@ import ( "google.golang.org/grpc/status" ) -// TestClientAuthority verifies that the remote driver derives the gRPC -// ":authority" pseudo-header from the configured endpoint address instead of -// defaulting to "localhost" (see docker/buildx#3880). It stands up an +// TestClientAuthorityValue exercises the authority derivation logic: the +// endpoint host is used by default, and a configured servername takes +// precedence (it is also used for TLS SNI). See docker/buildx#3880. +func TestClientAuthorityValue(t *testing.T) { + tests := []struct { + name string + endpoint string + tls *tlsOpts + expected string + }{ + { + name: "tcp endpoint without tls", + endpoint: "tcp://my-buildkit.example.com:443", + expected: "my-buildkit.example.com:443", + }, + { + name: "servername takes precedence over endpoint host", + endpoint: "tcp://10.0.0.5:443", + tls: &tlsOpts{serverName: "my-buildkit.example.com"}, + expected: "my-buildkit.example.com", + }, + { + name: "unix endpoint has no authority", + endpoint: "unix:///run/buildkit/buildkitd.sock", + expected: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + d := &Driver{ + InitConfig: driver.InitConfig{EndpointAddr: tt.endpoint}, + tlsOpts: tt.tls, + } + require.Equal(t, tt.expected, d.clientAuthority()) + }) + } +} + +// TestClientAuthority verifies end-to-end that the remote driver sends the +// configured endpoint address as the gRPC ":authority" pseudo-header instead +// of defaulting to "localhost" (see docker/buildx#3880). It stands up an // in-process gRPC server on a loopback listener and asserts the authority of // the request it receives matches the endpoint host. func TestClientAuthority(t *testing.T) {