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 }