Adds support for agent-side ACL token management via API instead of config files. (#3324)

* Adds token store and removes all runtime use of config for ACL tokens.
* Adds a new API for changing agent tokens on the fly.
This commit is contained in:
James Phillips 2017-07-26 11:03:43 -07:00 committed by GitHub
parent f2bc2b8d07
commit 496b0bcf07
21 changed files with 484 additions and 127 deletions

View File

@ -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
}

View File

@ -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 {

View File

@ -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
}

View File

@ -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)
}
})
}

View File

@ -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.

View File

@ -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 {

View File

@ -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 {

View File

@ -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 ""
}

View File

@ -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)
}
}

View File

@ -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,
},

View File

@ -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=<node> query parameter, used for

View File

@ -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)

View File

@ -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)

View File

@ -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")

View File

@ -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

78
agent/token/store.go Normal file
View File

@ -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)
}

58
agent/token/store_test.go Normal file
View File

@ -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")
}

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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
```

View File

@ -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