auth: add client-only ACL (#18730)

The RPC handlers expect to see `nil` ACL objects whenever ACLs are disabled. By
using `nil` as a sentinel value, we have the risk of nil pointer exceptions and
improper handling of `nil` when returned from our various auth methods that can
lead to privilege escalation bugs. This is the third in a series to eliminate
the use of `nil` ACLs as a sentinel value for when ACLs are disabled.

This patch involves creating a new "virtual" ACL object for checking permissions
on client operations and a matching `AuthenticateClientOnly` method for
client-only RPCs that can produce that ACL.

Unlike the server ACLs PR, this also includes a special case for "legacy" client
RPCs where the client was not previously sending the secret as it
should (leaning on mTLS only). Those client RPCs were fixed in Nomad 1.6.0, but
it'll take a while before we can guarantee they'll be present during upgrades.

Ref: https://github.com/hashicorp/nomad-enterprise/pull/1218
Ref: https://github.com/hashicorp/nomad/pull/18703
Ref: https://github.com/hashicorp/nomad/pull/18715
Ref: https://github.com/hashicorp/nomad/pull/16799
This commit is contained in:
Tim Gross
2023-10-12 12:21:48 -04:00
committed by GitHub
parent cecd9b0472
commit 3633ca0f8c
25 changed files with 467 additions and 244 deletions

View File

@@ -78,6 +78,7 @@ type ACL struct {
// The attributes below detail a virtual policy that we never expose
// directly to the end user.
client string
server string
isLeader bool
}
@@ -301,6 +302,7 @@ func NewACL(management bool, policies []*Policy) (*ACL, error) {
acl.variables = svTxn.Commit()
acl.wildcardVariables = wsvTxn.Commit()
acl.client = PolicyDeny
acl.server = PolicyDeny
acl.isLeader = false
@@ -332,6 +334,11 @@ func (a *ACL) AllowNamespaceOperation(ns string, op string) bool {
return true
}
// Clients need to be able to read their namespaced objects
if a.client != PolicyDeny {
return true
}
// If using the all namespaces wildcard, allow if any namespace allows the
// operation.
if ns == AllNamespacesSentinel && a.anyNamespaceAllowsOp(op) {
@@ -731,6 +738,9 @@ func (a *ACL) AllowNodeRead() bool {
return true
case a.node == PolicyRead:
return true
case a.client == PolicyRead,
a.client == PolicyWrite:
return true
case a.server == PolicyRead,
a.server == PolicyWrite:
return true
@@ -813,6 +823,9 @@ func (a *ACL) AllowPluginRead() bool {
return true
case a.management:
return true
case a.client == PolicyRead,
a.client == PolicyWrite:
return true
case a.plugin == PolicyRead:
return true
default:
@@ -828,6 +841,9 @@ func (a *ACL) AllowPluginList() bool {
return true
case a.management:
return true
case a.client == PolicyRead,
a.client == PolicyWrite:
return true
case a.plugin == PolicyList:
return true
case a.plugin == PolicyRead:
@@ -837,6 +853,15 @@ func (a *ACL) AllowPluginList() bool {
}
}
func (a *ACL) AllowServiceRegistrationReadList(ns string, isWorkload bool) bool {
if a == nil {
// ACL is nil only if ACLs are disabled
// TODO(tgross): return false when there are no nil ACLs
return true
}
return isWorkload || a.AllowNsOp(ns, NamespaceCapabilityReadJob)
}
// AllowServerOp checks if server-only operations are allowed
func (a *ACL) AllowServerOp() bool {
if a == nil {
@@ -847,6 +872,15 @@ func (a *ACL) AllowServerOp() bool {
return a.server != PolicyDeny || a.isLeader
}
func (a *ACL) AllowClientOp() bool {
if a == nil {
// ACL is nil only if ACLs are disabled
// TODO(tgross): return false when there are no nil ACLs
return true
}
return a.client != PolicyDeny
}
// IsManagement checks if this represents a management token
func (a *ACL) IsManagement() bool {
return a.management
@@ -856,14 +890,18 @@ func (a *ACL) IsManagement() bool {
// a list of operations. Returns true (allowed) if acls are disabled or if
// *any* capabilities match.
func NamespaceValidator(ops ...string) func(*ACL, string) bool {
return func(acl *ACL, ns string) bool {
return func(a *ACL, ns string) bool {
// Always allow if ACLs are disabled.
if acl == nil {
if a == nil {
return true
}
// Clients need to be able to read namespaced objects
if a.client != PolicyDeny {
return true
}
for _, op := range ops {
if acl.AllowNamespaceOperation(ns, op) {
if a.AllowNamespaceOperation(ns, op) {
// An operation is allowed, return true
return true
}

View File

@@ -100,6 +100,7 @@ func TestACLManagement(t *testing.T) {
must.True(t, acl.AllowQuotaRead())
must.True(t, acl.AllowQuotaWrite())
must.True(t, acl.AllowServerOp())
must.True(t, acl.AllowClientOp())
}
func TestACLMerge(t *testing.T) {
@@ -143,6 +144,7 @@ func TestACLMerge(t *testing.T) {
must.True(t, acl.AllowQuotaRead())
must.True(t, acl.AllowQuotaWrite())
must.False(t, acl.AllowServerOp())
must.False(t, acl.AllowClientOp())
// Merge read + blank
p3, err := Parse("")
@@ -178,6 +180,7 @@ func TestACLMerge(t *testing.T) {
must.True(t, acl.AllowQuotaRead())
must.False(t, acl.AllowQuotaWrite())
must.False(t, acl.AllowServerOp())
must.False(t, acl.AllowClientOp())
// Merge read + deny
p4, err := Parse(denyAll)

View File

@@ -3,8 +3,20 @@
package acl
var ClientACL = initClientACL()
var ServerACL = initServerACL()
func initClientACL() *ACL {
aclObj, err := NewACL(false, []*Policy{})
if err != nil {
panic(err)
}
aclObj.client = PolicyWrite
aclObj.agent = PolicyRead
aclObj.server = PolicyRead
return aclObj
}
func initServerACL() *ACL {
aclObj, err := NewACL(false, []*Policy{})
if err != nil {