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"