github.imxd.top/hashicorp/consul@v1.4.5/agent/xds/server_test.go (about) 1 package xds 2 3 import ( 4 "bytes" 5 "context" 6 "errors" 7 "fmt" 8 "log" 9 "os" 10 "strings" 11 "sync" 12 "sync/atomic" 13 "testing" 14 "text/template" 15 "time" 16 17 envoy "github.com/envoyproxy/go-control-plane/envoy/api/v2" 18 "github.com/gogo/protobuf/jsonpb" 19 "github.com/stretchr/testify/require" 20 "google.golang.org/grpc/codes" 21 "google.golang.org/grpc/metadata" 22 "google.golang.org/grpc/status" 23 24 "github.com/hashicorp/consul/acl" 25 "github.com/hashicorp/consul/agent/cache" 26 "github.com/hashicorp/consul/agent/proxycfg" 27 "github.com/hashicorp/consul/agent/structs" 28 ) 29 30 // testManager is a mock of proxycfg.Manager that's simpler to control for 31 // testing. It also implements ConnectAuthz to allow control over authorization. 32 type testManager struct { 33 sync.Mutex 34 chans map[string]chan *proxycfg.ConfigSnapshot 35 cancels chan string 36 authz map[string]connectAuthzResult 37 } 38 39 type connectAuthzResult struct { 40 authz bool 41 reason string 42 m *cache.ResultMeta 43 err error 44 } 45 46 func newTestManager(t *testing.T) *testManager { 47 return &testManager{ 48 chans: map[string]chan *proxycfg.ConfigSnapshot{}, 49 cancels: make(chan string, 10), 50 authz: make(map[string]connectAuthzResult), 51 } 52 } 53 54 // RegisterProxy simulates a proxy registration 55 func (m *testManager) RegisterProxy(t *testing.T, proxyID string) { 56 m.Lock() 57 defer m.Unlock() 58 m.chans[proxyID] = make(chan *proxycfg.ConfigSnapshot, 1) 59 } 60 61 // Deliver simulates a proxy registration 62 func (m *testManager) DeliverConfig(t *testing.T, proxyID string, cfg *proxycfg.ConfigSnapshot) { 63 t.Helper() 64 m.Lock() 65 defer m.Unlock() 66 select { 67 case m.chans[proxyID] <- cfg: 68 case <-time.After(10 * time.Millisecond): 69 t.Fatalf("took too long to deliver config") 70 } 71 } 72 73 // Watch implements ConfigManager 74 func (m *testManager) Watch(proxyID string) (<-chan *proxycfg.ConfigSnapshot, proxycfg.CancelFunc) { 75 m.Lock() 76 defer m.Unlock() 77 // ch might be nil but then it will just block forever 78 return m.chans[proxyID], func() { 79 m.cancels <- proxyID 80 } 81 } 82 83 // AssertWatchCancelled checks that the most recent call to a Watch cancel func 84 // was from the specified proxyID and that one is made in a short time. This 85 // probably won't work if you are running multiple Watches in parallel on 86 // multiple proxyIDS due to timing/ordering issues but I don't think we need to 87 // do that. 88 func (m *testManager) AssertWatchCancelled(t *testing.T, proxyID string) { 89 t.Helper() 90 select { 91 case got := <-m.cancels: 92 require.Equal(t, proxyID, got) 93 case <-time.After(50 * time.Millisecond): 94 t.Fatalf("timed out waiting for Watch cancel for %s", proxyID) 95 } 96 } 97 98 // ConnectAuthorize implements ConnectAuthz 99 func (m *testManager) ConnectAuthorize(token string, req *structs.ConnectAuthorizeRequest) (authz bool, reason string, meta *cache.ResultMeta, err error) { 100 m.Lock() 101 defer m.Unlock() 102 if res, ok := m.authz[token]; ok { 103 return res.authz, res.reason, res.m, res.err 104 } 105 // Default allow but with reason that won't match by accident in a test case 106 return true, "OK: allowed by default test implementation", nil, nil 107 } 108 109 func TestServer_StreamAggregatedResources_BasicProtocol(t *testing.T) { 110 logger := log.New(os.Stderr, "", log.LstdFlags) 111 mgr := newTestManager(t) 112 aclResolve := func(id string) (acl.Authorizer, error) { 113 // Allow all 114 return acl.RootAuthorizer("manage"), nil 115 } 116 envoy := NewTestEnvoy(t, "web-sidecar-proxy", "") 117 defer envoy.Close() 118 119 s := Server{ 120 Logger: logger, 121 CfgMgr: mgr, 122 Authz: mgr, 123 ResolveToken: aclResolve, 124 } 125 s.Initialize() 126 127 go func() { 128 err := s.StreamAggregatedResources(envoy.stream) 129 require.NoError(t, err) 130 }() 131 132 // Register the proxy to create state needed to Watch() on 133 mgr.RegisterProxy(t, "web-sidecar-proxy") 134 135 // Send initial cluster discover 136 envoy.SendReq(t, ClusterType, 0, 0) 137 138 // Check no response sent yet 139 assertChanBlocked(t, envoy.stream.sendCh) 140 141 // Deliver a new snapshot 142 snap := proxycfg.TestConfigSnapshot(t) 143 mgr.DeliverConfig(t, "web-sidecar-proxy", snap) 144 145 assertResponseSent(t, envoy.stream.sendCh, expectClustersJSON(t, snap, "", 1, 1)) 146 147 // Envoy then tries to discover endpoints for those clusters. Technically it 148 // includes the cluster names in the ResourceNames field but we ignore that 149 // completely for now so not bothering to simulate that. 150 envoy.SendReq(t, EndpointType, 0, 0) 151 152 // It also (in parallel) issues the next cluster request (which acts as an ACK 153 // of the version we sent) 154 envoy.SendReq(t, ClusterType, 1, 1) 155 156 // We should get a response immediately since the config is already present in 157 // the server for endpoints. Note that this should not be racy if the server 158 // is behaving well since the Cluster send above should be blocked until we 159 // deliver a new config version. 160 assertResponseSent(t, envoy.stream.sendCh, expectEndpointsJSON(t, snap, "", 1, 2)) 161 162 // And no other response yet 163 assertChanBlocked(t, envoy.stream.sendCh) 164 165 // Envoy now sends listener request along with next endpoint one 166 envoy.SendReq(t, ListenerType, 0, 0) 167 envoy.SendReq(t, EndpointType, 1, 2) 168 169 // And should get a response immediately. 170 assertResponseSent(t, envoy.stream.sendCh, expectListenerJSON(t, snap, "", 1, 3)) 171 172 // Now send Route request along with next listener one 173 envoy.SendReq(t, RouteType, 0, 0) 174 envoy.SendReq(t, ListenerType, 1, 3) 175 176 // We don't serve routes yet so this should block with no response 177 assertChanBlocked(t, envoy.stream.sendCh) 178 179 // WOOP! Envoy now has full connect config. Lets verify that if we update it, 180 // all the responses get resent with the new version. We don't actually want 181 // to change everything because that's tedious - our implementation will 182 // actually resend all blocked types on the new "version" anyway since it 183 // doesn't know _what_ changed. We could do something trivial but let's 184 // simulate a leaf cert expiring and being rotated. 185 snap.Leaf = proxycfg.TestLeafForCA(t, snap.Roots.Roots[0]) 186 mgr.DeliverConfig(t, "web-sidecar-proxy", snap) 187 188 // All 3 response that have something to return should return with new version 189 // note that the ordering is not deterministic in general. Trying to make this 190 // test order-agnostic though is a massive pain since we are comparing 191 // non-identical JSON strings (so can simply sort by anything) and because we 192 // don't know the order the nonces will be assigned. For now we rely and 193 // require our implementation to always deliver updates in a specific order 194 // which is reasonable anyway to ensure consistency of the config Envoy sees. 195 assertResponseSent(t, envoy.stream.sendCh, expectClustersJSON(t, snap, "", 2, 4)) 196 assertResponseSent(t, envoy.stream.sendCh, expectEndpointsJSON(t, snap, "", 2, 5)) 197 assertResponseSent(t, envoy.stream.sendCh, expectListenerJSON(t, snap, "", 2, 6)) 198 199 // Let's pretend that Envoy doesn't like that new listener config. It will ACK 200 // all the others (same version) but NACK the listener. This is the most 201 // subtle part of xDS and the server implementation so I'll elaborate. A full 202 // description of the protocol can be found at 203 // https://github.com/envoyproxy/data-plane-api/blob/master/XDS_PROTOCOL.md. 204 // Envoy delays making a followup request for a type until after it has 205 // processed and applied the last response. The next request then will include 206 // the nonce in the last response which acknowledges _receiving_ and handling 207 // that response. It also includes the currently applied version. If all is 208 // good and it successfully applies the config, then the version in the next 209 // response will be the same version just sent. This is considered to be an 210 // ACK of that version for that type. If envoy fails to apply the config for 211 // some reason, it will still acknowledge that it received it (still return 212 // the responses nonce), but will show the previous version it's still using. 213 // This is considered a NACK. It's important that the server pay attention to 214 // the _nonce_ and not the version when deciding what to send otherwise a bad 215 // version that can't be applied in Envoy will cause a busy loop. 216 // 217 // In this case we are simulating that Envoy failed to apply the Listener 218 // response but did apply the other types so all get the new nonces, but 219 // listener stays on v1. 220 envoy.SendReq(t, ClusterType, 2, 4) 221 envoy.SendReq(t, EndpointType, 2, 5) 222 envoy.SendReq(t, ListenerType, 1, 6) // v1 is a NACK 223 224 // Even though we nacked, we should still NOT get then v2 listeners 225 // redelivered since nothing has changed. 226 assertChanBlocked(t, envoy.stream.sendCh) 227 228 // Change config again and make sure it's delivered to everyone! 229 snap.Leaf = proxycfg.TestLeafForCA(t, snap.Roots.Roots[0]) 230 mgr.DeliverConfig(t, "web-sidecar-proxy", snap) 231 232 assertResponseSent(t, envoy.stream.sendCh, expectClustersJSON(t, snap, "", 3, 7)) 233 assertResponseSent(t, envoy.stream.sendCh, expectEndpointsJSON(t, snap, "", 3, 8)) 234 assertResponseSent(t, envoy.stream.sendCh, expectListenerJSON(t, snap, "", 3, 9)) 235 } 236 237 func expectListenerJSONResources(t *testing.T, snap *proxycfg.ConfigSnapshot, token string, v, n uint64) map[string]string { 238 tokenVal := "" 239 if token != "" { 240 tokenVal = fmt.Sprintf(",\n"+`"value": "%s"`, token) 241 } 242 return map[string]string{ 243 "public_listener": `{ 244 "@type": "type.googleapis.com/envoy.api.v2.Listener", 245 "name": "public_listener:0.0.0.0:9999", 246 "address": { 247 "socketAddress": { 248 "address": "0.0.0.0", 249 "portValue": 9999 250 } 251 }, 252 "filterChains": [ 253 { 254 "tlsContext": ` + expectedPublicTLSContextJSON(t, snap) + `, 255 "filters": [ 256 { 257 "name": "envoy.ext_authz", 258 "config": { 259 "grpc_service": { 260 "envoy_grpc": { 261 "cluster_name": "local_agent" 262 }, 263 "initial_metadata": [ 264 { 265 "key": "x-consul-token" 266 ` + tokenVal + ` 267 } 268 ] 269 }, 270 "stat_prefix": "connect_authz" 271 } 272 }, 273 { 274 "name": "envoy.tcp_proxy", 275 "config": { 276 "cluster": "local_app", 277 "stat_prefix": "public_listener" 278 } 279 } 280 ] 281 } 282 ] 283 }`, 284 "service:db": `{ 285 "@type": "type.googleapis.com/envoy.api.v2.Listener", 286 "name": "service:db:127.0.0.1:9191", 287 "address": { 288 "socketAddress": { 289 "address": "127.0.0.1", 290 "portValue": 9191 291 } 292 }, 293 "filterChains": [ 294 { 295 "filters": [ 296 { 297 "name": "envoy.tcp_proxy", 298 "config": { 299 "cluster": "service:db", 300 "stat_prefix": "service:db" 301 } 302 } 303 ] 304 } 305 ] 306 }`, 307 "prepared_query:geo-cache": `{ 308 "@type": "type.googleapis.com/envoy.api.v2.Listener", 309 "name": "prepared_query:geo-cache:127.10.10.10:8181", 310 "address": { 311 "socketAddress": { 312 "address": "127.10.10.10", 313 "portValue": 8181 314 } 315 }, 316 "filterChains": [ 317 { 318 "filters": [ 319 { 320 "name": "envoy.tcp_proxy", 321 "config": { 322 "cluster": "prepared_query:geo-cache", 323 "stat_prefix": "prepared_query:geo-cache" 324 } 325 } 326 ] 327 } 328 ] 329 }`, 330 } 331 } 332 333 func expectListenerJSONFromResources(t *testing.T, snap *proxycfg.ConfigSnapshot, token string, v, n uint64, resourcesJSON map[string]string) string { 334 resJSON := "" 335 // Sort resources into specific order because that matters in JSONEq 336 // comparison later. 337 keyOrder := []string{"public_listener"} 338 for _, u := range snap.Proxy.Upstreams { 339 keyOrder = append(keyOrder, u.Identifier()) 340 } 341 for _, k := range keyOrder { 342 j, ok := resourcesJSON[k] 343 if !ok { 344 continue 345 } 346 if resJSON != "" { 347 resJSON += ",\n" 348 } 349 resJSON += j 350 } 351 return `{ 352 "versionInfo": "` + hexString(v) + `", 353 "resources": [` + resJSON + `], 354 "typeUrl": "type.googleapis.com/envoy.api.v2.Listener", 355 "nonce": "` + hexString(n) + `" 356 }` 357 } 358 359 func expectListenerJSON(t *testing.T, snap *proxycfg.ConfigSnapshot, token string, v, n uint64) string { 360 return expectListenerJSONFromResources(t, snap, token, v, n, 361 expectListenerJSONResources(t, snap, token, v, n)) 362 } 363 364 func expectClustersJSONResources(t *testing.T, snap *proxycfg.ConfigSnapshot, token string, v, n uint64) map[string]string { 365 return map[string]string{ 366 "local_app": ` 367 { 368 "@type": "type.googleapis.com/envoy.api.v2.Cluster", 369 "name": "local_app", 370 "connectTimeout": "5s", 371 "hosts": [ 372 { 373 "socketAddress": { 374 "address": "127.0.0.1", 375 "portValue": 8080 376 } 377 } 378 ] 379 }`, 380 "service:db": ` 381 { 382 "@type": "type.googleapis.com/envoy.api.v2.Cluster", 383 "name": "service:db", 384 "type": "EDS", 385 "edsClusterConfig": { 386 "edsConfig": { 387 "ads": { 388 389 } 390 } 391 }, 392 "connectTimeout": "5s", 393 "tlsContext": ` + expectedUpstreamTLSContextJSON(t, snap) + ` 394 }`, 395 "prepared_query:geo-cache": ` 396 { 397 "@type": "type.googleapis.com/envoy.api.v2.Cluster", 398 "name": "prepared_query:geo-cache", 399 "type": "EDS", 400 "edsClusterConfig": { 401 "edsConfig": { 402 "ads": { 403 404 } 405 } 406 }, 407 "connectTimeout": "5s", 408 "tlsContext": ` + expectedUpstreamTLSContextJSON(t, snap) + ` 409 }`, 410 } 411 } 412 413 func expectClustersJSONFromResources(t *testing.T, snap *proxycfg.ConfigSnapshot, token string, v, n uint64, resourcesJSON map[string]string) string { 414 resJSON := "" 415 416 // Sort resources into specific order because that matters in JSONEq 417 // comparison later. 418 keyOrder := []string{"local_app"} 419 for _, u := range snap.Proxy.Upstreams { 420 keyOrder = append(keyOrder, u.Identifier()) 421 } 422 for _, k := range keyOrder { 423 j, ok := resourcesJSON[k] 424 if !ok { 425 continue 426 } 427 if resJSON != "" { 428 resJSON += ",\n" 429 } 430 resJSON += j 431 } 432 433 return `{ 434 "versionInfo": "` + hexString(v) + `", 435 "resources": [` + resJSON + `], 436 "typeUrl": "type.googleapis.com/envoy.api.v2.Cluster", 437 "nonce": "` + hexString(n) + `" 438 }` 439 } 440 441 func expectClustersJSON(t *testing.T, snap *proxycfg.ConfigSnapshot, token string, v, n uint64) string { 442 return expectClustersJSONFromResources(t, snap, token, v, n, 443 expectClustersJSONResources(t, snap, token, v, n)) 444 } 445 446 func expectEndpointsJSON(t *testing.T, snap *proxycfg.ConfigSnapshot, token string, v, n uint64) string { 447 return `{ 448 "versionInfo": "` + hexString(v) + `", 449 "resources": [ 450 { 451 "@type": "type.googleapis.com/envoy.api.v2.ClusterLoadAssignment", 452 "clusterName": "service:db", 453 "endpoints": [ 454 { 455 "lbEndpoints": [ 456 { 457 "endpoint": { 458 "address": { 459 "socketAddress": { 460 "address": "10.10.1.1", 461 "portValue": 0 462 } 463 } 464 } 465 }, 466 { 467 "endpoint": { 468 "address": { 469 "socketAddress": { 470 "address": "10.10.1.2", 471 "portValue": 0 472 } 473 } 474 } 475 } 476 ] 477 } 478 ] 479 } 480 ], 481 "typeUrl": "type.googleapis.com/envoy.api.v2.ClusterLoadAssignment", 482 "nonce": "` + hexString(n) + `" 483 }` 484 } 485 486 func expectedUpstreamTLSContextJSON(t *testing.T, snap *proxycfg.ConfigSnapshot) string { 487 return expectedTLSContextJSON(t, snap, false) 488 } 489 490 func expectedPublicTLSContextJSON(t *testing.T, snap *proxycfg.ConfigSnapshot) string { 491 return expectedTLSContextJSON(t, snap, true) 492 } 493 494 func expectedTLSContextJSON(t *testing.T, snap *proxycfg.ConfigSnapshot, requireClientCert bool) string { 495 // Assume just one root for now, can get fancier later if needed. 496 caPEM := snap.Roots.Roots[0].RootCert 497 reqClient := "" 498 if requireClientCert { 499 reqClient = `, 500 "requireClientCertificate": true` 501 } 502 return `{ 503 "commonTlsContext": { 504 "tlsParams": {}, 505 "tlsCertificates": [ 506 { 507 "certificateChain": { 508 "inlineString": "` + strings.Replace(snap.Leaf.CertPEM, "\n", "\\n", -1) + `" 509 }, 510 "privateKey": { 511 "inlineString": "` + strings.Replace(snap.Leaf.PrivateKeyPEM, "\n", "\\n", -1) + `" 512 } 513 } 514 ], 515 "validationContext": { 516 "trustedCa": { 517 "inlineString": "` + strings.Replace(caPEM, "\n", "\\n", -1) + `" 518 } 519 } 520 } 521 ` + reqClient + ` 522 }` 523 } 524 525 func assertChanBlocked(t *testing.T, ch chan *envoy.DiscoveryResponse) { 526 t.Helper() 527 select { 528 case r := <-ch: 529 t.Fatalf("chan should block but received: %v", r) 530 case <-time.After(10 * time.Millisecond): 531 return 532 } 533 } 534 535 func assertResponseSent(t *testing.T, ch chan *envoy.DiscoveryResponse, wantJSON string) { 536 t.Helper() 537 select { 538 case r := <-ch: 539 assertResponse(t, r, wantJSON) 540 case <-time.After(50 * time.Millisecond): 541 t.Fatalf("no response received after 50ms") 542 } 543 } 544 545 // assertResponse is a helper to test a envoy.DiscoveryResponse matches the 546 // JSON representation we expect. We use JSON because the responses use protobuf 547 // Any type which includes binary protobuf encoding and would make creating 548 // expected structs require the same code that is under test! 549 func assertResponse(t *testing.T, r *envoy.DiscoveryResponse, wantJSON string) { 550 t.Helper() 551 m := jsonpb.Marshaler{ 552 Indent: " ", 553 } 554 gotJSON, err := m.MarshalToString(r) 555 require.NoError(t, err) 556 require.JSONEqf(t, wantJSON, gotJSON, "got:\n%s", gotJSON) 557 } 558 559 func TestServer_StreamAggregatedResources_ACLEnforcement(t *testing.T) { 560 561 tests := []struct { 562 name string 563 defaultDeny bool 564 acl string 565 token string 566 wantDenied bool 567 }{ 568 // Note that although we've stubbed actual ACL checks in the testManager 569 // ConnectAuthorize mock, by asserting against specific reason strings here 570 // even in the happy case which can't match the default one returned by the 571 // mock we are implicitly validating that the implementation used the 572 // correct token from the context. 573 { 574 name: "no ACLs configured", 575 defaultDeny: false, 576 wantDenied: false, 577 }, 578 { 579 name: "default deny, no token", 580 defaultDeny: true, 581 wantDenied: true, 582 }, 583 { 584 name: "default deny, service:write token", 585 defaultDeny: true, 586 acl: `service "web" { policy = "write" }`, 587 token: "service-write-on-web", 588 wantDenied: false, 589 }, 590 { 591 name: "default deny, service:read token", 592 defaultDeny: true, 593 acl: `service "web" { policy = "read" }`, 594 token: "service-write-on-web", 595 wantDenied: true, 596 }, 597 { 598 name: "default deny, service:write token on different service", 599 defaultDeny: true, 600 acl: `service "not-web" { policy = "write" }`, 601 token: "service-write-on-not-web", 602 wantDenied: true, 603 }, 604 } 605 606 for _, tt := range tests { 607 t.Run(tt.name, func(t *testing.T) { 608 logger := log.New(os.Stderr, "", log.LstdFlags) 609 mgr := newTestManager(t) 610 aclResolve := func(id string) (acl.Authorizer, error) { 611 if !tt.defaultDeny { 612 // Allow all 613 return acl.RootAuthorizer("allow"), nil 614 } 615 if tt.acl == "" { 616 // No token and defaultDeny is denied 617 return acl.RootAuthorizer("deny"), nil 618 } 619 // Ensure the correct token was passed 620 require.Equal(t, tt.token, id) 621 // Parse the ACL and enforce it 622 policy, err := acl.NewPolicyFromSource("", 0, tt.acl, acl.SyntaxLegacy, nil) 623 require.NoError(t, err) 624 return acl.NewPolicyAuthorizer(acl.RootAuthorizer("deny"), []*acl.Policy{policy}, nil) 625 } 626 envoy := NewTestEnvoy(t, "web-sidecar-proxy", tt.token) 627 defer envoy.Close() 628 629 s := Server{ 630 Logger: logger, 631 CfgMgr: mgr, 632 Authz: mgr, 633 ResolveToken: aclResolve, 634 } 635 s.Initialize() 636 637 errCh := make(chan error, 1) 638 go func() { 639 errCh <- s.StreamAggregatedResources(envoy.stream) 640 }() 641 642 // Register the proxy to create state needed to Watch() on 643 mgr.RegisterProxy(t, "web-sidecar-proxy") 644 645 // Deliver a new snapshot 646 snap := proxycfg.TestConfigSnapshot(t) 647 mgr.DeliverConfig(t, "web-sidecar-proxy", snap) 648 649 // Send initial listener discover, in real life Envoy always sends cluster 650 // first but it doesn't really matter and listener has a response that 651 // includes the token in the ext authz filter so lets us test more stuff. 652 envoy.SendReq(t, ListenerType, 0, 0) 653 654 if !tt.wantDenied { 655 assertResponseSent(t, envoy.stream.sendCh, expectListenerJSON(t, snap, tt.token, 1, 1)) 656 // Close the client stream since all is well. We _don't_ do this in the 657 // expected error case because we want to verify the error closes the 658 // stream from server side. 659 envoy.Close() 660 } 661 662 select { 663 case err := <-errCh: 664 if tt.wantDenied { 665 require.Error(t, err) 666 require.Contains(t, err.Error(), "permission denied") 667 mgr.AssertWatchCancelled(t, "web-sidecar-proxy") 668 } else { 669 require.NoError(t, err) 670 } 671 case <-time.After(50 * time.Millisecond): 672 t.Fatalf("timed out waiting for handler to finish") 673 } 674 }) 675 } 676 } 677 678 func TestServer_StreamAggregatedResources_ACLTokenDeleted_StreamTerminatedDuringDiscoveryRequest(t *testing.T) { 679 aclRules := `service "web" { policy = "write" }` 680 token := "service-write-on-web" 681 682 policy, err := acl.NewPolicyFromSource("", 0, aclRules, acl.SyntaxLegacy, nil) 683 require.NoError(t, err) 684 685 var validToken atomic.Value 686 validToken.Store(token) 687 688 logger := log.New(os.Stderr, "", log.LstdFlags) 689 mgr := newTestManager(t) 690 aclResolve := func(id string) (acl.Authorizer, error) { 691 if token := validToken.Load(); token == nil || id != token.(string) { 692 return nil, acl.ErrNotFound 693 } 694 695 return acl.NewPolicyAuthorizer(acl.RootAuthorizer("deny"), []*acl.Policy{policy}, nil) 696 } 697 envoy := NewTestEnvoy(t, "web-sidecar-proxy", token) 698 defer envoy.Close() 699 700 s := Server{ 701 Logger: logger, 702 CfgMgr: mgr, 703 Authz: mgr, 704 ResolveToken: aclResolve, 705 AuthCheckFrequency: 1 * time.Hour, // make sure this doesn't kick in 706 } 707 s.Initialize() 708 709 errCh := make(chan error, 1) 710 go func() { 711 errCh <- s.StreamAggregatedResources(envoy.stream) 712 }() 713 714 getError := func() (gotErr error, ok bool) { 715 select { 716 case err := <-errCh: 717 return err, true 718 default: 719 return nil, false 720 } 721 } 722 723 // Register the proxy to create state needed to Watch() on 724 mgr.RegisterProxy(t, "web-sidecar-proxy") 725 726 // Send initial cluster discover (OK) 727 envoy.SendReq(t, ClusterType, 0, 0) 728 { 729 err, ok := getError() 730 require.NoError(t, err) 731 require.False(t, ok) 732 } 733 734 // Check no response sent yet 735 assertChanBlocked(t, envoy.stream.sendCh) 736 { 737 err, ok := getError() 738 require.NoError(t, err) 739 require.False(t, ok) 740 } 741 742 // Deliver a new snapshot 743 snap := proxycfg.TestConfigSnapshot(t) 744 mgr.DeliverConfig(t, "web-sidecar-proxy", snap) 745 746 assertResponseSent(t, envoy.stream.sendCh, expectClustersJSON(t, snap, token, 1, 1)) 747 748 // Now nuke the ACL token. 749 validToken.Store("") 750 751 // It also (in parallel) issues the next cluster request (which acts as an ACK 752 // of the version we sent) 753 envoy.SendReq(t, ClusterType, 1, 1) 754 755 select { 756 case err := <-errCh: 757 require.Error(t, err) 758 gerr, ok := status.FromError(err) 759 require.Truef(t, ok, "not a grpc status error: type='%T' value=%v", err, err) 760 require.Equal(t, codes.Unauthenticated, gerr.Code()) 761 require.Equal(t, "unauthenticated: ACL not found", gerr.Message()) 762 763 mgr.AssertWatchCancelled(t, "web-sidecar-proxy") 764 case <-time.After(50 * time.Millisecond): 765 t.Fatalf("timed out waiting for handler to finish") 766 } 767 } 768 769 func TestServer_StreamAggregatedResources_ACLTokenDeleted_StreamTerminatedInBackground(t *testing.T) { 770 aclRules := `service "web" { policy = "write" }` 771 token := "service-write-on-web" 772 773 policy, err := acl.NewPolicyFromSource("", 0, aclRules, acl.SyntaxLegacy, nil) 774 require.NoError(t, err) 775 776 var validToken atomic.Value 777 validToken.Store(token) 778 779 logger := log.New(os.Stderr, "", log.LstdFlags) 780 mgr := newTestManager(t) 781 aclResolve := func(id string) (acl.Authorizer, error) { 782 if token := validToken.Load(); token == nil || id != token.(string) { 783 return nil, acl.ErrNotFound 784 } 785 786 return acl.NewPolicyAuthorizer(acl.RootAuthorizer("deny"), []*acl.Policy{policy}, nil) 787 } 788 envoy := NewTestEnvoy(t, "web-sidecar-proxy", token) 789 defer envoy.Close() 790 791 s := Server{ 792 Logger: logger, 793 CfgMgr: mgr, 794 Authz: mgr, 795 ResolveToken: aclResolve, 796 AuthCheckFrequency: 100 * time.Millisecond, // Make this short. 797 } 798 s.Initialize() 799 800 errCh := make(chan error, 1) 801 go func() { 802 errCh <- s.StreamAggregatedResources(envoy.stream) 803 }() 804 805 getError := func() (gotErr error, ok bool) { 806 select { 807 case err := <-errCh: 808 return err, true 809 default: 810 return nil, false 811 } 812 } 813 814 // Register the proxy to create state needed to Watch() on 815 mgr.RegisterProxy(t, "web-sidecar-proxy") 816 817 // Send initial cluster discover (OK) 818 envoy.SendReq(t, ClusterType, 0, 0) 819 { 820 err, ok := getError() 821 require.NoError(t, err) 822 require.False(t, ok) 823 } 824 825 // Check no response sent yet 826 assertChanBlocked(t, envoy.stream.sendCh) 827 { 828 err, ok := getError() 829 require.NoError(t, err) 830 require.False(t, ok) 831 } 832 833 // Deliver a new snapshot 834 snap := proxycfg.TestConfigSnapshot(t) 835 mgr.DeliverConfig(t, "web-sidecar-proxy", snap) 836 837 assertResponseSent(t, envoy.stream.sendCh, expectClustersJSON(t, snap, token, 1, 1)) 838 839 // It also (in parallel) issues the next cluster request (which acts as an ACK 840 // of the version we sent) 841 envoy.SendReq(t, ClusterType, 1, 1) 842 843 // Check no response sent yet 844 assertChanBlocked(t, envoy.stream.sendCh) 845 { 846 err, ok := getError() 847 require.NoError(t, err) 848 require.False(t, ok) 849 } 850 851 // Now nuke the ACL token while there's no activity. 852 validToken.Store("") 853 854 select { 855 case err := <-errCh: 856 require.Error(t, err) 857 gerr, ok := status.FromError(err) 858 require.Truef(t, ok, "not a grpc status error: type='%T' value=%v", err, err) 859 require.Equal(t, codes.Unauthenticated, gerr.Code()) 860 require.Equal(t, "unauthenticated: ACL not found", gerr.Message()) 861 862 mgr.AssertWatchCancelled(t, "web-sidecar-proxy") 863 case <-time.After(200 * time.Millisecond): 864 t.Fatalf("timed out waiting for handler to finish") 865 } 866 } 867 868 // This tests the ext_authz service method that implements connect authz. 869 func TestServer_Check(t *testing.T) { 870 871 tests := []struct { 872 name string 873 source string 874 dest string 875 sourcePrincipal string 876 destPrincipal string 877 authzResult connectAuthzResult 878 wantErr bool 879 wantErrCode codes.Code 880 wantDenied bool 881 wantReason string 882 }{ 883 { 884 name: "auth allowed", 885 source: "web", 886 dest: "db", 887 authzResult: connectAuthzResult{true, "default allow", nil, nil}, 888 wantDenied: false, 889 wantReason: "default allow", 890 }, 891 { 892 name: "auth denied", 893 source: "web", 894 dest: "db", 895 authzResult: connectAuthzResult{false, "default deny", nil, nil}, 896 wantDenied: true, 897 wantReason: "default deny", 898 }, 899 { 900 name: "no source", 901 sourcePrincipal: "", 902 dest: "db", 903 // Should never make it to authz call. 904 wantErr: true, 905 wantErrCode: codes.InvalidArgument, 906 }, 907 { 908 name: "no dest", 909 source: "web", 910 dest: "", 911 // Should never make it to authz call. 912 wantErr: true, 913 wantErrCode: codes.InvalidArgument, 914 }, 915 { 916 name: "dest invalid format", 917 source: "web", 918 destPrincipal: "not-a-spiffe-id", 919 // Should never make it to authz call. 920 wantDenied: true, 921 wantReason: "Destination Principal is not a valid Connect identity", 922 }, 923 { 924 name: "dest not a service URI", 925 source: "web", 926 destPrincipal: "spiffe://trust-domain.consul", 927 // Should never make it to authz call. 928 wantDenied: true, 929 wantReason: "Destination Principal is not a valid Service identity", 930 }, 931 { 932 name: "ACL not got permission for authz call", 933 source: "web", 934 dest: "db", 935 authzResult: connectAuthzResult{false, "", nil, acl.ErrPermissionDenied}, 936 wantErr: true, 937 wantErrCode: codes.PermissionDenied, 938 }, 939 { 940 name: "Random error running authz", 941 source: "web", 942 dest: "db", 943 authzResult: connectAuthzResult{false, "", nil, errors.New("gremlin attack")}, 944 wantErr: true, 945 wantErrCode: codes.Internal, 946 }, 947 } 948 949 for _, tt := range tests { 950 t.Run(tt.name, func(t *testing.T) { 951 token := "my-real-acl-token" 952 logger := log.New(os.Stderr, "", log.LstdFlags) 953 mgr := newTestManager(t) 954 955 // Setup expected auth result against that token no lock as no other 956 // goroutine is touching this yet. 957 mgr.authz[token] = tt.authzResult 958 959 aclResolve := func(id string) (acl.Authorizer, error) { 960 return nil, nil 961 } 962 envoy := NewTestEnvoy(t, "web-sidecar-proxy", token) 963 defer envoy.Close() 964 965 s := Server{ 966 Logger: logger, 967 CfgMgr: mgr, 968 Authz: mgr, 969 ResolveToken: aclResolve, 970 } 971 s.Initialize() 972 973 // Create a context with the correct token 974 ctx := metadata.NewIncomingContext(context.Background(), 975 metadata.Pairs("x-consul-token", token)) 976 977 r := TestCheckRequest(t, tt.source, tt.dest) 978 // If sourcePrincipal is set override, or if source is also not set 979 // explicitly override to empty. 980 if tt.sourcePrincipal != "" || tt.source == "" { 981 r.Attributes.Source.Principal = tt.sourcePrincipal 982 } 983 if tt.destPrincipal != "" || tt.dest == "" { 984 r.Attributes.Destination.Principal = tt.destPrincipal 985 } 986 resp, err := s.Check(ctx, r) 987 // Denied is not an error 988 if tt.wantErr { 989 require.Error(t, err) 990 grpcStatus := status.Convert(err) 991 require.Equal(t, tt.wantErrCode, grpcStatus.Code()) 992 require.Nil(t, resp) 993 return 994 } 995 require.NoError(t, err) 996 if tt.wantDenied { 997 require.Equal(t, int32(codes.PermissionDenied), resp.Status.Code) 998 } else { 999 require.Equal(t, int32(codes.OK), resp.Status.Code) 1000 } 1001 require.Contains(t, resp.Status.Message, tt.wantReason) 1002 }) 1003 } 1004 } 1005 1006 func TestServer_ConfigOverridesListeners(t *testing.T) { 1007 1008 tests := []struct { 1009 name string 1010 setup func(snap *proxycfg.ConfigSnapshot) string 1011 }{ 1012 { 1013 name: "sanity check no custom", 1014 setup: func(snap *proxycfg.ConfigSnapshot) string { 1015 // Default snap and expectation 1016 return expectListenerJSON(t, snap, "my-token", 1, 1) 1017 }, 1018 }, 1019 { 1020 name: "custom public_listener no type", 1021 setup: func(snap *proxycfg.ConfigSnapshot) string { 1022 snap.Proxy.Config["envoy_public_listener_json"] = 1023 customListenerJSON(t, customListenerJSONOptions{ 1024 Name: "custom-public-listen", 1025 IncludeType: false, 1026 }) 1027 resources := expectListenerJSONResources(t, snap, "my-token", 1, 1) 1028 1029 // Replace the public listener with the custom one WITH type since 1030 // that's how it comes out the other end, and with TLS and authz 1031 // overridden. 1032 resources["public_listener"] = customListenerJSON(t, customListenerJSONOptions{ 1033 Name: "custom-public-listen", 1034 // We should add type, TLS and authz 1035 IncludeType: true, 1036 OverrideAuthz: true, 1037 TLSContext: expectedPublicTLSContextJSON(t, snap), 1038 }) 1039 return expectListenerJSONFromResources(t, snap, "my-token", 1, 1, resources) 1040 }, 1041 }, 1042 { 1043 name: "custom public_listener with type", 1044 setup: func(snap *proxycfg.ConfigSnapshot) string { 1045 snap.Proxy.Config["envoy_public_listener_json"] = 1046 customListenerJSON(t, customListenerJSONOptions{ 1047 Name: "custom-public-listen", 1048 IncludeType: true, 1049 }) 1050 resources := expectListenerJSONResources(t, snap, "my-token", 1, 1) 1051 1052 // Replace the public listener with the custom one WITH type since 1053 // that's how it comes out the other end, and with TLS and authz 1054 // overridden. 1055 resources["public_listener"] = customListenerJSON(t, customListenerJSONOptions{ 1056 Name: "custom-public-listen", 1057 // We should add type, TLS and authz 1058 IncludeType: true, 1059 OverrideAuthz: true, 1060 TLSContext: expectedPublicTLSContextJSON(t, snap), 1061 }) 1062 return expectListenerJSONFromResources(t, snap, "my-token", 1, 1, resources) 1063 }, 1064 }, 1065 { 1066 name: "custom public_listener with TLS should be overridden", 1067 setup: func(snap *proxycfg.ConfigSnapshot) string { 1068 snap.Proxy.Config["envoy_public_listener_json"] = 1069 customListenerJSON(t, customListenerJSONOptions{ 1070 Name: "custom-public-listen", 1071 IncludeType: true, 1072 TLSContext: `{"requireClientCertificate": false}`, 1073 }) 1074 resources := expectListenerJSONResources(t, snap, "my-token", 1, 1) 1075 1076 // Replace the public listener with the custom one WITH type since 1077 // that's how it comes out the other end, and with TLS and authz 1078 // overridden. 1079 resources["public_listener"] = customListenerJSON(t, customListenerJSONOptions{ 1080 Name: "custom-public-listen", 1081 // We should add type, TLS and authz 1082 IncludeType: true, 1083 OverrideAuthz: true, 1084 TLSContext: expectedPublicTLSContextJSON(t, snap), 1085 }) 1086 return expectListenerJSONFromResources(t, snap, "my-token", 1, 1, resources) 1087 }, 1088 }, 1089 { 1090 name: "custom upstream no type", 1091 setup: func(snap *proxycfg.ConfigSnapshot) string { 1092 snap.Proxy.Upstreams[0].Config["envoy_listener_json"] = 1093 customListenerJSON(t, customListenerJSONOptions{ 1094 Name: "custom-upstream", 1095 IncludeType: false, 1096 }) 1097 resources := expectListenerJSONResources(t, snap, "my-token", 1, 1) 1098 1099 // Replace an upstream listener with the custom one WITH type since 1100 // that's how it comes out the other end. Note we do override TLS 1101 resources["service:db"] = 1102 customListenerJSON(t, customListenerJSONOptions{ 1103 Name: "custom-upstream", 1104 // We should add type 1105 IncludeType: true, 1106 }) 1107 return expectListenerJSONFromResources(t, snap, "my-token", 1, 1, resources) 1108 }, 1109 }, 1110 { 1111 name: "custom upstream with type", 1112 setup: func(snap *proxycfg.ConfigSnapshot) string { 1113 snap.Proxy.Upstreams[0].Config["envoy_listener_json"] = 1114 customListenerJSON(t, customListenerJSONOptions{ 1115 Name: "custom-upstream", 1116 IncludeType: true, 1117 }) 1118 resources := expectListenerJSONResources(t, snap, "my-token", 1, 1) 1119 1120 // Replace an upstream listener with the custom one WITH type since 1121 // that's how it comes out the other end. 1122 resources["service:db"] = 1123 customListenerJSON(t, customListenerJSONOptions{ 1124 Name: "custom-upstream", 1125 // We should add type 1126 IncludeType: true, 1127 }) 1128 return expectListenerJSONFromResources(t, snap, "my-token", 1, 1, resources) 1129 }, 1130 }, 1131 } 1132 1133 for _, tt := range tests { 1134 t.Run(tt.name, func(t *testing.T) { 1135 require := require.New(t) 1136 1137 // Sanity check default with no overrides first 1138 snap := proxycfg.TestConfigSnapshot(t) 1139 expect := tt.setup(snap) 1140 1141 listeners, err := listenersFromSnapshot(snap, "my-token") 1142 require.NoError(err) 1143 r, err := createResponse(ListenerType, "00000001", "00000001", listeners) 1144 require.NoError(err) 1145 1146 assertResponse(t, r, expect) 1147 }) 1148 } 1149 } 1150 1151 func TestServer_ConfigOverridesClusters(t *testing.T) { 1152 1153 tests := []struct { 1154 name string 1155 setup func(snap *proxycfg.ConfigSnapshot) string 1156 }{ 1157 { 1158 name: "sanity check no custom", 1159 setup: func(snap *proxycfg.ConfigSnapshot) string { 1160 // Default snap and expectation 1161 return expectClustersJSON(t, snap, "my-token", 1, 1) 1162 }, 1163 }, 1164 { 1165 name: "custom public with no type", 1166 setup: func(snap *proxycfg.ConfigSnapshot) string { 1167 snap.Proxy.Config["envoy_local_cluster_json"] = 1168 customAppClusterJSON(t, customClusterJSONOptions{ 1169 Name: "mylocal", 1170 IncludeType: false, 1171 }) 1172 resources := expectClustersJSONResources(t, snap, "my-token", 1, 1) 1173 1174 // Replace an upstream listener with the custom one WITH type since 1175 // that's how it comes out the other end. 1176 resources["local_app"] = 1177 customAppClusterJSON(t, customClusterJSONOptions{ 1178 Name: "mylocal", 1179 IncludeType: true, 1180 }) 1181 return expectClustersJSONFromResources(t, snap, "my-token", 1, 1, resources) 1182 }, 1183 }, 1184 { 1185 name: "custom public with type", 1186 setup: func(snap *proxycfg.ConfigSnapshot) string { 1187 snap.Proxy.Config["envoy_local_cluster_json"] = 1188 customAppClusterJSON(t, customClusterJSONOptions{ 1189 Name: "mylocal", 1190 IncludeType: true, 1191 }) 1192 resources := expectClustersJSONResources(t, snap, "my-token", 1, 1) 1193 1194 // Replace an upstream listener with the custom one WITH type since 1195 // that's how it comes out the other end. 1196 resources["local_app"] = 1197 customAppClusterJSON(t, customClusterJSONOptions{ 1198 Name: "mylocal", 1199 IncludeType: true, 1200 }) 1201 return expectClustersJSONFromResources(t, snap, "my-token", 1, 1, resources) 1202 }, 1203 }, 1204 { 1205 name: "custom upstream with no type", 1206 setup: func(snap *proxycfg.ConfigSnapshot) string { 1207 snap.Proxy.Upstreams[0].Config["envoy_cluster_json"] = 1208 customEDSClusterJSON(t, customClusterJSONOptions{ 1209 Name: "myservice", 1210 IncludeType: false, 1211 }) 1212 resources := expectClustersJSONResources(t, snap, "my-token", 1, 1) 1213 1214 // Replace an upstream listener with the custom one WITH type since 1215 // that's how it comes out the other end. 1216 resources["service:db"] = 1217 customEDSClusterJSON(t, customClusterJSONOptions{ 1218 Name: "myservice", 1219 IncludeType: true, 1220 TLSContext: expectedUpstreamTLSContextJSON(t, snap), 1221 }) 1222 return expectClustersJSONFromResources(t, snap, "my-token", 1, 1, resources) 1223 }, 1224 }, 1225 { 1226 name: "custom upstream with type", 1227 setup: func(snap *proxycfg.ConfigSnapshot) string { 1228 snap.Proxy.Upstreams[0].Config["envoy_cluster_json"] = 1229 customEDSClusterJSON(t, customClusterJSONOptions{ 1230 Name: "myservice", 1231 IncludeType: true, 1232 }) 1233 resources := expectClustersJSONResources(t, snap, "my-token", 1, 1) 1234 1235 // Replace an upstream listener with the custom one WITH type since 1236 // that's how it comes out the other end. 1237 resources["service:db"] = 1238 customEDSClusterJSON(t, customClusterJSONOptions{ 1239 Name: "myservice", 1240 IncludeType: true, 1241 TLSContext: expectedUpstreamTLSContextJSON(t, snap), 1242 }) 1243 return expectClustersJSONFromResources(t, snap, "my-token", 1, 1, resources) 1244 }, 1245 }, 1246 } 1247 1248 for _, tt := range tests { 1249 t.Run(tt.name, func(t *testing.T) { 1250 require := require.New(t) 1251 1252 // Sanity check default with no overrides first 1253 snap := proxycfg.TestConfigSnapshot(t) 1254 expect := tt.setup(snap) 1255 1256 clusters, err := clustersFromSnapshot(snap, "my-token") 1257 require.NoError(err) 1258 r, err := createResponse(ClusterType, "00000001", "00000001", clusters) 1259 require.NoError(err) 1260 1261 fmt.Println(r) 1262 1263 assertResponse(t, r, expect) 1264 }) 1265 } 1266 } 1267 1268 type customListenerJSONOptions struct { 1269 Name string 1270 IncludeType bool 1271 OverrideAuthz bool 1272 TLSContext string 1273 } 1274 1275 const customListenerJSONTpl = `{ 1276 {{ if .IncludeType -}} 1277 "@type": "type.googleapis.com/envoy.api.v2.Listener", 1278 {{- end }} 1279 "name": "{{ .Name }}", 1280 "address": { 1281 "socketAddress": { 1282 "address": "11.11.11.11", 1283 "portValue": 11111 1284 } 1285 }, 1286 "filterChains": [ 1287 { 1288 {{ if .TLSContext -}} 1289 "tlsContext": {{ .TLSContext }}, 1290 {{- end }} 1291 "filters": [ 1292 {{ if .OverrideAuthz -}} 1293 { 1294 "name": "envoy.ext_authz", 1295 "config": { 1296 "grpc_service": { 1297 "envoy_grpc": { 1298 "cluster_name": "local_agent" 1299 }, 1300 "initial_metadata": [ 1301 { 1302 "key": "x-consul-token", 1303 "value": "my-token" 1304 } 1305 ] 1306 }, 1307 "stat_prefix": "connect_authz" 1308 } 1309 }, 1310 {{- end }} 1311 { 1312 "name": "envoy.tcp_proxy", 1313 "config": { 1314 "cluster": "random-cluster", 1315 "stat_prefix": "foo-stats" 1316 } 1317 } 1318 ] 1319 } 1320 ] 1321 }` 1322 1323 var customListenerJSONTemplate = template.Must(template.New("").Parse(customListenerJSONTpl)) 1324 1325 func customListenerJSON(t *testing.T, opts customListenerJSONOptions) string { 1326 t.Helper() 1327 var buf bytes.Buffer 1328 err := customListenerJSONTemplate.Execute(&buf, opts) 1329 require.NoError(t, err) 1330 return buf.String() 1331 } 1332 1333 type customClusterJSONOptions struct { 1334 Name string 1335 IncludeType bool 1336 TLSContext string 1337 } 1338 1339 var customEDSClusterJSONTpl = `{ 1340 {{ if .IncludeType -}} 1341 "@type": "type.googleapis.com/envoy.api.v2.Cluster", 1342 {{- end }} 1343 {{ if .TLSContext -}} 1344 "tlsContext": {{ .TLSContext }}, 1345 {{- end }} 1346 "name": "{{ .Name }}", 1347 "type": "EDS", 1348 "edsClusterConfig": { 1349 "edsConfig": { 1350 "ads": { 1351 1352 } 1353 } 1354 }, 1355 "connectTimeout": "5s" 1356 }` 1357 1358 var customEDSClusterJSONTemplate = template.Must(template.New("").Parse(customEDSClusterJSONTpl)) 1359 1360 func customEDSClusterJSON(t *testing.T, opts customClusterJSONOptions) string { 1361 t.Helper() 1362 var buf bytes.Buffer 1363 err := customEDSClusterJSONTemplate.Execute(&buf, opts) 1364 require.NoError(t, err) 1365 return buf.String() 1366 } 1367 1368 var customAppClusterJSONTpl = `{ 1369 {{ if .IncludeType -}} 1370 "@type": "type.googleapis.com/envoy.api.v2.Cluster", 1371 {{- end }} 1372 {{ if .TLSContext -}} 1373 "tlsContext": {{ .TLSContext }}, 1374 {{- end }} 1375 "name": "{{ .Name }}", 1376 "connectTimeout": "5s", 1377 "hosts": [ 1378 { 1379 "socketAddress": { 1380 "address": "127.0.0.1", 1381 "portValue": 8080 1382 } 1383 } 1384 ] 1385 }` 1386 1387 var customAppClusterJSONTemplate = template.Must(template.New("").Parse(customAppClusterJSONTpl)) 1388 1389 func customAppClusterJSON(t *testing.T, opts customClusterJSONOptions) string { 1390 t.Helper() 1391 var buf bytes.Buffer 1392 err := customAppClusterJSONTemplate.Execute(&buf, opts) 1393 require.NoError(t, err) 1394 return buf.String() 1395 }