Adds secure introduction for the ACL replication token. (#3357)

Adds secure introduction for the ACL replication token, as well as a separate enable config for ACL replication.
This commit is contained in:
James Phillips 2017-08-03 15:39:31 -07:00 committed by GitHub
parent 4c0c912a52
commit 803ed9a245
19 changed files with 276 additions and 77 deletions

View file

@ -231,6 +231,7 @@ func New(c *Config) (*Agent, error) {
a.tokens.UpdateUserToken(a.config.ACLToken) a.tokens.UpdateUserToken(a.config.ACLToken)
a.tokens.UpdateAgentToken(a.config.ACLAgentToken) a.tokens.UpdateAgentToken(a.config.ACLAgentToken)
a.tokens.UpdateAgentMasterToken(a.config.ACLAgentMasterToken) a.tokens.UpdateAgentMasterToken(a.config.ACLAgentMasterToken)
a.tokens.UpdateACLReplicationToken(a.config.ACLReplicationToken)
return a, nil return a, nil
} }
@ -266,7 +267,7 @@ func (a *Agent) Start() error {
// Setup either the client or the server. // Setup either the client or the server.
if c.Server { if c.Server {
server, err := consul.NewServerLogger(consulCfg, a.logger) server, err := consul.NewServerLogger(consulCfg, a.logger, a.tokens)
if err != nil { if err != nil {
return fmt.Errorf("Failed to start Consul server: %v", err) return fmt.Errorf("Failed to start Consul server: %v", err)
} }
@ -675,9 +676,7 @@ func (a *Agent) consulConfig() (*consul.Config, error) {
if a.config.ACLDownPolicy != "" { if a.config.ACLDownPolicy != "" {
base.ACLDownPolicy = a.config.ACLDownPolicy base.ACLDownPolicy = a.config.ACLDownPolicy
} }
if a.config.ACLReplicationToken != "" { base.EnableACLReplication = a.config.EnableACLReplication
base.ACLReplicationToken = a.config.ACLReplicationToken
}
if a.config.ACLEnforceVersion8 != nil { if a.config.ACLEnforceVersion8 != nil {
base.ACLEnforceVersion8 = *a.config.ACLEnforceVersion8 base.ACLEnforceVersion8 = *a.config.ACLEnforceVersion8
} }

View file

@ -737,6 +737,9 @@ func (s *HTTPServer) AgentToken(resp http.ResponseWriter, req *http.Request) (in
case "acl_agent_master_token": case "acl_agent_master_token":
s.agent.tokens.UpdateAgentMasterToken(args.Token) s.agent.tokens.UpdateAgentMasterToken(args.Token)
case "acl_replication_token":
s.agent.tokens.UpdateACLReplicationToken(args.Token)
default: default:
resp.WriteHeader(http.StatusNotFound) resp.WriteHeader(http.StatusNotFound)
fmt.Fprintf(resp, "Token %q is unknown", target) fmt.Fprintf(resp, "Token %q is unknown", target)

View file

@ -1664,7 +1664,18 @@ func TestAgent_Token(t *testing.T) {
a := NewTestAgent(t.Name(), cfg) a := NewTestAgent(t.Name(), cfg)
defer a.Shutdown() defer a.Shutdown()
b := func(token string) io.Reader { type tokens struct {
user, agent, master, repl string
}
resetTokens := func(got tokens) {
a.tokens.UpdateUserToken(got.user)
a.tokens.UpdateAgentToken(got.agent)
a.tokens.UpdateAgentMasterToken(got.master)
a.tokens.UpdateACLReplicationToken(got.repl)
}
body := func(token string) io.Reader {
return jsonReader(&api.AgentToken{Token: token}) return jsonReader(&api.AgentToken{Token: token})
} }
@ -1677,22 +1688,97 @@ func TestAgent_Token(t *testing.T) {
method, url string method, url string
body io.Reader body io.Reader
code int code int
userToken string got, want tokens
agentToken string
masterToken string
}{ }{
{"bad method", "GET", "acl_token", b("X"), http.StatusMethodNotAllowed, "", "", ""}, {
{"bad token name", "PUT", "nope?token=root", b("X"), http.StatusNotFound, "", "", ""}, name: "bad method",
{"bad JSON", "PUT", "acl_token?token=root", badJSON(), http.StatusBadRequest, "", "", ""}, method: "GET",
{"set user", "PUT", "acl_token?token=root", b("U"), http.StatusOK, "U", "U", ""}, url: "acl_token",
{"set agent", "PUT", "acl_agent_token?token=root", b("A"), http.StatusOK, "U", "A", ""}, code: http.StatusMethodNotAllowed,
{"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"}, name: "bad token name",
{"clear master", "PUT", "acl_agent_master_token?token=root", b(""), http.StatusOK, "", "", ""}, method: "PUT",
url: "nope?token=root",
body: body("X"),
code: http.StatusNotFound,
},
{
name: "bad JSON",
method: "PUT",
url: "acl_token?token=root",
body: badJSON(),
code: http.StatusBadRequest,
},
{
name: "set user",
method: "PUT",
url: "acl_token?token=root",
body: body("U"),
code: http.StatusOK,
want: tokens{user: "U", agent: "U"},
},
{
name: "set agent",
method: "PUT",
url: "acl_agent_token?token=root",
body: body("A"),
code: http.StatusOK,
got: tokens{user: "U", agent: "U"},
want: tokens{user: "U", agent: "A"},
},
{
name: "set master",
method: "PUT",
url: "acl_agent_master_token?token=root",
body: body("M"),
code: http.StatusOK,
want: tokens{master: "M"},
},
{
name: "set repl",
method: "PUT",
url: "acl_replication_token?token=root",
body: body("R"),
code: http.StatusOK,
want: tokens{repl: "R"},
},
{
name: "clear user",
method: "PUT",
url: "acl_token?token=root",
body: body(""),
code: http.StatusOK,
got: tokens{user: "U"},
},
{
name: "clear agent",
method: "PUT",
url: "acl_agent_token?token=root",
body: body(""),
code: http.StatusOK,
got: tokens{agent: "A"},
},
{
name: "clear master",
method: "PUT",
url: "acl_agent_master_token?token=root",
body: body(""),
code: http.StatusOK,
got: tokens{master: "M"},
},
{
name: "clear repl",
method: "PUT",
url: "acl_replication_token?token=root",
body: body(""),
code: http.StatusOK,
got: tokens{repl: "R"},
},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
resetTokens(tt.got)
url := fmt.Sprintf("/v1/agent/token/%s", tt.url) url := fmt.Sprintf("/v1/agent/token/%s", tt.url)
resp := httptest.NewRecorder() resp := httptest.NewRecorder()
req, _ := http.NewRequest(tt.method, url, tt.body) req, _ := http.NewRequest(tt.method, url, tt.body)
@ -1702,14 +1788,17 @@ func TestAgent_Token(t *testing.T) {
if got, want := resp.Code, tt.code; got != want { if got, want := resp.Code, tt.code; got != want {
t.Fatalf("got %d want %d", got, want) t.Fatalf("got %d want %d", got, want)
} }
if got, want := a.tokens.UserToken(), tt.userToken; got != want { if got, want := a.tokens.UserToken(), tt.want.user; got != want {
t.Fatalf("got %q want %q", got, want) t.Fatalf("got %q want %q", got, want)
} }
if got, want := a.tokens.AgentToken(), tt.agentToken; got != want { if got, want := a.tokens.AgentToken(), tt.want.agent; got != want {
t.Fatalf("got %q want %q", got, want) t.Fatalf("got %q want %q", got, want)
} }
if tt.masterToken != "" && !a.tokens.IsAgentMasterToken(tt.masterToken) { if tt.want.master != "" && !a.tokens.IsAgentMasterToken(tt.want.master) {
t.Fatalf("%q should be the master token", tt.masterToken) t.Fatalf("%q should be the master token", tt.want.master)
}
if got, want := a.tokens.ACLReplicationToken(), tt.want.repl; got != want {
t.Fatalf("got %q want %q", got, want)
} }
}) })
} }
@ -1717,7 +1806,8 @@ func TestAgent_Token(t *testing.T) {
// This one returns an error that is interpreted by the HTTP wrapper, so // This one returns an error that is interpreted by the HTTP wrapper, so
// doesn't fit into our table above. // doesn't fit into our table above.
t.Run("permission denied", func(t *testing.T) { t.Run("permission denied", func(t *testing.T) {
req, _ := http.NewRequest("PUT", "/v1/agent/token/acl_token", b("X")) resetTokens(tokens{})
req, _ := http.NewRequest("PUT", "/v1/agent/token/acl_token", body("X"))
if _, err := a.srv.AgentToken(nil, req); !isPermissionDenied(err) { if _, err := a.srv.AgentToken(nil, req); !isPermissionDenied(err) {
t.Fatalf("err: %v", err) t.Fatalf("err: %v", err)
} }

View file

@ -696,6 +696,12 @@ type Config struct {
// this acts like deny. // this acts like deny.
ACLDownPolicy string `mapstructure:"acl_down_policy"` ACLDownPolicy string `mapstructure:"acl_down_policy"`
// EnableACLReplication is used to turn on ACL replication when using
// /v1/agent/token/acl_replication_token to introduce the token, instead
// of setting acl_replication_token in the config. Setting the token via
// config will also set this to true for backward compatibility.
EnableACLReplication bool `mapstructure:"enable_acl_replication"`
// ACLReplicationToken is used to fetch ACLs from the ACLDatacenter in // ACLReplicationToken is used to fetch ACLs from the ACLDatacenter in
// order to replicate them locally. Setting this to a non-empty value // order to replicate them locally. Setting this to a non-empty value
// also enables replication. Replication is only available in datacenters // also enables replication. Replication is only available in datacenters
@ -1448,6 +1454,13 @@ func DecodeConfig(r io.Reader) (*Config, error) {
} }
result.DeprecatedHTTPAPIResponseHeaders = nil result.DeprecatedHTTPAPIResponseHeaders = nil
} }
// Set the ACL replication enable if they set a token, for backwards
// compatibility.
if result.ACLReplicationToken != "" {
result.EnableACLReplication = true
}
return &result, nil return &result, nil
} }
@ -2023,6 +2036,9 @@ func MergeConfig(a, b *Config) *Config {
if b.ACLDefaultPolicy != "" { if b.ACLDefaultPolicy != "" {
result.ACLDefaultPolicy = b.ACLDefaultPolicy result.ACLDefaultPolicy = b.ACLDefaultPolicy
} }
if b.EnableACLReplication {
result.EnableACLReplication = true
}
if b.ACLReplicationToken != "" { if b.ACLReplicationToken != "" {
result.ACLReplicationToken = b.ACLReplicationToken result.ACLReplicationToken = b.ACLReplicationToken
} }

View file

@ -108,7 +108,10 @@ func TestDecodeConfig(t *testing.T) {
}, },
{ {
in: `{"acl_replication_token":"a"}`, in: `{"acl_replication_token":"a"}`,
c: &Config{ACLReplicationToken: "a"}, c: &Config{
EnableACLReplication: true,
ACLReplicationToken: "a",
},
}, },
{ {
in: `{"acl_token":"a"}`, in: `{"acl_token":"a"}`,
@ -366,6 +369,10 @@ func TestDecodeConfig(t *testing.T) {
in: `{"domain":"a"}`, in: `{"domain":"a"}`,
c: &Config{Domain: "a"}, c: &Config{Domain: "a"},
}, },
{
in: `{"enable_acl_replication":true}`,
c: &Config{EnableACLReplication: true},
},
{ {
in: `{"enable_debug":true}`, in: `{"enable_debug":true}`,
c: &Config{EnableDebug: true}, c: &Config{EnableDebug: true},

View file

@ -535,9 +535,10 @@ func TestACLEndpoint_ReplicationStatus(t *testing.T) {
t.Parallel() t.Parallel()
dir1, s1 := testServerWithConfig(t, func(c *Config) { dir1, s1 := testServerWithConfig(t, func(c *Config) {
c.ACLDatacenter = "dc2" c.ACLDatacenter = "dc2"
c.ACLReplicationToken = "secret" c.EnableACLReplication = true
c.ACLReplicationInterval = 10 * time.Millisecond c.ACLReplicationInterval = 10 * time.Millisecond
}) })
s1.tokens.UpdateACLReplicationToken("secret")
defer os.RemoveAll(dir1) defer os.RemoveAll(dir1)
defer s1.Shutdown() defer s1.Shutdown()
codec := rpcClient(t, s1) codec := rpcClient(t, s1)

View file

@ -154,7 +154,7 @@ func (s *Server) fetchRemoteACLs(lastRemoteIndex uint64) (*structs.IndexedACLs,
args := structs.DCSpecificRequest{ args := structs.DCSpecificRequest{
Datacenter: s.config.ACLDatacenter, Datacenter: s.config.ACLDatacenter,
QueryOptions: structs.QueryOptions{ QueryOptions: structs.QueryOptions{
Token: s.config.ACLReplicationToken, Token: s.tokens.ACLReplicationToken(),
MinQueryIndex: lastRemoteIndex, MinQueryIndex: lastRemoteIndex,
AllowStale: true, AllowStale: true,
}, },
@ -247,7 +247,7 @@ func (s *Server) replicateACLs(lastRemoteIndex uint64) (uint64, error) {
func (s *Server) IsACLReplicationEnabled() bool { func (s *Server) IsACLReplicationEnabled() bool {
authDC := s.config.ACLDatacenter authDC := s.config.ACLDatacenter
return len(authDC) > 0 && (authDC != s.config.Datacenter) && return len(authDC) > 0 && (authDC != s.config.Datacenter) &&
len(s.config.ACLReplicationToken) > 0 s.config.EnableACLReplication
} }
// updateACLReplicationStatus safely updates the ACL replication status. // updateACLReplicationStatus safely updates the ACL replication status.

View file

@ -228,9 +228,9 @@ func TestACLReplication_updateLocalACLs_RateLimit(t *testing.T) {
dir1, s1 := testServerWithConfig(t, func(c *Config) { dir1, s1 := testServerWithConfig(t, func(c *Config) {
c.Datacenter = "dc2" c.Datacenter = "dc2"
c.ACLDatacenter = "dc1" c.ACLDatacenter = "dc1"
c.ACLReplicationToken = "secret"
c.ACLReplicationApplyLimit = 1 c.ACLReplicationApplyLimit = 1
}) })
s1.tokens.UpdateACLReplicationToken("secret")
defer os.RemoveAll(dir1) defer os.RemoveAll(dir1)
defer s1.Shutdown() defer s1.Shutdown()
testrpc.WaitForLeader(t, s1.RPC, "dc2") testrpc.WaitForLeader(t, s1.RPC, "dc2")
@ -300,7 +300,7 @@ func TestACLReplication_IsACLReplicationEnabled(t *testing.T) {
dir3, s3 := testServerWithConfig(t, func(c *Config) { dir3, s3 := testServerWithConfig(t, func(c *Config) {
c.Datacenter = "dc2" c.Datacenter = "dc2"
c.ACLDatacenter = "dc1" c.ACLDatacenter = "dc1"
c.ACLReplicationToken = "secret" c.EnableACLReplication = true
}) })
defer os.RemoveAll(dir3) defer os.RemoveAll(dir3)
defer s3.Shutdown() defer s3.Shutdown()
@ -308,12 +308,12 @@ func TestACLReplication_IsACLReplicationEnabled(t *testing.T) {
t.Fatalf("should be enabled") t.Fatalf("should be enabled")
} }
// ACLs enabled and replication token set, but inside the ACL datacenter // ACLs enabled with replication, but inside the ACL datacenter
// so replication should be disabled. // so replication should be disabled.
dir4, s4 := testServerWithConfig(t, func(c *Config) { dir4, s4 := testServerWithConfig(t, func(c *Config) {
c.Datacenter = "dc1" c.Datacenter = "dc1"
c.ACLDatacenter = "dc1" c.ACLDatacenter = "dc1"
c.ACLReplicationToken = "secret" c.EnableACLReplication = true
}) })
defer os.RemoveAll(dir4) defer os.RemoveAll(dir4)
defer s4.Shutdown() defer s4.Shutdown()
@ -336,10 +336,11 @@ func TestACLReplication(t *testing.T) {
dir2, s2 := testServerWithConfig(t, func(c *Config) { dir2, s2 := testServerWithConfig(t, func(c *Config) {
c.Datacenter = "dc2" c.Datacenter = "dc2"
c.ACLDatacenter = "dc1" c.ACLDatacenter = "dc1"
c.ACLReplicationToken = "root" c.EnableACLReplication = true
c.ACLReplicationInterval = 10 * time.Millisecond c.ACLReplicationInterval = 10 * time.Millisecond
c.ACLReplicationApplyLimit = 1000000 c.ACLReplicationApplyLimit = 1000000
}) })
s2.tokens.UpdateACLReplicationToken("root")
defer os.RemoveAll(dir2) defer os.RemoveAll(dir2)
defer s2.Shutdown() defer s2.Shutdown()

View file

@ -598,10 +598,11 @@ func TestACL_Replication(t *testing.T) {
c.ACLDatacenter = "dc1" c.ACLDatacenter = "dc1"
c.ACLDefaultPolicy = "deny" c.ACLDefaultPolicy = "deny"
c.ACLDownPolicy = "extend-cache" c.ACLDownPolicy = "extend-cache"
c.ACLReplicationToken = "root" c.EnableACLReplication = true
c.ACLReplicationInterval = 10 * time.Millisecond c.ACLReplicationInterval = 10 * time.Millisecond
c.ACLReplicationApplyLimit = 1000000 c.ACLReplicationApplyLimit = 1000000
}) })
s2.tokens.UpdateACLReplicationToken("root")
defer os.RemoveAll(dir2) defer os.RemoveAll(dir2)
defer s2.Shutdown() defer s2.Shutdown()
@ -609,10 +610,11 @@ func TestACL_Replication(t *testing.T) {
c.Datacenter = "dc3" c.Datacenter = "dc3"
c.ACLDatacenter = "dc1" c.ACLDatacenter = "dc1"
c.ACLDownPolicy = "deny" c.ACLDownPolicy = "deny"
c.ACLReplicationToken = "root" c.EnableACLReplication = true
c.ACLReplicationInterval = 10 * time.Millisecond c.ACLReplicationInterval = 10 * time.Millisecond
c.ACLReplicationApplyLimit = 1000000 c.ACLReplicationApplyLimit = 1000000
}) })
s3.tokens.UpdateACLReplicationToken("root")
defer os.RemoveAll(dir3) defer os.RemoveAll(dir3)
defer s3.Shutdown() defer s3.Shutdown()

View file

@ -218,11 +218,8 @@ type Config struct {
// "allow" can be used to allow all requests. This is not recommended. // "allow" can be used to allow all requests. This is not recommended.
ACLDownPolicy string ACLDownPolicy string
// ACLReplicationToken is used to fetch ACLs from the ACLDatacenter in // EnableACLReplication is used to control ACL replication.
// order to replicate them locally. Setting this to a non-empty value EnableACLReplication bool
// also enables replication. Replication is only available in datacenters
// other than the ACLDatacenter.
ACLReplicationToken string
// ACLReplicationInterval is the interval at which replication passes // ACLReplicationInterval is the interval at which replication passes
// will occur. Queries to the ACLDatacenter may block, so replication // will occur. Queries to the ACLDatacenter may block, so replication

View file

@ -23,6 +23,7 @@ import (
"github.com/hashicorp/consul/agent/consul/state" "github.com/hashicorp/consul/agent/consul/state"
"github.com/hashicorp/consul/agent/consul/structs" "github.com/hashicorp/consul/agent/consul/structs"
"github.com/hashicorp/consul/agent/pool" "github.com/hashicorp/consul/agent/pool"
"github.com/hashicorp/consul/agent/token"
"github.com/hashicorp/consul/lib" "github.com/hashicorp/consul/lib"
"github.com/hashicorp/consul/tlsutil" "github.com/hashicorp/consul/tlsutil"
"github.com/hashicorp/consul/types" "github.com/hashicorp/consul/types"
@ -99,6 +100,11 @@ type Server struct {
// Consul configuration // Consul configuration
config *Config config *Config
// 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
// Connection pool to other consul servers // Connection pool to other consul servers
connPool *pool.ConnPool connPool *pool.ConnPool
@ -215,12 +221,12 @@ type endpoints struct {
} }
func NewServer(config *Config) (*Server, error) { func NewServer(config *Config) (*Server, error) {
return NewServerLogger(config, nil) return NewServerLogger(config, nil, new(token.Store))
} }
// NewServer is used to construct a new Consul server from the // NewServer is used to construct a new Consul server from the
// configuration, potentially returning an error // configuration, potentially returning an error
func NewServerLogger(config *Config, logger *log.Logger) (*Server, error) { func NewServerLogger(config *Config, logger *log.Logger, tokens *token.Store) (*Server, error) {
// Check the protocol version. // Check the protocol version.
if err := config.CheckProtocolVersion(); err != nil { if err := config.CheckProtocolVersion(); err != nil {
return nil, err return nil, err
@ -285,6 +291,7 @@ func NewServerLogger(config *Config, logger *log.Logger) (*Server, error) {
autopilotRemoveDeadCh: make(chan struct{}), autopilotRemoveDeadCh: make(chan struct{}),
autopilotShutdownCh: make(chan struct{}), autopilotShutdownCh: make(chan struct{}),
config: config, config: config,
tokens: tokens,
connPool: connPool, connPool: connPool,
eventChLAN: make(chan serf.Event, 256), eventChLAN: make(chan serf.Event, 256),
eventChWAN: make(chan serf.Event, 256), eventChWAN: make(chan serf.Event, 256),

View file

@ -12,6 +12,7 @@ import (
"time" "time"
"github.com/hashicorp/consul/agent/consul/agent" "github.com/hashicorp/consul/agent/consul/agent"
"github.com/hashicorp/consul/agent/token"
"github.com/hashicorp/consul/testrpc" "github.com/hashicorp/consul/testrpc"
"github.com/hashicorp/consul/testutil" "github.com/hashicorp/consul/testutil"
"github.com/hashicorp/consul/testutil/retry" "github.com/hashicorp/consul/testutil/retry"
@ -146,7 +147,7 @@ func newServer(c *Config) (*Server, error) {
w = os.Stderr w = os.Stderr
} }
logger := log.New(w, c.NodeName+" - ", log.LstdFlags|log.Lmicroseconds) logger := log.New(w, c.NodeName+" - ", log.LstdFlags|log.Lmicroseconds)
srv, err := NewServerLogger(c, logger) srv, err := NewServerLogger(c, logger, new(token.Store))
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -26,6 +26,10 @@ type Store struct {
// access to the /v1/agent utility operations if the servers aren't // access to the /v1/agent utility operations if the servers aren't
// available. // available.
agentMasterToken string agentMasterToken string
// aclReplicationToken is a special token that's used by servers to
// replicate ACLs from the ACL datacenter.
aclReplicationToken string
} }
// UpdateUserToken replaces the current user token in the store. // UpdateUserToken replaces the current user token in the store.
@ -49,6 +53,13 @@ func (t *Store) UpdateAgentMasterToken(token string) {
t.l.Unlock() t.l.Unlock()
} }
// UpdateACLReplicationToken replaces the current ACL replication token in the store.
func (t *Store) UpdateACLReplicationToken(token string) {
t.l.Lock()
t.aclReplicationToken = token
t.l.Unlock()
}
// UserToken returns the best token to use for user operations. // UserToken returns the best token to use for user operations.
func (t *Store) UserToken() string { func (t *Store) UserToken() string {
t.l.RLock() t.l.RLock()
@ -68,6 +79,14 @@ func (t *Store) AgentToken() string {
return t.userToken return t.userToken
} }
// ACLReplicationToken returns the ACL replication token.
func (t *Store) ACLReplicationToken() string {
t.l.RLock()
defer t.l.RUnlock()
return t.aclReplicationToken
}
// IsAgentMasterToken checks to see if a given token is the agent master token. // IsAgentMasterToken checks to see if a given token is the agent master token.
// This will never match an empty token for safety. // This will never match an empty token for safety.
func (t *Store) IsAgentMasterToken(token string) bool { func (t *Store) IsAgentMasterToken(token string) bool {

View file

@ -4,40 +4,69 @@ import (
"testing" "testing"
) )
func TestStore_UserAndAgentTokens(t *testing.T) { func TestStore_RegularTokens(t *testing.T) {
t.Parallel() t.Parallel()
type tokens struct {
user, agent, repl string
}
tests := []struct { tests := []struct {
user, agent, wantUser, wantAgent string name string
set, want tokens
}{ }{
{"", "", "", ""}, {
{"user", "", "user", "user"}, name: "set user",
{"user", "agent", "user", "agent"}, set: tokens{user: "U"},
{"", "agent", "", "agent"}, want: tokens{user: "U", agent: "U"},
{"user", "agent", "user", "agent"}, },
{"user", "", "user", "user"}, {
{"", "", "", ""}, name: "set agent",
set: tokens{agent: "A"},
want: tokens{agent: "A"},
},
{
name: "set user and agent",
set: tokens{agent: "A", user: "U"},
want: tokens{agent: "A", user: "U"},
},
{
name: "set repl",
set: tokens{repl: "R"},
want: tokens{repl: "R"},
},
{
name: "set all",
set: tokens{user: "U", agent: "A", repl: "R"},
want: tokens{user: "U", agent: "A", repl: "R"},
},
} }
tokens := new(Store)
for _, tt := range tests { for _, tt := range tests {
tokens.UpdateUserToken(tt.user) t.Run(tt.name, func(t *testing.T) {
tokens.UpdateAgentToken(tt.agent) s := new(Store)
if got, want := tokens.UserToken(), tt.wantUser; got != want { s.UpdateUserToken(tt.set.user)
s.UpdateAgentToken(tt.set.agent)
s.UpdateACLReplicationToken(tt.set.repl)
if got, want := s.UserToken(), tt.want.user; got != want {
t.Fatalf("got token %q want %q", got, want) t.Fatalf("got token %q want %q", got, want)
} }
if got, want := tokens.AgentToken(), tt.wantAgent; got != want { if got, want := s.AgentToken(), tt.want.agent; got != want {
t.Fatalf("got token %q want %q", got, want) t.Fatalf("got token %q want %q", got, want)
} }
if got, want := s.ACLReplicationToken(), tt.want.repl; got != want {
t.Fatalf("got token %q want %q", got, want)
}
})
} }
} }
func TestStore_AgentMasterToken(t *testing.T) { func TestStore_AgentMasterToken(t *testing.T) {
t.Parallel() t.Parallel()
tokens := new(Store) s := new(Store)
verify := func(want bool, toks ...string) { verify := func(want bool, toks ...string) {
for _, tok := range toks { for _, tok := range toks {
if got := tokens.IsAgentMasterToken(tok); got != want { if got := s.IsAgentMasterToken(tok); got != want {
t.Fatalf("token %q got %v want %v", tok, got, want) t.Fatalf("token %q got %v want %v", tok, got, want)
} }
} }
@ -45,14 +74,14 @@ func TestStore_AgentMasterToken(t *testing.T) {
verify(false, "", "nope") verify(false, "", "nope")
tokens.UpdateAgentMasterToken("master") s.UpdateAgentMasterToken("master")
verify(true, "master") verify(true, "master")
verify(false, "", "nope") verify(false, "", "nope")
tokens.UpdateAgentMasterToken("another") s.UpdateAgentMasterToken("another")
verify(true, "another") verify(true, "another")
verify(false, "", "nope", "master") verify(false, "", "nope", "master")
tokens.UpdateAgentMasterToken("") s.UpdateAgentMasterToken("")
verify(false, "", "nope", "master", "another") verify(false, "", "nope", "master", "another")
} }

View file

@ -497,6 +497,12 @@ func (c *Agent) UpdateACLAgentMasterToken(token string, q *WriteOptions) (*Write
return c.updateToken("acl_agent_master_token", token, q) return c.updateToken("acl_agent_master_token", token, q)
} }
// UpdateACLReplicationToken updates the agent's "acl_replication_token". See
// updateToken for more details.
func (c *Agent) UpdateACLReplicationToken(token string, q *WriteOptions) (*WriteMeta, error) {
return c.updateToken("acl_replication_token", token, q)
}
// updateToken can be used to update an agent's ACL token after the agent has // 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 // started. The tokens are not persisted, so will need to be updated again if
// the agent is restarted. // the agent is restarted.

View file

@ -785,4 +785,8 @@ func TestAPI_AgentUpdateToken(t *testing.T) {
if _, err := agent.UpdateACLAgentMasterToken("root", nil); err != nil { if _, err := agent.UpdateACLAgentMasterToken("root", nil); err != nil {
t.Fatalf("err: %v", err) t.Fatalf("err: %v", err)
} }
if _, err := agent.UpdateACLReplicationToken("root", nil); err != nil {
t.Fatalf("err: %v", err)
}
} }

View file

@ -395,11 +395,12 @@ not persisted, so will need to be updated again if the agent is restarted.
| `PUT` | `/agent/token/acl_token` | `application/json` | | `PUT` | `/agent/token/acl_token` | `application/json` |
| `PUT` | `/agent/token/acl_agent_token` | `application/json` | | `PUT` | `/agent/token/acl_agent_token` | `application/json` |
| `PUT` | `/agent/token/acl_agent_master_token` | `application/json` | | `PUT` | `/agent/token/acl_agent_master_token` | `application/json` |
| `PUT` | `/agent/token/acl_replication_token` | `application/json` |
The paths above correspond to the token names as found in the agent configuration, The paths above correspond to the token names as found in the agent configuration:
[`acl_token`](/docs/agent/options.html#acl_token), [`acl_token`](/docs/agent/options.html#acl_token), [`acl_agent_token`](/docs/agent/options.html#acl_agent_token),
[`acl_agent_token`](/docs/agent/options.html#acl_agent_token), [`acl_agent_master_token`](/docs/agent/options.html#acl_agent_master_token), and
and [`acl_agent_master_token`](/docs/agent/options.html#acl_agent_master_token). [`acl_replication_token`](/docs/agent/options.html#acl_replication_token).
The table below shows this endpoint's support for The table below shows this endpoint's support for
[blocking queries](/api/index.html#blocking-queries), [blocking queries](/api/index.html#blocking-queries),

View file

@ -158,7 +158,7 @@ will exit with an error at startup.
in the "consul." domain. This flag can be used to change that domain. All queries in this domain in the "consul." domain. This flag can be used to change that domain. All queries in this domain
are assumed to be handled by Consul and will not be recursively resolved. are assumed to be handled by Consul and will not be recursively resolved.
* <a name="_enable_script_checks"></a><a href="#_enable_script_checks">`enable-script-checks`</a> This * <a name="_enable_script_checks"></a><a href="#_enable_script_checks">`-enable-script-checks`</a> This
controls whether [health checks that execute scripts](/docs/agent/checks.html) are enabled on controls whether [health checks that execute scripts](/docs/agent/checks.html) are enabled on
this agent, and defaults to `false` so operators must opt-in to allowing these. If enabled, this agent, and defaults to `false` so operators must opt-in to allowing these. If enabled,
it is recommended to [enable ACLs](/docs/guides/acl.html) as well to control which users are it is recommended to [enable ACLs](/docs/guides/acl.html) as well to control which users are
@ -583,7 +583,11 @@ Consul will not enable TLS for the HTTP API unless the `https` port has been ass
* <a name="acl_replication_token"></a><a href="#acl_replication_token">`acl_replication_token`</a> - * <a name="acl_replication_token"></a><a href="#acl_replication_token">`acl_replication_token`</a> -
Only used for servers outside the [`acl_datacenter`](#acl_datacenter) running Consul 0.7 or later. Only used for servers outside the [`acl_datacenter`](#acl_datacenter) running Consul 0.7 or later.
When provided, this will enable [ACL replication](/docs/guides/acl.html#replication) using this When provided, this will enable [ACL replication](/docs/guides/acl.html#replication) using this
token to retrieve and replicate the ACLs to the non-authoritative local datacenter. token to retrieve and replicate the ACLs to the non-authoritative local datacenter. In Consul 0.9.1
and later you can enable ACL replication using [`enable_acl_replication`](#enable_acl_replication)
and then set the token later using the [agent token API](/api/agent.html#update-acl-tokens) on each
server. If the `acl_replication_token` is set in the config, it will automatically set
[`enable_acl_replication`](#enable_acl_replication) to true for backward compatibility.
<br><br> <br><br>
If there's a partition or other outage affecting the authoritative datacenter, and the If there's a partition or other outage affecting the authoritative datacenter, and the
[`acl_down_policy`](/docs/agent/options.html#acl_down_policy) is set to "extend-cache", tokens not [`acl_down_policy`](/docs/agent/options.html#acl_down_policy) is set to "extend-cache", tokens not
@ -803,6 +807,12 @@ Consul will not enable TLS for the HTTP API unless the `https` port has been ass
* <a name="domain"></a><a href="#domain">`domain`</a> Equivalent to the * <a name="domain"></a><a href="#domain">`domain`</a> Equivalent to the
[`-domain` command-line flag](#_domain). [`-domain` command-line flag](#_domain).
* <a name="enable_acl_replication"></a><a href="#enable_acl_replication">`enable_acl_replication`</a> When
set on a Consul server, enables [ACL replication](/docs/guides/acl.html#replication) without having to set
the replication token via [`acl_replication_token`](#acl_replication_token). Instead, enable ACL replication
and then introduce the token using the [agent token API](/api/agent.html#update-acl-tokens) on each server.
See [`acl_replication_token`](#acl_replication_token) for more details.
* <a name="enable_debug"></a><a href="#enable_debug">`enable_debug`</a> When set, enables some * <a name="enable_debug"></a><a href="#enable_debug">`enable_debug`</a> When set, enables some
additional debugging features. Currently, this is only used to set the runtime profiling HTTP endpoints. additional debugging features. Currently, this is only used to set the runtime profiling HTTP endpoints.

View file

@ -969,11 +969,17 @@ for any previously resolved tokens and to deny any uncached tokens.
Consul 0.7 added an ACL Replication capability that can allow non-authoritative Consul 0.7 added an ACL Replication capability that can allow non-authoritative
datacenter agents to resolve even uncached tokens. This is enabled by setting an datacenter agents to resolve even uncached tokens. This is enabled by setting an
[`acl_replication_token`](/docs/agent/options.html#acl_replication_token) in the [`acl_replication_token`](/docs/agent/options.html#acl_replication_token) in the
configuration on the servers in the non-authoritative datacenters. With replication configuration on the servers in the non-authoritative datacenters. In Consul
enabled, the servers will maintain a replica of the authoritative datacenter's full 0.9.1 and later you can enable ACL replication using
set of ACLs on the non-authoritative servers. The ACL replication token needs to be [`enable_acl_replication`](/docs/agent/options.html#enable_acl_replication) and
a valid ACL token with management privileges, it can also be the same as the master then set the token later using the
ACL token. [agent token API](/api/agent.html#update-acl-tokens) on each server. This can
also be used to rotate the token without restarting the Consul servers.
With replication enabled, the servers will maintain a replica of the authoritative
datacenter's full set of ACLs on the non-authoritative servers. The ACL replication
token needs to be a valid ACL token with management privileges, it can also be the
same as the master ACL token.
Replication occurs with a background process that looks for new ACLs approximately Replication occurs with a background process that looks for new ACLs approximately
every 30 seconds. Replicated changes are written at a rate that's throttled to every 30 seconds. Replicated changes are written at a rate that's throttled to