mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-13 18:29:07 +02:00
Let an MDM policy set the groups the daemon sockets are restricted to
This commit is contained in:
@@ -6,6 +6,8 @@ import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/netbirdio/netbird/client/mdm"
|
||||
)
|
||||
|
||||
// Principal kinds an --allow-group value resolves to. The kind:value form is
|
||||
@@ -17,27 +19,33 @@ const (
|
||||
allowGroupKindSID = "sid"
|
||||
)
|
||||
|
||||
// resolveAllowGroups turns the values given on --allow-group into the typed
|
||||
// principals the daemon enforces on its sockets, dropping empty entries and
|
||||
// duplicates. Names are resolved through the platform's directory service, so
|
||||
// an entry that does not resolve is an error: a restriction the daemon cannot
|
||||
// evaluate must not become one that admits everybody. Values already in
|
||||
// kind:value form are validated and passed through, which is the form an
|
||||
// installed service hands to `service run`.
|
||||
// resolveAllowGroups turns configured group values into the typed principals
|
||||
// the daemon enforces on its sockets, dropping empty entries and duplicates.
|
||||
// Names are resolved through the platform's directory service, so an entry that
|
||||
// does not resolve is an error: a restriction the daemon cannot evaluate must
|
||||
// not become one that admits everybody. Values already in kind:value form are
|
||||
// validated and passed through, which is the form an installed service hands to
|
||||
// `service run`.
|
||||
//
|
||||
// Each value is split on commas, so one managed-configuration string listing
|
||||
// several principals behaves like the repeated flag. Neither a Unix group name
|
||||
// nor a Windows account name may contain a comma, so nothing is lost by it.
|
||||
func resolveAllowGroups(values []string) ([]string, error) {
|
||||
var resolved []string
|
||||
for _, value := range values {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
continue
|
||||
}
|
||||
for _, entry := range strings.Split(value, ",") {
|
||||
entry = strings.TrimSpace(entry)
|
||||
if entry == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
principal, err := resolveAllowGroup(value)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("resolve --allow-group %q: %w", value, err)
|
||||
}
|
||||
if !slices.Contains(resolved, principal) {
|
||||
resolved = append(resolved, principal)
|
||||
principal, err := resolveAllowGroup(entry)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("resolve allowed group %q: %w", entry, err)
|
||||
}
|
||||
if !slices.Contains(resolved, principal) {
|
||||
resolved = append(resolved, principal)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +55,30 @@ func resolveAllowGroups(values []string) ([]string, error) {
|
||||
return resolved, nil
|
||||
}
|
||||
|
||||
// daemonSocketPrincipals returns the principals the daemon restricts its
|
||||
// sockets to, and the configuration that asked for them. An MDM policy
|
||||
// overrides the install-time --allow-group in both directions, as the other
|
||||
// MDM-overridable service flags do: a managed host can be restricted without a
|
||||
// reinstall, and a managed empty value lifts a restriction the install set.
|
||||
//
|
||||
// A configured value that cannot be resolved is an error rather than an
|
||||
// unrestricted socket. The daemon then does not serve at all, which is loud
|
||||
// enough for an administrator to find and correct, where a silently ignored
|
||||
// restriction would leave every local account reaching the daemon on a host
|
||||
// meant to be locked down.
|
||||
func daemonSocketPrincipals(policy *mdm.Policy) ([]string, string, error) {
|
||||
values, source := allowGroups, "--allow-group"
|
||||
if managed, ok := policy.GetStringSlice(mdm.KeyAllowGroups); ok {
|
||||
values, source = managed, "MDM policy "+mdm.KeyAllowGroups
|
||||
}
|
||||
|
||||
resolved, err := resolveAllowGroups(values)
|
||||
if err != nil {
|
||||
return nil, source, fmt.Errorf("%s: %w", source, err)
|
||||
}
|
||||
return resolved, source, nil
|
||||
}
|
||||
|
||||
// cutKind splits a value on the kind separator. ok is false when the value
|
||||
// carries no kind, which is the case for a plain group or account name: neither
|
||||
// a Unix group name nor a Windows account name may contain a colon, so the
|
||||
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/netbirdio/netbird/client/mdm"
|
||||
)
|
||||
|
||||
func TestResolveAllowGroups_NoValuesLeavesSocketOpen(t *testing.T) {
|
||||
@@ -36,6 +38,68 @@ func TestResolveAllowGroups_UnresolvableIsAnError(t *testing.T) {
|
||||
assert.Contains(t, err.Error(), "no-such-group-08b1f0c4")
|
||||
}
|
||||
|
||||
func TestResolveAllowGroups_SplitsCommaSeparatedEntries(t *testing.T) {
|
||||
// One managed-configuration string listing several principals, as a
|
||||
// Windows REG_SZ delivers them.
|
||||
resolved, err := resolveAllowGroups([]string{testAllowGroupPrincipal + ", " + testAllowGroupPrincipal})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []string{testAllowGroupPrincipal}, resolved)
|
||||
}
|
||||
|
||||
func TestDaemonSocketPrincipals(t *testing.T) {
|
||||
original := allowGroups
|
||||
t.Cleanup(func() { allowGroups = original })
|
||||
|
||||
t.Run("no configuration leaves the sockets open", func(t *testing.T) {
|
||||
allowGroups = nil
|
||||
|
||||
resolved, _, err := daemonSocketPrincipals(mdm.NewPolicy(nil))
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, resolved)
|
||||
})
|
||||
|
||||
t.Run("the install-time flag applies when nothing is managed", func(t *testing.T) {
|
||||
allowGroups = []string{testAllowGroupPrincipal}
|
||||
|
||||
resolved, source, err := daemonSocketPrincipals(mdm.NewPolicy(nil))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []string{testAllowGroupPrincipal}, resolved)
|
||||
assert.Contains(t, source, "--allow-group")
|
||||
})
|
||||
|
||||
t.Run("an MDM policy overrides the install-time flag", func(t *testing.T) {
|
||||
allowGroups = nil
|
||||
policy := mdm.NewPolicy(map[string]any{mdm.KeyAllowGroups: testAllowGroupPrincipal})
|
||||
|
||||
resolved, source, err := daemonSocketPrincipals(policy)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []string{testAllowGroupPrincipal}, resolved)
|
||||
assert.Contains(t, source, mdm.KeyAllowGroups)
|
||||
})
|
||||
|
||||
t.Run("an empty MDM value lifts an install-time restriction", func(t *testing.T) {
|
||||
allowGroups = []string{testAllowGroupPrincipal}
|
||||
policy := mdm.NewPolicy(map[string]any{mdm.KeyAllowGroups: ""})
|
||||
|
||||
resolved, source, err := daemonSocketPrincipals(policy)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, resolved)
|
||||
assert.Contains(t, source, mdm.KeyAllowGroups)
|
||||
})
|
||||
|
||||
// A managed value that cannot be resolved must not fall back to the
|
||||
// install-time flag or to an open socket: the host was meant to be locked
|
||||
// down, so the daemon refuses to serve instead.
|
||||
t.Run("an unresolvable MDM value is an error", func(t *testing.T) {
|
||||
allowGroups = []string{testAllowGroupPrincipal}
|
||||
policy := mdm.NewPolicy(map[string]any{mdm.KeyAllowGroups: "no-such-group-08b1f0c4"})
|
||||
|
||||
_, _, err := daemonSocketPrincipals(policy)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), mdm.KeyAllowGroups)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCutKind(t *testing.T) {
|
||||
tests := []struct {
|
||||
value string
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
|
||||
"github.com/netbirdio/netbird/client/internal/daemonaddr"
|
||||
"github.com/netbirdio/netbird/client/internal/ipcauth"
|
||||
"github.com/netbirdio/netbird/client/mdm"
|
||||
"github.com/netbirdio/netbird/client/proto"
|
||||
"github.com/netbirdio/netbird/client/server"
|
||||
"github.com/netbirdio/netbird/client/system"
|
||||
@@ -89,12 +90,17 @@ func (p *program) Start(svc service.Service) error {
|
||||
)
|
||||
p.serv = grpc.NewServer(opts...)
|
||||
|
||||
allowed, err := resolveAllowGroups(allowGroups)
|
||||
allowed, source, err := daemonSocketPrincipals(mdm.LoadPolicy())
|
||||
if err != nil {
|
||||
// Logged as well as returned: the service manager is the only other
|
||||
// place this surfaces, and it reports a service that will not start
|
||||
// without saying why. Refusing to serve on a host whose lockdown
|
||||
// cannot be applied is deliberate, so the reason has to be findable.
|
||||
log.Errorf("failed to apply the daemon socket restriction, not serving: %v", err)
|
||||
return err
|
||||
}
|
||||
if len(allowed) > 0 {
|
||||
log.Infof("daemon sockets are restricted to %v", allowed)
|
||||
log.Infof("daemon sockets are restricted to %v by %s", allowed, source)
|
||||
}
|
||||
|
||||
daemonListener, jsonListener, err := listenDaemonSockets(allowed)
|
||||
|
||||
Reference in New Issue
Block a user