mirror of https://github.com/status-im/consul.git
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:
parent
f2bc2b8d07
commit
496b0bcf07
40
agent/acl.go
40
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
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 ""
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
},
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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")
|
||||
}
|
40
api/agent.go
40
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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
```
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue