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 }