From 9b537386d5151024eb37b047d2bef645d441fce2 Mon Sep 17 00:00:00 2001 From: James Phillips Date: Tue, 25 Jul 2017 21:24:38 -0700 Subject: [PATCH] Adds a new API for changing agent tokens on the fly. --- agent/agent_endpoint.go | 48 +++++++++++++ agent/agent_endpoint_test.go | 96 ++++++++++++++++++++++++++ agent/http.go | 2 + api/agent.go | 40 +++++++++++ api/agent_test.go | 20 ++++++ website/source/api/agent.html.md | 48 +++++++++++++ website/source/docs/guides/acl.html.md | 38 ++++++++++ 7 files changed, 292 insertions(+) diff --git a/agent/agent_endpoint.go b/agent/agent_endpoint.go index f4b4f7b82b8d..ff945f5b2218 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 84b9d3a7d0e9..1d5b0537958e 100644 --- a/agent/agent_endpoint_test.go +++ b/agent/agent_endpoint_test.go @@ -1654,3 +1654,99 @@ 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() + a := NewTestAgent(t.Name(), TestACLConfig()) + defer a.Shutdown() + + t.Run("bad method", func(t *testing.T) { + resp := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/v1/agent/token/acl_token", nil) + if _, err := a.srv.AgentToken(resp, req); err != nil { + t.Fatalf("err: %v", err) + } + if got, want := resp.Code, http.StatusMethodNotAllowed; got != want { + t.Fatalf("got %d want %d", got, want) + } + }) + + t.Run("bad token name", func(t *testing.T) { + args := &api.AgentToken{Token: "token"} + resp := httptest.NewRecorder() + req, _ := http.NewRequest("PUT", "/v1/agent/token/acl_token_nope?token=root", jsonReader(args)) + if _, err := a.srv.AgentToken(resp, req); err != nil { + t.Fatalf("err: %v", err) + } + if got, want := resp.Code, http.StatusNotFound; got != want { + t.Fatalf("got %d want %d", got, want) + } + }) + + t.Run("user token", func(t *testing.T) { + args := &api.AgentToken{Token: "USER_TOKEN"} + resp := httptest.NewRecorder() + req, _ := http.NewRequest("PUT", "/v1/agent/token/acl_token?token=root", jsonReader(args)) + if _, err := a.srv.AgentToken(resp, req); err != nil { + t.Fatalf("err: %v", err) + } + if got, want := resp.Code, http.StatusOK; got != want { + t.Fatalf("got %d want %d", got, want) + } + if got, want := a.tokens.GetTokenForUser(), "USER_TOKEN"; got != want { + t.Fatalf("got %q want %q", got, want) + } + }) + + t.Run("agent token", func(t *testing.T) { + args := &api.AgentToken{Token: "AGENT_TOKEN"} + resp := httptest.NewRecorder() + req, _ := http.NewRequest("PUT", "/v1/agent/token/acl_agent_token?token=root", jsonReader(args)) + if _, err := a.srv.AgentToken(resp, req); err != nil { + t.Fatalf("err: %v", err) + } + if got, want := resp.Code, http.StatusOK; got != want { + t.Fatalf("got %d want %d", got, want) + } + if got, want := a.tokens.GetTokenForAgent(), "AGENT_TOKEN"; got != want { + t.Fatalf("got %q want %q", got, want) + } + }) + + t.Run("master token", func(t *testing.T) { + args := &api.AgentToken{Token: "MASTER_TOKEN"} + resp := httptest.NewRecorder() + req, _ := http.NewRequest("PUT", "/v1/agent/token/acl_agent_master_token?token=root", jsonReader(args)) + if _, err := a.srv.AgentToken(resp, req); err != nil { + t.Fatalf("err: %v", err) + } + if got, want := resp.Code, http.StatusOK; got != want { + t.Fatalf("got %d want %d", got, want) + } + if got, want := a.tokens.IsAgentMasterToken("MASTER_TOKEN"), true; got != want { + t.Fatalf("got %v want %v", got, want) + } + }) +} + +func TestAgent_Token_ACLDeny(t *testing.T) { + t.Parallel() + a := NewTestAgent(t.Name(), TestACLConfig()) + defer a.Shutdown() + + args := &api.AgentToken{Token: "token"} + + t.Run("no token", func(t *testing.T) { + req, _ := http.NewRequest("PUT", "/v1/agent/token/acl_token", jsonReader(args)) + if _, err := a.srv.AgentToken(nil, req); !isPermissionDenied(err) { + t.Fatalf("err: %v", err) + } + }) + + t.Run("management token", func(t *testing.T) { + req, _ := http.NewRequest("PUT", "/v1/agent/token/acl_token?token=root", jsonReader(args)) + if _, err := a.srv.AgentToken(nil, req); err != nil { + t.Fatalf("err: %v", err) + } + }) +} diff --git a/agent/http.go b/agent/http.go index f5ee7c7bfe62..fef64cdd806f 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)) diff --git a/api/agent.go b/api/agent.go index 605592db9757..86c9414aeb9b 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 d49630d66071..2f6d02c81604 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 3928b88a92d2..2186a23e484b 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 d17e668b7ed3..afb6b7ab792d 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