diff --git a/common/log/log.go b/common/log/log.go index 63e7f886..9628066c 100644 --- a/common/log/log.go +++ b/common/log/log.go @@ -99,7 +99,7 @@ func NewDefaultLogger(additionalWriterLogger io.Writer) *zap.Logger { }), zap.ErrorOutput(stderrSink), zap.AddCaller(), - zap.AddStacktrace(zapcore.DPanicLevel), + zap.AddStacktrace(zapcore.ErrorLevel), ) zap.ReplaceGlobals(logger) return logger diff --git a/common/proto/const.go b/common/proto/const.go index 3c9ef065..cbb7ce5c 100644 --- a/common/proto/const.go +++ b/common/proto/const.go @@ -51,8 +51,9 @@ const ( ClientLoginCallbackAddress string = "127.0.0.1:3587" - ClientVerbConnect = "connect" - ClientVerbExec = "exec" + ClientVerbConnect = "connect" + ClientVerbExec = "exec" + ClientVerbPlainExec = "plain-exec" SessionPhaseClientConnect = "client-connect" SessionPhaseClientConnected = "client-connected" diff --git a/gateway/api/session/session.go b/gateway/api/session/session.go index 89dce230..d5377eb5 100644 --- a/gateway/api/session/session.go +++ b/gateway/api/session/session.go @@ -139,7 +139,7 @@ func Post(c *gin.Context) { {0, "e", base64.StdEncoding.EncodeToString([]byte(err.Error()))}, }, } - if err = pgsession.New().Upsert(ctx, newSession); err != nil { + if err := pgsession.New().Upsert(ctx, newSession); err != nil { log.Errorf("unable to update session, err=%v", err) } c.JSON(http.StatusOK, clientexec.Response{ diff --git a/gateway/clientexec/clientexec.go b/gateway/clientexec/clientexec.go index 32557415..4f59b02f 100644 --- a/gateway/clientexec/clientexec.go +++ b/gateway/clientexec/clientexec.go @@ -2,6 +2,9 @@ package clientexec import ( "context" + "crypto/rand" + "crypto/sha256" + "encoding/base64" "fmt" "os" "path/filepath" @@ -26,6 +29,9 @@ var ( walLogPath = filepath.Join(plugintypes.AuditPath, "clientexec") walFolderTmpl = `%s/%s-%s-wal` maxResponseBytes = sessionwal.DefaultMaxRead + + // PlainExecSecretKey is a key to execute plain executions in the gateway securely by this package + PlainExecSecretKey string = generateSecureRandomKeyOrDie() ) func init() { _ = os.MkdirAll(walLogPath, 0755) } @@ -52,6 +58,7 @@ type Options struct { ConnectionName string BearerToken string Origin string + Verb string UserAgent string } @@ -97,10 +104,15 @@ func New(opts *Options) (*clientExec, error) { if opts.SessionID == "" { opts.SessionID = uuid.NewString() } + if opts.Origin == "" { opts.Origin = pb.ConnectionOriginClientAPI } + if opts.Verb == "" { + opts.Verb = pb.ClientVerbExec + } + folderName := fmt.Sprintf(walFolderTmpl, walLogPath, opts.OrgID, opts.SessionID) wlog, err := wal.Open(folderName, wal.DefaultOptions) if err != nil { @@ -122,8 +134,9 @@ func New(opts *Options) (*clientExec, error) { }, grpc.WithOption(grpc.OptionConnectionName, opts.ConnectionName), grpc.WithOption("origin", opts.Origin), - grpc.WithOption("verb", pb.ClientVerbExec), + grpc.WithOption("verb", opts.Verb), grpc.WithOption("session-id", opts.SessionID), + grpc.WithOption("plain-exec-key", PlainExecSecretKey), ) if err != nil { _ = wlog.Close() @@ -290,3 +303,16 @@ func (c *clientExec) readAll() ([]byte, bool, error) { return stdoutData, isTruncated, nil } + +func generateSecureRandomKeyOrDie() string { + secretRandomBytes := make([]byte, 32) + if _, err := rand.Read(secretRandomBytes); err != nil { + log.Fatalf("failed generating entropy, err=%v", err) + } + secretKey := base64.RawURLEncoding.EncodeToString(secretRandomBytes) + h := sha256.New() + if _, err := h.Write([]byte(secretKey)); err != nil { + log.Fatalf("failed hashing secret key, err=%v", err) + } + return fmt.Sprintf("%x", h.Sum(nil)) +} diff --git a/gateway/transport/agent.go b/gateway/transport/agent.go index 46d63942..beecd408 100644 --- a/gateway/transport/agent.go +++ b/gateway/transport/agent.go @@ -80,6 +80,7 @@ func (s *Server) listenAgentMessages(pctx *plugintypes.Context, stream *streamcl OrgID: pctx.OrgID, SID: pctx.SID, ConnectionName: proxyStream.PluginContext().ConnectionName, + Verb: proxyStream.PluginContext().ClientVerb, } if err := transportext.OnReceive(extContext, pkt); err != nil { diff --git a/gateway/transport/client.go b/gateway/transport/client.go index d2b10519..e69050d8 100644 --- a/gateway/transport/client.go +++ b/gateway/transport/client.go @@ -184,6 +184,7 @@ func (s *Server) processClientPacket(stream *streamclient.ProxyStream, pkt *pb.P OrgID: pctx.OrgID, SID: pctx.SID, ConnectionName: pctx.ConnectionName, + Verb: pctx.ClientVerb, } if err := transportext.OnReceive(extContext, pkt); err != nil { diff --git a/gateway/transport/extensions/extension.go b/gateway/transport/extensions/extension.go index c7eef68a..8c8705e6 100644 --- a/gateway/transport/extensions/extension.go +++ b/gateway/transport/extensions/extension.go @@ -19,9 +19,14 @@ type Context struct { SID string OrgID string ConnectionName string + Verb string } func OnReceive(ctx Context, pkt *proto.Packet) error { + if ctx.Verb == proto.ClientVerbPlainExec { + return nil + } + switch pkt.Type { case pbagent.SessionOpen: conn, err := models.GetConnectionGuardRailRules(ctx.OrgID, ctx.ConnectionName) diff --git a/gateway/transport/interceptors/auth/auth.go b/gateway/transport/interceptors/auth/auth.go index 73fdb17a..2e00d08d 100644 --- a/gateway/transport/interceptors/auth/auth.go +++ b/gateway/transport/interceptors/auth/auth.go @@ -2,6 +2,7 @@ package authinterceptor import ( "context" + "errors" "os" "strings" @@ -15,6 +16,7 @@ import ( apiconnections "github.com/hoophq/hoop/gateway/api/connections" localauthapi "github.com/hoophq/hoop/gateway/api/localauth" "github.com/hoophq/hoop/gateway/appconfig" + "github.com/hoophq/hoop/gateway/clientexec" "github.com/hoophq/hoop/gateway/pgrest" pgagents "github.com/hoophq/hoop/gateway/pgrest/agents" pgorgs "github.com/hoophq/hoop/gateway/pgrest/orgs" @@ -87,13 +89,23 @@ func (i *interceptor) StreamServerInterceptor(srv any, ss grpc.ServerStream, inf if !ok { return status.Error(codes.InvalidArgument, "missing context metadata") } - clientOrigin := md.Get("origin") + clientOrigin, clientVerb := md.Get("origin"), md.Get("verb") if len(clientOrigin) == 0 { md.Delete("authorization") log.Debugf("client missing origin, client-metadata=%v", md) return status.Error(codes.InvalidArgument, "missing client origin") } + if len(clientVerb) > 0 && clientVerb[0] == pb.ClientVerbPlainExec { + plainExecKey := md.Get("plain-exec-key") + if len(plainExecKey) == 0 || plainExecKey[0] != clientexec.PlainExecSecretKey { + errMsg := "failed validating plain execution, plain-exec-key attribute is missing or does not match" + log.Error(errMsg) + sentry.CaptureException(errors.New(errMsg)) + return status.Errorf(codes.Unauthenticated, "invalid authentication") + } + } + bearerToken, err := parseBearerToken(md) if err != nil { return err diff --git a/gateway/transport/streamclient/pluginruntime.go b/gateway/transport/streamclient/pluginruntime.go index c83ace73..eab7543e 100644 --- a/gateway/transport/streamclient/pluginruntime.go +++ b/gateway/transport/streamclient/pluginruntime.go @@ -17,6 +17,9 @@ type runtimePlugin struct { func loadRuntimePlugins(ctx plugintypes.Context) ([]runtimePlugin, error) { pluginsConfig := make([]runtimePlugin, 0) + if ctx.ClientVerb == pb.ClientVerbPlainExec { + return pluginsConfig, nil + } var nonRegisteredPlugins []string for _, p := range plugintypes.RegisteredPlugins { p1, err := pgplugins.New().FetchOne(ctx, p.Name())