From 0e5131bd336c14d5b188199e350660c9023c0348 Mon Sep 17 00:00:00 2001 From: freddygv Date: Fri, 9 Sep 2022 13:05:38 -0600 Subject: [PATCH] Generate ACL token for server management This commit introduces a new ACL token used for internal server management purposes. It has a few key properties: - It has unlimited permissions. - It is persisted through Raft as System Metadata rather than in the ACL tokens table. This is to avoid users seeing or modifying it. - It is re-generated on leadership establishment. --- agent/consul/acl.go | 5 ++++ agent/consul/acl_client.go | 4 +++ agent/consul/acl_server.go | 14 +++++++++++ agent/consul/acl_test.go | 27 ++++++++++++++++++++ agent/consul/leader.go | 15 ++++++++++++ agent/consul/leader_test.go | 8 ++++++ agent/structs/acl.go | 49 +++++++++++++++++++++++++++++++++++++ 7 files changed, 122 insertions(+) diff --git a/agent/consul/acl.go b/agent/consul/acl.go index d2ed005962..bf7972e5d4 100644 --- a/agent/consul/acl.go +++ b/agent/consul/acl.go @@ -132,6 +132,7 @@ type ACLResolverBackend interface { ResolveIdentityFromToken(token string) (bool, structs.ACLIdentity, error) ResolvePolicyFromID(policyID string) (bool, *structs.ACLPolicy, error) ResolveRoleFromID(roleID string) (bool, *structs.ACLRole, error) + IsServerManagementToken(token string) bool // TODO: separate methods for each RPC call (there are 4) RPC(method string, args interface{}, reply interface{}) error EnterpriseACLResolverDelegate @@ -980,6 +981,10 @@ func (r *ACLResolver) resolveLocallyManagedToken(token string) (structs.ACLIdent return structs.NewAgentRecoveryTokenIdentity(r.config.NodeName, token), r.agentRecoveryAuthz, true } + if r.backend.IsServerManagementToken(token) { + return structs.NewACLServerIdentity(token), acl.ManageAll(), true + } + return r.resolveLocallyManagedEnterpriseToken(token) } diff --git a/agent/consul/acl_client.go b/agent/consul/acl_client.go index 1cd287cf67..d93923e654 100644 --- a/agent/consul/acl_client.go +++ b/agent/consul/acl_client.go @@ -27,6 +27,10 @@ type clientACLResolverBackend struct { *Client } +func (c *clientACLResolverBackend) IsServerManagementToken(_ string) bool { + return false +} + func (c *clientACLResolverBackend) ACLDatacenter() string { // For resolution running on clients servers within the current datacenter // must be queried first to pick up local tokens. diff --git a/agent/consul/acl_server.go b/agent/consul/acl_server.go index b6047752f8..f9f88e4087 100644 --- a/agent/consul/acl_server.go +++ b/agent/consul/acl_server.go @@ -1,6 +1,7 @@ package consul import ( + "crypto/subtle" "fmt" "time" @@ -108,6 +109,19 @@ type serverACLResolverBackend struct { *Server } +func (s *serverACLResolverBackend) IsServerManagementToken(token string) bool { + mgmt, err := s.getSystemMetadata(structs.ServerManagementToken) + if err != nil { + s.logger.Debug("failed to fetch server management token: %w", err) + return false + } + if mgmt == "" { + s.logger.Debug("server management token has not been initialized") + return false + } + return subtle.ConstantTimeCompare([]byte(mgmt), []byte(token)) == 1 +} + func (s *serverACLResolverBackend) ACLDatacenter() string { // For resolution running on servers the only option is to contact the // configured ACL Datacenter diff --git a/agent/consul/acl_test.go b/agent/consul/acl_test.go index b33cde102e..f08f966e90 100644 --- a/agent/consul/acl_test.go +++ b/agent/consul/acl_test.go @@ -438,6 +438,8 @@ type ACLResolverTestDelegate struct { // testRoles is used by plainRoleResolveFn if not nil testRoles map[string]*structs.ACLRole + testServerManagementToken string + localTokenResolutions int32 remoteTokenResolutions int32 localPolicyResolutions int32 @@ -456,6 +458,10 @@ type ACLResolverTestDelegate struct { EnterpriseACLResolverTestDelegate } +func (d *ACLResolverTestDelegate) IsServerManagementToken(token string) bool { + return token == d.testServerManagementToken +} + // UseTestLocalData will force delegate-local maps to be used in lieu of the // global factory functions. func (d *ACLResolverTestDelegate) UseTestLocalData(data []interface{}) { @@ -2187,6 +2193,27 @@ func TestACLResolver_AgentRecovery(t *testing.T) { require.Equal(t, acl.Deny, authz.NodeWrite("bar", nil)) } +func TestACLResolver_ServerManagementToken(t *testing.T) { + const testToken = "1bb0900e-3683-46a5-b04c-4882d7773b83" + + d := &ACLResolverTestDelegate{ + datacenter: "dc1", + enabled: true, + testServerManagementToken: testToken, + } + r := newTestACLResolver(t, d, func(cfg *ACLResolverConfig) { + cfg.Tokens = &token.Store{} + cfg.Config.NodeName = "foo" + }) + + authz, err := r.ResolveToken(testToken) + require.NoError(t, err) + require.NotNil(t, authz.ACLIdentity) + require.Equal(t, structs.ServerManagementToken, authz.ACLIdentity.ID()) + require.NotNil(t, authz.Authorizer) + require.Equal(t, acl.ManageAll(), authz.Authorizer) +} + func TestACLResolver_ACLsEnabled(t *testing.T) { type testCase struct { name string diff --git a/agent/consul/leader.go b/agent/consul/leader.go index 29cd216c9f..35626d1bc9 100644 --- a/agent/consul/leader.go +++ b/agent/consul/leader.go @@ -501,6 +501,7 @@ func (s *Server) initializeACLs(ctx context.Context) error { } } + // Insert the anonymous token if it does not exist. state := s.fsm.State() _, token, err := state.ACLTokenGetBySecret(nil, anonymousToken, nil) if err != nil { @@ -527,6 +528,20 @@ func (s *Server) initializeACLs(ctx context.Context) error { } s.logger.Info("Created ACL anonymous token from configuration") } + + // Generate or rotate the server management token on leadership transitions. + // This token is used by Consul servers for authn/authz when making + // requests to themselves through public APIs such as the agent cache. + // It is stored as system metadata because it is internally + // managed and users are not meant to see it or interact with it. + secretID, err := lib.GenerateUUID(nil) + if err != nil { + return fmt.Errorf("failed to generate the secret ID for the server management token: %w", err) + } + if err := s.setSystemMetadataKey(structs.ServerManagementToken, secretID); err != nil { + return fmt.Errorf("failed to persist server management token: %w", err) + } + // launch the upgrade go routine to generate accessors for everything s.startACLUpgrade(ctx) } else { diff --git a/agent/consul/leader_test.go b/agent/consul/leader_test.go index 1edaa88a3c..a1d19dd36c 100644 --- a/agent/consul/leader_test.go +++ b/agent/consul/leader_test.go @@ -11,6 +11,7 @@ import ( "time" "github.com/hashicorp/go-hclog" + "github.com/hashicorp/go-uuid" "github.com/hashicorp/serf/serf" "github.com/stretchr/testify/require" "google.golang.org/grpc" @@ -1295,6 +1296,13 @@ func TestLeader_ACL_Initialization(t *testing.T) { _, policy, err := s1.fsm.State().ACLPolicyGetByID(nil, structs.ACLPolicyGlobalManagementID, nil) require.NoError(t, err) require.NotNil(t, policy) + + serverToken, err := s1.getSystemMetadata(structs.ServerManagementToken) + require.NoError(t, err) + require.NotEmpty(t, serverToken) + + _, err = uuid.ParseUUID(serverToken) + require.NoError(t, err) }) } } diff --git a/agent/structs/acl.go b/agent/structs/acl.go index 1fd3f1d935..ce6b871eaf 100644 --- a/agent/structs/acl.go +++ b/agent/structs/acl.go @@ -104,6 +104,7 @@ type ACLIdentity interface { IsLocal() bool EnterpriseMetadata() *acl.EnterpriseMeta } + type ACLTokenPolicyLink struct { ID string Name string `hash:"ignore"` @@ -1838,3 +1839,51 @@ func (id *AgentRecoveryTokenIdentity) IsLocal() bool { func (id *AgentRecoveryTokenIdentity) EnterpriseMetadata() *acl.EnterpriseMeta { return nil } + +const ServerManagementToken = "server-management-token" + +type ACLServerIdentity struct { + secretID string +} + +func NewACLServerIdentity(secretID string) *ACLServerIdentity { + return &ACLServerIdentity{ + secretID: secretID, + } +} + +func (i *ACLServerIdentity) ID() string { + return ServerManagementToken +} + +func (i *ACLServerIdentity) SecretToken() string { + return i.secretID +} + +func (i *ACLServerIdentity) PolicyIDs() []string { + return nil +} + +func (i *ACLServerIdentity) RoleIDs() []string { + return nil +} + +func (i *ACLServerIdentity) ServiceIdentityList() []*ACLServiceIdentity { + return nil +} + +func (i *ACLServerIdentity) NodeIdentityList() []*ACLNodeIdentity { + return nil +} + +func (i *ACLServerIdentity) IsExpired(asOf time.Time) bool { + return false +} + +func (i *ACLServerIdentity) IsLocal() bool { + return true +} + +func (i *ACLServerIdentity) EnterpriseMetadata() *acl.EnterpriseMeta { + return acl.DefaultEnterpriseMeta() +}