1
0

sshd_reload_test.go 6.1 KB

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