google.golang.org/grpc@v1.74.2/xds/server_security_ext_test.go (about)

     1  /*
     2   *
     3   * Copyright 2025 gRPC authors.
     4   *
     5   * Licensed under the Apache License, Version 2.0 (the "License");
     6   * you may not use this file except in compliance with the License.
     7   * You may obtain a copy of the License at
     8   *
     9   *     http://www.apache.org/licenses/LICENSE-2.0
    10   *
    11   * Unless required by applicable law or agreed to in writing, software
    12   * distributed under the License is distributed on an "AS IS" BASIS,
    13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    14   * See the License for the specific language governing permissions and
    15   * limitations under the License.
    16   *
    17   */
    18  
    19  package xds_test
    20  
    21  import (
    22  	"context"
    23  	"fmt"
    24  	"net"
    25  	"strconv"
    26  	"testing"
    27  	"time"
    28  
    29  	"github.com/google/uuid"
    30  	"google.golang.org/grpc"
    31  	"google.golang.org/grpc/codes"
    32  	"google.golang.org/grpc/connectivity"
    33  	"google.golang.org/grpc/credentials/insecure"
    34  	xdscreds "google.golang.org/grpc/credentials/xds"
    35  	"google.golang.org/grpc/internal/testutils"
    36  	"google.golang.org/grpc/internal/testutils/xds/e2e"
    37  	"google.golang.org/grpc/internal/xds/bootstrap"
    38  	"google.golang.org/grpc/xds"
    39  	"google.golang.org/grpc/xds/internal/xdsclient"
    40  	"google.golang.org/protobuf/types/known/wrapperspb"
    41  
    42  	v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
    43  	v3listenerpb "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
    44  	v3routepb "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
    45  	v3httppb "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
    46  	v3tlspb "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3"
    47  	v3discoverypb "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3"
    48  	testgrpc "google.golang.org/grpc/interop/grpc_testing"
    49  	testpb "google.golang.org/grpc/interop/grpc_testing"
    50  )
    51  
    52  // Tests the case where the bootstrap configuration contains no certificate
    53  // providers, and xDS credentials with an insecure fallback is specified at
    54  // server creation time. The management server is configured to return a
    55  // server-side xDS Listener resource with no security configuration. The test
    56  // verifies that a gRPC client configured with insecure credentials is able to
    57  // make RPCs to the backend. This ensures that the insecure fallback
    58  // credentials are getting used on the server.
    59  func (s) TestServer_Security_NoCertProvidersInBootstrap_Success(t *testing.T) {
    60  	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
    61  	defer cancel()
    62  
    63  	// Start an xDS management server.
    64  	managementServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{})
    65  
    66  	// Create bootstrap configuration pointing to the above management server.
    67  	nodeID := uuid.New().String()
    68  	bootstrapContents := e2e.DefaultBootstrapContents(t, nodeID, managementServer.Address)
    69  
    70  	// Create a listener on a local port to act as the xDS enabled gRPC server.
    71  	lis, err := testutils.LocalTCPListener()
    72  	if err != nil {
    73  		t.Fatalf("Failed to listen to local port: %v", err)
    74  	}
    75  	host, port, err := hostPortFromListener(lis)
    76  	if err != nil {
    77  		t.Fatalf("Failed to retrieve host and port of server: %v", err)
    78  	}
    79  
    80  	// Configure the managegement server with a listener and route configuration
    81  	// resource for the above xDS enabled gRPC server.
    82  	const routeConfigName = "routeName"
    83  	resources := e2e.UpdateOptions{
    84  		NodeID:         nodeID,
    85  		Listeners:      []*v3listenerpb.Listener{e2e.DefaultServerListenerWithRouteConfigName(host, port, e2e.SecurityLevelNone, "routeName")},
    86  		Routes:         []*v3routepb.RouteConfiguration{e2e.RouteConfigNonForwardingAction(routeConfigName)},
    87  		SkipValidation: true,
    88  	}
    89  	if err := managementServer.Update(ctx, resources); err != nil {
    90  		t.Fatal(err)
    91  	}
    92  
    93  	// Start an xDS-enabled gRPC server with the above bootstrap configuration
    94  	// and configure xDS credentials to be used on the server-side.
    95  	creds, err := xdscreds.NewServerCredentials(xdscreds.ServerOptions{
    96  		FallbackCreds: insecure.NewCredentials(),
    97  	})
    98  	if err != nil {
    99  		t.Fatal(err)
   100  	}
   101  
   102  	config, err := bootstrap.NewConfigFromContents(bootstrapContents)
   103  	if err != nil {
   104  		t.Fatalf("Failed to parse bootstrap contents: %s, %v", string(bootstrapContents), err)
   105  	}
   106  	pool := xdsclient.NewPool(config)
   107  	modeChangeOpt := xds.ServingModeCallback(func(addr net.Addr, args xds.ServingModeChangeArgs) {
   108  		t.Logf("Serving mode for listener %q changed to %q, err: %v", addr.String(), args.Mode, args.Err)
   109  	})
   110  	createStubServer(t, lis, grpc.Creds(creds), modeChangeOpt, xds.ClientPoolForTesting(pool))
   111  
   112  	// Create a client that uses insecure creds and verify RPCs.
   113  	cc, err := grpc.NewClient(lis.Addr().String(), grpc.WithTransportCredentials(insecure.NewCredentials()))
   114  	if err != nil {
   115  		t.Fatalf("Failed to dial local test server: %v", err)
   116  	}
   117  	defer cc.Close()
   118  
   119  	client := testgrpc.NewTestServiceClient(cc)
   120  	if _, err := client.EmptyCall(ctx, &testpb.Empty{}, grpc.WaitForReady(true)); err != nil {
   121  		t.Fatalf("EmptyCall() failed: %v", err)
   122  	}
   123  }
   124  
   125  // Tests the case where the bootstrap configuration contains no certificate
   126  // providers, and xDS credentials with an insecure fallback is specified at
   127  // server creation time. The management server is configured to return a
   128  // server-side xDS Listener resource with mTLS security configuration. The xDS
   129  // client is expected to NACK this resource because the certificate provider
   130  // instance name specified in the Listener resource will not be present in the
   131  // bootstrap file. The test verifies that server creation does not fail and that
   132  // if the xDS-enabled gRPC server receives resource error causing mode change,
   133  // it does not enter "serving" mode.
   134  func (s) TestServer_Security_NoCertificateProvidersInBootstrap_Failure(t *testing.T) {
   135  	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
   136  	defer cancel()
   137  
   138  	// Spin up an xDS management server that pushes on a channel when it
   139  	// receives a NACK for an LDS response.
   140  	nackCh := make(chan struct{}, 1)
   141  	mgmtServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{
   142  		OnStreamRequest: func(_ int64, req *v3discoverypb.DiscoveryRequest) error {
   143  			if req.GetTypeUrl() != "type.googleapis.com/envoy.config.listener.v3.Listener" {
   144  				return nil
   145  			}
   146  			if req.GetErrorDetail() == nil {
   147  				return nil
   148  			}
   149  			select {
   150  			case nackCh <- struct{}{}:
   151  			default:
   152  			}
   153  			return nil
   154  		},
   155  		AllowResourceSubset: true,
   156  	})
   157  
   158  	// Create bootstrap configuration with no certificate providers.
   159  	nodeID := uuid.New().String()
   160  	bootstrapContents, err := bootstrap.NewContentsForTesting(bootstrap.ConfigOptionsForTesting{
   161  		Servers: []byte(fmt.Sprintf(`[{
   162  			"server_uri": %q,
   163  			"channel_creds": [{"type": "insecure"}]
   164  		}]`, mgmtServer.Address)),
   165  		Node:                               []byte(fmt.Sprintf(`{"id": "%s"}`, nodeID)),
   166  		ServerListenerResourceNameTemplate: e2e.ServerListenerResourceNameTemplate,
   167  	})
   168  	if err != nil {
   169  		t.Fatalf("Failed to create bootstrap configuration: %v", err)
   170  	}
   171  
   172  	// Create a listener on a local port to act as the xDS enabled gRPC server.
   173  	lis, err := testutils.LocalTCPListener()
   174  	if err != nil {
   175  		t.Fatalf("Failed to listen to local port: %v", err)
   176  	}
   177  	host, port, err := hostPortFromListener(lis)
   178  	if err != nil {
   179  		t.Fatalf("Failed to retrieve host and port of server: %v", err)
   180  	}
   181  	// Start an xDS-enabled gRPC server with the above bootstrap configuration
   182  	// and configure xDS credentials to be used on the server-side.
   183  	creds, err := xdscreds.NewServerCredentials(xdscreds.ServerOptions{
   184  		FallbackCreds: insecure.NewCredentials(),
   185  	})
   186  	if err != nil {
   187  		t.Fatal(err)
   188  	}
   189  
   190  	config, err := bootstrap.NewConfigFromContents(bootstrapContents)
   191  	if err != nil {
   192  		t.Fatalf("Failed to parse bootstrap contents: %s, %v", string(bootstrapContents), err)
   193  	}
   194  	pool := xdsclient.NewPool(config)
   195  	modeChangeHandler := newServingModeChangeHandler(t)
   196  	modeChangeOpt := xds.ServingModeCallback(modeChangeHandler.modeChangeCallback)
   197  	createStubServer(t, lis, grpc.Creds(creds), modeChangeOpt, xds.ClientPoolForTesting(pool))
   198  
   199  	// Create an inbound xDS listener resource for the server side that contains
   200  	// mTLS security configuration. Since the received certificate provider
   201  	// instance name would be missing in the bootstrap configuration, this
   202  	// resource is expected to NACKed by the xDS client.
   203  	resources := e2e.UpdateOptions{
   204  		NodeID:         nodeID,
   205  		Listeners:      []*v3listenerpb.Listener{e2e.DefaultServerListener(host, port, e2e.SecurityLevelMTLS, "routeName")},
   206  		SkipValidation: true,
   207  	}
   208  	if err := mgmtServer.Update(ctx, resources); err != nil {
   209  		t.Fatal(err)
   210  	}
   211  
   212  	// Wait for the NACK from the xDS client.
   213  	select {
   214  	case <-nackCh:
   215  	case <-ctx.Done():
   216  		t.Fatal("Timeout when waiting for an NACK from the xDS client for the LDS response")
   217  	}
   218  
   219  	// Wait a short duration and ensure that if the server receive mode change
   220  	// it does not enter "serving" mode.
   221  	sCtx, sCancel := context.WithTimeout(ctx, defaultTestShortTimeout)
   222  	defer sCancel()
   223  	select {
   224  	case <-sCtx.Done():
   225  	case stateCh := <-modeChangeHandler.modeCh:
   226  		if stateCh == connectivity.ServingModeServing {
   227  			t.Fatal("Server entered serving mode before the route config was received")
   228  		}
   229  	}
   230  
   231  	// Create a client that uses insecure creds and verify that RPCs don't
   232  	// succeed.
   233  	cc, err := grpc.NewClient(lis.Addr().String(), grpc.WithTransportCredentials(insecure.NewCredentials()))
   234  	if err != nil {
   235  		t.Fatalf("Failed to dial local test server: %v", err)
   236  	}
   237  	defer cc.Close()
   238  
   239  	waitForFailedRPCWithStatus(ctx, t, cc, codes.Unavailable, "", "")
   240  }
   241  
   242  // Tests the case where the bootstrap configuration contains one certificate
   243  // provider, and xDS credentials with an insecure fallback is specified at
   244  // server creation time. Two listeners are configured on the xDS-enabled gRPC
   245  // server. The management server responds with two listener resources:
   246  //  1. contains valid security configuration pointing to the certificate provider
   247  //     instance specified in the bootstrap
   248  //  2. contains invalid security configuration pointing to a non-existent
   249  //     certificate provider instance
   250  //
   251  // The test verifies that an RPC to the first listener succeeds, while the
   252  // second listener receive a resource error which cause the server mode change
   253  // but never moves to "serving" mode.
   254  func (s) TestServer_Security_WithValidAndInvalidSecurityConfiguration(t *testing.T) {
   255  	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
   256  	defer cancel()
   257  
   258  	// Spin up an xDS management server that pushes on a channel when it
   259  	// receives a NACK for an LDS response.
   260  	nackCh := make(chan struct{}, 1)
   261  	managementServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{
   262  		OnStreamRequest: func(_ int64, req *v3discoverypb.DiscoveryRequest) error {
   263  			if req.GetTypeUrl() != "type.googleapis.com/envoy.config.listener.v3.Listener" {
   264  				return nil
   265  			}
   266  			if req.GetErrorDetail() == nil {
   267  				return nil
   268  			}
   269  			select {
   270  			case nackCh <- struct{}{}:
   271  			default:
   272  			}
   273  			return nil
   274  		},
   275  		AllowResourceSubset: true,
   276  	})
   277  
   278  	// Create bootstrap configuration pointing to the above management server.
   279  	nodeID := uuid.New().String()
   280  	bootstrapContents := e2e.DefaultBootstrapContents(t, nodeID, managementServer.Address)
   281  
   282  	// Create two xDS-enabled gRPC servers using the above bootstrap configs.
   283  	lis1, err := testutils.LocalTCPListener()
   284  	if err != nil {
   285  		t.Fatalf("testutils.LocalTCPListener() failed: %v", err)
   286  	}
   287  	lis2, err := testutils.LocalTCPListener()
   288  	if err != nil {
   289  		t.Fatalf("testutils.LocalTCPListener() failed: %v", err)
   290  	}
   291  
   292  	modeChangeHandler1 := newServingModeChangeHandler(t)
   293  	modeChangeOpt1 := xds.ServingModeCallback(modeChangeHandler1.modeChangeCallback)
   294  	modeChangeHandler2 := newServingModeChangeHandler(t)
   295  	modeChangeOpt2 := xds.ServingModeCallback(modeChangeHandler2.modeChangeCallback)
   296  	creds, err := xdscreds.NewServerCredentials(xdscreds.ServerOptions{FallbackCreds: insecure.NewCredentials()})
   297  	if err != nil {
   298  		t.Fatal(err)
   299  	}
   300  	config, err := bootstrap.NewConfigFromContents(bootstrapContents)
   301  	if err != nil {
   302  		t.Fatalf("Failed to parse bootstrap contents: %s, %v", string(bootstrapContents), err)
   303  	}
   304  	pool := xdsclient.NewPool(config)
   305  	createStubServer(t, lis1, grpc.Creds(creds), modeChangeOpt1, xds.ClientPoolForTesting(pool))
   306  	createStubServer(t, lis2, grpc.Creds(creds), modeChangeOpt2, xds.ClientPoolForTesting(pool))
   307  
   308  	// Create inbound xDS listener resources for the server side that contains
   309  	// mTLS security configuration.
   310  	// lis1 --> security configuration pointing to a valid cert provider
   311  	// lis2 --> security configuration pointing to a non-existent cert provider
   312  	host1, port1, err := hostPortFromListener(lis1)
   313  	if err != nil {
   314  		t.Fatalf("Failed to retrieve host and port of server: %v", err)
   315  	}
   316  	resource1 := e2e.DefaultServerListener(host1, port1, e2e.SecurityLevelMTLS, "routeName")
   317  	host2, port2, err := hostPortFromListener(lis2)
   318  	if err != nil {
   319  		t.Fatalf("Failed to retrieve host and port of server: %v", err)
   320  	}
   321  	hcm := &v3httppb.HttpConnectionManager{
   322  		RouteSpecifier: &v3httppb.HttpConnectionManager_RouteConfig{
   323  			RouteConfig: &v3routepb.RouteConfiguration{
   324  				Name: "routeName",
   325  				VirtualHosts: []*v3routepb.VirtualHost{{
   326  					Domains: []string{"*"},
   327  					Routes: []*v3routepb.Route{{
   328  						Match: &v3routepb.RouteMatch{
   329  							PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/"},
   330  						},
   331  						Action: &v3routepb.Route_NonForwardingAction{},
   332  					}}}}},
   333  		},
   334  		HttpFilters: []*v3httppb.HttpFilter{e2e.RouterHTTPFilter},
   335  	}
   336  	ts := &v3corepb.TransportSocket{
   337  		Name: "envoy.transport_sockets.tls",
   338  		ConfigType: &v3corepb.TransportSocket_TypedConfig{
   339  			TypedConfig: testutils.MarshalAny(t, &v3tlspb.DownstreamTlsContext{
   340  				RequireClientCertificate: &wrapperspb.BoolValue{Value: true},
   341  				CommonTlsContext: &v3tlspb.CommonTlsContext{
   342  					TlsCertificateCertificateProviderInstance: &v3tlspb.CommonTlsContext_CertificateProviderInstance{
   343  						InstanceName: "non-existent-certificate-provider",
   344  					},
   345  					ValidationContextType: &v3tlspb.CommonTlsContext_ValidationContextCertificateProviderInstance{
   346  						ValidationContextCertificateProviderInstance: &v3tlspb.CommonTlsContext_CertificateProviderInstance{
   347  							InstanceName: "non-existent-certificate-provider",
   348  						},
   349  					},
   350  				},
   351  			}),
   352  		},
   353  	}
   354  	resource2 := &v3listenerpb.Listener{
   355  		Name: fmt.Sprintf(e2e.ServerListenerResourceNameTemplate, net.JoinHostPort(host2, strconv.Itoa(int(port2)))),
   356  		Address: &v3corepb.Address{
   357  			Address: &v3corepb.Address_SocketAddress{
   358  				SocketAddress: &v3corepb.SocketAddress{
   359  					Address: host2,
   360  					PortSpecifier: &v3corepb.SocketAddress_PortValue{
   361  						PortValue: port2,
   362  					},
   363  				},
   364  			},
   365  		},
   366  		FilterChains: []*v3listenerpb.FilterChain{
   367  			{
   368  				Name: "v4-wildcard",
   369  				FilterChainMatch: &v3listenerpb.FilterChainMatch{
   370  					PrefixRanges: []*v3corepb.CidrRange{
   371  						{
   372  							AddressPrefix: "0.0.0.0",
   373  							PrefixLen: &wrapperspb.UInt32Value{
   374  								Value: uint32(0),
   375  							},
   376  						},
   377  					},
   378  					SourceType: v3listenerpb.FilterChainMatch_SAME_IP_OR_LOOPBACK,
   379  					SourcePrefixRanges: []*v3corepb.CidrRange{
   380  						{
   381  							AddressPrefix: "0.0.0.0",
   382  							PrefixLen: &wrapperspb.UInt32Value{
   383  								Value: uint32(0),
   384  							},
   385  						},
   386  					},
   387  				},
   388  				Filters: []*v3listenerpb.Filter{
   389  					{
   390  						Name:       "filter-1",
   391  						ConfigType: &v3listenerpb.Filter_TypedConfig{TypedConfig: testutils.MarshalAny(t, hcm)},
   392  					},
   393  				},
   394  				TransportSocket: ts,
   395  			},
   396  			{
   397  				Name: "v6-wildcard",
   398  				FilterChainMatch: &v3listenerpb.FilterChainMatch{
   399  					PrefixRanges: []*v3corepb.CidrRange{
   400  						{
   401  							AddressPrefix: "::",
   402  							PrefixLen: &wrapperspb.UInt32Value{
   403  								Value: uint32(0),
   404  							},
   405  						},
   406  					},
   407  					SourceType: v3listenerpb.FilterChainMatch_SAME_IP_OR_LOOPBACK,
   408  					SourcePrefixRanges: []*v3corepb.CidrRange{
   409  						{
   410  							AddressPrefix: "::",
   411  							PrefixLen: &wrapperspb.UInt32Value{
   412  								Value: uint32(0),
   413  							},
   414  						},
   415  					},
   416  				},
   417  				Filters: []*v3listenerpb.Filter{
   418  					{
   419  						Name:       "filter-1",
   420  						ConfigType: &v3listenerpb.Filter_TypedConfig{TypedConfig: testutils.MarshalAny(t, hcm)},
   421  					},
   422  				},
   423  				TransportSocket: ts,
   424  			},
   425  		},
   426  	}
   427  	resources := e2e.UpdateOptions{
   428  		NodeID:         nodeID,
   429  		Listeners:      []*v3listenerpb.Listener{resource1, resource2},
   430  		SkipValidation: true,
   431  	}
   432  	if err := managementServer.Update(ctx, resources); err != nil {
   433  		t.Fatal(err)
   434  	}
   435  
   436  	// Create a client that uses TLS creds and verify RPCs to listener1.
   437  	clientCreds := testutils.CreateClientTLSCredentials(t)
   438  	cc1, err := grpc.NewClient(lis1.Addr().String(), grpc.WithTransportCredentials(clientCreds))
   439  	if err != nil {
   440  		t.Fatalf("Failed to dial local test server: %v", err)
   441  	}
   442  	defer cc1.Close()
   443  
   444  	client1 := testgrpc.NewTestServiceClient(cc1)
   445  	if _, err := client1.EmptyCall(ctx, &testpb.Empty{}, grpc.WaitForReady(true)); err != nil {
   446  		t.Fatalf("EmptyCall() failed: %v", err)
   447  	}
   448  
   449  	// Wait for the NACK from the xDS client.
   450  	select {
   451  	case <-nackCh:
   452  	case <-ctx.Done():
   453  		t.Fatal("Timeout when waiting for an NACK from the xDS client for the LDS response")
   454  	}
   455  
   456  	// Wait a short duration and ensure that if the server receives mode change
   457  	// it does not enter "serving" mode.
   458  	select {
   459  	case <-time.After(2 * defaultTestShortTimeout):
   460  	case mode := <-modeChangeHandler2.modeCh:
   461  		if mode == connectivity.ServingModeServing {
   462  			t.Fatal("Server changed to serving mode when not expected to")
   463  		}
   464  	}
   465  
   466  	// Create a client that uses insecure creds and verify that RPCs don't
   467  	// succeed to listener2.
   468  	cc2, err := grpc.NewClient(lis2.Addr().String(), grpc.WithTransportCredentials(insecure.NewCredentials()))
   469  	if err != nil {
   470  		t.Fatalf("Failed to dial local test server: %v", err)
   471  	}
   472  	defer cc2.Close()
   473  
   474  	waitForFailedRPCWithStatus(ctx, t, cc2, codes.Unavailable, "", "")
   475  }