diff --git a/command/agent/acl.go b/command/agent/acl.go new file mode 100644 index 0000000000..06d594667e --- /dev/null +++ b/command/agent/acl.go @@ -0,0 +1,251 @@ +package agent + +import ( + "errors" + "fmt" + "strings" + "sync" + "time" + + "github.com/armon/go-metrics" + "github.com/hashicorp/consul/acl" + "github.com/hashicorp/consul/consul/structs" + "github.com/hashicorp/golang-lru" +) + +// There's enough behavior difference with client-side ACLs that we've +// intentionally kept this code separate from the server-side ACL code in +// consul/acl.go. We may refactor some of the caching logic in the future, +// but for now we are developing this separately to see how things shake out. + +// These must be kept in sync with the constants in consul/acl.go. +const ( + // aclNotFound indicates there is no matching ACL. + aclNotFound = "ACL not found" + + // rootDenied is returned when attempting to resolve a root ACL. + rootDenied = "Cannot resolve root ACL" + + // permissionDenied is returned when an ACL based rejection happens. + permissionDenied = "Permission denied" + + // aclDisabled is returned when ACL changes are not permitted since they + // are disabled. + aclDisabled = "ACL support disabled" + + // anonymousToken is the token ID we re-write to if there is no token ID + // provided. + anonymousToken = "anonymous" + + // Maximum number of cached ACL entries. + aclCacheSize = 10 * 1024 +) + +var ( + permissionDeniedErr = errors.New(permissionDenied) +) + +// aclCacheEntry is used to cache ACL tokens. +type aclCacheEntry struct { + // ACL is the cached ACL. + ACL acl.ACL + + // Expires is set based on the TTL for the ACL. + Expires time.Time + + // ETag is used as an optimization when fetching ACLs from servers to + // avoid transmitting data back when the agent has a good copy, which is + // usually the case when refreshing a TTL. + ETag string +} + +// aclManager is used by the agent to keep track of state related to ACLs, +// including caching tokens from the servers. This has some internal state that +// we don't want to dump into the agent itself. +type aclManager struct { + // acls is a cache mapping ACL tokens to compiled policies. + 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 + // which means to try and use the cached policy if there is one (or + // deny if there isn't a policy in the cache). + down acl.ACL + + // disabled is used to keep track of feedback from the servers that ACLs + // are disabled. If the manager discovers that ACLs are disabled, this + // will be set to the next time we should check to see if they have been + // enabled. This helps cut useless traffic, but allows us to turn on ACL + // support at the servers without having to restart the whole cluster. + disabled time.Time + disabledLock sync.RWMutex +} + +// newACLManager returns an ACL manager based on the given config. +func newACLManager(config *Config) (*aclManager, error) { + // Set up the cache from ID to ACL (we don't cache policies like the + // servers; only one level). + acls, err := lru.New2Q(aclCacheSize) + if err != nil { + 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, + }, + }, + } + acl, err := acl.New(acl.DenyAll(), policy) + if err != nil { + return nil, err + } + master = acl + } + + var down acl.ACL + switch config.ACLDownPolicy { + case "allow": + down = acl.AllowAll() + case "deny": + down = acl.DenyAll() + case "extend-cache": + // Leave the down policy as nil to signal this. + default: + return nil, fmt.Errorf("invalid ACL down policy %q", config.ACLDownPolicy) + } + + // Give back a manager. + return &aclManager{ + acls: acls, + master: master, + down: down, + }, nil +} + +// isDisabled returns true if the manager has discovered that ACLs are disabled +// on the servers. +func (m *aclManager) isDisabled() bool { + m.disabledLock.RLock() + defer m.disabledLock.RUnlock() + return time.Now().Before(m.disabled) +} + +// lookupACL attempts to locate the compiled policy associated with the given +// token. The agent may be used to perform RPC calls to the servers to fetch +// policies that aren't in the cache. +func (m *aclManager) lookupACL(agent *Agent, id string) (acl.ACL, error) { + // Handle some special cases for the ID. + if len(id) == 0 { + id = anonymousToken + } else if acl.RootACL(id) != nil { + return nil, errors.New(rootDenied) + } else if m.master != nil && id == agent.config.ACLAgentMasterToken { + return m.master, nil + } + + // Try the cache first. + var cached *aclCacheEntry + if raw, ok := m.acls.Get(id); ok { + cached = raw.(*aclCacheEntry) + } + if cached != nil && time.Now().Before(cached.Expires) { + metrics.IncrCounter([]string{"consul", "acl", "cache_hit"}, 1) + return cached.ACL, nil + } else { + metrics.IncrCounter([]string{"consul", "acl", "cache_miss"}, 1) + } + + // At this point we might have a stale cached ACL, or none at all, so + // try to contact the servers. + args := structs.ACLPolicyRequest{ + Datacenter: agent.config.Datacenter, + ACL: id, + } + if cached != nil { + args.ETag = cached.ETag + } + var reply structs.ACLPolicy + err := agent.RPC(agent.getEndpoint("ACL")+".GetPolicy", &args, &reply) + if err != nil { + if strings.Contains(err.Error(), aclDisabled) { + agent.logger.Printf("[DEBUG] agent: ACLs disabled on servers, will check again after %s", agent.config.ACLDisabledTTL) + m.disabledLock.Lock() + m.disabled = time.Now().Add(agent.config.ACLDisabledTTL) + m.disabledLock.Unlock() + return nil, nil + } else if strings.Contains(err.Error(), aclNotFound) { + return nil, errors.New(aclNotFound) + } else { + agent.logger.Printf("[DEBUG] agent: Failed to get policy for ACL from servers: %v", err) + if m.down != nil { + return m.down, nil + } else if cached != nil { + return cached.ACL, nil + } else { + return acl.DenyAll(), nil + } + } + } + + // Use the old cached compiled ACL if we can, otherwise compile it and + // resolve any parents. + var compiled acl.ACL + if cached != nil && cached.ETag == reply.ETag { + compiled = cached.ACL + } else { + parent := acl.RootACL(reply.Parent) + if parent == nil { + parent, err = m.lookupACL(agent, reply.Parent) + if err != nil { + return nil, err + } + } + + acl, err := acl.New(parent, reply.Policy) + if err != nil { + return nil, err + } + compiled = acl + } + + // Update the cache. + cached = &aclCacheEntry{ + ACL: compiled, + ETag: reply.ETag, + } + if reply.TTL > 0 { + cached.Expires = time.Now().Add(reply.TTL) + } + m.acls.Add(id, cached) + return compiled, nil +} + +// resolveToken is the primary interface used by ACL-checkers in the agent +// endpoints, which is the one place where we do some ACL enforcement on +// clients. Some of the enforcement is normative (e.g. self and monitor) +// and some is informative (e.g. catalog and health). +func (a *Agent) resolveToken(id string) (acl.ACL, error) { + // Disable ACLs if version 8 enforcement isn't enabled. + if !(*a.config.ACLEnforceVersion8) { + return nil, nil + } + + // Bail if the ACL manager is disabled. This happens if it gets feedback + // from the servers that ACLs are disabled. + if a.acls.isDisabled() { + return nil, nil + } + + // This will look in the cache and fetch from the servers if necessary. + return a.acls.lookupACL(a, id) +} diff --git a/command/agent/acl_endpoint.go b/command/agent/acl_endpoint.go index b60502ce99..e97189e2c7 100644 --- a/command/agent/acl_endpoint.go +++ b/command/agent/acl_endpoint.go @@ -13,8 +13,8 @@ type aclCreateResponse struct { ID string } -// aclDisabled handles if ACL datacenter is not configured -func aclDisabled(resp http.ResponseWriter, req *http.Request) (interface{}, error) { +// ACLDisabled handles if ACL datacenter is not configured +func ACLDisabled(resp http.ResponseWriter, req *http.Request) (interface{}, error) { resp.WriteHeader(401) resp.Write([]byte("ACL support disabled")) return nil, nil diff --git a/command/agent/acl_test.go b/command/agent/acl_test.go new file mode 100644 index 0000000000..0f965edca6 --- /dev/null +++ b/command/agent/acl_test.go @@ -0,0 +1,474 @@ +package agent + +import ( + "errors" + "fmt" + "io/ioutil" + "os" + "strings" + "testing" + "time" + + rawacl "github.com/hashicorp/consul/acl" + "github.com/hashicorp/consul/consul/structs" + "github.com/hashicorp/consul/testutil" +) + +func TestACL_Bad_Config(t *testing.T) { + config := nextConfig() + config.ACLDownPolicy = "nope" + + var err error + config.DataDir, err = ioutil.TempDir("", "agent") + if err != nil { + t.Fatalf("err: %v", err) + } + defer os.RemoveAll(config.DataDir) + + _, err = Create(config, nil, nil, nil) + if err == nil || !strings.Contains(err.Error(), "invalid ACL down policy") { + t.Fatalf("err: %v", err) + } +} + +type MockServer struct { + getPolicyFn func(*structs.ACLPolicyRequest, *structs.ACLPolicy) error +} + +func (m *MockServer) GetPolicy(args *structs.ACLPolicyRequest, reply *structs.ACLPolicy) error { + if m.getPolicyFn != nil { + return m.getPolicyFn(args, reply) + } else { + return fmt.Errorf("should not have called GetPolicy") + } +} + +func TestACL_Version8(t *testing.T) { + config := nextConfig() + config.ACLEnforceVersion8 = Bool(false) + + dir, agent := makeAgent(t, config) + defer os.RemoveAll(dir) + defer agent.Shutdown() + + testutil.WaitForLeader(t, agent.RPC, "dc1") + + m := MockServer{} + if err := agent.InjectEndpoint("ACL", &m); err != nil { + t.Fatalf("err: %v", err) + } + + // With version 8 enforcement off, this should not get called. + m.getPolicyFn = func(*structs.ACLPolicyRequest, *structs.ACLPolicy) error { + t.Fatalf("should not have called to server") + return nil + } + if token, err := agent.resolveToken("nope"); token != nil || err != nil { + t.Fatalf("bad: %v err: %v", token, err) + } +} + +func TestACL_Disabled(t *testing.T) { + config := nextConfig() + config.ACLDisabledTTL = 10 * time.Millisecond + config.ACLEnforceVersion8 = Bool(true) + + dir, agent := makeAgent(t, config) + defer os.RemoveAll(dir) + defer agent.Shutdown() + + testutil.WaitForLeader(t, agent.RPC, "dc1") + + m := MockServer{} + if err := agent.InjectEndpoint("ACL", &m); err != nil { + t.Fatalf("err: %v", err) + } + + // Fetch a token without ACLs enabled and make sure the manager sees it. + m.getPolicyFn = func(*structs.ACLPolicyRequest, *structs.ACLPolicy) error { + return errors.New(aclDisabled) + } + if agent.acls.isDisabled() { + t.Fatalf("should not be disabled yet") + } + if token, err := agent.resolveToken("nope"); token != nil || err != nil { + t.Fatalf("bad: %v err: %v", token, err) + } + if !agent.acls.isDisabled() { + t.Fatalf("should be disabled") + } + + // Now turn on ACLs and check right away, it should still think ACLs are + // disabled since we don't check again right away. + m.getPolicyFn = func(*structs.ACLPolicyRequest, *structs.ACLPolicy) error { + return errors.New(aclNotFound) + } + if token, err := agent.resolveToken("nope"); token != nil || err != nil { + t.Fatalf("bad: %v err: %v", token, err) + } + if !agent.acls.isDisabled() { + t.Fatalf("should be disabled") + } + + // Wait the waiting period and make sure it checks again. Do a few tries + // to make sure we don't think it's disabled. + time.Sleep(2 * config.ACLDisabledTTL) + for i := 0; i < 10; i++ { + _, err := agent.resolveToken("nope") + if err == nil || !strings.Contains(err.Error(), aclNotFound) { + t.Fatalf("err: %v", err) + } + if agent.acls.isDisabled() { + t.Fatalf("should not be disabled") + } + } +} + +func TestACL_Special_IDs(t *testing.T) { + config := nextConfig() + config.ACLEnforceVersion8 = Bool(true) + config.ACLAgentMasterToken = "towel" + + dir, agent := makeAgent(t, config) + defer os.RemoveAll(dir) + defer agent.Shutdown() + + testutil.WaitForLeader(t, agent.RPC, "dc1") + + m := MockServer{} + if err := agent.InjectEndpoint("ACL", &m); err != nil { + t.Fatalf("err: %v", err) + } + + // An empty ID should get mapped to the anonymous token. + m.getPolicyFn = func(req *structs.ACLPolicyRequest, reply *structs.ACLPolicy) error { + if req.ACL != "anonymous" { + t.Fatalf("bad: %#v", *req) + } + return errors.New(aclNotFound) + } + _, err := agent.resolveToken("") + if err == nil || !strings.Contains(err.Error(), aclNotFound) { + t.Fatalf("err: %v", err) + } + + // A root ACL request should get rejected and not call the server. + m.getPolicyFn = func(*structs.ACLPolicyRequest, *structs.ACLPolicy) error { + t.Fatalf("should not have called to server") + return nil + } + _, err = agent.resolveToken("deny") + if err == nil || !strings.Contains(err.Error(), rootDenied) { + t.Fatalf("err: %v", err) + } + + // The ACL master token should also not call the server, but should give + // us a working agent token. + acl, err := agent.resolveToken("towel") + if err != nil { + t.Fatalf("err: %v", err) + } + if acl == nil { + t.Fatalf("should not be nil") + } + if !acl.AgentRead(config.NodeName) { + t.Fatalf("should be able to read agent") + } + if !acl.AgentWrite(config.NodeName) { + t.Fatalf("should be able to write agent") + } +} + +func TestACL_Down_Deny(t *testing.T) { + config := nextConfig() + config.ACLDownPolicy = "deny" + config.ACLEnforceVersion8 = Bool(true) + + dir, agent := makeAgent(t, config) + defer os.RemoveAll(dir) + defer agent.Shutdown() + + testutil.WaitForLeader(t, agent.RPC, "dc1") + + m := MockServer{} + if err := agent.InjectEndpoint("ACL", &m); err != nil { + t.Fatalf("err: %v", err) + } + + // Resolve with ACLs down. + m.getPolicyFn = func(*structs.ACLPolicyRequest, *structs.ACLPolicy) error { + return fmt.Errorf("ACLs are broken") + } + acl, err := agent.resolveToken("nope") + if err != nil { + t.Fatalf("err: %v", err) + } + if acl == nil { + t.Fatalf("should not be nil") + } + if acl.AgentRead(config.NodeName) { + t.Fatalf("should deny") + } +} + +func TestACL_Down_Allow(t *testing.T) { + config := nextConfig() + config.ACLDownPolicy = "allow" + config.ACLEnforceVersion8 = Bool(true) + + dir, agent := makeAgent(t, config) + defer os.RemoveAll(dir) + defer agent.Shutdown() + + testutil.WaitForLeader(t, agent.RPC, "dc1") + + m := MockServer{} + if err := agent.InjectEndpoint("ACL", &m); err != nil { + t.Fatalf("err: %v", err) + } + + // Resolve with ACLs down. + m.getPolicyFn = func(*structs.ACLPolicyRequest, *structs.ACLPolicy) error { + return fmt.Errorf("ACLs are broken") + } + acl, err := agent.resolveToken("nope") + if err != nil { + t.Fatalf("err: %v", err) + } + if acl == nil { + t.Fatalf("should not be nil") + } + if !acl.AgentRead(config.NodeName) { + t.Fatalf("should allow") + } +} + +func TestACL_Down_Extend(t *testing.T) { + config := nextConfig() + config.ACLDownPolicy = "extend-cache" + config.ACLEnforceVersion8 = Bool(true) + + dir, agent := makeAgent(t, config) + defer os.RemoveAll(dir) + defer agent.Shutdown() + + testutil.WaitForLeader(t, agent.RPC, "dc1") + + m := MockServer{} + if err := agent.InjectEndpoint("ACL", &m); err != nil { + t.Fatalf("err: %v", err) + } + + // Populate the cache for one of the tokens. + m.getPolicyFn = func(req *structs.ACLPolicyRequest, reply *structs.ACLPolicy) error { + *reply = structs.ACLPolicy{ + Parent: "allow", + Policy: &rawacl.Policy{ + Agents: []*rawacl.AgentPolicy{ + &rawacl.AgentPolicy{ + Node: config.NodeName, + Policy: "read", + }, + }, + }, + } + return nil + } + acl, err := agent.resolveToken("yep") + if err != nil { + t.Fatalf("err: %v", err) + } + if acl == nil { + t.Fatalf("should not be nil") + } + if !acl.AgentRead(config.NodeName) { + t.Fatalf("should allow") + } + if acl.AgentWrite(config.NodeName) { + t.Fatalf("should deny") + } + + // Now take down ACLs and make sure a new token fails to resolve. + m.getPolicyFn = func(*structs.ACLPolicyRequest, *structs.ACLPolicy) error { + return fmt.Errorf("ACLs are broken") + } + acl, err = agent.resolveToken("nope") + if err != nil { + t.Fatalf("err: %v", err) + } + if acl == nil { + t.Fatalf("should not be nil") + } + if acl.AgentRead(config.NodeName) { + t.Fatalf("should deny") + } + if acl.AgentWrite(config.NodeName) { + t.Fatalf("should deny") + } + + // Read the token from the cache while ACLs are broken, which should + // extend. + acl, err = agent.resolveToken("yep") + if err != nil { + t.Fatalf("err: %v", err) + } + if acl == nil { + t.Fatalf("should not be nil") + } + if !acl.AgentRead(config.NodeName) { + t.Fatalf("should allow") + } + if acl.AgentWrite(config.NodeName) { + t.Fatalf("should deny") + } +} + +func TestACL_Cache(t *testing.T) { + config := nextConfig() + config.ACLEnforceVersion8 = Bool(true) + + dir, agent := makeAgent(t, config) + defer os.RemoveAll(dir) + defer agent.Shutdown() + + testutil.WaitForLeader(t, agent.RPC, "dc1") + + m := MockServer{} + if err := agent.InjectEndpoint("ACL", &m); err != nil { + t.Fatalf("err: %v", err) + } + + // Populate the cache for one of the tokens. + m.getPolicyFn = func(req *structs.ACLPolicyRequest, reply *structs.ACLPolicy) error { + *reply = structs.ACLPolicy{ + ETag: "hash1", + Parent: "deny", + Policy: &rawacl.Policy{ + Agents: []*rawacl.AgentPolicy{ + &rawacl.AgentPolicy{ + Node: config.NodeName, + Policy: "read", + }, + }, + }, + TTL: 10 * time.Millisecond, + } + return nil + } + acl, err := agent.resolveToken("yep") + if err != nil { + t.Fatalf("err: %v", err) + } + if acl == nil { + t.Fatalf("should not be nil") + } + if !acl.AgentRead(config.NodeName) { + t.Fatalf("should allow") + } + if acl.AgentWrite(config.NodeName) { + t.Fatalf("should deny") + } + if acl.NodeRead("nope") { + t.Fatalf("should deny") + } + + // Fetch right away and make sure it uses the cache. + m.getPolicyFn = func(*structs.ACLPolicyRequest, *structs.ACLPolicy) error { + t.Fatalf("should not have called to server") + return nil + } + acl, err = agent.resolveToken("yep") + if err != nil { + t.Fatalf("err: %v", err) + } + if acl == nil { + t.Fatalf("should not be nil") + } + if !acl.AgentRead(config.NodeName) { + t.Fatalf("should allow") + } + if acl.AgentWrite(config.NodeName) { + t.Fatalf("should deny") + } + if acl.NodeRead("nope") { + t.Fatalf("should deny") + } + + // Wait for the TTL to expire and try again. This time the token will be + // gone. + time.Sleep(20 * time.Millisecond) + m.getPolicyFn = func(req *structs.ACLPolicyRequest, reply *structs.ACLPolicy) error { + return errors.New(aclNotFound) + } + _, err = agent.resolveToken("yep") + if err == nil || !strings.Contains(err.Error(), aclNotFound) { + t.Fatalf("err: %v", err) + } + + // Page it back in with a new tag and different policy + m.getPolicyFn = func(req *structs.ACLPolicyRequest, reply *structs.ACLPolicy) error { + *reply = structs.ACLPolicy{ + ETag: "hash2", + Parent: "deny", + Policy: &rawacl.Policy{ + Agents: []*rawacl.AgentPolicy{ + &rawacl.AgentPolicy{ + Node: config.NodeName, + Policy: "write", + }, + }, + }, + TTL: 10 * time.Millisecond, + } + return nil + } + acl, err = agent.resolveToken("yep") + if err != nil { + t.Fatalf("err: %v", err) + } + if acl == nil { + t.Fatalf("should not be nil") + } + if !acl.AgentRead(config.NodeName) { + t.Fatalf("should allow") + } + if !acl.AgentWrite(config.NodeName) { + t.Fatalf("should allow") + } + if acl.NodeRead("nope") { + t.Fatalf("should deny") + } + + // Wait for the TTL to expire and try again. This will match the tag + // and not send the policy back, but we should have the old token + // behavior. + time.Sleep(20 * time.Millisecond) + var didRefresh bool + m.getPolicyFn = func(req *structs.ACLPolicyRequest, reply *structs.ACLPolicy) error { + *reply = structs.ACLPolicy{ + ETag: "hash2", + TTL: 10 * time.Millisecond, + } + didRefresh = true + return nil + } + acl, err = agent.resolveToken("yep") + if err != nil { + t.Fatalf("err: %v", err) + } + if acl == nil { + t.Fatalf("should not be nil") + } + if !acl.AgentRead(config.NodeName) { + t.Fatalf("should allow") + } + if !acl.AgentWrite(config.NodeName) { + t.Fatalf("should allow") + } + if acl.NodeRead("nope") { + t.Fatalf("should deny") + } + if !didRefresh { + t.Fatalf("should refresh") + } +} diff --git a/command/agent/agent.go b/command/agent/agent.go index 6b9bcaa50d..789768a5b7 100644 --- a/command/agent/agent.go +++ b/command/agent/agent.go @@ -74,6 +74,9 @@ type Agent struct { server *consul.Server client *consul.Client + // acls is an object that helps manage local ACL enforcement. + acls *aclManager + // state stores a local representation of the node, // services and checks. Used for anti-entropy. state localState @@ -211,11 +214,17 @@ func Create(config *Config, logOutput io.Writer, logWriter *logger.LogWriter, return nil, err } + // Initialize the ACL manager. + acls, err := newACLManager(config) + if err != nil { + return nil, err + } + agent.acls = acls + // Initialize the local state. agent.state.Init(config, agent.logger) // Setup either the client or the server. - var err error if config.Server { err = agent.setupServer() agent.state.SetIface(agent.server) diff --git a/command/agent/agent_test.go b/command/agent/agent_test.go index 64d6d05e31..c84a5b0088 100644 --- a/command/agent/agent_test.go +++ b/command/agent/agent_test.go @@ -55,6 +55,7 @@ func nextConfig() *Config { conf.Ports.SerfWan = basePortNumber + idx + portOffsetSerfWan conf.Ports.Server = basePortNumber + idx + portOffsetServer conf.Server = true + conf.ACLEnforceVersion8 = Bool(false) conf.ACLDatacenter = "dc1" conf.ACLMasterToken = "root" diff --git a/command/agent/config.go b/command/agent/config.go index c4c4c06e29..4d95d62446 100644 --- a/command/agent/config.go +++ b/command/agent/config.go @@ -488,6 +488,11 @@ type Config struct { // token is not provided. If not configured the 'anonymous' token is used. ACLToken string `mapstructure:"acl_token" json:"-"` + // ACLAgentMasterToken is a special token that has full read and write + // privileges for this agent, and can be used to call agent endpoints + // when no servers are available. + ACLAgentMasterToken string `mapstructure:"acl_agent_master_token" json:"-"` + // ACLAgentToken is the default token used to make requests for the agent // itself, such as for registering itself with the catalog. If not // configured, the 'acl_token' will be used. @@ -514,9 +519,15 @@ type Config struct { // white-lists. ACLDefaultPolicy string `mapstructure:"acl_default_policy"` + // ACLDisabledTTL is used by clients to determine how long they will + // wait to check again with the servers if they discover ACLs are not + // enabled. + ACLDisabledTTL time.Duration `mapstructure:"-"` + // ACLDownPolicy is used to control the ACL interaction when we cannot // reach the ACLDatacenter and the token is not in the cache. // There are two modes: + // * allow - Allow all requests // * deny - Deny all requests // * extend-cache - Ignore the cache expiration, and allow cached // ACL's to be used to service requests. This @@ -717,6 +728,7 @@ func DefaultConfig() *Config { ACLTTL: 30 * time.Second, ACLDownPolicy: "extend-cache", ACLDefaultPolicy: "allow", + ACLDisabledTTL: 120 * time.Second, ACLEnforceVersion8: Bool(false), RetryInterval: 30 * time.Second, RetryIntervalWan: 30 * time.Second, @@ -1483,6 +1495,9 @@ func MergeConfig(a, b *Config) *Config { if b.ACLToken != "" { result.ACLToken = b.ACLToken } + if b.ACLAgentMasterToken != "" { + result.ACLAgentMasterToken = b.ACLAgentMasterToken + } if b.ACLAgentToken != "" { result.ACLAgentToken = b.ACLAgentToken } diff --git a/command/agent/config_test.go b/command/agent/config_test.go index 58f97213b3..caef0ce6f6 100644 --- a/command/agent/config_test.go +++ b/command/agent/config_test.go @@ -643,7 +643,8 @@ func TestDecodeConfig(t *testing.T) { } // ACLs - input = `{"acl_token": "1234", "acl_agent_token": "5678", "acl_datacenter": "dc2", + input = `{"acl_token": "1111", "acl_agent_master_token": "2222", + "acl_agent_token": "3333", "acl_datacenter": "dc2", "acl_ttl": "60s", "acl_down_policy": "deny", "acl_default_policy": "deny", "acl_master_token": "2345", "acl_replication_token": "8675309"}` @@ -652,10 +653,13 @@ func TestDecodeConfig(t *testing.T) { t.Fatalf("err: %s", err) } - if config.ACLToken != "1234" { + if config.ACLToken != "1111" { t.Fatalf("bad: %#v", config) } - if config.ACLAgentToken != "5678" { + if config.ACLAgentMasterToken != "2222" { + t.Fatalf("bad: %#v", config) + } + if config.ACLAgentToken != "3333" { t.Fatalf("bad: %#v", config) } if config.ACLMasterToken != "2345" { @@ -1589,9 +1593,10 @@ func TestMergeConfig(t *testing.T) { ReconnectTimeoutWan: 36 * time.Hour, CheckUpdateInterval: 8 * time.Minute, CheckUpdateIntervalRaw: "8m", - ACLToken: "1234", - ACLAgentToken: "5678", - ACLMasterToken: "2345", + ACLToken: "1111", + ACLAgentMasterToken: "2222", + ACLAgentToken: "3333", + ACLMasterToken: "4444", ACLDatacenter: "dc2", ACLTTL: 15 * time.Second, ACLTTLRaw: "15s", diff --git a/command/agent/http.go b/command/agent/http.go index 8f1a1a1db0..7070706f11 100644 --- a/command/agent/http.go +++ b/command/agent/http.go @@ -241,13 +241,13 @@ func (s *HTTPServer) registerHandlers(enableDebug bool) { s.handleFuncMetrics("/v1/acl/list", s.wrap(s.ACLList)) s.handleFuncMetrics("/v1/acl/replication", s.wrap(s.ACLReplicationStatus)) } else { - s.handleFuncMetrics("/v1/acl/create", s.wrap(aclDisabled)) - s.handleFuncMetrics("/v1/acl/update", s.wrap(aclDisabled)) - s.handleFuncMetrics("/v1/acl/destroy/", s.wrap(aclDisabled)) - s.handleFuncMetrics("/v1/acl/info/", s.wrap(aclDisabled)) - s.handleFuncMetrics("/v1/acl/clone/", s.wrap(aclDisabled)) - s.handleFuncMetrics("/v1/acl/list", s.wrap(aclDisabled)) - s.handleFuncMetrics("/v1/acl/replication", s.wrap(aclDisabled)) + s.handleFuncMetrics("/v1/acl/create", s.wrap(ACLDisabled)) + s.handleFuncMetrics("/v1/acl/update", s.wrap(ACLDisabled)) + s.handleFuncMetrics("/v1/acl/destroy/", s.wrap(ACLDisabled)) + s.handleFuncMetrics("/v1/acl/info/", s.wrap(ACLDisabled)) + s.handleFuncMetrics("/v1/acl/clone/", s.wrap(ACLDisabled)) + s.handleFuncMetrics("/v1/acl/list", s.wrap(ACLDisabled)) + s.handleFuncMetrics("/v1/acl/replication", s.wrap(ACLDisabled)) } s.handleFuncMetrics("/v1/agent/self", s.wrap(s.AgentSelf)) s.handleFuncMetrics("/v1/agent/maintenance", s.wrap(s.AgentNodeMaintenance)) diff --git a/command/agent/local.go b/command/agent/local.go index c324a15361..fdd3d37889 100644 --- a/command/agent/local.go +++ b/command/agent/local.go @@ -18,9 +18,6 @@ import ( const ( syncStaggerIntv = 3 * time.Second syncRetryIntv = 15 * time.Second - - // permissionDenied is returned when an ACL based rejection happens - permissionDenied = "Permission denied" ) // syncStatus is used to represent the difference between diff --git a/consul/acl.go b/consul/acl.go index 36135d9ac6..50b7b675c1 100644 --- a/consul/acl.go +++ b/consul/acl.go @@ -14,29 +14,30 @@ import ( "github.com/hashicorp/golang-lru" ) +// These must be kept in sync with the constants in command/agent/acl.go. const ( - // aclNotFound indicates there is no matching ACL + // aclNotFound indicates there is no matching ACL. aclNotFound = "ACL not found" - // rootDenied is returned when attempting to resolve a root ACL + // rootDenied is returned when attempting to resolve a root ACL. rootDenied = "Cannot resolve root ACL" - // permissionDenied is returned when an ACL based rejection happens + // permissionDenied is returned when an ACL based rejection happens. permissionDenied = "Permission denied" - // aclDisabled is returned when ACL changes are not permitted - // since they are disabled. + // aclDisabled is returned when ACL changes are not permitted since they + // are disabled. aclDisabled = "ACL support disabled" - // anonymousToken is the token ID we re-write to if there - // is no token ID provided + // anonymousToken is the token ID we re-write to if there is no token ID + // provided. anonymousToken = "anonymous" // redactedToken is shown in structures with embedded tokens when they - // are not allowed to be displayed + // are not allowed to be displayed. redactedToken = "" - // Maximum number of cached ACL entries + // Maximum number of cached ACL entries. aclCacheSize = 10 * 1024 ) @@ -264,6 +265,8 @@ func (c *aclCache) useACLPolicy(id, authDC string, cached *aclCacheEntry, p *str // Check if we can used the cached policy if cached != nil && cached.ETag == p.ETag { if p.TTL > 0 { + // TODO (slackpad) - This seems like it's an unsafe + // write. cached.Expires = time.Now().Add(p.TTL) } return cached.ACL, nil diff --git a/website/source/docs/agent/options.html.markdown b/website/source/docs/agent/options.html.markdown index 59118d9401..357f4a9589 100644 --- a/website/source/docs/agent/options.html.markdown +++ b/website/source/docs/agent/options.html.markdown @@ -377,7 +377,14 @@ Consul will not enable TLS for the HTTP API unless the `https` port has been ass all operations, and "extend-cache" allows any cached ACLs to be used, ignoring their TTL values. If a non-cached ACL is used, "extend-cache" acts like "deny". -* `acl_agent_token` - Used for clients +* `acl_agent_master_token` - + Used to access agent endpoints that require agent read + or write privileges even if Consul servers aren't present to validate any tokens. This should only + be used by operators during outages, regular ACL tokens should normally be used by applications. + This was added in Consul 0.7.2 and is only used when `acl_enforce_version_8` + is set to true. + +* `acl_agent_token` - Used for clients and servers to perform internal operations to the service catalog. If this isn't specified, then the `acl_token` will be used. This was added in Consul 0.7.2.