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