sshd_reload_test.go 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. package nebula
  2. import (
  3. "crypto"
  4. "crypto/ed25519"
  5. "crypto/rand"
  6. "crypto/rsa"
  7. "encoding/pem"
  8. "fmt"
  9. "net"
  10. "os"
  11. "path/filepath"
  12. "testing"
  13. "time"
  14. "github.com/slackhq/nebula/commands"
  15. "github.com/slackhq/nebula/config"
  16. "github.com/slackhq/nebula/sshd"
  17. "github.com/slackhq/nebula/test"
  18. "github.com/stretchr/testify/assert"
  19. "github.com/stretchr/testify/require"
  20. "golang.org/x/crypto/ssh"
  21. )
  22. type sshdReloadHarness struct {
  23. c *config.C
  24. server *sshd.SSHServer
  25. keyFile string
  26. signer ssh.Signer
  27. listen string
  28. users []string
  29. extra string
  30. }
  31. func newSSHDReloadHarness(t *testing.T) *sshdReloadHarness {
  32. t.Helper()
  33. l := test.NewLogger()
  34. h := &sshdReloadHarness{keyFile: filepath.Join(t.TempDir(), "host_key"), listen: "127.0.0.1:0", users: []string{"test"}}
  35. h.writeHostKey(t, newEd25519Key(t))
  36. _, userKey, err := ed25519.GenerateKey(rand.Reader)
  37. require.NoError(t, err)
  38. h.signer, err = ssh.NewSignerFromKey(userKey)
  39. require.NoError(t, err)
  40. h.c = config.NewC(l)
  41. require.NoError(t, h.c.LoadString(h.config()))
  42. h.server, err = sshd.NewSSHServer(t.Context(), l, commands.New())
  43. require.NoError(t, err)
  44. wireSSHReload(l, h.server, h.c)
  45. run, err := configSSH(l, h.server, h.c, nil)
  46. require.NoError(t, err)
  47. go run()
  48. t.Cleanup(h.server.Stop)
  49. require.Eventually(t, h.server.Running, 5*time.Second, 10*time.Millisecond)
  50. return h
  51. }
  52. func newEd25519Key(t *testing.T) crypto.Signer {
  53. _, key, err := ed25519.GenerateKey(rand.Reader)
  54. require.NoError(t, err)
  55. return key
  56. }
  57. // writeHostKey replaces the host key file and returns the public key a client should now see
  58. func (h *sshdReloadHarness) writeHostKey(t *testing.T, key crypto.Signer) ssh.PublicKey {
  59. t.Helper()
  60. block, err := ssh.MarshalPrivateKey(key, "")
  61. require.NoError(t, err)
  62. require.NoError(t, os.WriteFile(h.keyFile, pem.EncodeToMemory(block), 0o600))
  63. pub, err := ssh.NewPublicKey(key.Public())
  64. require.NoError(t, err)
  65. return pub
  66. }
  67. func (h *sshdReloadHarness) config() string {
  68. users := ""
  69. for _, u := range h.users {
  70. users += fmt.Sprintf(" - user: %s\n keys: [%q]\n", u, string(ssh.MarshalAuthorizedKey(h.signer.PublicKey())))
  71. }
  72. return fmt.Sprintf("sshd:\n enabled: true\n listen: %s\n host_key: %s\n authorized_users:\n%s%s",
  73. h.listen, h.keyFile, users, h.extra)
  74. }
  75. // dial connects to wherever the server listens now, a restart on :0 moves it. hostKeyAlgorithms limits which host keys
  76. // the client accepts, none means any
  77. func (h *sshdReloadHarness) dial(hostKeyAlgorithms ...string) (*ssh.Client, ssh.PublicKey, error) {
  78. addr := h.server.Addr()
  79. if addr == nil {
  80. return nil, nil, fmt.Errorf("not listening")
  81. }
  82. var hostKey ssh.PublicKey
  83. c, err := ssh.Dial("tcp", addr.String(), &ssh.ClientConfig{
  84. User: "test",
  85. Auth: []ssh.AuthMethod{ssh.PublicKeys(h.signer)},
  86. HostKeyCallback: func(_ string, _ net.Addr, key ssh.PublicKey) error {
  87. hostKey = key
  88. return nil
  89. },
  90. HostKeyAlgorithms: hostKeyAlgorithms,
  91. Timeout: time.Second,
  92. })
  93. return c, hostKey, err
  94. }
  95. func (h *sshdReloadHarness) connect(t *testing.T) (*ssh.Client, ssh.PublicKey) {
  96. t.Helper()
  97. var client *ssh.Client
  98. var hostKey ssh.PublicKey
  99. require.Eventually(t, func() bool {
  100. var err error
  101. client, hostKey, err = h.dial()
  102. return err == nil
  103. }, 5*time.Second, 20*time.Millisecond)
  104. t.Cleanup(func() { _ = client.Close() })
  105. return client, hostKey
  106. }
  107. // dropped is true when the server closed the client's connection within wait
  108. func dropped(client *ssh.Client, wait time.Duration) bool {
  109. done := make(chan struct{})
  110. go func() {
  111. _ = client.Wait()
  112. close(done)
  113. }()
  114. select {
  115. case <-done:
  116. return true
  117. case <-time.After(wait):
  118. return false
  119. }
  120. }
  121. // Restarting sshd drops every open session, the one that asked for the reload included. A reload that touches nothing
  122. // sshd reads leaves it alone
  123. func TestSSHDReload_UnchangedKeepsSessions(t *testing.T) {
  124. h := newSSHDReloadHarness(t)
  125. client, _ := h.connect(t)
  126. require.NoError(t, h.c.ReloadConfigString(h.config()+"punchy:\n punch: true\n").Err)
  127. assert.False(t, dropped(client, 500*time.Millisecond), "an unchanged sshd was restarted")
  128. }
  129. // Every key the server reads restarts it when it changes
  130. func TestSSHDReload_ChangedConfigRestarts(t *testing.T) {
  131. cases := map[string]func(t *testing.T, h *sshdReloadHarness){
  132. "listen": func(t *testing.T, h *sshdReloadHarness) { h.listen = "localhost:0" },
  133. "authorized_users": func(t *testing.T, h *sshdReloadHarness) { h.users = append(h.users, "other") },
  134. "trusted_cas": func(t *testing.T, h *sshdReloadHarness) { h.extra = " trusted_cas: []\n" },
  135. "host_key": func(t *testing.T, h *sshdReloadHarness) {
  136. h.keyFile = filepath.Join(t.TempDir(), "other_host_key")
  137. h.writeHostKey(t, newEd25519Key(t))
  138. },
  139. }
  140. for name, change := range cases {
  141. t.Run(name, func(t *testing.T) {
  142. h := newSSHDReloadHarness(t)
  143. client, _ := h.connect(t)
  144. change(t, h)
  145. require.NoError(t, h.c.ReloadConfigString(h.config()).Err)
  146. assert.True(t, dropped(client, 5*time.Second), "a changed sshd config was not applied")
  147. require.Eventually(t, h.server.Running, 5*time.Second, 10*time.Millisecond)
  148. h.connect(t)
  149. })
  150. }
  151. }
  152. // The config can stay the same while the host key file it names changes. New connections get the new key, open
  153. // sessions are left alone
  154. func TestSSHDReload_ChangedHostKeyFile(t *testing.T) {
  155. h := newSSHDReloadHarness(t)
  156. client, _ := h.connect(t)
  157. want := h.writeHostKey(t, newEd25519Key(t))
  158. require.NoError(t, h.c.ReloadConfigString(h.config()).Err)
  159. _, got := h.connect(t)
  160. assert.Equal(t, want.Marshal(), got.Marshal(), "a new host key was not applied")
  161. assert.False(t, dropped(client, 500*time.Millisecond), "a new host key restarted sshd")
  162. }
  163. // A host key of another algorithm replaces the old one rather than being offered next to it
  164. func TestSSHDReload_HostKeyAlgorithmChange(t *testing.T) {
  165. h := newSSHDReloadHarness(t)
  166. h.connect(t)
  167. rsaKey, err := rsa.GenerateKey(rand.Reader, 2048)
  168. require.NoError(t, err)
  169. want := h.writeHostKey(t, rsaKey)
  170. require.NoError(t, h.c.ReloadConfigString(h.config()).Err)
  171. _, got := h.connect(t)
  172. assert.Equal(t, want.Marshal(), got.Marshal())
  173. _, _, err = h.dial(ssh.KeyAlgoED25519)
  174. assert.Error(t, err, "the old ed25519 host key is still offered")
  175. }
  176. // A server that isn't running, say its port was busy, is tried again on every reload as before
  177. func TestSSHDReload_NotRunningRestarts(t *testing.T) {
  178. h := newSSHDReloadHarness(t)
  179. h.server.Stop()
  180. require.Eventually(t, func() bool { return !h.server.Running() }, 5*time.Second, 10*time.Millisecond)
  181. require.NoError(t, h.c.ReloadConfigString(h.config()).Err)
  182. require.Eventually(t, h.server.Running, 5*time.Second, 10*time.Millisecond)
  183. h.connect(t)
  184. }
  185. // A host key that no longer loads stops sshd, the same as a config that doesn't
  186. func TestSSHDReload_BadHostKeyFileStops(t *testing.T) {
  187. h := newSSHDReloadHarness(t)
  188. h.connect(t)
  189. require.NoError(t, os.WriteFile(h.keyFile, []byte("not a key"), 0o600))
  190. assert.True(t, h.c.ReloadConfigString(h.config()).Failed)
  191. require.Eventually(t, func() bool { return !h.server.Running() }, 5*time.Second, 10*time.Millisecond)
  192. }
  193. // A trusted CA or authorized user that is skipped with a warning fails the reload, the valid ones are still used
  194. func TestSSHDReload_SkippedEntriesFail(t *testing.T) {
  195. cases := map[string]string{
  196. "bad trusted ca": " trusted_cas: [nope]\n",
  197. "user entry not a map": " - nope\n",
  198. "user missing": " - keys: [nope]\n",
  199. "bad key": " - user: other\n keys: nope\n",
  200. "key in a list not a string": " - user: other\n keys: [5]\n",
  201. "bad key in a list": " - user: other\n keys: [nope]\n",
  202. "keys missing": " - user: other\n",
  203. }
  204. for name, extra := range cases {
  205. t.Run(name, func(t *testing.T) {
  206. h := newSSHDReloadHarness(t)
  207. h.extra = extra
  208. res := h.c.ReloadConfigString(h.config())
  209. require.NoError(t, res.Err)
  210. assert.True(t, res.Failed)
  211. require.Eventually(t, h.server.Running, 5*time.Second, 10*time.Millisecond)
  212. h.connect(t)
  213. })
  214. }
  215. }