From b26d9e6bbd699e1ad928a013f81fd7c60c7fa3f2 Mon Sep 17 00:00:00 2001 From: "Luis Gustavo S. Barreto" Date: Tue, 23 Feb 2021 11:33:03 -0300 Subject: [PATCH 1/2] pkg: add webhook types --- pkg/api/webhook/types.go | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 pkg/api/webhook/types.go diff --git a/pkg/api/webhook/types.go b/pkg/api/webhook/types.go new file mode 100644 index 00000000000..e3df4002f12 --- /dev/null +++ b/pkg/api/webhook/types.go @@ -0,0 +1,31 @@ +package webhook + +// Webhook request headers +const ( + // A unique ID that identifies the delivered webhook + WebhookIDHeader = "X-SHELLHUB-WEBHOOK-ID" + // Name of the event that has been triggered + WebhookEventHeader = "X-SHELLHUB-WEBHOOK-EVENT" + // A signature created using the webhook secret key + WebhookSignatureHeader = "X-SHELLHUB-WEBHOOK-SIGNATURE" +) + +// Webhook event types +const ( + // A new connection was made to the SSH Server + WebhookIncomingConnectionEvent = "incoming_connection" +) + +// IncomingConnectionWebhookRequest is the body payload +type IncomingConnectionWebhookRequest struct { + Username string `json:"username"` + Hostname string `json:"hostname"` + Namespace string `json:"namespace"` + SourceIP string `json:"source_ip"` +} + +// IncommingConnectionWebhookResponse is the expected response body +type IncomingConnectionWebhookResponse struct { + // Timeout to wait for connection to be established + Timeout int `json:"timeout"` +} From 3e74a01827e538d66527708a1a67bc82f05172d8 Mon Sep 17 00:00:00 2001 From: Eduardo Kluwe Veiga Date: Tue, 2 Mar 2021 18:19:58 -0300 Subject: [PATCH 2/2] ssh: add webhook to delay connection for a specified time --- docker-compose.yml | 3 + pkg/api/client/client.go | 2 +- pkg/api/client/logger.go | 20 +++---- pkg/api/webhook/webhook.go | 119 +++++++++++++++++++++++++++++++++++++ ssh/server.go | 19 ++++++ ssh/session.go | 5 ++ 6 files changed, 157 insertions(+), 11 deletions(-) create mode 100644 pkg/api/webhook/webhook.go diff --git a/docker-compose.yml b/docker-compose.yml index 51ac48adb44..17dd23c49a3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,6 +8,9 @@ services: - PRIVATE_KEY=/run/secrets/ssh_private_key - SHELLHUB_ENTERPRISE=${SHELLHUB_ENTERPRISE} - RECORD_URL=${SHELLHUB_RECORD_URL} + - WEBHOOK_URL=${SHELLHUB_WEBHOOK_URL} + - WEBHOOK_PORT=${SHELLHUB_WEBHOOK_PORT} + - WEBHOOK_SCHEME=${SHELLHUB_WEBHOOK_SCHEME} ports: - "${SHELLHUB_SSH_PORT}:2222" secrets: diff --git a/pkg/api/client/client.go b/pkg/api/client/client.go index 89204d64ab6..336bba11f73 100644 --- a/pkg/api/client/client.go +++ b/pkg/api/client/client.go @@ -55,7 +55,7 @@ func NewClient(opts ...Opt) Client { } if c.logger != nil { - retryClient.Logger = &leveledLogger{c.logger} + retryClient.Logger = &LeveledLogger{c.logger} } return c diff --git a/pkg/api/client/logger.go b/pkg/api/client/logger.go index cb8d5a52a86..87cca2fc629 100644 --- a/pkg/api/client/logger.go +++ b/pkg/api/client/logger.go @@ -4,24 +4,24 @@ import ( "github.com/sirupsen/logrus" ) -type leveledLogger struct { - logger *logrus.Logger +type LeveledLogger struct { + Logger *logrus.Logger } -func (l *leveledLogger) Error(msg string, keysAndValues ...interface{}) { - l.logger.WithFields(toFields(keysAndValues)).Error(msg) +func (l *LeveledLogger) Error(msg string, keysAndValues ...interface{}) { + l.Logger.WithFields(toFields(keysAndValues)).Error(msg) } -func (l *leveledLogger) Info(msg string, keysAndValues ...interface{}) { - l.logger.WithFields(toFields(keysAndValues)).Info(msg) +func (l *LeveledLogger) Info(msg string, keysAndValues ...interface{}) { + l.Logger.WithFields(toFields(keysAndValues)).Info(msg) } -func (l *leveledLogger) Debug(msg string, keysAndValues ...interface{}) { - l.logger.WithFields(toFields(keysAndValues)).Debug(msg) +func (l *LeveledLogger) Debug(msg string, keysAndValues ...interface{}) { + l.Logger.WithFields(toFields(keysAndValues)).Debug(msg) } -func (l *leveledLogger) Warn(msg string, keysAndValues ...interface{}) { - l.logger.WithFields(toFields(keysAndValues)).Warn(msg) +func (l *LeveledLogger) Warn(msg string, keysAndValues ...interface{}) { + l.Logger.WithFields(toFields(keysAndValues)).Warn(msg) } func toFields(keysAndValues []interface{}) logrus.Fields { diff --git a/pkg/api/webhook/webhook.go b/pkg/api/webhook/webhook.go new file mode 100644 index 00000000000..09eb43d94bb --- /dev/null +++ b/pkg/api/webhook/webhook.go @@ -0,0 +1,119 @@ +package webhook + +import ( + "context" + "crypto/hmac" + "crypto/sha256" + "encoding/hex" + "errors" + "fmt" + "net" + "net/http" + "net/url" + "path" + + "github.com/hashicorp/go-retryablehttp" + "github.com/kelseyhightower/envconfig" + "github.com/parnurzeal/gorequest" + uuid "github.com/satori/go.uuid" + "github.com/shellhub-io/shellhub/pkg/api/client" + "github.com/sirupsen/logrus" +) + +const ( + ConnectionFailedErr = "Connection failed" + ForbiddenErr = "Not allowed" + UnknownErr = "Unknown error" +) + +type Webhook interface { + Connect(m map[string]string) (*IncomingConnectionWebhookResponse, error) +} + +type WebhookOptions struct { + WebhookURL string `envconfig:"webhook_url"` + WebhookPort int `envconfig:"webhook_port"` + WebhookScheme string `envconfig:"webhook_scheme"` +} + +func NewClient() Webhook { + retryClient := retryablehttp.NewClient() + retryClient.HTTPClient = &http.Client{} + retryClient.RetryMax = 3 + retryClient.CheckRetry = func(ctx context.Context, resp *http.Response, err error) (bool, error) { + if _, ok := err.(net.Error); ok { + return true, nil + } + + return retryablehttp.DefaultRetryPolicy(ctx, resp, err) + } + + gorequest.DisableTransportSwap = true + + httpClient := gorequest.New() + httpClient.Client = retryClient.StandardClient() + opts := WebhookOptions{} + err := envconfig.Process("", &opts) + if err != nil { + return nil + } + + w := &webhookClient{ + host: opts.WebhookURL, + port: opts.WebhookPort, + scheme: opts.WebhookScheme, + http: httpClient, + } + + if w.logger != nil { + retryClient.Logger = &client.LeveledLogger{w.logger} + } + + return w +} + +type webhookClient struct { + scheme string + host string + port int + http *gorequest.SuperAgent + logger *logrus.Logger +} + +func (w *webhookClient) Connect(m map[string]string) (*IncomingConnectionWebhookResponse, error) { + payload := &IncomingConnectionWebhookRequest{ + Username: m["username"], + Hostname: m["name"], + Namespace: m["domain"], + SourceIP: m["ip_address"], + } + secret := "secret" + uuid := uuid.Must(uuid.NewV4(), nil).String() + mac := hmac.New(sha256.New, []byte(secret)) + if _, err := mac.Write([]byte(fmt.Sprintf("%v", payload))); err != nil { + return nil, err + } + signature := mac.Sum(nil) + + var res *IncomingConnectionWebhookResponse + resp, _, errs := w.http.Post(buildURL(w, "/")).Set(WebhookIDHeader, uuid).Set(WebhookEventHeader, WebhookIncomingConnectionEvent).Set(WebhookSignatureHeader, hex.EncodeToString(signature)).Send(payload).EndStruct(&res) + if len(errs) > 0 { + return nil, errors.New(ConnectionFailedErr) + } + + if resp.StatusCode == http.StatusForbidden { + return nil, errors.New(ForbiddenErr) + } + + if resp.StatusCode == http.StatusOK { + return res, nil + } + + return nil, errors.New(UnknownErr) +} + +func buildURL(w *webhookClient, uri string) string { + u, _ := url.Parse(fmt.Sprintf("%s://%s:%d", w.scheme, w.host, w.port)) + u.Path = path.Join(u.Path, uri) + return u.String() +} diff --git a/ssh/server.go b/ssh/server.go index 699c3ebd00c..8dad1d61248 100644 --- a/ssh/server.go +++ b/ssh/server.go @@ -10,10 +10,12 @@ import ( "net" "net/http" "os" + "time" sshserver "github.com/gliderlabs/ssh" "github.com/pires/go-proxyproto" "github.com/shellhub-io/shellhub/pkg/api/client" + "github.com/shellhub-io/shellhub/pkg/api/webhook" "github.com/shellhub-io/shellhub/pkg/httptunnel" "github.com/sirupsen/logrus" "golang.org/x/crypto/ssh" @@ -71,6 +73,23 @@ func (s *Server) sessionHandler(session sshserver.Session) { return } + wh := webhook.NewClient() + if wh != nil { + res, err := wh.Connect(sess.Lookup) + if err == nil { + if sess.Pty { + session.Write([]byte(fmt.Sprintf("Wait %d seconds while the agent starts\n", res.Timeout))) // nolint:errcheck + } + time.Sleep(time.Duration(res.Timeout) * time.Second) + } else { + if err.Error() == webhook.ForbiddenErr { + session.Write([]byte("Connection rejected by Webhook endpoint\n")) // nolint:errcheck + session.Close() + return + } + } + } + conn, err := s.tunnel.Dial(context.Background(), sess.Target) if err != nil { logrus.WithFields(logrus.Fields{ diff --git a/ssh/session.go b/ssh/session.go index 097f972c2fe..7a228f544de 100644 --- a/ssh/session.go +++ b/ssh/session.go @@ -29,6 +29,8 @@ type Session struct { UID string `json:"uid"` IPAddress string `json:"ip_address"` Authenticated bool `json:"authenticated"` + Lookup map[string]string + Pty bool } type ConfigOptions struct { @@ -102,6 +104,7 @@ func NewSession(target string, session sshserver.Session) (*Session, error) { } s.Target = device.UID + s.Lookup = lookup if os.Getenv("SHELLHUB_ENTERPRISE") == "true" { res, _, errs := gorequest.New().Get("http://cloud-api:8080/internal/firewall/rules/evaluate").Query(lookup).End() @@ -109,6 +112,8 @@ func NewSession(target string, session sshserver.Session) (*Session, error) { return nil, ErrInvalidSessionTarget } } + _, _, isPty := s.session.Pty() + s.Pty = isPty return s, nil }