2019-07-18 21:29:16 +00:00
|
|
|
//+build !windows
|
|
|
|
|
|
|
|
package sshserver
|
|
|
|
|
|
|
|
import (
|
2019-09-06 21:37:58 +00:00
|
|
|
"encoding/json"
|
2019-07-18 21:29:16 +00:00
|
|
|
"fmt"
|
|
|
|
"io"
|
2019-09-11 18:46:23 +00:00
|
|
|
"net"
|
2019-09-04 15:37:53 +00:00
|
|
|
"runtime"
|
2019-09-06 21:37:58 +00:00
|
|
|
"strings"
|
2019-08-28 15:48:30 +00:00
|
|
|
"time"
|
2019-07-18 21:29:16 +00:00
|
|
|
|
2019-08-26 20:25:24 +00:00
|
|
|
"github.com/cloudflare/cloudflared/sshlog"
|
2019-07-18 21:29:16 +00:00
|
|
|
"github.com/gliderlabs/ssh"
|
2019-08-26 20:25:24 +00:00
|
|
|
"github.com/google/uuid"
|
2019-09-30 20:44:23 +00:00
|
|
|
"github.com/pkg/errors"
|
2019-07-18 21:29:16 +00:00
|
|
|
"github.com/sirupsen/logrus"
|
2019-09-30 20:44:23 +00:00
|
|
|
gossh "golang.org/x/crypto/ssh"
|
2019-07-18 21:29:16 +00:00
|
|
|
)
|
|
|
|
|
2019-09-06 21:37:58 +00:00
|
|
|
const (
|
2019-09-30 20:44:23 +00:00
|
|
|
auditEventStart = "session_start"
|
|
|
|
auditEventStop = "session_stop"
|
|
|
|
auditEventExec = "exec"
|
|
|
|
auditEventScp = "scp"
|
|
|
|
auditEventResize = "resize"
|
|
|
|
auditEventShell = "shell"
|
|
|
|
sshContextSessionID = "sessionID"
|
2019-09-18 19:11:12 +00:00
|
|
|
sshContextEventLogger = "eventLogger"
|
2019-09-06 21:37:58 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
type auditEvent struct {
|
|
|
|
Event string `json:"event,omitempty"`
|
|
|
|
EventType string `json:"event_type,omitempty"`
|
|
|
|
SessionID string `json:"session_id,omitempty"`
|
|
|
|
User string `json:"user,omitempty"`
|
|
|
|
Login string `json:"login,omitempty"`
|
|
|
|
Datetime string `json:"datetime,omitempty"`
|
|
|
|
IPAddress string `json:"ip_address,omitempty"`
|
|
|
|
}
|
|
|
|
|
2019-09-30 20:44:23 +00:00
|
|
|
type SSHProxy struct {
|
2019-07-18 21:29:16 +00:00
|
|
|
ssh.Server
|
2019-09-04 15:37:53 +00:00
|
|
|
logger *logrus.Logger
|
|
|
|
shutdownC chan struct{}
|
|
|
|
caCert ssh.PublicKey
|
|
|
|
logManager sshlog.Manager
|
2019-07-18 21:29:16 +00:00
|
|
|
}
|
|
|
|
|
2019-09-30 20:44:23 +00:00
|
|
|
// New creates a new SSHProxy and configures its host keys and authentication by the data provided
|
|
|
|
func New(logManager sshlog.Manager, logger *logrus.Logger, version, address string, shutdownC chan struct{}, idleTimeout, maxTimeout time.Duration) (*SSHProxy, error) {
|
|
|
|
sshProxy := SSHProxy{
|
2019-09-04 15:37:53 +00:00
|
|
|
logger: logger,
|
|
|
|
shutdownC: shutdownC,
|
|
|
|
logManager: logManager,
|
2019-08-22 16:36:21 +00:00
|
|
|
}
|
|
|
|
|
2019-09-30 20:44:23 +00:00
|
|
|
sshProxy.Server = ssh.Server{
|
|
|
|
Addr: address,
|
|
|
|
MaxTimeout: maxTimeout,
|
|
|
|
IdleTimeout: idleTimeout,
|
|
|
|
Version: fmt.Sprintf("SSH-2.0-Cloudflare-Access_%s_%s", version, runtime.GOOS),
|
|
|
|
ChannelHandlers: map[string]ssh.ChannelHandler{
|
|
|
|
"session": sshProxy.channelHandler,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
2019-09-11 18:46:23 +00:00
|
|
|
// AUTH-2050: This is a temporary workaround of a timing issue in the tunnel muxer to allow further testing.
|
2019-09-18 16:33:13 +00:00
|
|
|
// TODO: Remove this
|
2019-09-30 20:44:23 +00:00
|
|
|
sshProxy.ConnCallback = func(conn net.Conn) net.Conn {
|
2019-09-11 18:46:23 +00:00
|
|
|
time.Sleep(10 * time.Millisecond)
|
|
|
|
return conn
|
|
|
|
}
|
|
|
|
|
2019-09-30 20:44:23 +00:00
|
|
|
if err := sshProxy.configureHostKeys(); err != nil {
|
2019-08-19 18:51:59 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2019-09-30 20:44:23 +00:00
|
|
|
return &sshProxy, nil
|
2019-07-18 21:29:16 +00:00
|
|
|
}
|
|
|
|
|
2019-09-30 20:44:23 +00:00
|
|
|
// Start the SSH proxy listener to start handling SSH connections from clients
|
|
|
|
func (s *SSHProxy) Start() error {
|
2019-07-18 21:29:16 +00:00
|
|
|
s.logger.Infof("Starting SSH server at %s", s.Addr)
|
|
|
|
|
|
|
|
go func() {
|
|
|
|
<-s.shutdownC
|
|
|
|
if err := s.Close(); err != nil {
|
|
|
|
s.logger.WithError(err).Error("Cannot close SSH server")
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
|
|
|
return s.ListenAndServe()
|
|
|
|
}
|
|
|
|
|
2019-09-30 20:44:23 +00:00
|
|
|
// channelHandler proxies incoming and outgoing SSH traffic back and forth over an SSH Channel
|
|
|
|
func (s *SSHProxy) channelHandler(srv *ssh.Server, conn *gossh.ServerConn, newChan gossh.NewChannel, ctx ssh.Context) {
|
|
|
|
if err := s.configureAuditLogger(ctx); err != nil {
|
|
|
|
s.logger.WithError(err).Error("Failed to configure audit logging")
|
2019-07-18 21:29:16 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2019-09-30 20:44:23 +00:00
|
|
|
clientConfig := &gossh.ClientConfig{
|
|
|
|
User: conn.User(),
|
|
|
|
// AUTH-2103 TODO: proper host key check
|
|
|
|
HostKeyCallback: gossh.InsecureIgnoreHostKey(),
|
|
|
|
// AUTH-2114 TODO: replace with short lived cert auth
|
|
|
|
Auth: []gossh.AuthMethod{gossh.Password("test")},
|
|
|
|
ClientVersion: s.Version,
|
2019-07-18 21:29:16 +00:00
|
|
|
}
|
2019-09-06 21:37:58 +00:00
|
|
|
|
2019-09-30 20:44:23 +00:00
|
|
|
switch newChan.ChannelType() {
|
|
|
|
case "session":
|
|
|
|
// Accept incoming channel request from client
|
|
|
|
localChan, localChanReqs, err := newChan.Accept()
|
|
|
|
if err != nil {
|
|
|
|
s.logger.WithError(err).Error("Failed to accept session channel")
|
|
|
|
return
|
2019-09-18 19:11:12 +00:00
|
|
|
}
|
2019-09-30 20:44:23 +00:00
|
|
|
defer localChan.Close()
|
|
|
|
|
|
|
|
// AUTH-2088 TODO: retrieve ssh target from tunnel
|
|
|
|
// Create outgoing ssh connection to destination SSH server
|
|
|
|
client, err := gossh.Dial("tcp", "localhost:22", clientConfig)
|
2019-08-29 17:40:21 +00:00
|
|
|
if err != nil {
|
2019-09-30 20:44:23 +00:00
|
|
|
s.logger.WithError(err).Error("Failed to dial remote server")
|
2019-08-29 17:40:21 +00:00
|
|
|
return
|
2019-07-18 21:29:16 +00:00
|
|
|
}
|
2019-09-30 20:44:23 +00:00
|
|
|
defer client.Close()
|
|
|
|
|
|
|
|
// Open channel session channel to destination server
|
|
|
|
remoteChan, remoteChanReqs, err := client.OpenChannel("session", []byte{})
|
2019-08-29 17:40:21 +00:00
|
|
|
if err != nil {
|
2019-09-30 20:44:23 +00:00
|
|
|
s.logger.WithError(err).Error("Failed to open remote channel")
|
2019-08-29 17:40:21 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2019-09-30 20:44:23 +00:00
|
|
|
defer remoteChan.Close()
|
2019-08-29 17:40:21 +00:00
|
|
|
|
2019-09-30 20:44:23 +00:00
|
|
|
// Proxy ssh traffic back and forth between client and destination
|
|
|
|
s.proxyChannel(localChan, remoteChan, localChanReqs, remoteChanReqs, conn, ctx)
|
2019-08-26 20:25:24 +00:00
|
|
|
}
|
2019-09-30 20:44:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// proxyChannel couples two SSH channels and proxies SSH traffic and channel requests back and forth.
|
|
|
|
func (s *SSHProxy) proxyChannel(localChan, remoteChan gossh.Channel, localChanReqs, remoteChanReqs <-chan *gossh.Request, conn *gossh.ServerConn, ctx ssh.Context) {
|
|
|
|
done := make(chan struct{}, 2)
|
2019-08-26 20:25:24 +00:00
|
|
|
go func() {
|
2019-09-30 20:44:23 +00:00
|
|
|
if _, err := io.Copy(localChan, remoteChan); err != nil {
|
|
|
|
s.logger.WithError(err).Error("remote to local copy error")
|
2019-09-10 23:50:04 +00:00
|
|
|
}
|
2019-09-30 20:44:23 +00:00
|
|
|
done <- struct{}{}
|
2019-09-10 23:50:04 +00:00
|
|
|
}()
|
|
|
|
go func() {
|
2019-09-30 20:44:23 +00:00
|
|
|
if _, err := io.Copy(remoteChan, localChan); err != nil {
|
|
|
|
s.logger.WithError(err).Error("local to remote copy error")
|
2019-09-10 23:50:04 +00:00
|
|
|
}
|
2019-09-30 20:44:23 +00:00
|
|
|
done <- struct{}{}
|
2019-07-18 21:29:16 +00:00
|
|
|
}()
|
2019-09-30 20:44:23 +00:00
|
|
|
s.logAuditEvent(conn, "", auditEventStart, ctx)
|
|
|
|
defer s.logAuditEvent(conn, "", auditEventStop, ctx)
|
|
|
|
|
|
|
|
// Proxy channel requests
|
|
|
|
for {
|
|
|
|
select {
|
|
|
|
case req := <-localChanReqs:
|
|
|
|
if req == nil {
|
|
|
|
return
|
|
|
|
}
|
2019-07-18 21:29:16 +00:00
|
|
|
|
2019-09-30 20:44:23 +00:00
|
|
|
if err := s.forwardChannelRequest(remoteChan, req); err != nil {
|
|
|
|
s.logger.WithError(err).Error("Failed to forward request")
|
|
|
|
return
|
|
|
|
}
|
2019-07-18 21:29:16 +00:00
|
|
|
|
2019-09-30 20:44:23 +00:00
|
|
|
s.logChannelRequest(req, conn, ctx)
|
2019-07-18 21:29:16 +00:00
|
|
|
|
2019-09-30 20:44:23 +00:00
|
|
|
case req := <-remoteChanReqs:
|
|
|
|
if req == nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if err := s.forwardChannelRequest(localChan, req); err != nil {
|
|
|
|
s.logger.WithError(err).Error("Failed to forward request")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
case <-done:
|
|
|
|
return
|
|
|
|
}
|
2019-09-06 21:37:58 +00:00
|
|
|
}
|
2019-09-30 20:44:23 +00:00
|
|
|
}
|
2019-09-06 21:37:58 +00:00
|
|
|
|
2019-09-30 20:44:23 +00:00
|
|
|
// forwardChannelRequest sends request req to SSH channel sshChan, waits for reply, and sends the reply back.
|
|
|
|
func (s *SSHProxy) forwardChannelRequest(sshChan gossh.Channel, req *gossh.Request) error {
|
|
|
|
reply, err := sshChan.SendRequest(req.Type, req.WantReply, req.Payload)
|
2019-09-06 21:37:58 +00:00
|
|
|
if err != nil {
|
2019-09-30 20:44:23 +00:00
|
|
|
return errors.Wrap(err, "Failed to send request")
|
2019-09-06 21:37:58 +00:00
|
|
|
}
|
2019-09-30 20:44:23 +00:00
|
|
|
if err := req.Reply(reply, nil); err != nil {
|
|
|
|
return errors.Wrap(err, "Failed to reply to request")
|
2019-09-06 21:37:58 +00:00
|
|
|
}
|
2019-09-30 20:44:23 +00:00
|
|
|
return nil
|
2019-09-06 21:37:58 +00:00
|
|
|
}
|
|
|
|
|
2019-09-30 20:44:23 +00:00
|
|
|
// logChannelRequest creates an audit log for different types of channel requests
|
|
|
|
func (s *SSHProxy) logChannelRequest(req *gossh.Request, conn *gossh.ServerConn, ctx ssh.Context) {
|
|
|
|
var eventType string
|
|
|
|
var event string
|
|
|
|
switch req.Type {
|
|
|
|
case "exec":
|
|
|
|
var payload struct{ Value string }
|
|
|
|
if err := gossh.Unmarshal(req.Payload, &payload); err != nil {
|
|
|
|
s.logger.WithError(err).Errorf("Failed to unmarshal channel request payload: %s:%s", req.Type, req.Payload)
|
|
|
|
}
|
|
|
|
event = payload.Value
|
2019-08-20 17:48:47 +00:00
|
|
|
|
2019-09-30 20:44:23 +00:00
|
|
|
eventType = auditEventExec
|
|
|
|
if strings.HasPrefix(string(req.Payload), "scp") {
|
|
|
|
eventType = auditEventScp
|
|
|
|
}
|
|
|
|
case "shell":
|
|
|
|
eventType = auditEventShell
|
|
|
|
case "window-change":
|
|
|
|
eventType = auditEventResize
|
2019-08-29 17:40:21 +00:00
|
|
|
}
|
2019-09-30 20:44:23 +00:00
|
|
|
s.logAuditEvent(conn, event, eventType, ctx)
|
|
|
|
}
|
2019-09-10 23:50:04 +00:00
|
|
|
|
2019-09-30 20:44:23 +00:00
|
|
|
func (s *SSHProxy) configureAuditLogger(ctx ssh.Context) error {
|
|
|
|
sessionUUID, err := uuid.NewRandom()
|
2019-08-29 17:40:21 +00:00
|
|
|
if err != nil {
|
2019-09-30 20:44:23 +00:00
|
|
|
return errors.New("failed to generate session ID")
|
2019-08-29 17:40:21 +00:00
|
|
|
}
|
2019-09-30 20:44:23 +00:00
|
|
|
sessionID := sessionUUID.String()
|
2019-08-29 17:40:21 +00:00
|
|
|
|
2019-09-30 20:44:23 +00:00
|
|
|
eventLogger, err := s.logManager.NewLogger(fmt.Sprintf("%s-event.log", sessionID), s.logger)
|
2019-08-29 17:40:21 +00:00
|
|
|
if err != nil {
|
2019-09-30 20:44:23 +00:00
|
|
|
return errors.New("failed to create event log")
|
2019-08-29 17:40:21 +00:00
|
|
|
}
|
|
|
|
|
2019-09-30 20:44:23 +00:00
|
|
|
ctx.SetValue(sshContextSessionID, sessionID)
|
|
|
|
ctx.SetValue(sshContextEventLogger, eventLogger)
|
|
|
|
return nil
|
2019-08-29 17:40:21 +00:00
|
|
|
}
|
|
|
|
|
2019-09-30 20:44:23 +00:00
|
|
|
func (s *SSHProxy) logAuditEvent(conn *gossh.ServerConn, event, eventType string, ctx ssh.Context) {
|
|
|
|
sessionID, ok := ctx.Value(sshContextSessionID).(string)
|
2019-09-18 19:11:12 +00:00
|
|
|
if !ok {
|
|
|
|
s.logger.Error("Failed to retrieve sessionID from context")
|
|
|
|
return
|
|
|
|
}
|
2019-09-30 20:44:23 +00:00
|
|
|
writer, ok := ctx.Value(sshContextEventLogger).(io.WriteCloser)
|
2019-09-18 19:11:12 +00:00
|
|
|
if !ok {
|
|
|
|
s.logger.Error("Failed to retrieve eventLogger from context")
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2019-09-30 20:44:23 +00:00
|
|
|
ae := auditEvent{
|
|
|
|
Event: event,
|
2019-09-06 21:37:58 +00:00
|
|
|
EventType: eventType,
|
|
|
|
SessionID: sessionID,
|
2019-09-30 20:44:23 +00:00
|
|
|
User: conn.User(),
|
|
|
|
Login: conn.User(),
|
2019-09-06 21:37:58 +00:00
|
|
|
Datetime: time.Now().UTC().Format(time.RFC3339),
|
2019-09-30 20:44:23 +00:00
|
|
|
IPAddress: conn.RemoteAddr().String(),
|
2019-09-06 21:37:58 +00:00
|
|
|
}
|
2019-09-30 20:44:23 +00:00
|
|
|
data, err := json.Marshal(&ae)
|
2019-09-06 21:37:58 +00:00
|
|
|
if err != nil {
|
2019-09-10 23:50:04 +00:00
|
|
|
s.logger.WithError(err).Error("Failed to marshal audit event. malformed audit object")
|
2019-09-06 21:37:58 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
line := string(data) + "\n"
|
2019-09-10 23:50:04 +00:00
|
|
|
if _, err := writer.Write([]byte(line)); err != nil {
|
|
|
|
s.logger.WithError(err).Error("Failed to write audit event.")
|
|
|
|
}
|
|
|
|
|
2019-09-06 21:37:58 +00:00
|
|
|
}
|