1package autoconf 2 3import ( 4 "context" 5 "fmt" 6 "io/ioutil" 7 "net" 8 "os" 9 "path/filepath" 10 "sync" 11 "testing" 12 "time" 13 14 "github.com/stretchr/testify/mock" 15 "github.com/stretchr/testify/require" 16 17 "github.com/hashicorp/consul/agent/cache" 18 cachetype "github.com/hashicorp/consul/agent/cache-types" 19 "github.com/hashicorp/consul/agent/config" 20 "github.com/hashicorp/consul/agent/connect" 21 "github.com/hashicorp/consul/agent/metadata" 22 "github.com/hashicorp/consul/agent/structs" 23 "github.com/hashicorp/consul/agent/token" 24 "github.com/hashicorp/consul/lib/retry" 25 "github.com/hashicorp/consul/proto/pbautoconf" 26 "github.com/hashicorp/consul/proto/pbconfig" 27 "github.com/hashicorp/consul/sdk/testutil" 28 testretry "github.com/hashicorp/consul/sdk/testutil/retry" 29) 30 31type configLoader struct { 32 opts config.LoadOpts 33} 34 35func (c *configLoader) Load(source config.Source) (config.LoadResult, error) { 36 opts := c.opts 37 opts.DefaultConfig = source 38 return config.Load(opts) 39} 40 41func (c *configLoader) addConfigHCL(cfg string) { 42 c.opts.HCL = append(c.opts.HCL, cfg) 43} 44 45func requireChanNotReady(t *testing.T, ch <-chan struct{}) { 46 select { 47 case <-ch: 48 require.Fail(t, "chan is ready when it shouldn't be") 49 default: 50 return 51 } 52} 53 54func requireChanReady(t *testing.T, ch <-chan struct{}) { 55 select { 56 case <-ch: 57 return 58 default: 59 require.Fail(t, "chan is not ready when it should be") 60 } 61} 62 63func waitForChan(timer *time.Timer, ch <-chan struct{}) bool { 64 select { 65 case <-timer.C: 66 return false 67 case <-ch: 68 return true 69 } 70} 71 72func waitForChans(timeout time.Duration, chans ...<-chan struct{}) bool { 73 timer := time.NewTimer(timeout) 74 defer timer.Stop() 75 76 for _, ch := range chans { 77 if !waitForChan(timer, ch) { 78 return false 79 } 80 } 81 return true 82} 83 84func TestNew(t *testing.T) { 85 type testCase struct { 86 modify func(*Config) 87 err string 88 validate func(t *testing.T, ac *AutoConfig) 89 } 90 91 cases := map[string]testCase{ 92 "no-direct-rpc": { 93 modify: func(c *Config) { 94 c.DirectRPC = nil 95 }, 96 err: "must provide a direct RPC delegate", 97 }, 98 "no-config-loader": { 99 modify: func(c *Config) { 100 c.Loader = nil 101 }, 102 err: "must provide a config loader", 103 }, 104 "no-cache": { 105 modify: func(c *Config) { 106 c.Cache = nil 107 }, 108 err: "must provide a cache", 109 }, 110 "no-tls-configurator": { 111 modify: func(c *Config) { 112 c.TLSConfigurator = nil 113 }, 114 err: "must provide a TLS configurator", 115 }, 116 "no-tokens": { 117 modify: func(c *Config) { 118 c.Tokens = nil 119 }, 120 err: "must provide a token store", 121 }, 122 "ok": { 123 validate: func(t *testing.T, ac *AutoConfig) { 124 t.Helper() 125 require.NotNil(t, ac.logger) 126 require.NotNil(t, ac.acConfig.Waiter) 127 require.Equal(t, time.Minute, ac.acConfig.FallbackRetry) 128 require.Equal(t, 10*time.Second, ac.acConfig.FallbackLeeway) 129 }, 130 }, 131 } 132 133 for name, tcase := range cases { 134 t.Run(name, func(t *testing.T) { 135 cfg := Config{ 136 Loader: func(source config.Source) (result config.LoadResult, err error) { 137 return config.LoadResult{}, nil 138 }, 139 DirectRPC: newMockDirectRPC(t), 140 Tokens: newMockTokenStore(t), 141 Cache: newMockCache(t), 142 TLSConfigurator: newMockTLSConfigurator(t), 143 ServerProvider: newMockServerProvider(t), 144 EnterpriseConfig: newEnterpriseConfig(t), 145 } 146 147 if tcase.modify != nil { 148 tcase.modify(&cfg) 149 } 150 151 ac, err := New(cfg) 152 if tcase.err != "" { 153 testutil.RequireErrorContains(t, err, tcase.err) 154 } else { 155 require.NoError(t, err) 156 require.NotNil(t, ac) 157 if tcase.validate != nil { 158 tcase.validate(t, ac) 159 } 160 } 161 }) 162 } 163} 164 165func TestReadConfig(t *testing.T) { 166 // just testing that some auto config source gets injected 167 ac := AutoConfig{ 168 autoConfigSource: config.LiteralSource{ 169 Name: autoConfigFileName, 170 Config: config.Config{NodeName: stringPointer("hobbiton")}, 171 }, 172 logger: testutil.Logger(t), 173 acConfig: Config{ 174 Loader: func(source config.Source) (config.LoadResult, error) { 175 r := config.LoadResult{} 176 cfg, _, err := source.Parse() 177 if err != nil { 178 return r, err 179 } 180 181 r.RuntimeConfig = &config.RuntimeConfig{ 182 DevMode: true, 183 NodeName: *cfg.NodeName, 184 } 185 return r, nil 186 }, 187 }, 188 } 189 190 cfg, err := ac.ReadConfig() 191 require.NoError(t, err) 192 require.NotNil(t, cfg) 193 require.Equal(t, "hobbiton", cfg.NodeName) 194 require.True(t, cfg.DevMode) 195 require.Same(t, ac.config, cfg) 196} 197 198func setupRuntimeConfig(t *testing.T) *configLoader { 199 t.Helper() 200 201 dataDir := testutil.TempDir(t, "auto-config") 202 203 opts := config.LoadOpts{ 204 FlagValues: config.Config{ 205 DataDir: &dataDir, 206 Datacenter: stringPointer("dc1"), 207 NodeName: stringPointer("autoconf"), 208 BindAddr: stringPointer("127.0.0.1"), 209 }, 210 } 211 return &configLoader{opts: opts} 212} 213 214func TestInitialConfiguration_disabled(t *testing.T) { 215 mcfg := newMockedConfig(t) 216 mcfg.loader.addConfigHCL(` 217 primary_datacenter = "primary" 218 auto_config = { 219 enabled = false 220 } 221 `) 222 223 ac, err := New(mcfg.Config) 224 require.NoError(t, err) 225 require.NotNil(t, ac) 226 227 cfg, err := ac.InitialConfiguration(context.Background()) 228 require.NoError(t, err) 229 require.NotNil(t, cfg) 230 require.Equal(t, "primary", cfg.PrimaryDatacenter) 231 require.NoFileExists(t, filepath.Join(*mcfg.loader.opts.FlagValues.DataDir, autoConfigFileName)) 232} 233 234func TestInitialConfiguration_cancelled(t *testing.T) { 235 if testing.Short() { 236 t.Skip("too slow for testing.Short") 237 } 238 239 mcfg := newMockedConfig(t) 240 241 loader := setupRuntimeConfig(t) 242 loader.addConfigHCL(` 243 primary_datacenter = "primary" 244 auto_config = { 245 enabled = true 246 intro_token = "blarg" 247 server_addresses = ["127.0.0.1:8300"] 248 } 249 verify_outgoing = true 250 `) 251 mcfg.Config.Loader = loader.Load 252 253 expectedRequest := pbautoconf.AutoConfigRequest{ 254 Datacenter: "dc1", 255 Node: "autoconf", 256 JWT: "blarg", 257 } 258 259 mcfg.directRPC.On("RPC", "dc1", "autoconf", &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 8300}, "AutoConfig.InitialConfiguration", &expectedRequest, mock.Anything).Return(fmt.Errorf("injected error")).Times(0).Maybe() 260 mcfg.serverProvider.On("FindLANServer").Return(nil).Times(0).Maybe() 261 262 ac, err := New(mcfg.Config) 263 require.NoError(t, err) 264 require.NotNil(t, ac) 265 266 ctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(100*time.Millisecond)) 267 defer cancelFn() 268 269 cfg, err := ac.InitialConfiguration(ctx) 270 testutil.RequireErrorContains(t, err, context.DeadlineExceeded.Error()) 271 require.Nil(t, cfg) 272} 273 274func TestInitialConfiguration_restored(t *testing.T) { 275 mcfg := newMockedConfig(t) 276 277 loader := setupRuntimeConfig(t) 278 loader.addConfigHCL(` 279 auto_config = { 280 enabled = true 281 intro_token ="blarg" 282 server_addresses = ["127.0.0.1:8300"] 283 } 284 verify_outgoing = true 285 `) 286 287 mcfg.Config.Loader = loader.Load 288 289 indexedRoots, cert, extraCACerts := mcfg.setupInitialTLS(t, "autoconf", "dc1", "secret") 290 291 // persist an auto config response to the data dir where it is expected 292 persistedFile := filepath.Join(*loader.opts.FlagValues.DataDir, autoConfigFileName) 293 response := &pbautoconf.AutoConfigResponse{ 294 Config: &pbconfig.Config{ 295 PrimaryDatacenter: "primary", 296 TLS: &pbconfig.TLS{ 297 VerifyServerHostname: true, 298 }, 299 ACL: &pbconfig.ACL{ 300 Tokens: &pbconfig.ACLTokens{ 301 Agent: "secret", 302 }, 303 }, 304 }, 305 CARoots: mustTranslateCARootsToProtobuf(t, indexedRoots), 306 Certificate: mustTranslateIssuedCertToProtobuf(t, cert), 307 ExtraCACertificates: extraCACerts, 308 } 309 data, err := pbMarshaler.MarshalToString(response) 310 require.NoError(t, err) 311 require.NoError(t, ioutil.WriteFile(persistedFile, []byte(data), 0600)) 312 313 // recording the initial configuration even when restoring is going to update 314 // the agent token in the token store 315 mcfg.tokens.On("UpdateAgentToken", "secret", token.TokenSourceConfig).Return(true).Once() 316 317 // prepopulation is going to grab the token to populate the correct cache key 318 mcfg.tokens.On("AgentToken").Return("secret").Times(0) 319 320 ac, err := New(mcfg.Config) 321 require.NoError(t, err) 322 require.NotNil(t, ac) 323 324 cfg, err := ac.InitialConfiguration(context.Background()) 325 require.NoError(t, err, data) 326 require.NotNil(t, cfg) 327 require.Equal(t, "primary", cfg.PrimaryDatacenter) 328} 329 330func TestInitialConfiguration_success(t *testing.T) { 331 mcfg := newMockedConfig(t) 332 loader := setupRuntimeConfig(t) 333 loader.addConfigHCL(` 334 auto_config = { 335 enabled = true 336 intro_token ="blarg" 337 server_addresses = ["127.0.0.1:8300"] 338 } 339 verify_outgoing = true 340 `) 341 mcfg.Config.Loader = loader.Load 342 343 indexedRoots, cert, extraCerts := mcfg.setupInitialTLS(t, "autoconf", "dc1", "secret") 344 345 // this gets called when InitialConfiguration is invoked to record the token from the 346 // auto-config response 347 mcfg.tokens.On("UpdateAgentToken", "secret", token.TokenSourceConfig).Return(true).Once() 348 349 // prepopulation is going to grab the token to populate the correct cache key 350 mcfg.tokens.On("AgentToken").Return("secret").Times(0) 351 352 // no server provider 353 mcfg.serverProvider.On("FindLANServer").Return(nil).Times(0) 354 355 populateResponse := func(args mock.Arguments) { 356 resp, ok := args.Get(5).(*pbautoconf.AutoConfigResponse) 357 require.True(t, ok) 358 resp.Config = &pbconfig.Config{ 359 PrimaryDatacenter: "primary", 360 TLS: &pbconfig.TLS{ 361 VerifyServerHostname: true, 362 }, 363 ACL: &pbconfig.ACL{ 364 Tokens: &pbconfig.ACLTokens{ 365 Agent: "secret", 366 }, 367 }, 368 } 369 370 resp.CARoots = mustTranslateCARootsToProtobuf(t, indexedRoots) 371 resp.Certificate = mustTranslateIssuedCertToProtobuf(t, cert) 372 resp.ExtraCACertificates = extraCerts 373 } 374 375 expectedRequest := pbautoconf.AutoConfigRequest{ 376 Datacenter: "dc1", 377 Node: "autoconf", 378 JWT: "blarg", 379 } 380 381 mcfg.directRPC.On( 382 "RPC", 383 "dc1", 384 "autoconf", 385 &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 8300}, 386 "AutoConfig.InitialConfiguration", 387 &expectedRequest, 388 &pbautoconf.AutoConfigResponse{}).Return(nil).Run(populateResponse) 389 390 ac, err := New(mcfg.Config) 391 require.NoError(t, err) 392 require.NotNil(t, ac) 393 394 cfg, err := ac.InitialConfiguration(context.Background()) 395 require.NoError(t, err) 396 require.NotNil(t, cfg) 397 require.Equal(t, "primary", cfg.PrimaryDatacenter) 398 399 // the file was written to. 400 persistedFile := filepath.Join(*loader.opts.FlagValues.DataDir, autoConfigFileName) 401 require.FileExists(t, persistedFile) 402} 403 404func TestInitialConfiguration_retries(t *testing.T) { 405 mcfg := newMockedConfig(t) 406 loader := setupRuntimeConfig(t) 407 loader.addConfigHCL(` 408 auto_config = { 409 enabled = true 410 intro_token ="blarg" 411 server_addresses = [ 412 "198.18.0.1:8300", 413 "198.18.0.2:8398", 414 "198.18.0.3:8399", 415 "127.0.0.1:1234" 416 ] 417 } 418 verify_outgoing = true 419 `) 420 mcfg.Config.Loader = loader.Load 421 422 // reduce the retry wait times to make this test run faster 423 mcfg.Config.Waiter = &retry.Waiter{MinFailures: 2, MaxWait: time.Millisecond} 424 425 indexedRoots, cert, extraCerts := mcfg.setupInitialTLS(t, "autoconf", "dc1", "secret") 426 427 // this gets called when InitialConfiguration is invoked to record the token from the 428 // auto-config response 429 mcfg.tokens.On("UpdateAgentToken", "secret", token.TokenSourceConfig).Return(true).Once() 430 431 // prepopulation is going to grab the token to populate the correct cache key 432 mcfg.tokens.On("AgentToken").Return("secret").Times(0) 433 434 // no server provider 435 mcfg.serverProvider.On("FindLANServer").Return(nil).Times(0) 436 437 populateResponse := func(args mock.Arguments) { 438 resp, ok := args.Get(5).(*pbautoconf.AutoConfigResponse) 439 require.True(t, ok) 440 resp.Config = &pbconfig.Config{ 441 PrimaryDatacenter: "primary", 442 TLS: &pbconfig.TLS{ 443 VerifyServerHostname: true, 444 }, 445 ACL: &pbconfig.ACL{ 446 Tokens: &pbconfig.ACLTokens{ 447 Agent: "secret", 448 }, 449 }, 450 } 451 452 resp.CARoots = mustTranslateCARootsToProtobuf(t, indexedRoots) 453 resp.Certificate = mustTranslateIssuedCertToProtobuf(t, cert) 454 resp.ExtraCACertificates = extraCerts 455 } 456 457 expectedRequest := pbautoconf.AutoConfigRequest{ 458 Datacenter: "dc1", 459 Node: "autoconf", 460 JWT: "blarg", 461 } 462 463 // basically the 198.18.0.* addresses should fail indefinitely. the first time through the 464 // outer loop we inject a failure for the DNS resolution of localhost to 127.0.0.1. Then 465 // the second time through the outer loop we allow the localhost one to work. 466 mcfg.directRPC.On( 467 "RPC", 468 "dc1", 469 "autoconf", 470 &net.TCPAddr{IP: net.IPv4(198, 18, 0, 1), Port: 8300}, 471 "AutoConfig.InitialConfiguration", 472 &expectedRequest, 473 &pbautoconf.AutoConfigResponse{}).Return(fmt.Errorf("injected failure")).Times(0) 474 mcfg.directRPC.On( 475 "RPC", 476 "dc1", 477 "autoconf", 478 &net.TCPAddr{IP: net.IPv4(198, 18, 0, 2), Port: 8398}, 479 "AutoConfig.InitialConfiguration", 480 &expectedRequest, 481 &pbautoconf.AutoConfigResponse{}).Return(fmt.Errorf("injected failure")).Times(0) 482 mcfg.directRPC.On( 483 "RPC", 484 "dc1", 485 "autoconf", 486 &net.TCPAddr{IP: net.IPv4(198, 18, 0, 3), Port: 8399}, 487 "AutoConfig.InitialConfiguration", 488 &expectedRequest, 489 &pbautoconf.AutoConfigResponse{}).Return(fmt.Errorf("injected failure")).Times(0) 490 mcfg.directRPC.On( 491 "RPC", 492 "dc1", 493 "autoconf", 494 &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 1234}, 495 "AutoConfig.InitialConfiguration", 496 &expectedRequest, 497 &pbautoconf.AutoConfigResponse{}).Return(fmt.Errorf("injected failure")).Once() 498 mcfg.directRPC.On( 499 "RPC", 500 "dc1", 501 "autoconf", 502 &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 1234}, 503 "AutoConfig.InitialConfiguration", 504 &expectedRequest, 505 &pbautoconf.AutoConfigResponse{}).Return(nil).Run(populateResponse).Once() 506 507 ac, err := New(mcfg.Config) 508 require.NoError(t, err) 509 require.NotNil(t, ac) 510 511 cfg, err := ac.InitialConfiguration(context.Background()) 512 require.NoError(t, err) 513 require.NotNil(t, cfg) 514 require.Equal(t, "primary", cfg.PrimaryDatacenter) 515 516 // the file was written to. 517 persistedFile := filepath.Join(*loader.opts.FlagValues.DataDir, autoConfigFileName) 518 require.FileExists(t, persistedFile) 519} 520 521func TestGoRoutineManagement(t *testing.T) { 522 mcfg := newMockedConfig(t) 523 loader := setupRuntimeConfig(t) 524 loader.addConfigHCL(` 525 auto_config = { 526 enabled = true 527 intro_token ="blarg" 528 server_addresses = ["127.0.0.1:8300"] 529 } 530 verify_outgoing = true 531 `) 532 mcfg.Config.Loader = loader.Load 533 534 // prepopulation is going to grab the token to populate the correct cache key 535 mcfg.tokens.On("AgentToken").Return("secret").Times(0) 536 537 ac, err := New(mcfg.Config) 538 require.NoError(t, err) 539 540 // priming the config so some other requests will work properly that need to 541 // read from the configuration. We are going to avoid doing InitialConfiguration 542 // for this test as we only are really concerned with the go routine management 543 _, err = ac.ReadConfig() 544 require.NoError(t, err) 545 546 var rootsCtx context.Context 547 var leafCtx context.Context 548 var ctxLock sync.Mutex 549 550 rootsReq := ac.caRootsRequest() 551 mcfg.cache.On("Notify", 552 mock.Anything, 553 cachetype.ConnectCARootName, 554 &rootsReq, 555 rootsWatchID, 556 mock.Anything, 557 ).Return(nil).Times(2).Run(func(args mock.Arguments) { 558 ctxLock.Lock() 559 rootsCtx = args.Get(0).(context.Context) 560 ctxLock.Unlock() 561 }) 562 563 leafReq := ac.leafCertRequest() 564 mcfg.cache.On("Notify", 565 mock.Anything, 566 cachetype.ConnectCALeafName, 567 &leafReq, 568 leafWatchID, 569 mock.Anything, 570 ).Return(nil).Times(2).Run(func(args mock.Arguments) { 571 ctxLock.Lock() 572 leafCtx = args.Get(0).(context.Context) 573 ctxLock.Unlock() 574 }) 575 576 // we will start/stop things twice 577 mcfg.tokens.On("Notify", token.TokenKindAgent).Return(token.Notifier{}).Times(2) 578 mcfg.tokens.On("StopNotify", token.Notifier{}).Times(2) 579 580 mcfg.tlsCfg.On("AutoEncryptCertNotAfter").Return(time.Now().Add(10 * time.Minute)).Times(0) 581 582 // ensure that auto-config isn't running 583 require.False(t, ac.IsRunning()) 584 585 // ensure that nothing bad happens and that it reports as stopped 586 require.False(t, ac.Stop()) 587 588 // ensure that the Done chan also reports that things are not running 589 // in other words the chan is immediately selectable 590 requireChanReady(t, ac.Done()) 591 592 // start auto-config 593 ctx, cancel := context.WithCancel(context.Background()) 594 defer cancel() 595 require.NoError(t, ac.Start(ctx)) 596 597 waitForContexts := func() bool { 598 ctxLock.Lock() 599 defer ctxLock.Unlock() 600 return !(rootsCtx == nil || leafCtx == nil) 601 } 602 603 // wait for the cache notifications to get started 604 require.Eventually(t, waitForContexts, 100*time.Millisecond, 10*time.Millisecond) 605 606 // hold onto the Done chan to test for the go routine exiting 607 done := ac.Done() 608 609 // ensure we report as running 610 require.True(t, ac.IsRunning()) 611 612 // ensure the done chan is not selectable yet 613 requireChanNotReady(t, done) 614 615 // ensure we error if we attempt to start again 616 err = ac.Start(ctx) 617 testutil.RequireErrorContains(t, err, "AutoConfig is already running") 618 619 // now stop things - it should return true indicating that it was running 620 // when we attempted to stop it. 621 require.True(t, ac.Stop()) 622 623 // ensure that the go routine shuts down - it will close the done chan. Also it should cancel 624 // the cache watches by cancelling the context it passed into the Notify call. 625 require.True(t, waitForChans(100*time.Millisecond, done, leafCtx.Done(), rootsCtx.Done()), "AutoConfig didn't shut down") 626 require.False(t, ac.IsRunning()) 627 628 // restart it 629 require.NoError(t, ac.Start(ctx)) 630 631 // get the new Done chan 632 done = ac.Done() 633 634 // ensure that context cancellation causes us to stop as well 635 cancel() 636 require.True(t, waitForChans(100*time.Millisecond, done)) 637} 638 639type testAutoConfig struct { 640 mcfg *mockedConfig 641 ac *AutoConfig 642 tokenUpdates chan struct{} 643 originalToken string 644 stop func() 645 646 initialRoots *structs.IndexedCARoots 647 initialCert *structs.IssuedCert 648 extraCerts []string 649} 650 651func startedAutoConfig(t *testing.T, autoEncrypt bool) testAutoConfig { 652 t.Helper() 653 mcfg := newMockedConfig(t) 654 loader := setupRuntimeConfig(t) 655 if !autoEncrypt { 656 loader.addConfigHCL(` 657 auto_config = { 658 enabled = true 659 intro_token ="blarg" 660 server_addresses = ["127.0.0.1:8300"] 661 } 662 verify_outgoing = true 663 `) 664 } else { 665 loader.addConfigHCL(` 666 auto_encrypt { 667 tls = true 668 } 669 verify_outgoing = true 670 `) 671 } 672 mcfg.Config.Loader = loader.Load 673 mcfg.Config.FallbackLeeway = time.Nanosecond 674 675 originalToken := "a5deaa25-11ca-48bf-a979-4c3a7aa4b9a9" 676 677 if !autoEncrypt { 678 // this gets called when InitialConfiguration is invoked to record the token from the 679 // auto-config response 680 mcfg.tokens.On("UpdateAgentToken", originalToken, token.TokenSourceConfig).Return(true).Once() 681 } 682 683 // we expect this to be retrieved twice: first during cache prepopulation 684 // and then again when setting up the cache watch for the leaf cert. 685 // However one of those expectations is setup in the expectInitialTLS 686 // method so we only need one more here 687 mcfg.tokens.On("AgentToken").Return(originalToken).Once() 688 689 if autoEncrypt { 690 // when using AutoEncrypt we also have to grab the token once more 691 // when setting up the initial RPC as the ACL token is what is used 692 // to authorize the request. 693 mcfg.tokens.On("AgentToken").Return(originalToken).Once() 694 } 695 696 // this is called once during Start to initialze the token watches 697 tokenUpdateCh := make(chan struct{}) 698 tokenNotifier := token.Notifier{ 699 Ch: tokenUpdateCh, 700 } 701 mcfg.tokens.On("Notify", token.TokenKindAgent).Once().Return(tokenNotifier) 702 mcfg.tokens.On("StopNotify", tokenNotifier).Once() 703 704 // expect the roots watch on the cache 705 mcfg.cache.On("Notify", 706 mock.Anything, 707 cachetype.ConnectCARootName, 708 &structs.DCSpecificRequest{Datacenter: "dc1"}, 709 rootsWatchID, 710 mock.Anything, 711 ).Return(nil).Once() 712 713 mcfg.cache.On("Notify", 714 mock.Anything, 715 cachetype.ConnectCALeafName, 716 &cachetype.ConnectCALeafRequest{ 717 Datacenter: "dc1", 718 Agent: "autoconf", 719 Token: originalToken, 720 DNSSAN: defaultDNSSANs, 721 IPSAN: defaultIPSANs, 722 }, 723 leafWatchID, 724 mock.Anything, 725 ).Return(nil).Once() 726 727 // override the server provider - most of the other tests set it up so that this 728 // always returns no server (simulating a state where we haven't joined gossip). 729 // this seems like a good place to ensure this other way of finding server information 730 // works 731 mcfg.serverProvider.On("FindLANServer").Once().Return(&metadata.Server{ 732 Addr: &net.TCPAddr{IP: net.IPv4(198, 18, 0, 1), Port: 8300}, 733 }) 734 735 indexedRoots, cert, extraCerts := mcfg.setupInitialTLS(t, "autoconf", "dc1", originalToken) 736 737 mcfg.tlsCfg.On("AutoEncryptCertNotAfter").Return(cert.ValidBefore).Once() 738 739 populateResponse := func(args mock.Arguments) { 740 method := args.String(3) 741 742 switch method { 743 case "AutoConfig.InitialConfiguration": 744 resp, ok := args.Get(5).(*pbautoconf.AutoConfigResponse) 745 require.True(t, ok) 746 resp.Config = &pbconfig.Config{ 747 PrimaryDatacenter: "primary", 748 TLS: &pbconfig.TLS{ 749 VerifyServerHostname: true, 750 }, 751 ACL: &pbconfig.ACL{ 752 Tokens: &pbconfig.ACLTokens{ 753 Agent: originalToken, 754 }, 755 }, 756 } 757 758 resp.CARoots = mustTranslateCARootsToProtobuf(t, indexedRoots) 759 resp.Certificate = mustTranslateIssuedCertToProtobuf(t, cert) 760 resp.ExtraCACertificates = extraCerts 761 case "AutoEncrypt.Sign": 762 resp, ok := args.Get(5).(*structs.SignedResponse) 763 require.True(t, ok) 764 *resp = structs.SignedResponse{ 765 VerifyServerHostname: true, 766 ConnectCARoots: *indexedRoots, 767 IssuedCert: *cert, 768 ManualCARoots: extraCerts, 769 } 770 } 771 } 772 773 if !autoEncrypt { 774 expectedRequest := pbautoconf.AutoConfigRequest{ 775 Datacenter: "dc1", 776 Node: "autoconf", 777 JWT: "blarg", 778 } 779 780 mcfg.directRPC.On( 781 "RPC", 782 "dc1", 783 "autoconf", 784 &net.TCPAddr{IP: net.IPv4(198, 18, 0, 1), Port: 8300}, 785 "AutoConfig.InitialConfiguration", 786 &expectedRequest, 787 &pbautoconf.AutoConfigResponse{}).Return(nil).Run(populateResponse).Once() 788 } else { 789 expectedRequest := structs.CASignRequest{ 790 WriteRequest: structs.WriteRequest{Token: originalToken}, 791 Datacenter: "dc1", 792 // TODO (autoconf) Maybe in the future we should populate a CSR 793 // and do some manual parsing/verification of the contents. The 794 // bits not having to do with the signing key such as the requested 795 // SANs and CN. For now though the mockDirectRPC type will empty 796 // the CSR so we have to pass in an empty string to the expectation. 797 CSR: "", 798 } 799 800 mcfg.directRPC.On( 801 "RPC", 802 "dc1", 803 "autoconf", // reusing the same name to prevent needing more configurability 804 &net.TCPAddr{IP: net.IPv4(198, 18, 0, 1), Port: 8300}, 805 "AutoEncrypt.Sign", 806 &expectedRequest, 807 &structs.SignedResponse{}).Return(nil).Run(populateResponse) 808 } 809 810 ac, err := New(mcfg.Config) 811 require.NoError(t, err) 812 require.NotNil(t, ac) 813 814 cfg, err := ac.InitialConfiguration(context.Background()) 815 require.NoError(t, err) 816 require.NotNil(t, cfg) 817 if !autoEncrypt { 818 // auto-encrypt doesn't modify the config but rather sets the value 819 // in the TLS configurator 820 require.True(t, cfg.VerifyServerHostname) 821 } 822 823 ctx, cancel := context.WithCancel(context.Background()) 824 require.NoError(t, ac.Start(ctx)) 825 t.Cleanup(func() { 826 done := ac.Done() 827 cancel() 828 timer := time.NewTimer(1 * time.Second) 829 defer timer.Stop() 830 select { 831 case <-done: 832 // do nothing 833 case <-timer.C: 834 t.Fatalf("AutoConfig wasn't stopped within 1 second after test completion") 835 } 836 }) 837 838 return testAutoConfig{ 839 mcfg: mcfg, 840 ac: ac, 841 tokenUpdates: tokenUpdateCh, 842 originalToken: originalToken, 843 initialRoots: indexedRoots, 844 initialCert: cert, 845 extraCerts: extraCerts, 846 stop: cancel, 847 } 848} 849 850// this test ensures that the cache watches are restarted with 851// the updated token after receiving a token update 852func TestTokenUpdate(t *testing.T) { 853 testAC := startedAutoConfig(t, false) 854 855 newToken := "1a4cc445-86ed-46b4-a355-bbf5a11dddb0" 856 857 rootsCtx, rootsCancel := context.WithCancel(context.Background()) 858 testAC.mcfg.cache.On("Notify", 859 mock.Anything, 860 cachetype.ConnectCARootName, 861 &structs.DCSpecificRequest{Datacenter: testAC.ac.config.Datacenter}, 862 rootsWatchID, 863 mock.Anything, 864 ).Return(nil).Once().Run(func(args mock.Arguments) { 865 rootsCancel() 866 }) 867 868 leafCtx, leafCancel := context.WithCancel(context.Background()) 869 testAC.mcfg.cache.On("Notify", 870 mock.Anything, 871 cachetype.ConnectCALeafName, 872 &cachetype.ConnectCALeafRequest{ 873 Datacenter: "dc1", 874 Agent: "autoconf", 875 Token: newToken, 876 DNSSAN: defaultDNSSANs, 877 IPSAN: defaultIPSANs, 878 }, 879 leafWatchID, 880 mock.Anything, 881 ).Return(nil).Once().Run(func(args mock.Arguments) { 882 leafCancel() 883 }) 884 885 // this will be retrieved once when resetting the leaf cert watch 886 testAC.mcfg.tokens.On("AgentToken").Return(newToken).Once() 887 888 // send the notification about the token update 889 testAC.tokenUpdates <- struct{}{} 890 891 // wait for the leaf cert watches 892 require.True(t, waitForChans(100*time.Millisecond, leafCtx.Done(), rootsCtx.Done()), "New cache watches were not started within 100ms") 893} 894 895func TestRootsUpdate(t *testing.T) { 896 testAC := startedAutoConfig(t, false) 897 898 secondCA := connect.TestCA(t, testAC.initialRoots.Roots[0]) 899 secondRoots := structs.IndexedCARoots{ 900 ActiveRootID: secondCA.ID, 901 TrustDomain: connect.TestClusterID, 902 Roots: []*structs.CARoot{ 903 secondCA, 904 testAC.initialRoots.Roots[0], 905 }, 906 QueryMeta: structs.QueryMeta{ 907 Index: 99, 908 }, 909 } 910 911 updatedCtx, cancel := context.WithCancel(context.Background()) 912 testAC.mcfg.tlsCfg.On("UpdateAutoTLS", 913 testAC.extraCerts, 914 []string{secondCA.RootCert, testAC.initialRoots.Roots[0].RootCert}, 915 testAC.initialCert.CertPEM, 916 "redacted", 917 true, 918 ).Return(nil).Once().Run(func(args mock.Arguments) { 919 cancel() 920 }) 921 922 // when a cache event comes in we end up recalculating the fallback timer which requires this call 923 testAC.mcfg.tlsCfg.On("AutoEncryptCertNotAfter").Return(time.Now().Add(10 * time.Minute)).Once() 924 925 req := structs.DCSpecificRequest{Datacenter: "dc1"} 926 require.True(t, testAC.mcfg.cache.sendNotification(context.Background(), req.CacheInfo().Key, cache.UpdateEvent{ 927 CorrelationID: rootsWatchID, 928 Result: &secondRoots, 929 Meta: cache.ResultMeta{ 930 Index: secondRoots.Index, 931 }, 932 })) 933 934 require.True(t, waitForChans(100*time.Millisecond, updatedCtx.Done()), "TLS certificates were not updated within the alotted time") 935 936 // persisting these to disk happens right after the chan we are waiting for will have fired above 937 // however there is no deterministic way to know once its been written outside of maybe a filesystem 938 // event notifier. That seems a little heavy handed just for this and especially to do in any sort 939 // of cross platform way. 940 testretry.Run(t, func(r *testretry.R) { 941 resp, err := testAC.ac.readPersistedAutoConfig() 942 require.NoError(r, err) 943 require.Equal(r, secondRoots.ActiveRootID, resp.CARoots.GetActiveRootID()) 944 }) 945} 946 947func TestCertUpdate(t *testing.T) { 948 testAC := startedAutoConfig(t, false) 949 secondCert := newLeaf(t, "autoconf", "dc1", testAC.initialRoots.Roots[0], 99, 10*time.Minute) 950 951 updatedCtx, cancel := context.WithCancel(context.Background()) 952 testAC.mcfg.tlsCfg.On("UpdateAutoTLS", 953 testAC.extraCerts, 954 []string{testAC.initialRoots.Roots[0].RootCert}, 955 secondCert.CertPEM, 956 "redacted", 957 true, 958 ).Return(nil).Once().Run(func(args mock.Arguments) { 959 cancel() 960 }) 961 962 // when a cache event comes in we end up recalculating the fallback timer which requires this call 963 testAC.mcfg.tlsCfg.On("AutoEncryptCertNotAfter").Return(secondCert.ValidBefore).Once() 964 965 req := cachetype.ConnectCALeafRequest{ 966 Datacenter: "dc1", 967 Agent: "autoconf", 968 Token: testAC.originalToken, 969 DNSSAN: defaultDNSSANs, 970 IPSAN: defaultIPSANs, 971 } 972 require.True(t, testAC.mcfg.cache.sendNotification(context.Background(), req.CacheInfo().Key, cache.UpdateEvent{ 973 CorrelationID: leafWatchID, 974 Result: secondCert, 975 Meta: cache.ResultMeta{ 976 Index: secondCert.ModifyIndex, 977 }, 978 })) 979 980 require.True(t, waitForChans(100*time.Millisecond, updatedCtx.Done()), "TLS certificates were not updated within the alotted time") 981 982 // persisting these to disk happens after all the things we would wait for in assertCertUpdated 983 // will have fired. There is no deterministic way to know once its been written so we wrap 984 // this in a retry. 985 testretry.Run(t, func(r *testretry.R) { 986 resp, err := testAC.ac.readPersistedAutoConfig() 987 require.NoError(r, err) 988 989 // ensure the roots got persisted to disk 990 require.Equal(r, secondCert.CertPEM, resp.Certificate.GetCertPEM()) 991 }) 992} 993 994func TestFallback(t *testing.T) { 995 testAC := startedAutoConfig(t, false) 996 997 // at this point everything is operating normally and we are just 998 // waiting for events. We are going to send a new cert that is basically 999 // already expired and then allow the fallback routine to kick in. 1000 secondCert := newLeaf(t, "autoconf", "dc1", testAC.initialRoots.Roots[0], 100, time.Nanosecond) 1001 secondCA := connect.TestCA(t, testAC.initialRoots.Roots[0]) 1002 secondRoots := structs.IndexedCARoots{ 1003 ActiveRootID: secondCA.ID, 1004 TrustDomain: connect.TestClusterID, 1005 Roots: []*structs.CARoot{ 1006 secondCA, 1007 testAC.initialRoots.Roots[0], 1008 }, 1009 QueryMeta: structs.QueryMeta{ 1010 Index: 101, 1011 }, 1012 } 1013 thirdCert := newLeaf(t, "autoconf", "dc1", secondCA, 102, 10*time.Minute) 1014 1015 // setup the expectation for when the certs got updated initially 1016 updatedCtx, updateCancel := context.WithCancel(context.Background()) 1017 testAC.mcfg.tlsCfg.On("UpdateAutoTLS", 1018 testAC.extraCerts, 1019 []string{testAC.initialRoots.Roots[0].RootCert}, 1020 secondCert.CertPEM, 1021 "redacted", 1022 true, 1023 ).Return(nil).Once().Run(func(args mock.Arguments) { 1024 updateCancel() 1025 }) 1026 1027 // when a cache event comes in we end up recalculating the fallback timer which requires this call 1028 testAC.mcfg.tlsCfg.On("AutoEncryptCertNotAfter").Return(secondCert.ValidBefore).Once() 1029 testAC.mcfg.tlsCfg.On("AutoEncryptCertExpired").Return(true).Once() 1030 1031 fallbackCtx, fallbackCancel := context.WithCancel(context.Background()) 1032 1033 // also testing here that we can change server IPs for ongoing operations 1034 testAC.mcfg.serverProvider.On("FindLANServer").Once().Return(&metadata.Server{ 1035 Addr: &net.TCPAddr{IP: net.IPv4(198, 18, 23, 2), Port: 8300}, 1036 }) 1037 1038 // after sending the notification for the cert update another InitialConfiguration RPC 1039 // will be made to pull down the latest configuration. So we need to set up the response 1040 // for the second RPC 1041 populateResponse := func(args mock.Arguments) { 1042 resp, ok := args.Get(5).(*pbautoconf.AutoConfigResponse) 1043 require.True(t, ok) 1044 resp.Config = &pbconfig.Config{ 1045 PrimaryDatacenter: "primary", 1046 TLS: &pbconfig.TLS{ 1047 VerifyServerHostname: true, 1048 }, 1049 ACL: &pbconfig.ACL{ 1050 Tokens: &pbconfig.ACLTokens{ 1051 Agent: testAC.originalToken, 1052 }, 1053 }, 1054 } 1055 1056 resp.CARoots = mustTranslateCARootsToProtobuf(t, &secondRoots) 1057 resp.Certificate = mustTranslateIssuedCertToProtobuf(t, thirdCert) 1058 resp.ExtraCACertificates = testAC.extraCerts 1059 1060 fallbackCancel() 1061 } 1062 1063 expectedRequest := pbautoconf.AutoConfigRequest{ 1064 Datacenter: "dc1", 1065 Node: "autoconf", 1066 JWT: "blarg", 1067 } 1068 1069 testAC.mcfg.directRPC.On( 1070 "RPC", 1071 "dc1", 1072 "autoconf", 1073 &net.TCPAddr{IP: net.IPv4(198, 18, 23, 2), Port: 8300}, 1074 "AutoConfig.InitialConfiguration", 1075 &expectedRequest, 1076 &pbautoconf.AutoConfigResponse{}).Return(nil).Run(populateResponse).Once() 1077 1078 // this gets called when InitialConfiguration is invoked to record the token from the 1079 // auto-config response which is how the Fallback for auto-config works 1080 testAC.mcfg.tokens.On("UpdateAgentToken", testAC.originalToken, token.TokenSourceConfig).Return(true).Once() 1081 1082 testAC.mcfg.expectInitialTLS(t, "autoconf", "dc1", testAC.originalToken, secondCA, &secondRoots, thirdCert, testAC.extraCerts) 1083 1084 // after the second RPC we now will use the new certs validity period in the next run loop iteration 1085 testAC.mcfg.tlsCfg.On("AutoEncryptCertNotAfter").Return(time.Now().Add(10 * time.Minute)).Once() 1086 1087 // now that all the mocks are set up we can trigger the whole thing by sending the second expired cert 1088 // as a cache update event. 1089 req := cachetype.ConnectCALeafRequest{ 1090 Datacenter: "dc1", 1091 Agent: "autoconf", 1092 Token: testAC.originalToken, 1093 DNSSAN: defaultDNSSANs, 1094 IPSAN: defaultIPSANs, 1095 } 1096 require.True(t, testAC.mcfg.cache.sendNotification(context.Background(), req.CacheInfo().Key, cache.UpdateEvent{ 1097 CorrelationID: leafWatchID, 1098 Result: secondCert, 1099 Meta: cache.ResultMeta{ 1100 Index: secondCert.ModifyIndex, 1101 }, 1102 })) 1103 1104 // wait for the TLS certificates to get updated 1105 require.True(t, waitForChans(100*time.Millisecond, updatedCtx.Done()), "TLS certificates were not updated within the alotted time") 1106 1107 // now wait for the fallback routine to be invoked 1108 require.True(t, waitForChans(100*time.Millisecond, fallbackCtx.Done()), "fallback routines did not get invoked within the alotted time") 1109 1110 testAC.stop() 1111 <-testAC.ac.done 1112 1113 resp, err := testAC.ac.readPersistedAutoConfig() 1114 require.NoError(t, err) 1115 1116 // ensure the roots got persisted to disk 1117 require.Equal(t, thirdCert.CertPEM, resp.Certificate.GetCertPEM()) 1118 require.Equal(t, secondRoots.ActiveRootID, resp.CARoots.GetActiveRootID()) 1119} 1120 1121func TestIntroToken(t *testing.T) { 1122 tokenFile := testutil.TempFile(t, "intro-token") 1123 t.Cleanup(func() { os.Remove(tokenFile.Name()) }) 1124 1125 tokenFileEmpty := testutil.TempFile(t, "intro-token-empty") 1126 t.Cleanup(func() { os.Remove(tokenFileEmpty.Name()) }) 1127 1128 tokenFromFile := "8ae34d3a-8adf-446a-b236-69874597cb5b" 1129 tokenFromConfig := "3ad9b572-ea42-4e47-9cd0-53a398a98abf" 1130 require.NoError(t, ioutil.WriteFile(tokenFile.Name(), []byte(tokenFromFile), 0600)) 1131 1132 type testCase struct { 1133 config *config.RuntimeConfig 1134 err string 1135 token string 1136 } 1137 1138 cases := map[string]testCase{ 1139 "config": { 1140 config: &config.RuntimeConfig{ 1141 AutoConfig: config.AutoConfig{ 1142 IntroToken: tokenFromConfig, 1143 IntroTokenFile: tokenFile.Name(), 1144 }, 1145 }, 1146 token: tokenFromConfig, 1147 }, 1148 "file": { 1149 config: &config.RuntimeConfig{ 1150 AutoConfig: config.AutoConfig{ 1151 IntroTokenFile: tokenFile.Name(), 1152 }, 1153 }, 1154 token: tokenFromFile, 1155 }, 1156 "file-empty": { 1157 config: &config.RuntimeConfig{ 1158 AutoConfig: config.AutoConfig{ 1159 IntroTokenFile: tokenFileEmpty.Name(), 1160 }, 1161 }, 1162 err: "intro_token_file did not contain any token", 1163 }, 1164 } 1165 1166 for name, tcase := range cases { 1167 t.Run(name, func(t *testing.T) { 1168 ac := AutoConfig{ 1169 config: tcase.config, 1170 } 1171 1172 token, err := ac.introToken() 1173 if tcase.err != "" { 1174 testutil.RequireErrorContains(t, err, tcase.err) 1175 } else { 1176 require.NoError(t, err) 1177 require.Equal(t, tcase.token, token) 1178 } 1179 }) 1180 } 1181 1182} 1183