diff --git a/agent/acl.go b/agent/acl.go index 2e30848a43..f77642bae5 100644 --- a/agent/acl.go +++ b/agent/acl.go @@ -67,7 +67,6 @@ type aclManager struct { acls *lru.TwoQueueCache // master is the ACL to use when the agent master token is supplied. - // This may be nil if that option isn't set in the agent config. master acl.ACL // down is the ACL to use when the servers are down. This may be nil @@ -93,29 +92,24 @@ func newACLManager(config *Config) (*aclManager, error) { return nil, err } - // If an agent master token is configured, build a policy and ACL for - // it, otherwise leave it nil. - var master acl.ACL - if len(config.ACLAgentMasterToken) > 0 { - policy := &acl.Policy{ - Agents: []*acl.AgentPolicy{ - &acl.AgentPolicy{ - Node: config.NodeName, - Policy: acl.PolicyWrite, - }, + // Build a policy for the agent master token. + policy := &acl.Policy{ + Agents: []*acl.AgentPolicy{ + &acl.AgentPolicy{ + Node: config.NodeName, + Policy: acl.PolicyWrite, }, - Nodes: []*acl.NodePolicy{ - &acl.NodePolicy{ - Name: "", - Policy: acl.PolicyRead, - }, + }, + Nodes: []*acl.NodePolicy{ + &acl.NodePolicy{ + Name: "", + Policy: acl.PolicyRead, }, - } - acl, err := acl.New(acl.DenyAll(), policy) - if err != nil { - return nil, err - } - master = acl + }, + } + master, err := acl.New(acl.DenyAll(), policy) + if err != nil { + return nil, err } var down acl.ACL @@ -155,7 +149,7 @@ func (m *aclManager) lookupACL(a *Agent, id string) (acl.ACL, error) { id = anonymousToken } else if acl.RootACL(id) != nil { return nil, errors.New(rootDenied) - } else if m.master != nil && id == a.config.ACLAgentMasterToken { + } else if a.tokens.IsAgentMasterToken(id) { return m.master, nil } diff --git a/agent/agent.go b/agent/agent.go index 3550b10e8a..4ac1a4314e 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -23,6 +23,7 @@ import ( "github.com/hashicorp/consul/agent/consul" "github.com/hashicorp/consul/agent/consul/structs" "github.com/hashicorp/consul/agent/systemd" + "github.com/hashicorp/consul/agent/token" "github.com/hashicorp/consul/api" "github.com/hashicorp/consul/ipaddr" "github.com/hashicorp/consul/lib" @@ -180,6 +181,11 @@ type Agent struct { // watchPlans tracks all the currently-running watch plans for the // agent. watchPlans []*watch.Plan + + // tokens holds ACL tokens initially from the configuration, but can + // be updated at runtime, so should always be used instead of going to + // the configuration directly. + tokens *token.Store } func New(c *Config) (*Agent, error) { @@ -220,6 +226,7 @@ func New(c *Config) (*Agent, error) { endpoints: make(map[string]string), dnsAddrs: dnsAddrs, httpAddrs: httpAddrs, + tokens: new(token.Store), } if err := a.resolveTmplAddrs(); err != nil { return nil, err @@ -271,6 +278,11 @@ func New(c *Config) (*Agent, error) { "wan": a.config.AdvertiseAddrWan, } + // Set up the initial state of the token store based on the config. + a.tokens.UpdateUserToken(a.config.ACLToken) + a.tokens.UpdateAgentToken(a.config.ACLAgentToken) + a.tokens.UpdateAgentMasterToken(a.config.ACLAgentMasterToken) + return a, nil } @@ -292,7 +304,7 @@ func (a *Agent) Start() error { } // create the local state - a.state = NewLocalState(c, a.logger) + a.state = NewLocalState(c, a.logger, a.tokens) // create the config for the rpc server/client consulCfg, err := a.consulConfig() @@ -1344,7 +1356,7 @@ func (a *Agent) sendCoordinate() { Datacenter: a.config.Datacenter, Node: a.config.NodeName, Coord: c, - WriteRequest: structs.WriteRequest{Token: a.config.GetTokenForAgent()}, + WriteRequest: structs.WriteRequest{Token: a.tokens.AgentToken()}, } var reply struct{} if err := a.RPC("Coordinate.Update", &req, &reply); err != nil { diff --git a/agent/agent_endpoint.go b/agent/agent_endpoint.go index f4b4f7b82b..ff945f5b22 100644 --- a/agent/agent_endpoint.go +++ b/agent/agent_endpoint.go @@ -698,3 +698,51 @@ func (h *httpLogHandler) HandleLog(log string) { h.droppedCount++ } } + +func (s *HTTPServer) AgentToken(resp http.ResponseWriter, req *http.Request) (interface{}, error) { + if req.Method != "PUT" { + resp.WriteHeader(http.StatusMethodNotAllowed) + return nil, nil + } + + // Fetch the ACL token, if any, and enforce agent policy. + var token string + s.parseToken(req, &token) + acl, err := s.agent.resolveToken(token) + if err != nil { + return nil, err + } + if acl != nil && !acl.AgentWrite(s.agent.config.NodeName) { + return nil, errPermissionDenied + } + + // The body is just the token, but it's in a JSON object so we can add + // fields to this later if needed. + var args api.AgentToken + if err := decodeBody(req, &args, nil); err != nil { + resp.WriteHeader(http.StatusBadRequest) + fmt.Fprintf(resp, "Request decode failed: %v", err) + return nil, nil + } + + // Figure out the target token. + target := strings.TrimPrefix(req.URL.Path, "/v1/agent/token/") + switch target { + case "acl_token": + s.agent.tokens.UpdateUserToken(args.Token) + + case "acl_agent_token": + s.agent.tokens.UpdateAgentToken(args.Token) + + case "acl_agent_master_token": + s.agent.tokens.UpdateAgentMasterToken(args.Token) + + default: + resp.WriteHeader(http.StatusNotFound) + fmt.Fprintf(resp, "Token %q is unknown", target) + return nil, nil + } + + s.agent.logger.Printf("[INFO] Updated agent's %q", target) + return nil, nil +} diff --git a/agent/agent_endpoint_test.go b/agent/agent_endpoint_test.go index 84b9d3a7d0..e9bee3f54c 100644 --- a/agent/agent_endpoint_test.go +++ b/agent/agent_endpoint_test.go @@ -1654,3 +1654,75 @@ func TestAgent_Monitor_ACLDeny(t *testing.T) { // logic is a little complex to set up so isn't worth repeating again // here. } + +func TestAgent_Token(t *testing.T) { + t.Parallel() + cfg := TestACLConfig() + cfg.ACLToken = "" + cfg.ACLAgentToken = "" + cfg.ACLAgentMasterToken = "" + a := NewTestAgent(t.Name(), cfg) + defer a.Shutdown() + + b := func(token string) io.Reader { + return jsonReader(&api.AgentToken{Token: token}) + } + + badJSON := func() io.Reader { + return jsonReader(false) + } + + tests := []struct { + name string + method, url string + body io.Reader + code int + userToken string + agentToken string + masterToken string + }{ + {"bad method", "GET", "acl_token", b("X"), http.StatusMethodNotAllowed, "", "", ""}, + {"bad token name", "PUT", "nope?token=root", b("X"), http.StatusNotFound, "", "", ""}, + {"bad JSON", "PUT", "acl_token?token=root", badJSON(), http.StatusBadRequest, "", "", ""}, + {"set user", "PUT", "acl_token?token=root", b("U"), http.StatusOK, "U", "U", ""}, + {"set agent", "PUT", "acl_agent_token?token=root", b("A"), http.StatusOK, "U", "A", ""}, + {"set master", "PUT", "acl_agent_master_token?token=root", b("M"), http.StatusOK, "U", "A", "M"}, + {"clear user", "PUT", "acl_token?token=root", b(""), http.StatusOK, "", "A", "M"}, + {"clear agent", "PUT", "acl_agent_token?token=root", b(""), http.StatusOK, "", "", "M"}, + {"clear master", "PUT", "acl_agent_master_token?token=root", b(""), http.StatusOK, "", "", ""}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + url := fmt.Sprintf("/v1/agent/token/%s", tt.url) + resp := httptest.NewRecorder() + req, _ := http.NewRequest(tt.method, url, tt.body) + if _, err := a.srv.AgentToken(resp, req); err != nil { + t.Fatalf("err: %v", err) + } + if got, want := resp.Code, tt.code; got != want { + t.Fatalf("got %d want %d", got, want) + } + if got, want := a.tokens.UserToken(), tt.userToken; got != want { + t.Fatalf("got %q want %q", got, want) + } + if got, want := a.tokens.AgentToken(), tt.agentToken; got != want { + t.Fatalf("got %q want %q", got, want) + } + if tt.masterToken != "" && !a.tokens.IsAgentMasterToken(tt.masterToken) { + t.Fatalf("%q should be the master token", tt.masterToken) + } + }) + } + + // This one returns an error that is interpreted by the HTTP wrapper, so + // doesn't fit into our table above. + t.Run("permission denied", func(t *testing.T) { + req, _ := http.NewRequest("PUT", "/v1/agent/token/acl_token", b("X")) + if _, err := a.srv.AgentToken(nil, req); !isPermissionDenied(err) { + t.Fatalf("err: %v", err) + } + if got, want := a.tokens.UserToken(), ""; got != want { + t.Fatalf("got %q want %q", got, want) + } + }) +} diff --git a/agent/agent_test.go b/agent/agent_test.go index d366d5b7e2..ab7463c644 100644 --- a/agent/agent_test.go +++ b/agent/agent_test.go @@ -149,6 +149,27 @@ func TestAgent_CheckAdvertiseAddrsSettings(t *testing.T) { } } +func TestAgent_TokenStore(t *testing.T) { + t.Parallel() + + cfg := TestConfig() + cfg.ACLToken = "user" + cfg.ACLAgentToken = "agent" + cfg.ACLAgentMasterToken = "master" + a := NewTestAgent(t.Name(), cfg) + defer a.Shutdown() + + if got, want := a.tokens.UserToken(), "user"; got != want { + t.Fatalf("got %q want %q", got, want) + } + if got, want := a.tokens.AgentToken(), "agent"; got != want { + t.Fatalf("got %q want %q", got, want) + } + if got, want := a.tokens.IsAgentMasterToken("master"), true; got != want { + t.Fatalf("got %v want %v", got, want) + } +} + func TestAgent_CheckPerformanceSettings(t *testing.T) { t.Parallel() // Try a default config. diff --git a/agent/config.go b/agent/config.go index d7af3a2626..dd21cd6088 100644 --- a/agent/config.go +++ b/agent/config.go @@ -1021,18 +1021,6 @@ func (c *Config) ClientListener(override string, port int) (net.Addr, error) { return &net.TCPAddr{IP: ip, Port: port}, nil } -// GetTokenForAgent returns the token the agent should use for its own internal -// operations, such as registering itself with the catalog. -func (c *Config) GetTokenForAgent() string { - if c.ACLAgentToken != "" { - return c.ACLAgentToken - } - if c.ACLToken != "" { - return c.ACLToken - } - return "" -} - // VerifyUniqueListeners checks to see if an address was used more than once in // the config func (c *Config) VerifyUniqueListeners() error { diff --git a/agent/config_test.go b/agent/config_test.go index 0d988314e2..9ad77ab4a1 100644 --- a/agent/config_test.go +++ b/agent/config_test.go @@ -1171,37 +1171,6 @@ func TestDecodeConfig(t *testing.T) { } } -func TestDecodeConfig_ACLTokenPreference(t *testing.T) { - tests := []struct { - in string - tok string - }{ - { - in: `{}`, - tok: "", - }, - { - in: `{"acl_token":"a"}`, - tok: "a", - }, - { - in: `{"acl_token":"a","acl_agent_token":"b"}`, - tok: "b", - }, - } - for _, tt := range tests { - t.Run(tt.in, func(t *testing.T) { - c, err := DecodeConfig(strings.NewReader(tt.in)) - if err != nil { - t.Fatalf("got error %v want nil", err) - } - if got, want := c.GetTokenForAgent(), tt.tok; got != want { - t.Fatalf("got token for agent %q want %q", got, want) - } - }) - } -} - func TestDecodeConfig_VerifyUniqueListeners(t *testing.T) { t.Parallel() tests := []struct { diff --git a/agent/consul/config.go b/agent/consul/config.go index 2c752064df..5b62848a7b 100644 --- a/agent/consul/config.go +++ b/agent/consul/config.go @@ -460,15 +460,3 @@ func (c *Config) tlsConfig() *tlsutil.Config { } return tlsConf } - -// GetTokenForAgent returns the token the agent should use for its own internal -// operations, such as registering itself with the catalog. -func (c *Config) GetTokenForAgent() string { - if c.ACLAgentToken != "" { - return c.ACLAgentToken - } - if c.ACLToken != "" { - return c.ACLToken - } - return "" -} diff --git a/agent/consul/config_test.go b/agent/consul/config_test.go deleted file mode 100644 index eea989d73f..0000000000 --- a/agent/consul/config_test.go +++ /dev/null @@ -1,21 +0,0 @@ -package consul - -import ( - "testing" -) - -func TestConfig_GetTokenForAgent(t *testing.T) { - t.Parallel() - config := DefaultConfig() - if token := config.GetTokenForAgent(); token != "" { - t.Fatalf("bad: %s", token) - } - config.ACLToken = "hello" - if token := config.GetTokenForAgent(); token != "hello" { - t.Fatalf("bad: %s", token) - } - config.ACLAgentToken = "world" - if token := config.GetTokenForAgent(); token != "world" { - t.Fatalf("bad: %s", token) - } -} diff --git a/agent/dns.go b/agent/dns.go index 8f95ea137c..bf01bd1e85 100644 --- a/agent/dns.go +++ b/agent/dns.go @@ -144,7 +144,7 @@ func (d *DNSServer) handlePtr(resp dns.ResponseWriter, req *dns.Msg) { args := structs.DCSpecificRequest{ Datacenter: datacenter, QueryOptions: structs.QueryOptions{ - Token: d.agent.config.ACLToken, + Token: d.agent.tokens.UserToken(), AllowStale: *d.config.AllowStale, }, } @@ -388,7 +388,7 @@ func (d *DNSServer) nodeLookup(network, datacenter, node string, req, resp *dns. Datacenter: datacenter, Node: node, QueryOptions: structs.QueryOptions{ - Token: d.agent.config.ACLToken, + Token: d.agent.tokens.UserToken(), AllowStale: *d.config.AllowStale, }, } @@ -602,7 +602,7 @@ func (d *DNSServer) serviceLookup(network, datacenter, service, tag string, req, ServiceTag: tag, TagFilter: tag != "", QueryOptions: structs.QueryOptions{ - Token: d.agent.config.ACLToken, + Token: d.agent.tokens.UserToken(), AllowStale: *d.config.AllowStale, }, } @@ -680,7 +680,7 @@ func (d *DNSServer) preparedQueryLookup(network, datacenter, query string, req, Datacenter: datacenter, QueryIDOrName: query, QueryOptions: structs.QueryOptions{ - Token: d.agent.config.ACLToken, + Token: d.agent.tokens.UserToken(), AllowStale: *d.config.AllowStale, }, diff --git a/agent/http.go b/agent/http.go index 046a62d235..5ba2601140 100644 --- a/agent/http.go +++ b/agent/http.go @@ -80,6 +80,7 @@ func (s *HTTPServer) handler(enableDebug bool) http.Handler { handleFuncMetrics("/v1/acl/clone/", s.wrap(s.ACLClone)) handleFuncMetrics("/v1/acl/list", s.wrap(s.ACLList)) handleFuncMetrics("/v1/acl/replication", s.wrap(s.ACLReplicationStatus)) + handleFuncMetrics("/v1/agent/token/", s.wrap(s.AgentToken)) } else { handleFuncMetrics("/v1/acl/create", s.wrap(ACLDisabled)) handleFuncMetrics("/v1/acl/update", s.wrap(ACLDisabled)) @@ -88,6 +89,7 @@ func (s *HTTPServer) handler(enableDebug bool) http.Handler { handleFuncMetrics("/v1/acl/clone/", s.wrap(ACLDisabled)) handleFuncMetrics("/v1/acl/list", s.wrap(ACLDisabled)) handleFuncMetrics("/v1/acl/replication", s.wrap(ACLDisabled)) + handleFuncMetrics("/v1/agent/token/", s.wrap(ACLDisabled)) } handleFuncMetrics("/v1/agent/self", s.wrap(s.AgentSelf)) handleFuncMetrics("/v1/agent/maintenance", s.wrap(s.AgentNodeMaintenance)) @@ -428,7 +430,7 @@ func (s *HTTPServer) parseToken(req *http.Request, token *string) { } // Set the default ACLToken - *token = s.agent.config.ACLToken + *token = s.agent.tokens.UserToken() } // parseSource is used to parse the ?near= query parameter, used for diff --git a/agent/http_test.go b/agent/http_test.go index 29a606e89e..13567ab6ab 100644 --- a/agent/http_test.go +++ b/agent/http_test.go @@ -569,14 +569,14 @@ func TestACLResolution(t *testing.T) { defer a.Shutdown() // Check when no token is set - a.Config.ACLToken = "" + a.tokens.UpdateUserToken("") a.srv.parseToken(req, &token) if token != "" { t.Fatalf("bad: %s", token) } // Check when ACLToken set - a.Config.ACLToken = "agent" + a.tokens.UpdateUserToken("agent") a.srv.parseToken(req, &token) if token != "agent" { t.Fatalf("bad: %s", token) diff --git a/agent/local.go b/agent/local.go index decf7acddf..57bb944ff1 100644 --- a/agent/local.go +++ b/agent/local.go @@ -10,6 +10,7 @@ import ( "time" "github.com/hashicorp/consul/agent/consul/structs" + "github.com/hashicorp/consul/agent/token" "github.com/hashicorp/consul/api" "github.com/hashicorp/consul/lib" "github.com/hashicorp/consul/types" @@ -30,7 +31,6 @@ type syncStatus struct { // populated during NewLocalAgent from the agent configuration to avoid // race conditions with the agent configuration. type localStateConfig struct { - ACLToken string AEInterval time.Duration AdvertiseAddr string CheckUpdateInterval time.Duration @@ -38,7 +38,7 @@ type localStateConfig struct { NodeID types.NodeID NodeName string TaggedAddresses map[string]string - TokenForAgent string + Tokens *token.Store } // localState is used to represent the node's services, @@ -89,9 +89,8 @@ type localState struct { } // NewLocalState creates a is used to initialize the local state -func NewLocalState(c *Config, lg *log.Logger) *localState { +func NewLocalState(c *Config, lg *log.Logger, tokens *token.Store) *localState { lc := localStateConfig{ - ACLToken: c.ACLToken, AEInterval: c.AEInterval, AdvertiseAddr: c.AdvertiseAddr, CheckUpdateInterval: c.CheckUpdateInterval, @@ -99,7 +98,7 @@ func NewLocalState(c *Config, lg *log.Logger) *localState { NodeID: c.NodeID, NodeName: c.NodeName, TaggedAddresses: map[string]string{}, - TokenForAgent: c.GetTokenForAgent(), + Tokens: tokens, } for k, v := range c.TaggedAddresses { lc.TaggedAddresses[k] = v @@ -172,7 +171,7 @@ func (l *localState) ServiceToken(id string) string { func (l *localState) serviceToken(id string) string { token := l.serviceTokens[id] if token == "" { - token = l.config.ACLToken + token = l.config.Tokens.UserToken() } return token } @@ -239,7 +238,7 @@ func (l *localState) CheckToken(checkID types.CheckID) string { func (l *localState) checkToken(checkID types.CheckID) string { token := l.checkTokens[checkID] if token == "" { - token = l.config.ACLToken + token = l.config.Tokens.UserToken() } return token } @@ -449,7 +448,7 @@ func (l *localState) setSyncState() error { req := structs.NodeSpecificRequest{ Datacenter: l.config.Datacenter, Node: l.config.NodeName, - QueryOptions: structs.QueryOptions{Token: l.config.TokenForAgent}, + QueryOptions: structs.QueryOptions{Token: l.config.Tokens.AgentToken()}, } var out1 structs.IndexedNodeServices var out2 structs.IndexedHealthChecks @@ -785,7 +784,7 @@ func (l *localState) syncNodeInfo() error { Address: l.config.AdvertiseAddr, TaggedAddresses: l.config.TaggedAddresses, NodeMeta: l.metadata, - WriteRequest: structs.WriteRequest{Token: l.config.TokenForAgent}, + WriteRequest: structs.WriteRequest{Token: l.config.Tokens.AgentToken()}, } var out struct{} err := l.delegate.RPC("Catalog.Register", &req, &out) diff --git a/agent/local_test.go b/agent/local_test.go index 1808dba087..98406a3763 100644 --- a/agent/local_test.go +++ b/agent/local_test.go @@ -6,6 +6,7 @@ import ( "time" "github.com/hashicorp/consul/agent/consul/structs" + "github.com/hashicorp/consul/agent/token" "github.com/hashicorp/consul/api" "github.com/hashicorp/consul/testutil/retry" "github.com/hashicorp/consul/types" @@ -1420,9 +1421,10 @@ func TestAgentAntiEntropy_deleteCheck_fails(t *testing.T) { func TestAgent_serviceTokens(t *testing.T) { t.Parallel() - cfg := TestConfig() - cfg.ACLToken = "default" - l := NewLocalState(cfg, nil) + + tokens := new(token.Store) + tokens.UpdateUserToken("default") + l := NewLocalState(TestConfig(), nil, tokens) l.AddService(&structs.NodeService{ ID: "redis", @@ -1448,9 +1450,10 @@ func TestAgent_serviceTokens(t *testing.T) { func TestAgent_checkTokens(t *testing.T) { t.Parallel() - cfg := TestConfig() - cfg.ACLToken = "default" - l := NewLocalState(cfg, nil) + + tokens := new(token.Store) + tokens.UpdateUserToken("default") + l := NewLocalState(TestConfig(), nil, tokens) // Returns default when no token is set if token := l.CheckToken("mem"); token != "default" { @@ -1473,7 +1476,7 @@ func TestAgent_checkTokens(t *testing.T) { func TestAgent_checkCriticalTime(t *testing.T) { t.Parallel() cfg := TestConfig() - l := NewLocalState(cfg, nil) + l := NewLocalState(cfg, nil, new(token.Store)) svc := &structs.NodeService{ID: "redis", Service: "redis", Port: 8000} l.AddService(svc, "") @@ -1536,7 +1539,7 @@ func TestAgent_checkCriticalTime(t *testing.T) { func TestAgent_AddCheckFailure(t *testing.T) { t.Parallel() cfg := TestConfig() - l := NewLocalState(cfg, nil) + l := NewLocalState(cfg, nil, new(token.Store)) // Add a check for a service that does not exist and verify that it fails checkID := types.CheckID("redis:1") diff --git a/agent/remote_exec.go b/agent/remote_exec.go index 9ea6eedbf1..eda398e7eb 100644 --- a/agent/remote_exec.go +++ b/agent/remote_exec.go @@ -243,7 +243,7 @@ func (a *Agent) remoteExecGetSpec(event *remoteExecEvent, spec *remoteExecSpec) AllowStale: true, // Stale read for scale! Retry on failure. }, } - get.Token = a.config.GetTokenForAgent() + get.Token = a.tokens.AgentToken() var out structs.IndexedDirEntries QUERY: if err := a.RPC("KVS.Get", &get, &out); err != nil { @@ -310,7 +310,7 @@ func (a *Agent) remoteExecWriteKey(event *remoteExecEvent, suffix string, val [] Session: event.Session, }, } - write.Token = a.config.GetTokenForAgent() + write.Token = a.tokens.AgentToken() var success bool if err := a.RPC("KVS.Apply", &write, &success); err != nil { return err diff --git a/agent/token/store.go b/agent/token/store.go new file mode 100644 index 0000000000..119f2a9d2e --- /dev/null +++ b/agent/token/store.go @@ -0,0 +1,78 @@ +package token + +import ( + "sync" +) + +// Store is used to hold the special ACL tokens used by Consul agents. It is +// designed to update the tokens on the fly, so the token store itself should be +// plumbed around and used to get tokens at runtime, don't save the resulting +// tokens. +type Store struct { + // l synchronizes access to the token store. + l sync.RWMutex + + // userToken is passed along for requests when the user didn't supply a + // token, and may be left blank to use the anonymous token. This will + // also be used for agent operations if the agent token isn't set. + userToken string + + // agentToken is used for internal agent operations like self-registering + // with the catalog and anti-entropy, but should never be used for + // user-initiated operations. + agentToken string + + // agentMasterToken is a special token that's only used locally for + // access to the /v1/agent utility operations if the servers aren't + // available. + agentMasterToken string +} + +// UpdateUserToken replaces the current user token in the store. +func (t *Store) UpdateUserToken(token string) { + t.l.Lock() + t.userToken = token + t.l.Unlock() +} + +// UpdateAgentToken replaces the current agent token in the store. +func (t *Store) UpdateAgentToken(token string) { + t.l.Lock() + t.agentToken = token + t.l.Unlock() +} + +// UpdateAgentMasterToken replaces the current agent master token in the store. +func (t *Store) UpdateAgentMasterToken(token string) { + t.l.Lock() + t.agentMasterToken = token + t.l.Unlock() +} + +// UserToken returns the best token to use for user operations. +func (t *Store) UserToken() string { + t.l.RLock() + defer t.l.RUnlock() + + return t.userToken +} + +// AgentToken returns the best token to use for internal agent operations. +func (t *Store) AgentToken() string { + t.l.RLock() + defer t.l.RUnlock() + + if t.agentToken != "" { + return t.agentToken + } + return t.userToken +} + +// IsAgentMasterToken checks to see if a given token is the agent master token. +// This will never match an empty token for safety. +func (t *Store) IsAgentMasterToken(token string) bool { + t.l.RLock() + defer t.l.RUnlock() + + return (token != "") && (token == t.agentMasterToken) +} diff --git a/agent/token/store_test.go b/agent/token/store_test.go new file mode 100644 index 0000000000..a21bc54e2c --- /dev/null +++ b/agent/token/store_test.go @@ -0,0 +1,58 @@ +package token + +import ( + "testing" +) + +func TestStore_UserAndAgentTokens(t *testing.T) { + t.Parallel() + + tests := []struct { + user, agent, wantUser, wantAgent string + }{ + {"", "", "", ""}, + {"user", "", "user", "user"}, + {"user", "agent", "user", "agent"}, + {"", "agent", "", "agent"}, + {"user", "agent", "user", "agent"}, + {"user", "", "user", "user"}, + {"", "", "", ""}, + } + tokens := new(Store) + for _, tt := range tests { + tokens.UpdateUserToken(tt.user) + tokens.UpdateAgentToken(tt.agent) + if got, want := tokens.UserToken(), tt.wantUser; got != want { + t.Fatalf("got token %q want %q", got, want) + } + if got, want := tokens.AgentToken(), tt.wantAgent; got != want { + t.Fatalf("got token %q want %q", got, want) + } + } +} + +func TestStore_AgentMasterToken(t *testing.T) { + t.Parallel() + tokens := new(Store) + + verify := func(want bool, toks ...string) { + for _, tok := range toks { + if got := tokens.IsAgentMasterToken(tok); got != want { + t.Fatalf("token %q got %v want %v", tok, got, want) + } + } + } + + verify(false, "", "nope") + + tokens.UpdateAgentMasterToken("master") + verify(true, "master") + verify(false, "", "nope") + + tokens.UpdateAgentMasterToken("another") + verify(true, "another") + verify(false, "", "nope", "master") + + tokens.UpdateAgentMasterToken("") + verify(false, "", "nope", "master", "another") +} diff --git a/api/agent.go b/api/agent.go index 605592db97..86c9414aeb 100644 --- a/api/agent.go +++ b/api/agent.go @@ -91,6 +91,11 @@ type AgentServiceCheck struct { } type AgentServiceChecks []*AgentServiceCheck +// AgentToken is used when updating ACL tokens for an agent. +type AgentToken struct { + Token string +} + // Agent can be used to query the Agent endpoints type Agent struct { c *Client @@ -473,3 +478,38 @@ func (a *Agent) Monitor(loglevel string, stopCh <-chan struct{}, q *QueryOptions return logCh, nil } + +// UpdateACLToken updates the agent's "acl_token". See updateToken for more +// details. +func (c *Agent) UpdateACLToken(token string, q *WriteOptions) (*WriteMeta, error) { + return c.updateToken("acl_token", token, q) +} + +// UpdateACLAgentToken updates the agent's "acl_agent_token". See updateToken +// for more details. +func (c *Agent) UpdateACLAgentToken(token string, q *WriteOptions) (*WriteMeta, error) { + return c.updateToken("acl_agent_token", token, q) +} + +// UpdateACLAgentMasterToken updates the agent's "acl_agent_master_token". See +// updateToken for more details. +func (c *Agent) UpdateACLAgentMasterToken(token string, q *WriteOptions) (*WriteMeta, error) { + return c.updateToken("acl_agent_master_token", token, q) +} + +// updateToken can be used to update an agent's ACL token after the agent has +// started. The tokens are not persisted, so will need to be updated again if +// the agent is restarted. +func (c *Agent) updateToken(target, token string, q *WriteOptions) (*WriteMeta, error) { + r := c.c.newRequest("PUT", fmt.Sprintf("/v1/agent/token/%s", target)) + r.setWriteOptions(q) + r.obj = &AgentToken{Token: token} + rtt, resp, err := requireOK(c.c.doRequest(r)) + if err != nil { + return nil, err + } + resp.Body.Close() + + wm := &WriteMeta{RequestTime: rtt} + return wm, nil +} diff --git a/api/agent_test.go b/api/agent_test.go index d49630d660..2f6d02c816 100644 --- a/api/agent_test.go +++ b/api/agent_test.go @@ -766,3 +766,23 @@ func TestAPI_NodeMaintenance(t *testing.T) { } } } + +func TestAPI_AgentUpdateToken(t *testing.T) { + t.Parallel() + c, s := makeACLClient(t) + defer s.Stop() + + agent := c.Agent() + + if _, err := agent.UpdateACLToken("root", nil); err != nil { + t.Fatalf("err: %v", err) + } + + if _, err := agent.UpdateACLAgentToken("root", nil); err != nil { + t.Fatalf("err: %v", err) + } + + if _, err := agent.UpdateACLAgentMasterToken("root", nil); err != nil { + t.Fatalf("err: %v", err) + } +} diff --git a/website/source/api/agent.html.md b/website/source/api/agent.html.md index 3928b88a92..2186a23e48 100644 --- a/website/source/api/agent.html.md +++ b/website/source/api/agent.html.md @@ -382,3 +382,51 @@ $ curl \ --request PUT \ https://consul.rocks/v1/agent/force-leave ``` + +## Update ACL Tokens + +This endpoint updates the ACL tokens currently in use by the agent. It can be +used to introduce ACL tokens to the agent for the first time, or to update +tokens that were initially loaded from the agent's configuration. Tokens are +not persisted, so will need to be updated again if the agent is restarted. + +| Method | Path | Produces | +| ------ | ------------------------------------- | -------------------------- | +| `PUT` | `/agent/token/acl_token` | `application/json` | +| `PUT` | `/agent/token/acl_agent_token` | `application/json` | +| `PUT` | `/agent/token/acl_agent_master_token` | `application/json` | + +The paths above correspond to the token names as found in the agent configuration, +[`acl_token`](/docs/agent/options.html#acl_token), +[`acl_agent_token`](/docs/agent/options.html#acl_agent_token), +and [`acl_agent_master_token`](/docs/agent/options.html#acl_agent_master_token). + +The table below shows this endpoint's support for +[blocking queries](/api/index.html#blocking-queries), +[consistency modes](/api/index.html#consistency-modes), and +[required ACLs](/api/index.html#acls). + +| Blocking Queries | Consistency Modes | ACL Required | +| ---------------- | ----------------- | ------------- | +| `NO` | `none` | `agent:write` | + +### Parameters + +- `Token` `(string: "")` - Specifies the ACL token to set. + +### Sample Payload + +```json +{ + "Token": "adf4238a-882b-9ddc-4a9d-5b6758e4159e" +} +``` + +### Sample Request + +```text +$ curl \ + --request PUT \ + --data @payload.json \ + https://consul.rocks/v1/agent/token/acl_token +``` diff --git a/website/source/docs/guides/acl.html.md b/website/source/docs/guides/acl.html.md index d17e668b7e..afb6b7ab79 100644 --- a/website/source/docs/guides/acl.html.md +++ b/website/source/docs/guides/acl.html.md @@ -133,6 +133,9 @@ system, or accessing Consul in special situations: | [`acl_master_token`](/docs/agent/options.html#acl_master_token) | `REQUIRED` | `N/A` | Special token used to bootstrap the ACL system, see the [Bootstrapping ACLs](#bootstrapping-acls) section for more details | | [`acl_token`](/docs/agent/options.html#acl_token) | `OPTIONAL` | `OPTIONAL` | Default token to use for client requests where no token is supplied; this is often configured with read-only access to services to enable DNS service discovery on agents | +In Consul 0.9.1 and later, the agent ACL tokens can be introduced or updated via the +[/v1/agent/token API](/api/agent.html#update-acl-tokens). + #### ACL Agent Master Token Since the [`acl_agent_master_token`](/docs/agent/options.html#acl_agent_master_token) is designed to be used when the Consul servers are not available, its policy is managed locally on the agent and does not need to have a token defined on the Consul servers via the ACL API. Once set, it implicitly has the following policy associated with it (the `node` policy was added in Consul 0.9.0): @@ -146,6 +149,9 @@ node "" { } ``` +In Consul 0.9.1 and later, the agent ACL tokens can be introduced or updated via the +[/v1/agent/token API](/api/agent.html#update-acl-tokens). + #### ACL Agent Token The [`acl_agent_token`](/docs/agent/options.html#acl_agent_token) is a special token that is used for an agent's internal operations. It isn't used directly for any user-initiated operations like the [`acl_token`](/docs/agent/options.html#acl_token), though if the `acl_agent_token` isn't configured the `acl_token` will be used. The ACL agent token is used for the following operations by the agent: @@ -170,6 +176,9 @@ key "_rexec" { The `service` policy needs `read` access for any services that can be registered on the agent. If [remote exec is disabled](/docs/agent/options.html#disable_remote_exec), the default, then the `key` policy can be omitted. +In Consul 0.9.1 and later, the agent ACL tokens can be introduced or updated via the +[/v1/agent/token API](/api/agent.html#update-acl-tokens). + ## Bootstrapping ACLs Bootstrapping ACLs on a new cluster requires a few steps, outlined in the examples in this @@ -255,6 +264,19 @@ configuration and restart the servers once more to apply it: } ``` +In Consul 0.9.1 and later you can also introduce the agent token using an API, +so it doesn't need to be set in the configuration file: + +``` +$ curl \ + --request PUT \ + --header "X-Consul-Token: b1gs33cr3t" \ + --data \ +'{ + "Token": "fe3b8d40-0ee0-8783-6cc2-ab1aa9bb16c1" +}' http://127.0.0.1:8500/v1/agent/token/acl_agent_token +``` + With that ACL agent token set, the servers will be able to sync themselves with the catalog: @@ -277,6 +299,19 @@ with a configuration file that enables ACLs: } ``` +Similar to the previous example, in Consul 0.9.1 and later you can also introduce the +agent token using an API, so it doesn't need to be set in the configuration file: + +``` +$ curl \ + --request PUT \ + --header "X-Consul-Token: b1gs33cr3t" \ + --data \ +'{ + "Token": "fe3b8d40-0ee0-8783-6cc2-ab1aa9bb16c1" +}' http://127.0.0.1:8500/v1/agent/token/acl_agent_token +``` + We used the same ACL agent token that we created for the servers, which will work since it was not specific to any node or set of service prefixes. In a more locked-down environment it is recommended that each client get an ACL agent token with `node` write @@ -420,6 +455,9 @@ configuration item. When a request is made to a particular Consul agent and no t supplied, the [`acl_token`](/docs/agent/options.html#acl_token) will be used for the token, instead of being left empty which would normally invoke the anonymous token. +In Consul 0.9.1 and later, the agent ACL tokens can be introduced or updated via the +[/v1/agent/token API](/api/agent.html#update-acl-tokens). + This behaves very similarly to the anonymous token, but can be configured differently on each agent, if desired. For example, this allows more fine grained control of what DNS requests a given agent can service, or can give the agent read access to some key-value store prefixes by