mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-06 05:29:07 +02:00
Add an optional --allow-group flag restricting the daemon sockets (#7478)
This commit is contained in:
@@ -29,6 +29,11 @@ func LookupGroupID(gid string) (*user.Group, error) {
|
||||
return user.LookupGroupId(gid)
|
||||
}
|
||||
|
||||
// LookupGroupName looks up a group by name.
|
||||
func LookupGroupName(name string) (*user.Group, error) {
|
||||
return user.LookupGroup(name)
|
||||
}
|
||||
|
||||
// GroupIDs returns the IDs of the groups the user is a member of; libc's
|
||||
// getgrouplist handles NSS groups natively.
|
||||
func GroupIDs(u *user.User) ([]string, error) {
|
||||
|
||||
@@ -88,6 +88,25 @@ func LookupGroupID(gid string) (*user.Group, error) {
|
||||
return g, nil
|
||||
}
|
||||
|
||||
// LookupGroupName looks up a group by name, falling back to getent if os/user
|
||||
// fails.
|
||||
func LookupGroupName(name string) (*user.Group, error) {
|
||||
g, err := user.LookupGroup(name)
|
||||
if err == nil {
|
||||
return g, nil
|
||||
}
|
||||
|
||||
stdErr := err
|
||||
log.Debugf("os/user.LookupGroup(%q) failed, trying getent: %v", name, err)
|
||||
|
||||
g, _, getentErr := groupLookup(name)
|
||||
if getentErr != nil {
|
||||
log.Debugf("getent fallback for group %q also failed: %v", name, getentErr)
|
||||
return nil, stdErr
|
||||
}
|
||||
return g, nil
|
||||
}
|
||||
|
||||
// GroupIDs returns the IDs of the groups the user is a member of.
|
||||
// NOTE: unlike the lookups above, which try the standard library first, this
|
||||
// intentionally tries `id -G` first because without cgo, user.GroupIds only
|
||||
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/sys/windows"
|
||||
@@ -38,6 +40,48 @@ func DefaultPipeSDDL() string {
|
||||
return "D:P(A;;GA;;;SY)(A;;GA;;;WD)"
|
||||
}
|
||||
|
||||
// RestrictedPipeSDDL returns the security descriptor for a daemon control pipe
|
||||
// that only the named principals may open, replacing DefaultPipeSDDL's ACE for
|
||||
// Everyone. Each SID is a user or group SID in string form; the caller is
|
||||
// expected to have resolved and validated them already. An empty list yields
|
||||
// the default descriptor, so a missing configuration cannot silently produce a
|
||||
// pipe nobody can reach.
|
||||
//
|
||||
// Three ACEs are always present besides the configured ones:
|
||||
//
|
||||
// SY LocalSystem, the account the daemon runs as when installed as a service
|
||||
// BA BUILTIN\Administrators, so an elevated caller is never locked out
|
||||
// the daemon's own user SID, so a daemon an ordinary user runs themselves can
|
||||
// still dial itself, which is what the JSON gateway does
|
||||
//
|
||||
// BUILTIN\Administrators carries no access for a UAC-filtered administrator,
|
||||
// whose token has that group deny-only, which matches the authorization model:
|
||||
// such a caller is not privileged either.
|
||||
func RestrictedPipeSDDL(sids []string) string {
|
||||
if len(sids) == 0 {
|
||||
return DefaultPipeSDDL()
|
||||
}
|
||||
|
||||
allowed := []string{"SY", "BA"}
|
||||
if selfIdentity.Known() && selfIdentity.SID != "" {
|
||||
allowed = append(allowed, selfIdentity.SID)
|
||||
}
|
||||
for _, sid := range sids {
|
||||
if !slices.Contains(allowed, sid) {
|
||||
allowed = append(allowed, sid)
|
||||
}
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
b.WriteString("D:P")
|
||||
for _, sid := range allowed {
|
||||
b.WriteString("(A;;GA;;;")
|
||||
b.WriteString(sid)
|
||||
b.WriteString(")")
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// NewTransportCredentials returns gRPC transport credentials that derive the
|
||||
// caller's identity from the named-pipe client token.
|
||||
//
|
||||
|
||||
@@ -164,6 +164,7 @@ type PrincipalKind string
|
||||
|
||||
const (
|
||||
KindUID PrincipalKind = "uid" // Unix user ID
|
||||
KindGID PrincipalKind = "gid" // Unix group ID
|
||||
KindSID PrincipalKind = "sid" // Windows user or group SID
|
||||
)
|
||||
|
||||
@@ -181,7 +182,7 @@ func ParsePrincipal(s string) (Principal, bool) {
|
||||
return Principal{}, false
|
||||
}
|
||||
switch PrincipalKind(kind) {
|
||||
case KindUID, KindSID:
|
||||
case KindUID, KindGID, KindSID:
|
||||
return Principal{Kind: PrincipalKind(kind), Value: value}, true
|
||||
default:
|
||||
return Principal{}, false
|
||||
@@ -193,6 +194,11 @@ func UIDPrincipal(uid uint32) string {
|
||||
return string(KindUID) + ":" + strconv.FormatUint(uint64(uid), 10)
|
||||
}
|
||||
|
||||
// GIDPrincipal builds the principal string for a Unix group ID.
|
||||
func GIDPrincipal(gid uint32) string {
|
||||
return string(KindGID) + ":" + strconv.FormatUint(uint64(gid), 10)
|
||||
}
|
||||
|
||||
// SIDPrincipal builds the owner string for a Windows SID.
|
||||
func SIDPrincipal(sid string) string { return string(KindSID) + ":" + sid }
|
||||
|
||||
@@ -227,6 +233,15 @@ func (p Principal) Matches(id Identity) bool {
|
||||
}
|
||||
// Only the user SID. Group ownership is not supported yet.
|
||||
return id.SID == p.Value
|
||||
case KindGID:
|
||||
// A group principal never confers ownership. It exists for the daemon
|
||||
// socket restriction, which the kernel enforces at connect() from the
|
||||
// caller's full group set; the identity here carries only the primary
|
||||
// GID, so matching on it would grant ownership to members of a group
|
||||
// and deny it to others in the same group, depending on which one
|
||||
// happens to be primary. Deciding this properly is the group-ownership
|
||||
// work that is still ahead.
|
||||
return false
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -74,6 +74,21 @@ func TestPrincipalDoesNotMatchGroupSID(t *testing.T) {
|
||||
assert.False(t, group.Matches(member), "a group SID owner must not match a group member")
|
||||
}
|
||||
|
||||
// A GID principal is parseable because the daemon socket restriction stores one,
|
||||
// but it confers no ownership: an owner field holding one must match nobody
|
||||
// rather than admit everyone whose primary group happens to be it.
|
||||
func TestGIDPrincipalNeverMatches(t *testing.T) {
|
||||
group, ok := ParsePrincipal(GIDPrincipal(1000))
|
||||
require.True(t, ok, "a gid principal must parse, the socket restriction stores it")
|
||||
assert.Equal(t, KindGID, group.Kind)
|
||||
assert.Equal(t, "gid:1000", group.String())
|
||||
|
||||
assert.False(t, group.Matches(KnownForTest(Identity{UID: 1000, GID: 1000})),
|
||||
"a gid owner must not match a caller whose primary group it is")
|
||||
assert.False(t, group.Matches(KnownForTest(Identity{UID: 0, GID: 0})))
|
||||
assert.False(t, group.Matches(Identity{}))
|
||||
}
|
||||
|
||||
func TestPrincipalMatchingIsPlatformScoped(t *testing.T) {
|
||||
unix, ok := ParsePrincipal("uid:1000")
|
||||
require.True(t, ok)
|
||||
|
||||
Reference in New Issue
Block a user