access_unix_test.go 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  1. //go:build !windows
  2. package localapi
  3. import (
  4. "context"
  5. "os"
  6. "os/user"
  7. "strconv"
  8. "syscall"
  9. "testing"
  10. "github.com/slackhq/nebula/config"
  11. "github.com/slackhq/nebula/test"
  12. "github.com/stretchr/testify/assert"
  13. "github.com/stretchr/testify/require"
  14. "golang.org/x/sys/unix"
  15. )
  16. // ownGroup returns a group this process may hand a socket to: its own effective group,
  17. // by name and by gid, which is the one chown accepts from an unprivileged user.
  18. func ownGroup(t *testing.T) (name, gid string) {
  19. t.Helper()
  20. gid = strconv.Itoa(os.Getegid())
  21. g, err := user.LookupGroupId(gid)
  22. require.NoError(t, err)
  23. return g.Name, gid
  24. }
  25. func socketGroup(t *testing.T, path string) uint32 {
  26. t.Helper()
  27. fi, err := os.Lstat(path)
  28. require.NoError(t, err)
  29. st, ok := fi.Sys().(*syscall.Stat_t)
  30. require.True(t, ok)
  31. return uint32(st.Gid)
  32. }
  33. // TestLookupGroupID: a group is named as the host names it or by its gid; a gid the
  34. // host does not list is accepted as chown would accept it, and a name it does not list
  35. // is a mistake to report.
  36. func TestLookupGroupID(t *testing.T) {
  37. name, gid := ownGroup(t)
  38. got, err := lookupGroupID(name)
  39. require.NoError(t, err)
  40. assert.Equal(t, gid, got)
  41. got, err = lookupGroupID(gid)
  42. require.NoError(t, err)
  43. assert.Equal(t, gid, got)
  44. got, err = lookupGroupID("4000000000")
  45. require.NoError(t, err)
  46. assert.Equal(t, "4000000000", got)
  47. _, err = lookupGroupID("no-such-group-nebula-test")
  48. require.ErrorContains(t, err, "does not exist")
  49. }
  50. // TestControlAccessFromConfig: no group is the owner-only socket; a group is 0660 with
  51. // that group; a group the host does not know is a config error naming the key.
  52. func TestControlAccessFromConfig(t *testing.T) {
  53. name, gid := ownGroup(t)
  54. load := func(body string) (Access, error) {
  55. c := config.NewC(test.NewLogger())
  56. require.NoError(t, c.LoadString(body))
  57. return controlAccessFrom(c)
  58. }
  59. acc, err := load("local_api: {}\n")
  60. require.NoError(t, err)
  61. assert.Equal(t, ownerOnly(), acc)
  62. assert.Nil(t, acc.Group)
  63. acc, err = load("local_api:\n control:\n group: " + name + "\n")
  64. require.NoError(t, err)
  65. assert.Equal(t, os.FileMode(0660), acc.Mode)
  66. require.NotNil(t, acc.Group)
  67. assert.Equal(t, name, acc.Group.Name)
  68. assert.Equal(t, gid, acc.Group.ID)
  69. _, err = load("local_api:\n control:\n group: no-such-group-nebula-test\n")
  70. require.ErrorContains(t, err, CtlGroupKey)
  71. assert.ErrorContains(t, err, "does not exist")
  72. }
  73. // TestControlSocketIsHandedToTheGroup: the socket is created with the group's gid and
  74. // 0660, whatever the umask, and a reload that takes the group away tightens the socket
  75. // in place, without moving it. The endpoint is shared, so this covers the info API too.
  76. func TestControlSocketIsHandedToTheGroup(t *testing.T) {
  77. old := unix.Umask(0077)
  78. defer unix.Umask(old)
  79. name, gid := ownGroup(t)
  80. want, err := strconv.ParseUint(gid, 10, 32)
  81. require.NoError(t, err)
  82. s := NewServer(test.NewLogger(), basicRegistry())
  83. path := testSocket(t)
  84. require.NoError(t, s.Rebind(context.Background(), path, Access{Mode: 0660, Group: &Group{Name: name, ID: gid}}))
  85. defer s.Stop()
  86. fi, err := os.Lstat(path)
  87. require.NoError(t, err)
  88. assert.Equal(t, os.FileMode(0660), fi.Mode().Perm())
  89. assert.Equal(t, uint32(want), socketGroup(t, path))
  90. require.NoError(t, s.Rebind(context.Background(), path, ownerOnly()))
  91. fi, err = os.Lstat(path)
  92. require.NoError(t, err)
  93. assert.Equal(t, os.FileMode(0600), fi.Mode().Perm())
  94. assert.Equal(t, path, s.Path())
  95. _, err = (&Client{Path: path}).Describe()
  96. require.NoError(t, err, "the same listener is still serving")
  97. }
  98. // TestControlSocketWithAGroupTrustsTheKernel: with a group sharing the socket, the
  99. // kernel's check on connect is the access control and the daemon's peer check stands
  100. // aside, so a connection the peer check alone would refuse is served. Without a group
  101. // the same connection is refused; see TestPeerCheckRefusesAnotherUser.
  102. func TestControlSocketWithAGroupTrustsTheKernel(t *testing.T) {
  103. if !peerCheckSupported {
  104. t.Skip("no peer credentials on this platform")
  105. }
  106. name, gid := ownGroup(t)
  107. me := currentUID()
  108. asUID(t, me+1)
  109. s := NewServer(test.NewLogger(), basicRegistry())
  110. asUID(t, me)
  111. path := testSocket(t)
  112. require.NoError(t, s.Rebind(context.Background(), path, Access{Mode: 0660, Group: &Group{Name: name, ID: gid}}))
  113. defer s.Stop()
  114. _, err := (&Client{Path: path}).Describe()
  115. require.NoError(t, err, "the socket's group admitted us; the peer check must not refuse")
  116. // Taking the group away on a reload puts the peer check back.
  117. require.NoError(t, s.Rebind(context.Background(), path, ownerOnly()))
  118. _, err = (&Client{Path: path}).Describe()
  119. require.ErrorContains(t, err, "not permitted")
  120. }
  121. // TestInfoAccessFromConfig: a group with no mode is 0660; a mode that gives the group
  122. // nothing is refused, with the mode that would; an explicit mode with group bits is
  123. // kept as given.
  124. func TestInfoAccessFromConfig(t *testing.T) {
  125. name, gid := ownGroup(t)
  126. load := func(body string) (infoConfig, error) {
  127. c := config.NewC(test.NewLogger())
  128. require.NoError(t, c.LoadString(body))
  129. return infoConfigFrom(c)
  130. }
  131. section := func(extra string) string {
  132. return "local_api:\n info:\n enabled: true\n socket: /run/nebula/info.sock\n" + extra
  133. }
  134. cfg, err := load(section(" group: " + name + "\n"))
  135. require.NoError(t, err)
  136. assert.Equal(t, os.FileMode(0660), cfg.Access.Mode)
  137. require.NotNil(t, cfg.Access.Group)
  138. assert.Equal(t, gid, cfg.Access.Group.ID)
  139. cfg, err = load(section(" group: " + name + "\n socket_mode: \"0664\"\n"))
  140. require.NoError(t, err)
  141. assert.Equal(t, os.FileMode(0664), cfg.Access.Mode)
  142. _, err = load(section(" group: " + name + "\n socket_mode: \"0600\"\n"))
  143. require.ErrorContains(t, err, "gives group "+name+" no access")
  144. require.ErrorContains(t, err, `"0660"`)
  145. cfg, err = load(section(""))
  146. require.NoError(t, err)
  147. assert.Equal(t, os.FileMode(0600), cfg.Access.Mode)
  148. assert.Nil(t, cfg.Access.Group)
  149. }
  150. // TestRemovingTheGroupHandsTheSocketBack: a reload that drops the group key leaves the
  151. // mode the operator set but returns the socket to the daemon's own group, so the old
  152. // group's members are no longer admitted. The socket is first handed to a group this
  153. // process belongs to other than its own, which not every host offers.
  154. func TestRemovingTheGroupHandsTheSocketBack(t *testing.T) {
  155. own := os.Getegid()
  156. groups, err := os.Getgroups()
  157. require.NoError(t, err)
  158. other := -1
  159. for _, g := range groups {
  160. if g != own {
  161. other = g
  162. break
  163. }
  164. }
  165. if other < 0 {
  166. t.Skip("this process belongs to no group but its own")
  167. }
  168. gid := strconv.Itoa(other)
  169. s, path := serveInfo(t, infoRegistry(), 0600)
  170. require.NoError(t, s.Rebind(context.Background(), path, Access{Mode: 0660, Group: &Group{Name: gid, ID: gid}}))
  171. assert.Equal(t, uint32(other), socketGroup(t, path))
  172. require.NoError(t, s.Rebind(context.Background(), path, Access{Mode: 0660}))
  173. assert.Equal(t, uint32(own), socketGroup(t, path), "the daemon's own group again")
  174. fi, err := os.Lstat(path)
  175. require.NoError(t, err)
  176. assert.Equal(t, os.FileMode(0660), fi.Mode().Perm(), "the mode is the operator's")
  177. }