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.UpdateAgentToken(a.config.ACLAgentToken)
a.tokens.UpdateAgentMasterToken(a.config.ACLAgentMasterToken)
a.tokens.UpdateACLReplicationToken(a.config.ACLReplicationToken)
return a, nil
}
@ -266,7 +267,7 @@ func (a *Agent) Start() error {
// Setup either the client or the server.
if c.Server {
server, err := consul.NewServerLogger(consulCfg, a.logger)
server, err := consul.NewServerLogger(consulCfg, a.logger, a.tokens)
if err != nil {
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 != "" {
base.ACLDownPolicy = a.config.ACLDownPolicy
}
if a.config.ACLReplicationToken != "" {
base.ACLReplicationToken = a.config.ACLReplicationToken
}
base.EnableACLReplication = a.config.EnableACLReplication
if a.config.ACLEnforceVersion8 != nil {
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":
s.agent.tokens.UpdateAgentMasterToken(args.Token)
case "acl_replication_token":
s.agent.tokens.UpdateACLReplicationToken(args.Token)
default:
resp.WriteHeader(http.StatusNotFound)
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)
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})
}
@ -1677,22 +1688,97 @@ func TestAgent_Token(t *testing.T) {
method, url string
body io.Reader
code int
userToken string
agentToken string
masterToken string
got, want tokens
}{
{"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, "", "", ""},
{
name: "bad method",
method: "GET",
url: "acl_token",
code: http.StatusMethodNotAllowed,
},
{
name: "bad token name",
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 {
t.Run(tt.name, func(t *testing.T) {
resetTokens(tt.got)
url := fmt.Sprintf("/v1/agent/token/%s", tt.url)
resp := httptest.NewRecorder()
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 {
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)
}
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)
}
if tt.masterToken != "" && !a.tokens.IsAgentMasterToken(tt.masterToken) {
t.Fatalf("%q should be the master token", tt.masterToken)
if tt.want.master != "" && !a.tokens.IsAgentMasterToken(tt.want.master) {
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
// 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"))
resetTokens(tokens{})
req, _ := http.NewRequest("PUT", "/v1/agent/token/acl_token", body("X"))
if _, err := a.srv.AgentToken(nil, req); !isPermissionDenied(err) {
t.Fatalf("err: %v", err)
}

View File

@ -696,6 +696,12 @@ type Config struct {
// this acts like deny.
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
// order to replicate them locally. Setting this to a non-empty value
// also enables replication. Replication is only available in datacenters
@ -1448,6 +1454,13 @@ func DecodeConfig(r io.Reader) (*Config, error) {
}
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
}
@ -2023,6 +2036,9 @@ func MergeConfig(a, b *Config) *Config {
if b.ACLDefaultPolicy != "" {
result.ACLDefaultPolicy = b.ACLDefaultPolicy
}
if b.EnableACLReplication {
result.EnableACLReplication = true
}
if b.ACLReplicationToken != "" {
result.ACLReplicationToken = b.ACLReplicationToken
}

View File

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

View File

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

View File

@ -154,7 +154,7 @@ func (s *Server) fetchRemoteACLs(lastRemoteIndex uint64) (*structs.IndexedACLs,
args := structs.DCSpecificRequest{
Datacenter: s.config.ACLDatacenter,
QueryOptions: structs.QueryOptions{
Token: s.config.ACLReplicationToken,
Token: s.tokens.ACLReplicationToken(),
MinQueryIndex: lastRemoteIndex,
AllowStale: true,
},
@ -247,7 +247,7 @@ func (s *Server) replicateACLs(lastRemoteIndex uint64) (uint64, error) {
func (s *Server) IsACLReplicationEnabled() bool {
authDC := s.config.ACLDatacenter
return len(authDC) > 0 && (authDC != s.config.Datacenter) &&
len(s.config.ACLReplicationToken) > 0
s.config.EnableACLReplication
}
// 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) {
c.Datacenter = "dc2"
c.ACLDatacenter = "dc1"
c.ACLReplicationToken = "secret"
c.ACLReplicationApplyLimit = 1
})
s1.tokens.UpdateACLReplicationToken("secret")
defer os.RemoveAll(dir1)
defer s1.Shutdown()
testrpc.WaitForLeader(t, s1.RPC, "dc2")
@ -300,7 +300,7 @@ func TestACLReplication_IsACLReplicationEnabled(t *testing.T) {
dir3, s3 := testServerWithConfig(t, func(c *Config) {
c.Datacenter = "dc2"
c.ACLDatacenter = "dc1"
c.ACLReplicationToken = "secret"
c.EnableACLReplication = true
})
defer os.RemoveAll(dir3)
defer s3.Shutdown()
@ -308,12 +308,12 @@ func TestACLReplication_IsACLReplicationEnabled(t *testing.T) {
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.
dir4, s4 := testServerWithConfig(t, func(c *Config) {
c.Datacenter = "dc1"
c.ACLDatacenter = "dc1"
c.ACLReplicationToken = "secret"
c.EnableACLReplication = true
})
defer os.RemoveAll(dir4)
defer s4.Shutdown()
@ -336,10 +336,11 @@ func TestACLReplication(t *testing.T) {
dir2, s2 := testServerWithConfig(t, func(c *Config) {
c.Datacenter = "dc2"
c.ACLDatacenter = "dc1"
c.ACLReplicationToken = "root"
c.EnableACLReplication = true
c.ACLReplicationInterval = 10 * time.Millisecond
c.ACLReplicationApplyLimit = 1000000
})
s2.tokens.UpdateACLReplicationToken("root")
defer os.RemoveAll(dir2)
defer s2.Shutdown()

View File

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

View File

@ -218,11 +218,8 @@ type Config struct {
// "allow" can be used to allow all requests. This is not recommended.
ACLDownPolicy string
// ACLReplicationToken is used to fetch ACLs from the ACLDatacenter in
// order to replicate them locally. Setting this to a non-empty value
// also enables replication. Replication is only available in datacenters
// other than the ACLDatacenter.
ACLReplicationToken string
// EnableACLReplication is used to control ACL replication.
EnableACLReplication bool
// ACLReplicationInterval is the interval at which replication passes
// 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/structs"
"github.com/hashicorp/consul/agent/pool"
"github.com/hashicorp/consul/agent/token"
"github.com/hashicorp/consul/lib"
"github.com/hashicorp/consul/tlsutil"
"github.com/hashicorp/consul/types"
@ -99,6 +100,11 @@ type Server struct {
// Consul configuration
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
connPool *pool.ConnPool
@ -215,12 +221,12 @@ type endpoints struct {
}
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
// 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.
if err := config.CheckProtocolVersion(); err != nil {
return nil, err
@ -285,6 +291,7 @@ func NewServerLogger(config *Config, logger *log.Logger) (*Server, error) {
autopilotRemoveDeadCh: make(chan struct{}),
autopilotShutdownCh: make(chan struct{}),
config: config,
tokens: tokens,
connPool: connPool,
eventChLAN: make(chan serf.Event, 256),
eventChWAN: make(chan serf.Event, 256),

View File

@ -12,6 +12,7 @@ import (
"time"
"github.com/hashicorp/consul/agent/consul/agent"
"github.com/hashicorp/consul/agent/token"
"github.com/hashicorp/consul/testrpc"
"github.com/hashicorp/consul/testutil"
"github.com/hashicorp/consul/testutil/retry"
@ -146,7 +147,7 @@ func newServer(c *Config) (*Server, error) {
w = os.Stderr
}
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 {
return nil, err
}

View File

@ -26,6 +26,10 @@ type Store struct {
// access to the /v1/agent utility operations if the servers aren't
// available.
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.
@ -49,6 +53,13 @@ func (t *Store) UpdateAgentMasterToken(token string) {
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.
func (t *Store) UserToken() string {
t.l.RLock()
@ -68,6 +79,14 @@ func (t *Store) AgentToken() string {
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.
// This will never match an empty token for safety.
func (t *Store) IsAgentMasterToken(token string) bool {

View File

@ -4,40 +4,69 @@ import (
"testing"
)
func TestStore_UserAndAgentTokens(t *testing.T) {
func TestStore_RegularTokens(t *testing.T) {
t.Parallel()
type tokens struct {
user, agent, repl string
}
tests := []struct {
user, agent, wantUser, wantAgent string
name string
set, want tokens
}{
{"", "", "", ""},
{"user", "", "user", "user"},
{"user", "agent", "user", "agent"},
{"", "agent", "", "agent"},
{"user", "agent", "user", "agent"},
{"user", "", "user", "user"},
{"", "", "", ""},
{
name: "set user",
set: tokens{user: "U"},
want: tokens{user: "U", agent: "U"},
},
{
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 {
tokens.UpdateUserToken(tt.user)
tokens.UpdateAgentToken(tt.agent)
if got, want := tokens.UserToken(), tt.wantUser; got != want {
t.Run(tt.name, func(t *testing.T) {
s := new(Store)
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)
}
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)
}
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) {
t.Parallel()
tokens := new(Store)
s := new(Store)
verify := func(want bool, toks ...string) {
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)
}
}
@ -45,14 +74,14 @@ func TestStore_AgentMasterToken(t *testing.T) {
verify(false, "", "nope")
tokens.UpdateAgentMasterToken("master")
s.UpdateAgentMasterToken("master")
verify(true, "master")
verify(false, "", "nope")
tokens.UpdateAgentMasterToken("another")
s.UpdateAgentMasterToken("another")
verify(true, "another")
verify(false, "", "nope", "master")
tokens.UpdateAgentMasterToken("")
s.UpdateAgentMasterToken("")
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)
}
// 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
// started. The tokens are not persisted, so will need to be updated again if
// the agent is restarted.

View File

@ -785,4 +785,8 @@ func TestAPI_AgentUpdateToken(t *testing.T) {
if _, err := agent.UpdateACLAgentMasterToken("root", nil); err != nil {
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_agent_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,
[`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 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),
[`acl_agent_master_token`](/docs/agent/options.html#acl_agent_master_token), and
[`acl_replication_token`](/docs/agent/options.html#acl_replication_token).
The table below shows this endpoint's support for
[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
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
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
@ -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> -
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
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>
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
@ -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
[`-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
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
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
configuration on the servers in the non-authoritative datacenters. 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.
configuration on the servers in the non-authoritative datacenters. In Consul
0.9.1 and later you can enable ACL replication using
[`enable_acl_replication`](/docs/agent/options.html#enable_acl_replication) and
then set the token later using the
[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
every 30 seconds. Replicated changes are written at a rate that's throttled to