Skip to content

Commit

Permalink
Adds a new API for changing agent tokens on the fly.
Browse files Browse the repository at this point in the history
  • Loading branch information
slackpad committed Jul 26, 2017
1 parent 9ff0447 commit 9b53738
Show file tree
Hide file tree
Showing 7 changed files with 292 additions and 0 deletions.
48 changes: 48 additions & 0 deletions agent/agent_endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
96 changes: 96 additions & 0 deletions agent/agent_endpoint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
})
}
2 changes: 2 additions & 0 deletions agent/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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))
Expand Down
40 changes: 40 additions & 0 deletions api/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
20 changes: 20 additions & 0 deletions api/agent_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
48 changes: 48 additions & 0 deletions website/source/api/agent.html.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
38 changes: 38 additions & 0 deletions website/source/docs/guides/acl.html.md
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -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:

Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 9b53738

Please sign in to comment.