mirror of
https://github.com/slackhq/nebula.git
synced 2026-11-15 09:16:57 +01:00
The info API is for other programs on the host, and on unix the only setting between "the user nebula runs as" and "everyone" was a manual chgrp that the next bind undid; on windows it was not served at all, because which accounts a pipe admits had not been decided. The control socket's `group` decides it, and the info API takes the same key. - `local_api.info.group`: the socket is handed to the group at 0660 unless `socket_mode` says otherwise, and a mode that gives a named group nothing is refused with the mode that would - windows: served on a named pipe, the group an ACE on it as on the control pipe; the e2e test's helpers dial unix sockets, so it still skips there - a changed group or mode on a reload is applied to the socket in place Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
238 lines
7.9 KiB
Go
238 lines
7.9 KiB
Go
//go:build !windows
|
|
|
|
package localapi
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"os"
|
|
"os/user"
|
|
"strconv"
|
|
"syscall"
|
|
"testing"
|
|
|
|
"github.com/slackhq/nebula/config"
|
|
"github.com/slackhq/nebula/test"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"golang.org/x/sys/unix"
|
|
)
|
|
|
|
// ownGroup returns a group this process may hand a socket to: its own effective group,
|
|
// by name and by gid, which is the one chown accepts from an unprivileged user.
|
|
func ownGroup(t *testing.T) (name, gid string) {
|
|
t.Helper()
|
|
gid = strconv.Itoa(os.Getegid())
|
|
g, err := user.LookupGroupId(gid)
|
|
require.NoError(t, err)
|
|
return g.Name, gid
|
|
}
|
|
|
|
func socketGroup(t *testing.T, path string) uint32 {
|
|
t.Helper()
|
|
fi, err := os.Lstat(path)
|
|
require.NoError(t, err)
|
|
st, ok := fi.Sys().(*syscall.Stat_t)
|
|
require.True(t, ok)
|
|
return uint32(st.Gid)
|
|
}
|
|
|
|
// TestLookupGroupID: a group is named as the host names it or by its gid; a gid the
|
|
// host does not list is accepted as chown would accept it, and a name it does not list
|
|
// is a mistake to report.
|
|
func TestLookupGroupID(t *testing.T) {
|
|
name, gid := ownGroup(t)
|
|
|
|
got, err := lookupGroupID(name)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, gid, got)
|
|
|
|
got, err = lookupGroupID(gid)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, gid, got)
|
|
|
|
got, err = lookupGroupID("4000000000")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "4000000000", got)
|
|
|
|
_, err = lookupGroupID("no-such-group-nebula-test")
|
|
require.ErrorContains(t, err, "does not exist")
|
|
}
|
|
|
|
// TestControlAccessFromConfig: no group is the owner-only socket; a group is 0660 with
|
|
// that group; a group the host does not know is a config error naming the key.
|
|
func TestControlAccessFromConfig(t *testing.T) {
|
|
name, gid := ownGroup(t)
|
|
load := func(body string) (Access, error) {
|
|
c := config.NewC(test.NewLogger())
|
|
require.NoError(t, c.LoadString(body))
|
|
return controlAccessFrom(c)
|
|
}
|
|
|
|
acc, err := load("local_api: {}\n")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, ownerOnly(), acc)
|
|
|
|
acc, err = load("local_api:\n control:\n group: " + name + "\n")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, os.FileMode(0660), acc.Mode)
|
|
assert.Equal(t, name, acc.Group.Name)
|
|
assert.Equal(t, gid, acc.Group.ID)
|
|
|
|
_, err = load("local_api:\n control:\n group: no-such-group-nebula-test\n")
|
|
require.ErrorContains(t, err, CtlGroupKey)
|
|
assert.ErrorContains(t, err, "does not exist")
|
|
}
|
|
|
|
// TestControlSocketIsHandedToTheGroup: the socket is created with the group's gid and
|
|
// 0660, whatever the umask, and a reload that takes the group away tightens the socket
|
|
// in place, without moving it.
|
|
func TestControlSocketIsHandedToTheGroup(t *testing.T) {
|
|
old := unix.Umask(0077)
|
|
defer unix.Umask(old)
|
|
|
|
name, gid := ownGroup(t)
|
|
want, err := strconv.ParseUint(gid, 10, 32)
|
|
require.NoError(t, err)
|
|
|
|
s := NewServer(test.NewLogger(), basicRegistry())
|
|
path := testSocket(t)
|
|
require.NoError(t, s.Rebind(context.Background(), path, Access{Mode: 0660, Group: Group{Name: name, ID: gid}}))
|
|
defer s.Stop()
|
|
|
|
fi, err := os.Lstat(path)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, os.FileMode(0660), fi.Mode().Perm())
|
|
assert.Equal(t, uint32(want), socketGroup(t, path))
|
|
|
|
require.NoError(t, s.Rebind(context.Background(), path, ownerOnly()))
|
|
fi, err = os.Lstat(path)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, os.FileMode(0600), fi.Mode().Perm())
|
|
assert.Equal(t, path, s.Path())
|
|
|
|
_, err = (&Client{Path: path}).Describe()
|
|
require.NoError(t, err, "the same listener is still serving")
|
|
}
|
|
|
|
// TestControlSocketWithAGroupTrustsTheKernel: with a group sharing the socket, the
|
|
// kernel's check on connect is the access control and the daemon's peer check stands
|
|
// aside, so a connection the peer check alone would refuse is served. Without a group
|
|
// the same connection is refused; see TestPeerCheckRefusesAnotherUser.
|
|
func TestControlSocketWithAGroupTrustsTheKernel(t *testing.T) {
|
|
if !peerCheckSupported {
|
|
t.Skip("no peer credentials on this platform")
|
|
}
|
|
name, gid := ownGroup(t)
|
|
|
|
me := currentUID()
|
|
asUID(t, me+1)
|
|
s := NewServer(test.NewLogger(), basicRegistry())
|
|
asUID(t, me)
|
|
|
|
path := testSocket(t)
|
|
require.NoError(t, s.Rebind(context.Background(), path, Access{Mode: 0660, Group: Group{Name: name, ID: gid}}))
|
|
defer s.Stop()
|
|
|
|
_, err := (&Client{Path: path}).Describe()
|
|
require.NoError(t, err, "the socket's group admitted us; the peer check must not refuse")
|
|
|
|
// Taking the group away on a reload puts the peer check back.
|
|
require.NoError(t, s.Rebind(context.Background(), path, ownerOnly()))
|
|
_, err = (&Client{Path: path}).Describe()
|
|
require.ErrorContains(t, err, "not permitted")
|
|
}
|
|
|
|
// TestInfoAccessFromConfig: a group with no mode is 0660; a mode that gives the group
|
|
// nothing is refused, with the mode that would; an explicit mode with group bits is
|
|
// kept as given.
|
|
func TestInfoAccessFromConfig(t *testing.T) {
|
|
name, gid := ownGroup(t)
|
|
load := func(body string) (infoConfig, error) {
|
|
c := config.NewC(test.NewLogger())
|
|
require.NoError(t, c.LoadString(body))
|
|
return infoConfigFrom(c)
|
|
}
|
|
section := func(extra string) string {
|
|
return "local_api:\n info:\n enabled: true\n socket: /run/nebula/info.sock\n" + extra
|
|
}
|
|
|
|
cfg, err := load(section(" group: " + name + "\n"))
|
|
require.NoError(t, err)
|
|
assert.Equal(t, os.FileMode(0660), cfg.Access.Mode)
|
|
require.NotNil(t, cfg.Access.Group)
|
|
assert.Equal(t, gid, cfg.Access.Group.ID)
|
|
|
|
cfg, err = load(section(" group: " + name + "\n socket_mode: \"0664\"\n"))
|
|
require.NoError(t, err)
|
|
assert.Equal(t, os.FileMode(0664), cfg.Access.Mode)
|
|
|
|
_, err = load(section(" group: " + name + "\n socket_mode: \"0600\"\n"))
|
|
require.ErrorContains(t, err, "gives group "+name+" no access")
|
|
require.ErrorContains(t, err, `"0660"`)
|
|
|
|
cfg, err = load(section(""))
|
|
require.NoError(t, err)
|
|
assert.Equal(t, os.FileMode(0600), cfg.Access.Mode)
|
|
assert.Nil(t, cfg.Access.Group)
|
|
}
|
|
|
|
// TestSocketIsHandedToTheGroup: the socket is created with the group's gid and the
|
|
// mode, and a changed group or mode on a reload is applied to the socket in place.
|
|
func TestSocketIsHandedToTheGroup(t *testing.T) {
|
|
name, gid := ownGroup(t)
|
|
group := &Group{Name: name, ID: gid}
|
|
want, err := strconv.ParseUint(gid, 10, 32)
|
|
require.NoError(t, err)
|
|
|
|
s, path := serveInfo(t, infoRegistry(), 0600)
|
|
require.NoError(t, s.Rebind(context.Background(), path, Access{Mode: 0660, Group: group}))
|
|
fi, err := os.Lstat(path)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, os.FileMode(0660), fi.Mode().Perm())
|
|
assert.Equal(t, uint32(want), socketGroup(t, path))
|
|
|
|
status, _ := test.HTTPGet(t, path, "/v1/whoami")
|
|
assert.Equal(t, http.StatusOK, status, "the same listener is still serving")
|
|
|
|
// The mode is the operator's to widen further.
|
|
require.NoError(t, s.Rebind(context.Background(), path, Access{Mode: 0664, Group: group}))
|
|
fi, err = os.Lstat(path)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, os.FileMode(0664), fi.Mode().Perm())
|
|
assert.Equal(t, path, s.Path())
|
|
}
|
|
|
|
// TestRemovingTheGroupHandsTheSocketBack: a reload that drops the group key leaves the
|
|
// mode the operator set but returns the socket to the daemon's own group, so the old
|
|
// group's members are no longer admitted. The socket is first handed to a group this
|
|
// process belongs to other than its own, which not every host offers.
|
|
func TestRemovingTheGroupHandsTheSocketBack(t *testing.T) {
|
|
own := os.Getegid()
|
|
groups, err := os.Getgroups()
|
|
require.NoError(t, err)
|
|
other := -1
|
|
for _, g := range groups {
|
|
if g != own {
|
|
other = g
|
|
break
|
|
}
|
|
}
|
|
if other < 0 {
|
|
t.Skip("this process belongs to no group but its own")
|
|
}
|
|
gid := strconv.Itoa(other)
|
|
|
|
s := NewServer(test.NewLogger(), basicRegistry())
|
|
path := testSocket(t)
|
|
require.NoError(t, s.Rebind(context.Background(), path, Access{Mode: 0660, Group: Group{Name: gid, ID: gid}}))
|
|
defer s.Stop()
|
|
assert.Equal(t, uint32(other), socketGroup(t, path))
|
|
|
|
require.NoError(t, s.Rebind(context.Background(), path, Access{Mode: 0660}))
|
|
assert.Equal(t, uint32(own), socketGroup(t, path), "the daemon's own group again")
|
|
fi, err := os.Lstat(path)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, os.FileMode(0660), fi.Mode().Perm(), "the mode is the operator's")
|
|
}
|