server.go 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  1. package sshd
  2. import (
  3. "bytes"
  4. "context"
  5. "errors"
  6. "fmt"
  7. "log/slog"
  8. "net"
  9. "sync"
  10. "sync/atomic"
  11. "github.com/slackhq/nebula/commands"
  12. "golang.org/x/crypto/ssh"
  13. )
  14. type SSHServer struct {
  15. // config is replaced whole when the host key changes, ssh.ServerConfig.AddHostKey only replaces a key of the same
  16. // algorithm and appends any other
  17. config atomic.Pointer[ssh.ServerConfig]
  18. authenticate func(ssh.ConnMetadata, ssh.PublicKey) (*ssh.Permissions, error)
  19. l *slog.Logger
  20. certChecker *ssh.CertChecker
  21. // authLock guards trustedKeys and trustedCAs
  22. authLock sync.RWMutex
  23. // Map of user -> authorized keys
  24. trustedKeys map[string]map[string]bool
  25. trustedCAs []ssh.PublicKey
  26. // Available commands.
  27. commands *commands.Registry
  28. listener atomic.Pointer[net.Listener]
  29. // runs counts the Runs that are listening. A reload's Run can start before the one it replaced has returned
  30. runs atomic.Int32
  31. // addr is where the newest Run listens, nil when none is
  32. addr atomic.Pointer[net.Addr]
  33. // ctx parents per-Run contexts. Cancelling it (e.g. via Control.Stop) tears the server down even
  34. // across reloads, since each Run derives a fresh child rather than reusing this one directly.
  35. ctx context.Context
  36. }
  37. // NewSSHServer creates a new ssh server serving the supplied command registry and
  38. // prepares to listen. The ssh server's context is parented off the supplied ctx so
  39. // cancelling it (e.g. on Control.Stop) tears down active sessions and closes the listener.
  40. func NewSSHServer(ctx context.Context, l *slog.Logger, reg *commands.Registry) (*SSHServer, error) {
  41. s := &SSHServer{
  42. trustedKeys: make(map[string]map[string]bool),
  43. l: l,
  44. commands: reg,
  45. ctx: ctx,
  46. }
  47. cc := ssh.CertChecker{
  48. IsUserAuthority: func(auth ssh.PublicKey) bool {
  49. s.authLock.RLock()
  50. defer s.authLock.RUnlock()
  51. for _, ca := range s.trustedCAs {
  52. if bytes.Equal(ca.Marshal(), auth.Marshal()) {
  53. return true
  54. }
  55. }
  56. return false
  57. },
  58. UserKeyFallback: func(c ssh.ConnMetadata, pubKey ssh.PublicKey) (*ssh.Permissions, error) {
  59. pk := string(pubKey.Marshal())
  60. fp := ssh.FingerprintSHA256(pubKey)
  61. s.authLock.RLock()
  62. defer s.authLock.RUnlock()
  63. tk, ok := s.trustedKeys[c.User()]
  64. if !ok {
  65. return nil, fmt.Errorf("unknown user %s", c.User())
  66. }
  67. _, ok = tk[pk]
  68. if !ok {
  69. return nil, fmt.Errorf("unknown public key for %s (%s)", c.User(), fp)
  70. }
  71. return &ssh.Permissions{
  72. // Record the public key used for authentication.
  73. Extensions: map[string]string{
  74. "fp": fp,
  75. "user": c.User(),
  76. },
  77. }, nil
  78. },
  79. }
  80. s.authenticate = cc.Authenticate
  81. s.config.Store(s.newConfig())
  82. return s, nil
  83. }
  84. func (s *SSHServer) SetHostKey(hostPrivateKey []byte) error {
  85. private, err := ssh.ParsePrivateKey(hostPrivateKey)
  86. if err != nil {
  87. return fmt.Errorf("failed to parse private key: %s", err)
  88. }
  89. config := s.newConfig()
  90. config.AddHostKey(private)
  91. s.config.Store(config)
  92. return nil
  93. }
  94. func (s *SSHServer) newConfig() *ssh.ServerConfig {
  95. return &ssh.ServerConfig{
  96. PublicKeyCallback: s.authenticate,
  97. ServerVersion: fmt.Sprintf("SSH-2.0-Nebula???"),
  98. }
  99. }
  100. // Addr is the address the newest Run listens on, nil when none is.
  101. func (s *SSHServer) Addr() net.Addr {
  102. if a := s.addr.Load(); a != nil {
  103. return *a
  104. }
  105. return nil
  106. }
  107. // Running is true while a Run is listening.
  108. func (s *SSHServer) Running() bool {
  109. return s.runs.Load() > 0
  110. }
  111. func (s *SSHServer) ClearTrustedCAs() {
  112. s.authLock.Lock()
  113. s.trustedCAs = []ssh.PublicKey{}
  114. s.authLock.Unlock()
  115. }
  116. func (s *SSHServer) ClearAuthorizedKeys() {
  117. s.authLock.Lock()
  118. s.trustedKeys = make(map[string]map[string]bool)
  119. s.authLock.Unlock()
  120. }
  121. // AddTrustedCA adds a trusted CA for user certificates
  122. func (s *SSHServer) AddTrustedCA(pubKey string) error {
  123. pk, _, _, _, err := ssh.ParseAuthorizedKey([]byte(pubKey))
  124. if err != nil {
  125. return err
  126. }
  127. s.authLock.Lock()
  128. s.trustedCAs = append(s.trustedCAs, pk)
  129. s.authLock.Unlock()
  130. s.l.Info("Trusted CA key", "sshKey", pubKey)
  131. return nil
  132. }
  133. // AddAuthorizedKey adds an ssh public key for a user
  134. func (s *SSHServer) AddAuthorizedKey(user, pubKey string) error {
  135. pk, _, _, _, err := ssh.ParseAuthorizedKey([]byte(pubKey))
  136. if err != nil {
  137. return err
  138. }
  139. s.authLock.Lock()
  140. tk, ok := s.trustedKeys[user]
  141. if !ok {
  142. tk = make(map[string]bool)
  143. s.trustedKeys[user] = tk
  144. }
  145. tk[string(pk.Marshal())] = true
  146. s.authLock.Unlock()
  147. s.l.Info("Authorized ssh key",
  148. "sshKey", pubKey,
  149. "sshUser", user,
  150. )
  151. return nil
  152. }
  153. // Run begins listening and accepting connections. Each invocation derives a fresh per-Run context
  154. // from the constructor-supplied ctx so a Stop+Run sequence (used by config reload) starts clean
  155. // rather than carrying a permanently-cancelled context across runs.
  156. func (s *SSHServer) Run(addr string) error {
  157. if s.ctx.Err() != nil {
  158. return s.ctx.Err()
  159. }
  160. listener, err := net.Listen("tcp", addr)
  161. if err != nil {
  162. return err
  163. }
  164. // s.listener is the public handle Stop uses to interrupt the active run; listener (the local) is what
  165. // this run owns. They start equal but a fast reload may overwrite s.listener with the next run's
  166. // listener before this run's watcher fires, so each run must close its own listener via the local
  167. // reference.
  168. s.listener.Store(&listener)
  169. s.runs.Add(1)
  170. bound := listener.Addr()
  171. s.addr.Store(&bound)
  172. runCtx, cancel := context.WithCancel(s.ctx)
  173. defer cancel()
  174. // Deferred after cancel so they run first, a run on its way out no longer counts as listening
  175. defer s.runs.Add(-1)
  176. defer s.addr.CompareAndSwap(&bound, nil)
  177. // Close the listener when this run's context is cancelled. That can come from the parent
  178. // (Control.Stop), from Run returning normally (defer cancel above), or transitively when a sibling
  179. // run cancels through Stop closing the listener. net.Listener.Close is idempotent so a duplicate
  180. // close from Stop is benign.
  181. go func() {
  182. <-runCtx.Done()
  183. if err := listener.Close(); err != nil && !errors.Is(err, net.ErrClosed) {
  184. s.l.Warn("Failed to close the sshd listener", "error", err)
  185. }
  186. }()
  187. s.l.Info("SSH server is listening", "sshListener", bound)
  188. // Run loops until there is an error
  189. s.run(runCtx, listener)
  190. s.l.Info("SSH server stopped listening")
  191. // We don't return an error because run logs for us
  192. return nil
  193. }
  194. func (s *SSHServer) run(ctx context.Context, listener net.Listener) {
  195. for {
  196. c, err := listener.Accept()
  197. if err != nil {
  198. if !errors.Is(err, net.ErrClosed) {
  199. s.l.Warn("Error in listener, shutting down", "error", err)
  200. }
  201. return
  202. }
  203. go func(c net.Conn) {
  204. // NewServerConn may block while waiting for the client to complete the handshake.
  205. // Ensure that a bad client doesn't hurt us by checking for the parent context
  206. // cancellation before calling NewServerConn, and forcing the socket to close when
  207. // the context is cancelled.
  208. sessionContext, sessionCancel := context.WithCancel(ctx)
  209. go func() {
  210. <-sessionContext.Done()
  211. c.Close()
  212. }()
  213. conn, chans, reqs, err := ssh.NewServerConn(c, s.config.Load())
  214. fp := ""
  215. if conn != nil {
  216. fp = conn.Permissions.Extensions["fp"]
  217. }
  218. if err != nil {
  219. l := s.l.With(
  220. "error", err,
  221. "remoteAddress", c.RemoteAddr(),
  222. )
  223. if conn != nil {
  224. l = l.With("sshUser", conn.User())
  225. conn.Close()
  226. }
  227. if fp != "" {
  228. l = l.With("sshFingerprint", fp)
  229. }
  230. l.Warn("failed to handshake")
  231. sessionCancel()
  232. return
  233. }
  234. l := s.l.With("sshUser", conn.User())
  235. l.Info("ssh user logged in",
  236. "remoteAddress", c.RemoteAddr(),
  237. "sshFingerprint", fp,
  238. )
  239. NewSession(s.commands, conn, chans, sessionCancel, l.With("subsystem", "sshd.session"))
  240. go ssh.DiscardRequests(reqs)
  241. }(c)
  242. }
  243. }
  244. func (s *SSHServer) Stop() {
  245. if l := s.listener.Load(); l != nil {
  246. if err := (*l).Close(); err != nil && !errors.Is(err, net.ErrClosed) {
  247. s.l.Warn("Failed to close the sshd listener", "error", err)
  248. }
  249. }
  250. }