diff --git a/go.mod b/go.mod index e063e0db0..a99663d02 100644 --- a/go.mod +++ b/go.mod @@ -48,3 +48,5 @@ require ( k8s.io/client-go v0.0.0-20180806134042-1f13a808da65 k8s.io/kube-openapi v0.0.0-20180731170545-e3762e86a74c // indirect ) + +replace golang.org/x/crypto => github.com/doodlesbykumbi/sshr.crypto v0.0.0-20191016154246-b1c2b8ffdb9c diff --git a/go.sum b/go.sum index 3448cb6ea..a47e5618f 100644 --- a/go.sum +++ b/go.sum @@ -25,6 +25,10 @@ github.com/cyberark/summon v0.7.0/go.mod h1:S7grcxHeUxfL1vRTQUyq9jGK8yG6V/tSlLPQ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/doodlesbykumbi/sshr.crypto v0.0.0-20191016132516-629195587fad h1:dqregO1InniAGdH6O8tVPGPYGlL9kj2Tvkie9UYmlUs= +github.com/doodlesbykumbi/sshr.crypto v0.0.0-20191016132516-629195587fad/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= +github.com/doodlesbykumbi/sshr.crypto v0.0.0-20191016154246-b1c2b8ffdb9c h1:dTPA7kSjboC+CYBH0z7cwFKX2GwOb78yyLSqbVRb4qQ= +github.com/doodlesbykumbi/sshr.crypto v0.0.0-20191016154246-b1c2b8ffdb9c/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= diff --git a/internal/plugin/connectors/ssh/proxy_service.go b/internal/plugin/connectors/ssh/proxy_service.go index 638b5db8c..ff86d4fcd 100644 --- a/internal/plugin/connectors/ssh/proxy_service.go +++ b/internal/plugin/connectors/ssh/proxy_service.go @@ -5,6 +5,7 @@ import ( "io/ioutil" "net" "os" + "strings" validation "github.com/go-ozzo/ozzo-validation" "golang.org/x/crypto/ssh" @@ -59,21 +60,6 @@ func NewProxyService( }, nil } -func (proxy *proxyService) handleConnections(channels <-chan ssh.NewChannel) error { - var connector = ServiceConnector{ - channels: channels, - logger: proxy.logger, - } - - backendCredentials, err := proxy.retrieveCredentials() - defer internal.ZeroizeCredentials(backendCredentials) - if err != nil { - return fmt.Errorf("failed on retrieve credentials: %s", err) - } - - return connector.Connect(backendCredentials) -} - // Start initiates the net.Listener to listen for incoming connections // Listen accepts SSH connections and MITMs them using a ServiceConnector. func (proxy *proxyService) Start() error { @@ -128,32 +114,72 @@ func (proxy *proxyService) Start() error { return } - // https://godoc.org/golang.org/x/crypto/ssh#NewServerConn - conn, chans, reqs, err := ssh.NewServerConn(nConn, serverConfig) - if err != nil { - logger.Debugf("Failed to handshake: %s", err) - continue - } - logger.Debugf( - "New connection accepted for user %s from %s", - conn.User(), - conn.RemoteAddr(), - ) + tcpConn := nConn.(*net.TCPConn) + logger.Debugf("SSH Client connected. ClientIP=%v", tcpConn.RemoteAddr()) + - // The incoming Request channel must be serviced. go func() { - for req := range reqs { - logger.Debugf("Global SSH request : %v", req) + backendCredentials, err := proxy.retrieveCredentials() + defer internal.ZeroizeCredentials(backendCredentials) + if err != nil { + logger.Errorf("Failed on retrieve credentials: %s", err) + return } - }() - go func() { - if err := proxy.handleConnections(chans); err != nil { - logger.Errorf("Failed on handle connection: %s", err) + clientConfig := &ssh.ClientConfig{} + var address string + if addressBytes, ok := backendCredentials["address"]; ok { + address = string(addressBytes) + if !strings.Contains(address, ":") { + address = address + ":22" + } + } + + if user, ok := backendCredentials["user"]; ok { + clientConfig.User = string(user) + } + + logger.Debugf("Trying to connect with user: %s", clientConfig.User) + + if hostKeyStr, ok := backendCredentials["hostKey"]; ok { + var hostKey ssh.PublicKey + if hostKey, err = ssh.ParsePublicKey([]byte(hostKeyStr)); err != nil { + logger.Errorf("Unable to parse public key: %v", err) + return + } + clientConfig.HostKeyCallback = ssh.FixedHostKey(hostKey) + } else { + clientConfig.HostKeyCallback = ssh.InsecureIgnoreHostKey() + } + + if password, ok := backendCredentials["password"]; ok { + clientConfig.Auth = append(clientConfig.Auth, ssh.Password(string(password))) + } + + if privateKeyBytes, ok := backendCredentials["privateKey"]; ok { + var signer ssh.Signer + if signer, err = ssh.ParsePrivateKey([]byte(privateKeyBytes)); err != nil { + logger.Debugf("Unable to parse private key: %v", err) + return + } + + clientConfig.Auth = append(clientConfig.Auth, ssh.PublicKeys(signer)) + } + + p, err := newSSHProxyConn( + tcpConn, + serverConfig, + clientConfig, + address, + ) + if err != nil { + logger.Errorln("Connection from %v closed. %v", tcpConn.RemoteAddr(), err) return } + logger.Infof("Establish a proxy connection between %v and %v", tcpConn.RemoteAddr(), p.DestinationHost) - logger.Infof("Connection closed on %v", conn.LocalAddr()) + err = p.Wait() + logger.Debugf("Connection from %v closed. %v", tcpConn.RemoteAddr(), err) }() } }() @@ -167,3 +193,55 @@ func (proxy *proxyService) Stop() error { proxy.done = true return proxy.listener.Close() } + +func newSSHProxyConn( + conn net.Conn, + serverConfig *ssh.ServerConfig, + clientConfig *ssh.ClientConfig, + upstreamHostAndPort string, +) (proxyConn *ssh.ProxyConn, err error) { + d, err := ssh.NewDownstreamConn(conn, serverConfig) + if err != nil { + return nil, err + } + defer func() { + if proxyConn == nil { + d.Close() + } + }() + + authRequestMsg, err := d.GetAuthRequestMsg() + if err != nil { + return nil, err + } + + // use client provided user if client config is empty + if clientConfig.User == "" { + clientConfig.User = authRequestMsg.User + } + + upConn, err := net.Dial("tcp", upstreamHostAndPort) + if err != nil { + return nil, err + } + + u, err := ssh.NewUpstreamConn(upConn, clientConfig) + if err != nil { + return nil, err + } + defer func() { + if proxyConn == nil { + u.Close() + } + }() + p := &ssh.ProxyConn{ + Upstream: u, + Downstream: d, + } + + if err = p.AuthenticateProxyConn(clientConfig); err != nil { + return nil, err + } + + return p, nil +} diff --git a/internal/plugin/connectors/ssh/service_connector.go b/internal/plugin/connectors/ssh/service_connector.go deleted file mode 100644 index 65843db3c..000000000 --- a/internal/plugin/connectors/ssh/service_connector.go +++ /dev/null @@ -1,190 +0,0 @@ -package ssh - -import ( - "fmt" - "io" - "reflect" - "strings" - "time" - - validation "github.com/go-ozzo/ozzo-validation" - "golang.org/x/crypto/ssh" - - "github.com/cyberark/secretless-broker/pkg/secretless/log" - "github.com/cyberark/secretless-broker/pkg/secretless/plugin/connector" -) - -// ServerConfig is the configuration info for the target server -type ServerConfig struct { - Network string - Address string - ClientConfig ssh.ClientConfig -} - -// ServiceConnector contains the configuration and channels -type ServiceConnector struct { - channels <-chan ssh.NewChannel - logger log.Logger -} - -func (h *ServiceConnector) serverConfig(values map[string][]byte) (config ServerConfig, err error) { - keys := reflect.ValueOf(values).MapKeys() - h.logger.Debugf("SSH backend connection parameters: %s", keys) - - config.Network = "tcp" - if address, ok := values["address"]; ok { - config.Address = string(address) - if !strings.Contains(config.Address, ":") { - config.Address = config.Address + ":22" - } - } - - // XXX: Should this be the user that the client was trying to connect as? - config.ClientConfig.User = "root" - if user, ok := values["user"]; ok { - config.ClientConfig.User = string(user) - - } - - h.logger.Debugf("Trying to connect with user: %s", config.ClientConfig.User) - - if hostKeyStr, ok := values["hostKey"]; ok { - var hostKey ssh.PublicKey - if hostKey, err = ssh.ParsePublicKey([]byte(hostKeyStr)); err != nil { - h.logger.Debugf("Unable to parse public key: %v", err) - return - } - config.ClientConfig.HostKeyCallback = ssh.FixedHostKey(hostKey) - } else { - h.logger.Warnf("No SSH hostKey specified. Secretless will accept any backend host key!") - config.ClientConfig.HostKeyCallback = ssh.InsecureIgnoreHostKey() - } - - if privateKeyStr, ok := values["privateKey"]; ok { - var signer ssh.Signer - if signer, err = ssh.ParsePrivateKey([]byte(privateKeyStr)); err != nil { - h.logger.Debugf("Unable to parse private key: %v", err) - return - } - config.ClientConfig.Auth = []ssh.AuthMethod{ - ssh.PublicKeys(signer), - } - } - - return -} - -// Run opens the connection to the target server and proxies requests -func (h *ServiceConnector) Connect( - credentialValuesByID connector.CredentialValuesByID, -) error { - var err error - var serverConfig ServerConfig - var server ssh.Conn - - errors := validation.Errors{} - for _, credential := range [...]string{"address", "privateKey"} { - if _, hasCredential := credentialValuesByID[credential]; !hasCredential { - errors[credential] = fmt.Errorf("must have credential '%s'", credential) - } - } - - if err := errors.Filter(); err != nil { - return err - } - - if serverConfig, err = h.serverConfig(credentialValuesByID); err != nil { - return fmt.Errorf("ERROR: Could not resolve server config: %s\n", err) - } - - if server, err = ssh.Dial(serverConfig.Network, serverConfig.Address, &serverConfig.ClientConfig); err != nil { - return fmt.Errorf("Failed to dial SSH backend '%s': %s", serverConfig.Address, err) - } - - // Service the incoming Channel channel. - for newChannel := range h.channels { - serverChannel, serverRequests, err := server.OpenChannel(newChannel.ChannelType(), newChannel.ExtraData()) - if err != nil { - sshError := err.(*ssh.OpenChannelError) - if err := newChannel.Reject(sshError.Reason, sshError.Message); err != nil { - h.logger.Errorf("Failed to send new channel rejection : %s", err) - } - return err - } - - clientChannel, clientRequests, err := newChannel.Accept() - if err != nil { - h.logger.Errorf("Failed to accept client channel : %s", err) - serverChannel.Close() - return err - } - - go func() { - for clientRequest := range clientRequests { - h.logger.Debugf("Client request : %s", clientRequest.Type) - ok, err := serverChannel.SendRequest(clientRequest.Type, clientRequest.WantReply, clientRequest.Payload) - if err != nil { - h.logger.Warnf("Failed to send client request to server channel : %s", err) - } - if clientRequest.WantReply { - h.logger.Debugf("Server reply is %v", ok) - } - } - }() - - go func() { - for serverRequest := range serverRequests { - h.logger.Debugf("Server request : %s", serverRequest.Type) - ok, err := clientChannel.SendRequest(serverRequest.Type, serverRequest.WantReply, serverRequest.Payload) - if err != nil { - h.logger.Debugf("WARN: Failed to send server request to client channel : %s", err) - } - if serverRequest.WantReply { - h.logger.Debugf("Client reply is %v", ok) - } - } - }() - - // This delay is to prevent closing of channels on the other side - // too early when we receive an EOF but have not had the chance to - // pass that on to the client/server. - // TODO: Maybe use a better logic for handling EOF conditions - softDelay := time.Second * 2 - - go func() { - for { - data := make([]byte, 1024) - len, err := clientChannel.Read(data) - if err == io.EOF { - h.logger.Debugf("Client channel is closed") - time.Sleep(softDelay) - serverChannel.Close() - return - } - _, err = serverChannel.Write(data[0:len]) - if err != nil { - h.logger.Debugf("Error writing %d bytes to server channel : %s", len, err) - } - } - }() - - go func() { - for { - data := make([]byte, 1024) - len, err := serverChannel.Read(data) - if err == io.EOF { - h.logger.Debugf("Server channel is closed") - time.Sleep(softDelay) - clientChannel.Close() - return - } - _, err = clientChannel.Write(data[0:len]) - if err != nil { - h.logger.Debugf("Error writing %d bytes to client channel : %s", len, err) - } - } - }() - } - - return nil -}