From 9eaa8eb026825b724bab0988bd2826cea4e09436 Mon Sep 17 00:00:00 2001 From: John Landa Date: Wed, 20 Sep 2023 15:50:06 -0600 Subject: [PATCH] dns token (#17936) * dns token fix whitespace for docs and comments fix test cases fix test cases remove tabs in help text Add changelog Peering dns test Peering dns test Partial implementation of Peered DNS test Swap to new topology lib expose dns port for integration tests on client remove partial test implementation remove extra port exposure remove changelog from the ent pr Add dns token to set-agent-token switch Add enterprise golden file Use builtin/dns template in tests Update ent dns policy Update ent dns template test remove local gen certs fix templated policy specs * add changelog * go mod tidy --- .changelog/17936.txt | 3 + agent/agent_endpoint.go | 3 + agent/config/builder.go | 1 + agent/config/config.go | 1 + .../TestRuntimeConfig_Sanitize.golden | 1 + agent/dns.go | 22 ++- agent/dns_ce_test.go | 5 +- agent/dns_test.go | 45 ++++- agent/testagent.go | 5 +- agent/token/persistence.go | 16 ++ agent/token/persistence_test.go | 182 ++++++++++-------- agent/token/store.go | 39 +++- agent/token/store_test.go | 50 ++++- api/agent.go | 4 + api/agent_test.go | 7 + command/acl/agenttokens/agent_tokens.go | 10 +- test-integ/go.mod | 2 +- test/integration/consul-container/go.mod | 2 +- .../consul-container/libs/assert/envoy.go | 2 +- .../libs/cluster/container.go | 1 + .../libs/topology/peering_topology.go | 10 +- testing/deployer/topology/default_cdp.go | 2 +- 22 files changed, 305 insertions(+), 108 deletions(-) create mode 100644 .changelog/17936.txt diff --git a/.changelog/17936.txt b/.changelog/17936.txt new file mode 100644 index 0000000000..61f5117d87 --- /dev/null +++ b/.changelog/17936.txt @@ -0,0 +1,3 @@ +```release-note:feature +acl: Add new `acl.tokens.dns` config field which specifies the token used implicitly during dns checks. +``` diff --git a/agent/agent_endpoint.go b/agent/agent_endpoint.go index 96857409ce..3fab6e8c6a 100644 --- a/agent/agent_endpoint.go +++ b/agent/agent_endpoint.go @@ -1531,6 +1531,9 @@ func (s *HTTPHandlers) AgentToken(resp http.ResponseWriter, req *http.Request) ( case "config_file_service_registration": s.agent.tokens.UpdateConfigFileRegistrationToken(args.Token, token_store.TokenSourceAPI) + case "dns_token", "dns": + s.agent.tokens.UpdateDNSToken(args.Token, token_store.TokenSourceAPI) + default: return HTTPError{StatusCode: http.StatusNotFound, Reason: fmt.Sprintf("Token %q is unknown", target)} } diff --git a/agent/config/builder.go b/agent/config/builder.go index 667d822480..ed89d4b030 100644 --- a/agent/config/builder.go +++ b/agent/config/builder.go @@ -882,6 +882,7 @@ func (b *builder) build() (rt RuntimeConfig, err error) { ACLAgentRecoveryToken: stringVal(c.ACL.Tokens.AgentRecovery), ACLReplicationToken: stringVal(c.ACL.Tokens.Replication), ACLConfigFileRegistrationToken: stringVal(c.ACL.Tokens.ConfigFileRegistration), + ACLDNSToken: stringVal(c.ACL.Tokens.DNS), }, // Autopilot diff --git a/agent/config/config.go b/agent/config/config.go index 91db8e42a1..816f7ae85f 100644 --- a/agent/config/config.go +++ b/agent/config/config.go @@ -778,6 +778,7 @@ type Tokens struct { Default *string `mapstructure:"default"` Agent *string `mapstructure:"agent"` ConfigFileRegistration *string `mapstructure:"config_file_service_registration"` + DNS *string `mapstructure:"dns"` // Enterprise Only ManagedServiceProvider []ServiceProviderToken `mapstructure:"managed_service_provider"` diff --git a/agent/config/testdata/TestRuntimeConfig_Sanitize.golden b/agent/config/testdata/TestRuntimeConfig_Sanitize.golden index e339923dc9..b82baea535 100644 --- a/agent/config/testdata/TestRuntimeConfig_Sanitize.golden +++ b/agent/config/testdata/TestRuntimeConfig_Sanitize.golden @@ -17,6 +17,7 @@ "ACLAgentRecoveryToken": "hidden", "ACLAgentToken": "hidden", "ACLConfigFileRegistrationToken": "hidden", + "ACLDNSToken": "hidden", "ACLDefaultToken": "hidden", "ACLReplicationToken": "hidden", "DataDir": "", diff --git a/agent/dns.go b/agent/dns.go index 3ae9a18b67..b55338ea3e 100644 --- a/agent/dns.go +++ b/agent/dns.go @@ -416,7 +416,7 @@ func (d *DNSServer) handlePtr(resp dns.ResponseWriter, req *dns.Msg) { args := structs.DCSpecificRequest{ Datacenter: datacenter, QueryOptions: structs.QueryOptions{ - Token: d.agent.tokens.UserToken(), + Token: d.coalesceDNSToken(), AllowStale: cfg.AllowStale, }, } @@ -452,7 +452,7 @@ func (d *DNSServer) handlePtr(resp dns.ResponseWriter, req *dns.Msg) { sargs := structs.ServiceSpecificRequest{ Datacenter: datacenter, QueryOptions: structs.QueryOptions{ - Token: d.agent.tokens.UserToken(), + Token: d.coalesceDNSToken(), AllowStale: cfg.AllowStale, }, ServiceAddress: serviceAddress, @@ -513,7 +513,7 @@ func (d *DNSServer) handleQuery(resp dns.ResponseWriter, req *dns.Msg) { cfg := d.config.Load().(*dnsConfig) - // Setup the message response + // Set up the message response m := new(dns.Msg) m.SetReply(req) m.Compress = !cfg.DisableCompression @@ -875,7 +875,7 @@ func (d *DNSServer) dispatch(remoteAddr net.Addr, req, resp *dns.Msg, maxRecursi ServiceName: queryParts[len(queryParts)-1], EnterpriseMeta: locality.EnterpriseMeta, QueryOptions: structs.QueryOptions{ - Token: d.agent.tokens.UserToken(), + Token: d.coalesceDNSToken(), }, } if args.PeerName == "" { @@ -1093,7 +1093,7 @@ func (d *DNSServer) nodeLookup(cfg *dnsConfig, lookup nodeLookup, req, resp *dns PeerName: lookup.PeerName, Node: lookup.Node, QueryOptions: structs.QueryOptions{ - Token: d.agent.tokens.UserToken(), + Token: d.coalesceDNSToken(), AllowStale: cfg.AllowStale, }, EnterpriseMeta: lookup.EnterpriseMeta, @@ -1425,7 +1425,7 @@ func (d *DNSServer) lookupServiceNodes(cfg *dnsConfig, lookup serviceLookup) (st ServiceTags: serviceTags, TagFilter: lookup.Tag != "", QueryOptions: structs.QueryOptions{ - Token: d.agent.tokens.UserToken(), + Token: d.coalesceDNSToken(), AllowStale: cfg.AllowStale, MaxAge: cfg.CacheMaxAge, UseCache: cfg.UseCache, @@ -1503,7 +1503,7 @@ func (d *DNSServer) preparedQueryLookup(cfg *dnsConfig, datacenter, query string Datacenter: datacenter, QueryIDOrName: query, QueryOptions: structs.QueryOptions{ - Token: d.agent.tokens.UserToken(), + Token: d.coalesceDNSToken(), AllowStale: cfg.AllowStale, MaxAge: cfg.CacheMaxAge, }, @@ -2172,3 +2172,11 @@ func (d *DNSServer) resolveCNAME(cfg *dnsConfig, name string, maxRecursionLevel d.logger.Error("all resolvers failed for name", "name", name) return nil } + +func (d *DNSServer) coalesceDNSToken() string { + if d.agent.tokens.DNSToken() != "" { + return d.agent.tokens.DNSToken() + } else { + return d.agent.tokens.UserToken() + } +} diff --git a/agent/dns_ce_test.go b/agent/dns_ce_test.go index 50b100a989..e9dd9a0921 100644 --- a/agent/dns_ce_test.go +++ b/agent/dns_ce_test.go @@ -10,11 +10,12 @@ import ( "context" "testing" + "github.com/miekg/dns" + "github.com/stretchr/testify/require" + "github.com/hashicorp/consul/acl" "github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/testrpc" - "github.com/miekg/dns" - "github.com/stretchr/testify/require" ) func TestDNS_CE_PeeredServices(t *testing.T) { diff --git a/agent/dns_test.go b/agent/dns_test.go index 3998995272..3c447db36e 100644 --- a/agent/dns_test.go +++ b/agent/dns_test.go @@ -6310,6 +6310,22 @@ func TestDNS_ServiceLookup_SRV_RFC_TCP_Default(t *testing.T) { } +func initDNSToken(t *testing.T, rpc RPC) { + t.Helper() + + reqToken := structs.ACLTokenSetRequest{ + Datacenter: "dc1", + ACLToken: structs.ACLToken{ + SecretID: "279d4735-f8ca-4d48-b5cc-c00a9713bbf8", + Policies: nil, + TemplatedPolicies: []*structs.ACLTemplatedPolicy{{TemplateName: "builtin/dns"}}, + }, + WriteRequest: structs.WriteRequest{Token: "root"}, + } + err := rpc.RPC(context.Background(), "ACL.TokenSet", &reqToken, &structs.ACLToken{}) + require.NoError(t, err) +} + func TestDNS_ServiceLookup_FilterACL(t *testing.T) { if testing.Short() { t.Skip("too slow for testing.Short") @@ -6322,10 +6338,11 @@ func TestDNS_ServiceLookup_FilterACL(t *testing.T) { }{ {"root", 1}, {"anonymous", 0}, + {"dns", 1}, } for _, tt := range tests { t.Run("ACLToken == "+tt.token, func(t *testing.T) { - a := NewTestAgent(t, ` + hcl := ` primary_datacenter = "dc1" acl { @@ -6335,13 +6352,34 @@ func TestDNS_ServiceLookup_FilterACL(t *testing.T) { tokens { initial_management = "root" - default = "`+tt.token+`" +` + if tt.token == "dns" { + // Create a UUID for dns token since it doesn't have an alias + dnsToken := "279d4735-f8ca-4d48-b5cc-c00a9713bbf8" + + hcl = hcl + ` + default = "anonymous" + dns = "` + dnsToken + `" +` + } else { + hcl = hcl + ` + default = "` + tt.token + `" +` + } + + hcl = hcl + ` } } - `) + ` + + a := NewTestAgent(t, hcl) defer a.Shutdown() testrpc.WaitForLeader(t, a.RPC, "dc1") + if tt.token == "dns" { + initDNSToken(t, a) + } + // Register a service args := &structs.RegisterRequest{ Datacenter: "dc1", @@ -6373,6 +6411,7 @@ func TestDNS_ServiceLookup_FilterACL(t *testing.T) { }) } } + func TestDNS_ServiceLookup_MetaTXT(t *testing.T) { if testing.Short() { t.Skip("too slow for testing.Short") diff --git a/agent/testagent.go b/agent/testagent.go index 3d0e2c8038..39a83a087f 100644 --- a/agent/testagent.go +++ b/agent/testagent.go @@ -529,6 +529,7 @@ type TestACLConfigParams struct { DefaultToken string AgentRecoveryToken string ReplicationToken string + DNSToken string EnableTokenReplication bool } @@ -547,7 +548,8 @@ func (p *TestACLConfigParams) HasConfiguredTokens() bool { p.AgentToken != "" || p.DefaultToken != "" || p.AgentRecoveryToken != "" || - p.ReplicationToken != "" + p.ReplicationToken != "" || + p.DNSToken != "" } func TestACLConfigNew() string { @@ -557,6 +559,7 @@ func TestACLConfigNew() string { InitialManagementToken: "root", AgentToken: "root", AgentRecoveryToken: "towel", + DNSToken: "dns", }) } diff --git a/agent/token/persistence.go b/agent/token/persistence.go index 117f72359d..1a3898e12e 100644 --- a/agent/token/persistence.go +++ b/agent/token/persistence.go @@ -26,6 +26,7 @@ type Config struct { ACLAgentRecoveryToken string ACLReplicationToken string ACLConfigFileRegistrationToken string + ACLDNSToken string EnterpriseConfig } @@ -77,6 +78,7 @@ type persistedTokens struct { Default string `json:"default,omitempty"` Agent string `json:"agent,omitempty"` ConfigFileRegistration string `json:"config_file_service_registration,omitempty"` + DNS string `json:"dns,omitempty"` } type fileStore struct { @@ -144,6 +146,16 @@ func loadTokens(s *Store, cfg Config, tokens persistedTokens, logger Logger) { s.UpdateConfigFileRegistrationToken(cfg.ACLConfigFileRegistrationToken, TokenSourceConfig) } + if tokens.DNS != "" { + s.UpdateDNSToken(tokens.DNS, TokenSourceAPI) + + if cfg.ACLDNSToken != "" { + logger.Warn("\"dns\" token present in both the configuration and persisted token store, using the persisted token") + } + } else { + s.UpdateDNSToken(cfg.ACLDNSToken, TokenSourceConfig) + } + loadEnterpriseTokens(s, cfg) } @@ -206,6 +218,10 @@ func (p *fileStore) saveToFile(s *Store) error { tokens.ConfigFileRegistration = tok } + if tok, source := s.DNSTokenAndSource(); tok != "" && source == TokenSourceAPI { + tokens.DNS = tok + } + data, err := json.Marshal(tokens) if err != nil { p.logger.Warn("failed to persist tokens", "error", err) diff --git a/agent/token/persistence_test.go b/agent/token/persistence_test.go index 5d6e73f0d2..4351efd135 100644 --- a/agent/token/persistence_test.go +++ b/agent/token/persistence_test.go @@ -8,9 +8,10 @@ import ( "path/filepath" "testing" - "github.com/hashicorp/consul/sdk/testutil" "github.com/hashicorp/go-hclog" "github.com/stretchr/testify/require" + + "github.com/hashicorp/consul/sdk/testutil" ) func TestStore_Load(t *testing.T) { @@ -27,6 +28,7 @@ func TestStore_Load(t *testing.T) { ACLDefaultToken: "charlie", ACLReplicationToken: "delta", ACLConfigFileRegistrationToken: "echo", + ACLDNSToken: "foxtrot", } require.NoError(t, store.Load(cfg, logger)) require.Equal(t, "alfa", store.AgentToken()) @@ -34,62 +36,69 @@ func TestStore_Load(t *testing.T) { require.Equal(t, "charlie", store.UserToken()) require.Equal(t, "delta", store.ReplicationToken()) require.Equal(t, "echo", store.ConfigFileRegistrationToken()) + require.Equal(t, "foxtrot", store.DNSToken()) }) t.Run("updated from Config", func(t *testing.T) { cfg := Config{ DataDir: dataDir, - ACLDefaultToken: "echo", - ACLAgentToken: "foxtrot", - ACLAgentRecoveryToken: "golf", - ACLReplicationToken: "hotel", - ACLConfigFileRegistrationToken: "india", + ACLDefaultToken: "sierra", + ACLAgentToken: "tango", + ACLAgentRecoveryToken: "uniform", + ACLReplicationToken: "victor", + ACLConfigFileRegistrationToken: "xray", + ACLDNSToken: "zulu", } // ensures no error for missing persisted tokens file require.NoError(t, store.Load(cfg, logger)) - require.Equal(t, "echo", store.UserToken()) - require.Equal(t, "foxtrot", store.AgentToken()) - require.Equal(t, "golf", store.AgentRecoveryToken()) - require.Equal(t, "hotel", store.ReplicationToken()) - require.Equal(t, "india", store.ConfigFileRegistrationToken()) + require.Equal(t, "sierra", store.UserToken()) + require.Equal(t, "tango", store.AgentToken()) + require.Equal(t, "uniform", store.AgentRecoveryToken()) + require.Equal(t, "victor", store.ReplicationToken()) + require.Equal(t, "xray", store.ConfigFileRegistrationToken()) + require.Equal(t, "zulu", store.DNSToken()) }) t.Run("with persisted tokens", func(t *testing.T) { cfg := Config{ DataDir: dataDir, - ACLDefaultToken: "echo", - ACLAgentToken: "foxtrot", - ACLAgentRecoveryToken: "golf", - ACLReplicationToken: "hotel", - ACLConfigFileRegistrationToken: "delta", + ACLDefaultToken: "alpha", + ACLAgentToken: "bravo", + ACLAgentRecoveryToken: "charlie", + ACLReplicationToken: "delta", + ACLConfigFileRegistrationToken: "echo", + ACLDNSToken: "foxtrot", } tokens := `{ - "agent" : "india", - "agent_recovery" : "juliett", - "default": "kilo", - "replication": "lima", - "config_file_service_registration": "mike" + "agent" : "golf", + "agent_recovery" : "hotel", + "default": "india", + "replication": "juliet", + "config_file_service_registration": "kilo", + "dns": "lima" }` require.NoError(t, os.WriteFile(tokenFile, []byte(tokens), 0600)) require.NoError(t, store.Load(cfg, logger)) // no updates since token persistence is not enabled - require.Equal(t, "echo", store.UserToken()) - require.Equal(t, "foxtrot", store.AgentToken()) - require.Equal(t, "golf", store.AgentRecoveryToken()) - require.Equal(t, "hotel", store.ReplicationToken()) - require.Equal(t, "delta", store.ConfigFileRegistrationToken()) + require.Equal(t, "alpha", store.UserToken()) + require.Equal(t, "bravo", store.AgentToken()) + require.Equal(t, "charlie", store.AgentRecoveryToken()) + require.Equal(t, "delta", store.ReplicationToken()) + require.Equal(t, "echo", store.ConfigFileRegistrationToken()) + require.Equal(t, "foxtrot", store.DNSToken()) cfg.EnablePersistence = true require.NoError(t, store.Load(cfg, logger)) - require.Equal(t, "india", store.AgentToken()) - require.Equal(t, "juliett", store.AgentRecoveryToken()) - require.Equal(t, "kilo", store.UserToken()) - require.Equal(t, "lima", store.ReplicationToken()) - require.Equal(t, "mike", store.ConfigFileRegistrationToken()) + require.Equal(t, "golf", store.AgentToken()) + require.Equal(t, "hotel", store.AgentRecoveryToken()) + require.Equal(t, "india", store.UserToken()) + require.Equal(t, "juliet", store.ReplicationToken()) + require.Equal(t, "kilo", store.ConfigFileRegistrationToken()) + require.Equal(t, "lima", store.DNSToken()) // check store persistence was enabled require.NotNil(t, store.persistence) @@ -115,7 +124,8 @@ func TestStore_Load(t *testing.T) { "agent_recovery" : "november", "default": "oscar", "replication" : "papa", - "config_file_service_registration" : "lima" + "config_file_service_registration" : "lima", + "dns": "kilo" }` cfg := Config{ @@ -126,6 +136,7 @@ func TestStore_Load(t *testing.T) { ACLAgentRecoveryToken: "sierra", ACLReplicationToken: "tango", ACLConfigFileRegistrationToken: "uniform", + ACLDNSToken: "victor", } require.NoError(t, os.WriteFile(tokenFile, []byte(tokens), 0600)) @@ -136,43 +147,48 @@ func TestStore_Load(t *testing.T) { require.Equal(t, "oscar", store.UserToken()) require.Equal(t, "papa", store.ReplicationToken()) require.Equal(t, "lima", store.ConfigFileRegistrationToken()) + require.Equal(t, "kilo", store.DNSToken()) }) t.Run("with some persisted tokens", func(t *testing.T) { tokens := `{ - "agent" : "uniform", - "agent_recovery" : "victor" + "agent" : "xray", + "agent_recovery" : "zulu" }` cfg := Config{ EnablePersistence: true, DataDir: dataDir, - ACLDefaultToken: "whiskey", - ACLAgentToken: "xray", - ACLAgentRecoveryToken: "yankee", - ACLReplicationToken: "zulu", - ACLConfigFileRegistrationToken: "victor", + ACLDefaultToken: "alpha", + ACLAgentToken: "bravo", + ACLAgentRecoveryToken: "charlie", + ACLReplicationToken: "delta", + ACLConfigFileRegistrationToken: "echo", + ACLDNSToken: "foxtrot", } require.NoError(t, os.WriteFile(tokenFile, []byte(tokens), 0600)) require.NoError(t, store.Load(cfg, logger)) - require.Equal(t, "uniform", store.AgentToken()) - require.Equal(t, "victor", store.AgentRecoveryToken()) - require.Equal(t, "whiskey", store.UserToken()) - require.Equal(t, "zulu", store.ReplicationToken()) - require.Equal(t, "victor", store.ConfigFileRegistrationToken()) + require.Equal(t, "xray", store.AgentToken()) + require.Equal(t, "zulu", store.AgentRecoveryToken()) + + require.Equal(t, "alpha", store.UserToken()) + require.Equal(t, "delta", store.ReplicationToken()) + require.Equal(t, "echo", store.ConfigFileRegistrationToken()) + require.Equal(t, "foxtrot", store.DNSToken()) }) t.Run("persisted file contains invalid data", func(t *testing.T) { cfg := Config{ EnablePersistence: true, DataDir: dataDir, - ACLDefaultToken: "one", - ACLAgentToken: "two", - ACLAgentRecoveryToken: "three", - ACLReplicationToken: "four", - ACLConfigFileRegistrationToken: "five", + ACLDefaultToken: "alpha", + ACLAgentToken: "bravo", + ACLAgentRecoveryToken: "charlie", + ACLReplicationToken: "delta", + ACLConfigFileRegistrationToken: "echo", + ACLDNSToken: "foxtrot", } require.NoError(t, os.WriteFile(tokenFile, []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}, 0600)) @@ -180,11 +196,12 @@ func TestStore_Load(t *testing.T) { require.Error(t, err) require.Contains(t, err.Error(), "failed to decode tokens file") - require.Equal(t, "one", store.UserToken()) - require.Equal(t, "two", store.AgentToken()) - require.Equal(t, "three", store.AgentRecoveryToken()) - require.Equal(t, "four", store.ReplicationToken()) - require.Equal(t, "five", store.ConfigFileRegistrationToken()) + require.Equal(t, "alpha", store.UserToken()) + require.Equal(t, "bravo", store.AgentToken()) + require.Equal(t, "charlie", store.AgentRecoveryToken()) + require.Equal(t, "delta", store.ReplicationToken()) + require.Equal(t, "echo", store.ConfigFileRegistrationToken()) + require.Equal(t, "foxtrot", store.DNSToken()) }) t.Run("persisted file contains invalid json", func(t *testing.T) { @@ -194,8 +211,9 @@ func TestStore_Load(t *testing.T) { ACLDefaultToken: "alfa", ACLAgentToken: "bravo", ACLAgentRecoveryToken: "charlie", - ACLReplicationToken: "foxtrot", - ACLConfigFileRegistrationToken: "golf", + ACLReplicationToken: "delta", + ACLConfigFileRegistrationToken: "echo", + ACLDNSToken: "foxtrot", } require.NoError(t, os.WriteFile(tokenFile, []byte("[1,2,3]"), 0600)) @@ -206,23 +224,31 @@ func TestStore_Load(t *testing.T) { require.Equal(t, "alfa", store.UserToken()) require.Equal(t, "bravo", store.AgentToken()) require.Equal(t, "charlie", store.AgentRecoveryToken()) - require.Equal(t, "foxtrot", store.ReplicationToken()) - require.Equal(t, "golf", store.ConfigFileRegistrationToken()) + require.Equal(t, "delta", store.ReplicationToken()) + require.Equal(t, "echo", store.ConfigFileRegistrationToken()) + require.Equal(t, "foxtrot", store.DNSToken()) }) } func TestStore_WithPersistenceLock(t *testing.T) { + // ACLDefaultToken: alpha --> sierra + // ACLAgentToken: bravo --> tango + // ACLAgentRecoveryToken: charlie --> uniform + // ACLReplicationToken: delta --> victor + // ACLConfigFileRegistrationToken: echo --> xray + // ACLDNSToken: foxtrot --> zulu setupStore := func() (string, *Store) { dataDir := testutil.TempDir(t, "datadir") store := new(Store) cfg := Config{ EnablePersistence: true, DataDir: dataDir, - ACLDefaultToken: "default-token", - ACLAgentToken: "agent-token", - ACLAgentRecoveryToken: "recovery-token", - ACLReplicationToken: "replication-token", - ACLConfigFileRegistrationToken: "registration-token", + ACLDefaultToken: "alpha", + ACLAgentToken: "bravo", + ACLAgentRecoveryToken: "charlie", + ACLReplicationToken: "delta", + ACLConfigFileRegistrationToken: "echo", + ACLDNSToken: "foxtrot", } err := store.Load(cfg, hclog.New(nil)) require.NoError(t, err) @@ -240,37 +266,39 @@ func TestStore_WithPersistenceLock(t *testing.T) { t.Run("persist some tokens", func(t *testing.T) { dataDir, store := setupStore() err := store.WithPersistenceLock(func() error { - require.True(t, store.UpdateUserToken("the-new-default-token", TokenSourceAPI)) - require.True(t, store.UpdateAgentRecoveryToken("the-new-recovery-token", TokenSourceAPI)) + require.True(t, store.UpdateUserToken("sierra", TokenSourceAPI)) + require.True(t, store.UpdateAgentRecoveryToken("tango", TokenSourceAPI)) return nil }) require.NoError(t, err) // Only API-sourced tokens are persisted. requirePersistedTokens(t, dataDir, persistedTokens{ - Default: "the-new-default-token", - AgentRecovery: "the-new-recovery-token", + Default: "sierra", + AgentRecovery: "tango", }) }) t.Run("persist all tokens", func(t *testing.T) { dataDir, store := setupStore() err := store.WithPersistenceLock(func() error { - require.True(t, store.UpdateUserToken("the-new-default-token", TokenSourceAPI)) - require.True(t, store.UpdateAgentToken("the-new-agent-token", TokenSourceAPI)) - require.True(t, store.UpdateAgentRecoveryToken("the-new-recovery-token", TokenSourceAPI)) - require.True(t, store.UpdateReplicationToken("the-new-replication-token", TokenSourceAPI)) - require.True(t, store.UpdateConfigFileRegistrationToken("the-new-registration-token", TokenSourceAPI)) + require.True(t, store.UpdateUserToken("sierra", TokenSourceAPI)) + require.True(t, store.UpdateAgentToken("tango", TokenSourceAPI)) + require.True(t, store.UpdateAgentRecoveryToken("uniform", TokenSourceAPI)) + require.True(t, store.UpdateReplicationToken("victor", TokenSourceAPI)) + require.True(t, store.UpdateConfigFileRegistrationToken("xray", TokenSourceAPI)) + require.True(t, store.UpdateDNSToken("zulu", TokenSourceAPI)) return nil }) require.NoError(t, err) requirePersistedTokens(t, dataDir, persistedTokens{ - Default: "the-new-default-token", - Agent: "the-new-agent-token", - AgentRecovery: "the-new-recovery-token", - Replication: "the-new-replication-token", - ConfigFileRegistration: "the-new-registration-token", + Default: "sierra", + Agent: "tango", + AgentRecovery: "uniform", + Replication: "victor", + ConfigFileRegistration: "xray", + DNS: "zulu", }) }) diff --git a/agent/token/store.go b/agent/token/store.go index a32475eb47..848d539a2a 100644 --- a/agent/token/store.go +++ b/agent/token/store.go @@ -24,6 +24,7 @@ const ( TokenKindUser TokenKindReplication TokenKindConfigFileRegistration + TokenKindDNS ) type watcher struct { @@ -52,7 +53,7 @@ type Store struct { // also be used for agent operations if the agent token isn't set. userToken string - // userTokenSource indicates where this token originated from + // userTokenSource indicates where this token originated from. userTokenSource TokenSource // agentToken is used for internal agent operations like self-registering @@ -60,7 +61,7 @@ type Store struct { // user-initiated operations. agentToken string - // agentTokenSource indicates where this token originated from + // agentTokenSource indicates where this token originated from. agentTokenSource TokenSource // agentRecoveryToken is a special token that's only used locally for @@ -68,23 +69,30 @@ type Store struct { // available. agentRecoveryToken string - // agentRecoveryTokenSource indicates where this token originated from + // agentRecoveryTokenSource indicates where this token originated from. agentRecoveryTokenSource TokenSource // replicationToken is a special token that's used by servers to // replicate data from the primary datacenter. replicationToken string - // replicationTokenSource indicates where this token originated from + // replicationTokenSource indicates where this token originated from. replicationTokenSource TokenSource // configFileRegistrationToken is used to register services and checks // that are defined in configuration files. configFileRegistrationToken string - // configFileRegistrationTokenSource indicates where this token originated from + // configFileRegistrationTokenSource indicates where this token originated from. configFileRegistrationTokenSource TokenSource + // dnsToken is a special token that is used as the implicit token for DNS requests + // as well as for DNS-specific RPC requests. + dnsToken string + + // dnsTokenSource indicates where the dnsToken originated from. + dnsTokenSource TokenSource + watchers map[int]watcher watcherIndex int @@ -204,6 +212,12 @@ func (t *Store) UpdateConfigFileRegistrationToken(token string, source TokenSour &t.configFileRegistrationTokenSource, TokenKindConfigFileRegistration) } +// UpdateDNSToken replaces the current DNS token in the store. +// Returns true if it was changed. +func (t *Store) UpdateDNSToken(token string, source TokenSource) bool { + return t.updateToken(token, source, &t.dnsToken, &t.dnsTokenSource, TokenKindDNS) +} + func (t *Store) updateToken(token string, source TokenSource, dstToken *string, dstSource *TokenSource, kind TokenKind) bool { t.l.Lock() changed := *dstToken != token || *dstSource != source @@ -261,6 +275,13 @@ func (t *Store) ConfigFileRegistrationToken() string { return t.configFileRegistrationToken } +func (t *Store) DNSToken() string { + t.l.RLock() + defer t.l.RUnlock() + + return t.dnsToken +} + // UserToken returns the best token to use for user operations. func (t *Store) UserTokenAndSource() (string, TokenSource) { t.l.RLock() @@ -299,6 +320,14 @@ func (t *Store) ConfigFileRegistrationTokenAndSource() (string, TokenSource) { return t.configFileRegistrationToken, t.configFileRegistrationTokenSource } +// DNSTokenAndSource returns the best token to use for DNS-specific RPC requests and DNS requests +func (t *Store) DNSTokenAndSource() (string, TokenSource) { + t.l.RLock() + defer t.l.RUnlock() + + return t.dnsToken, t.dnsTokenSource +} + // IsAgentRecoveryToken checks to see if a given token is the agent recovery token. // This will never match an empty token for safety. func (t *Store) IsAgentRecoveryToken(token string) bool { diff --git a/agent/token/store_test.go b/agent/token/store_test.go index c35c44a7b6..2f91614085 100644 --- a/agent/token/store_test.go +++ b/agent/token/store_test.go @@ -21,6 +21,8 @@ func TestStore_RegularTokens(t *testing.T) { replSource TokenSource registration string registrationSource TokenSource + dns string + dnsSource TokenSource } tests := []struct { @@ -95,11 +97,23 @@ func TestStore_RegularTokens(t *testing.T) { raw: tokens{registration: "G", registrationSource: TokenSourceAPI}, effective: tokens{registration: "G"}, }, + { + name: "set dns - config", + set: tokens{dns: "D", dnsSource: TokenSourceConfig}, + raw: tokens{dns: "D", dnsSource: TokenSourceConfig}, + effective: tokens{dns: "D"}, + }, + { + name: "set dns - api", + set: tokens{dns: "D", dnsSource: TokenSourceAPI}, + raw: tokens{dns: "D", dnsSource: TokenSourceAPI}, + effective: tokens{dns: "D"}, + }, { name: "set all", - set: tokens{user: "U", agent: "A", repl: "R", recovery: "M", registration: "G"}, - raw: tokens{user: "U", agent: "A", repl: "R", recovery: "M", registration: "G"}, - effective: tokens{user: "U", agent: "A", repl: "R", recovery: "M", registration: "G"}, + set: tokens{user: "U", agent: "A", repl: "R", recovery: "M", registration: "G", dns: "D"}, + raw: tokens{user: "U", agent: "A", repl: "R", recovery: "M", registration: "G", dns: "D"}, + effective: tokens{user: "U", agent: "A", repl: "R", recovery: "M", registration: "G", dns: "D"}, }, } for _, tt := range tests { @@ -125,18 +139,24 @@ func TestStore_RegularTokens(t *testing.T) { require.True(t, s.UpdateConfigFileRegistrationToken(tt.set.registration, tt.set.registrationSource)) } + if tt.set.dns != "" { + require.True(t, s.UpdateDNSToken(tt.set.dns, tt.set.dnsSource)) + } + // If they don't change then they return false. require.False(t, s.UpdateUserToken(tt.set.user, tt.set.userSource)) require.False(t, s.UpdateAgentToken(tt.set.agent, tt.set.agentSource)) require.False(t, s.UpdateReplicationToken(tt.set.repl, tt.set.replSource)) require.False(t, s.UpdateAgentRecoveryToken(tt.set.recovery, tt.set.recoverySource)) require.False(t, s.UpdateConfigFileRegistrationToken(tt.set.registration, tt.set.registrationSource)) + require.False(t, s.UpdateDNSToken(tt.set.dns, tt.set.dnsSource)) require.Equal(t, tt.effective.user, s.UserToken()) require.Equal(t, tt.effective.agent, s.AgentToken()) require.Equal(t, tt.effective.recovery, s.AgentRecoveryToken()) require.Equal(t, tt.effective.repl, s.ReplicationToken()) require.Equal(t, tt.effective.registration, s.ConfigFileRegistrationToken()) + require.Equal(t, tt.effective.dns, s.DNSToken()) tok, src := s.UserTokenAndSource() require.Equal(t, tt.raw.user, tok) @@ -157,6 +177,10 @@ func TestStore_RegularTokens(t *testing.T) { tok, src = s.ConfigFileRegistrationTokenAndSource() require.Equal(t, tt.raw.registration, tok) require.Equal(t, tt.raw.registrationSource, src) + + tok, src = s.DNSTokenAndSource() + require.Equal(t, tt.raw.dns, tok) + require.Equal(t, tt.raw.dnsSource, src) }) } } @@ -211,6 +235,7 @@ func TestStore_Notify(t *testing.T) { replicationNotifier := newNotification(t, s, TokenKindReplication) replicationNotifier2 := newNotification(t, s, TokenKindReplication) registrationNotifier := newNotification(t, s, TokenKindConfigFileRegistration) + dnsNotifier := newNotification(t, s, TokenKindDNS) // perform an update of the user token require.True(t, s.UpdateUserToken("edcae2a2-3b51-4864-b412-c7a568f49cb1", TokenSourceConfig)) @@ -224,6 +249,7 @@ func TestStore_Notify(t *testing.T) { requireNotNotified(t, agentRecoveryNotifier.Ch) requireNotNotified(t, replicationNotifier2.Ch) requireNotNotified(t, registrationNotifier.Ch) + requireNotNotified(t, dnsNotifier.Ch) // update the agent token which should send a notification to the agent notifier. require.True(t, s.UpdateAgentToken("5d748ec2-d536-461f-8e2a-1f7eae98d559", TokenSourceAPI)) @@ -234,6 +260,7 @@ func TestStore_Notify(t *testing.T) { requireNotNotified(t, agentRecoveryNotifier.Ch) requireNotNotified(t, replicationNotifier2.Ch) requireNotNotified(t, registrationNotifier.Ch) + requireNotNotified(t, dnsNotifier.Ch) // update the agent recovery token which should send a notification to the agent recovery notifier. require.True(t, s.UpdateAgentRecoveryToken("789badc8-f850-43e1-8742-9b9f484957cc", TokenSourceAPI)) @@ -244,6 +271,7 @@ func TestStore_Notify(t *testing.T) { requireNotifiedOnce(t, agentRecoveryNotifier.Ch) requireNotNotified(t, replicationNotifier2.Ch) requireNotNotified(t, registrationNotifier.Ch) + requireNotNotified(t, dnsNotifier.Ch) // update the replication token which should send a notification to the replication notifier. require.True(t, s.UpdateReplicationToken("789badc8-f850-43e1-8742-9b9f484957cc", TokenSourceAPI)) @@ -254,6 +282,7 @@ func TestStore_Notify(t *testing.T) { requireNotNotified(t, agentRecoveryNotifier.Ch) requireNotifiedOnce(t, replicationNotifier2.Ch) requireNotNotified(t, registrationNotifier.Ch) + requireNotNotified(t, dnsNotifier.Ch) s.StopNotify(replicationNotifier2) @@ -266,6 +295,7 @@ func TestStore_Notify(t *testing.T) { requireNotNotified(t, agentRecoveryNotifier.Ch) requireNotNotified(t, replicationNotifier2.Ch) requireNotNotified(t, registrationNotifier.Ch) + requireNotNotified(t, dnsNotifier.Ch) // update the config file registration token which should send a notification to the replication notifier. require.True(t, s.UpdateConfigFileRegistrationToken("82fe7362-7d83-4f43-bb27-c35f1f15083c", TokenSourceAPI)) @@ -276,6 +306,18 @@ func TestStore_Notify(t *testing.T) { requireNotNotified(t, agentRecoveryNotifier.Ch) requireNotNotified(t, replicationNotifier2.Ch) requireNotifiedOnce(t, registrationNotifier.Ch) + requireNotNotified(t, dnsNotifier.Ch) + + // update the dns token which should send a notification to the replication notifier. + require.True(t, s.UpdateDNSToken("ce8e829f-dc45-4ba7-9dd3-1dbbe070f573", TokenSourceAPI)) + + requireNotNotified(t, agentNotifier.Ch) + requireNotNotified(t, userNotifier.Ch) + requireNotNotified(t, replicationNotifier.Ch) + requireNotNotified(t, agentRecoveryNotifier.Ch) + requireNotNotified(t, replicationNotifier2.Ch) + requireNotNotified(t, registrationNotifier.Ch) + requireNotifiedOnce(t, dnsNotifier.Ch) // request updates that are not changes require.False(t, s.UpdateAgentToken("5d748ec2-d536-461f-8e2a-1f7eae98d559", TokenSourceAPI)) @@ -283,6 +325,7 @@ func TestStore_Notify(t *testing.T) { require.False(t, s.UpdateUserToken("47788919-f944-476a-bda5-446d64be1df8", TokenSourceAPI)) require.False(t, s.UpdateReplicationToken("eb0b56b9-fa65-4ae1-902a-c64457c62ac6", TokenSourceAPI)) require.False(t, s.UpdateConfigFileRegistrationToken("82fe7362-7d83-4f43-bb27-c35f1f15083c", TokenSourceAPI)) + require.False(t, s.UpdateDNSToken("ce8e829f-dc45-4ba7-9dd3-1dbbe070f573", TokenSourceAPI)) // ensure that notifications were not sent requireNotNotified(t, agentNotifier.Ch) @@ -290,4 +333,5 @@ func TestStore_Notify(t *testing.T) { requireNotNotified(t, replicationNotifier.Ch) requireNotNotified(t, agentRecoveryNotifier.Ch) requireNotNotified(t, registrationNotifier.Ch) + requireNotNotified(t, dnsNotifier.Ch) } diff --git a/api/agent.go b/api/agent.go index 6775edf425..3c0934f675 100644 --- a/api/agent.go +++ b/api/agent.go @@ -1379,6 +1379,10 @@ func (a *Agent) UpdateConfigFileRegistrationToken(token string, q *WriteOptions) return a.updateToken("config_file_service_registration", token, q) } +func (a *Agent) UpdateDNSToken(token string, q *WriteOptions) (*WriteMeta, error) { + return a.updateToken("dns", token, q) +} + // updateToken can be used to update one of an agent's ACL tokens after the agent has // started. The tokens are may not be persisted, so will need to be updated again if // the agent is restarted unless the agent is configured to persist them. diff --git a/api/agent_test.go b/api/agent_test.go index e6731bb29a..133cbd968f 100644 --- a/api/agent_test.go +++ b/api/agent_test.go @@ -1635,6 +1635,10 @@ func TestAPI_AgentUpdateToken(t *testing.T) { t.Fatalf("err: %v", err) } + if _, err := agent.UpdateDNSToken("root", nil); err != nil { + t.Fatalf("err: %v", err) + } + }) t.Run("new with fallback", func(t *testing.T) { @@ -1723,6 +1727,9 @@ func TestAPI_AgentUpdateToken(t *testing.T) { _, err = agent.UpdateConfigFileRegistrationToken("root", nil) require.Error(t, err) + + _, err = agent.UpdateDNSToken("root", nil) + require.Error(t, err) }) } diff --git a/command/acl/agenttokens/agent_tokens.go b/command/acl/agenttokens/agent_tokens.go index f4e0c496ac..8aca79e5d0 100644 --- a/command/acl/agenttokens/agent_tokens.go +++ b/command/acl/agenttokens/agent_tokens.go @@ -8,9 +8,10 @@ import ( "fmt" "io" + "github.com/mitchellh/cli" + "github.com/hashicorp/consul/command/flags" "github.com/hashicorp/consul/command/helpers" - "github.com/mitchellh/cli" ) func New(ui cli.Ui) *cmd { @@ -63,6 +64,8 @@ func (c *cmd) Run(args []string) int { _, err = client.Agent().UpdateReplicationACLToken(token, nil) case "config_file_service_registration": _, err = client.Agent().UpdateConfigFileRegistrationToken(token, nil) + case "dns": + _, err = client.Agent().UpdateDNSToken(token, nil) default: c.UI.Error(fmt.Sprintf("Unknown token type")) return 1 @@ -140,6 +143,11 @@ Usage: consul acl set-agent-token [options] TYPE TOKEN If a service or check definition contains a 'token' field, then that token is used instead. + dns This is the token that the will be used in place of the default + token when specified for DNS requests and for DNS-specific RPCs. + If not provided the agent will attempt to use the default token + if one is present, then fallback to the anonymous token. + Example: $ consul acl set-agent-token default c4d0f8df-3aba-4ab6-a7a0-35b760dc29a1 diff --git a/test-integ/go.mod b/test-integ/go.mod index eac8a8ef12..f86dfd0433 100644 --- a/test-integ/go.mod +++ b/test-integ/go.mod @@ -6,7 +6,7 @@ require ( github.com/hashicorp/consul/api v1.24.0 github.com/hashicorp/consul/sdk v0.14.1 github.com/hashicorp/consul/test/integration/consul-container v0.0.0-20230628201853-bdf4fad7c5a5 - github.com/hashicorp/consul/testing/deployer v0.0.0-00010101000000-000000000000 + github.com/hashicorp/consul/testing/deployer v0.0.0-20230811171106-4a0afb5d1373 github.com/hashicorp/go-cleanhttp v0.5.2 github.com/itchyny/gojq v0.12.13 github.com/mitchellh/copystructure v1.2.0 diff --git a/test/integration/consul-container/go.mod b/test/integration/consul-container/go.mod index 4bdd39740f..8917a5c5a2 100644 --- a/test/integration/consul-container/go.mod +++ b/test/integration/consul-container/go.mod @@ -14,7 +14,7 @@ require ( github.com/hashicorp/consul/envoyextensions v0.4.1 github.com/hashicorp/consul/proto-public v0.4.1 github.com/hashicorp/consul/sdk v0.14.1 - github.com/hashicorp/consul/testing/deployer v0.0.0-00010101000000-000000000000 + github.com/hashicorp/consul/testing/deployer v0.0.0-20230811171106-4a0afb5d1373 github.com/hashicorp/go-cleanhttp v0.5.2 github.com/hashicorp/go-multierror v1.1.1 github.com/hashicorp/go-uuid v1.0.3 diff --git a/test/integration/consul-container/libs/assert/envoy.go b/test/integration/consul-container/libs/assert/envoy.go index 05add74c92..076367f1a6 100644 --- a/test/integration/consul-container/libs/assert/envoy.go +++ b/test/integration/consul-container/libs/assert/envoy.go @@ -114,7 +114,7 @@ func AssertUpstreamEndpointStatusWithClient( | length`, clusterName, healthStatus) results, err := utils.JQFilter(clusters, filter) - require.NoErrorf(r, err, "could not found cluster name %q: %v \n%s", clusterName, err, clusters) + require.NoErrorf(r, err, "could not find cluster name %q: %v \n%s", clusterName, err, clusters) require.Len(r, results, 1) // the final part of the pipeline is "length" which only ever returns 1 result result, err := strconv.Atoi(results[0]) diff --git a/test/integration/consul-container/libs/cluster/container.go b/test/integration/consul-container/libs/cluster/container.go index d444162364..bcb7c3f3f6 100644 --- a/test/integration/consul-container/libs/cluster/container.go +++ b/test/integration/consul-container/libs/cluster/container.go @@ -614,6 +614,7 @@ func newContainerRequest(config Config, opts containerOpts, ports ...int) (podRe "8500/tcp", // Consul HTTP API "8501/tcp", // Consul HTTPs API "8502/tcp", // Consul gRPC API + "8600/udp", // Consul DNS API "8443/tcp", // Envoy Gateway Listener diff --git a/test/integration/consul-container/libs/topology/peering_topology.go b/test/integration/consul-container/libs/topology/peering_topology.go index 1d11851753..df72598082 100644 --- a/test/integration/consul-container/libs/topology/peering_topology.go +++ b/test/integration/consul-container/libs/topology/peering_topology.go @@ -43,7 +43,7 @@ type PeeringClusterSize struct { // // - an accepting cluster with 3 servers and 1 client agent. The client should be used to // host a service for export: staticServerSvc. -// - an dialing cluster with 1 server and 1 client. The client should be used to host a +// - a dialing cluster with 1 server and 1 client. The client should be used to host a // service connecting to staticServerSvc. // - Create the peering, export the service from accepting cluster, and verify service // connectivity. @@ -120,7 +120,7 @@ func BasicPeeringTwoClustersSetup( libassert.PeeringStatus(t, acceptingClient, AcceptingPeerName, api.PeeringStateActive) // libassert.PeeringExports(t, acceptingClient, acceptingPeerName, 1) - // Register an static-server service in acceptingCluster and export to dialing cluster + // Register a static-server service in acceptingCluster and export to dialing cluster var serverService, serverSidecarService libservice.Service { clientNode := acceptingCluster.Clients()[0] @@ -144,7 +144,7 @@ func BasicPeeringTwoClustersSetup( require.NoError(t, serverService.Export("default", AcceptingPeerName, acceptingClient)) } - // Register an static-client service in dialing cluster and set upstream to static-server service + // Register a static-client service in dialing cluster and set upstream to static-server service var clientSidecarService *libservice.ConnectContainer { clientNode := dialingCluster.Clients()[0] @@ -268,11 +268,11 @@ func NewClusterWithConfig( } // Add numClients static clients to register the service - configbuiilder := libcluster.NewConfigBuilder(ctx). + configBuilder := libcluster.NewConfigBuilder(ctx). Client(). Peering(true). RetryJoin(retryJoin...) - clientConf := configbuiilder.ToAgentConfig(t) + clientConf := configBuilder.ToAgentConfig(t) t.Logf("%s client config: \n%s", opts.Datacenter, clientConf.JSON) if clientHclConfig != "" { clientConf.MutatebyAgentConfig(clientHclConfig) diff --git a/testing/deployer/topology/default_cdp.go b/testing/deployer/topology/default_cdp.go index 22424d2dd1..f20df3d1a9 100644 --- a/testing/deployer/topology/default_cdp.go +++ b/testing/deployer/topology/default_cdp.go @@ -3,4 +3,4 @@ package topology -const DefaultDataplaneImage = "hashicorp/consul-dataplane:1.1.0" +const DefaultDataplaneImage = "hashicorp/consul-dataplane:1.2.1"