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