From 64d66772d48129f9aad8c17402d7021e937b3e27 Mon Sep 17 00:00:00 2001 From: Abhinav Singh Date: Mon, 13 Jan 2025 15:56:57 +0000 Subject: [PATCH] feat: [CODE-2865]: ssh support changes (#3052) --- app/api/request/context.go | 6 ++ app/services/publickey/publickey.go | 7 +- ssh/log.go | 33 +++++++ ssh/middleware.go | 138 ++++++++++++++++++++++++++++ ssh/server.go | 48 ++++++---- ssh/wire.go | 5 +- types/config.go | 1 + 7 files changed, 219 insertions(+), 19 deletions(-) create mode 100644 ssh/log.go create mode 100644 ssh/middleware.go diff --git a/app/api/request/context.go b/app/api/request/context.go index e3f80cf1f..08d31be9b 100644 --- a/app/api/request/context.go +++ b/app/api/request/context.go @@ -22,6 +22,8 @@ import ( "github.com/harness/gitness/app/auth" "github.com/harness/gitness/types" + + "github.com/gliderlabs/ssh" ) type key int @@ -119,3 +121,7 @@ func RequestIDFrom(ctx context.Context) (string, bool) { v, ok := ctx.Value(requestIDKey).(string) return v, ok && v != "" } + +func WithRequestIDSSH(parent ssh.Context, v string) { + ssh.Context.SetValue(parent, requestIDKey, v) +} diff --git a/app/services/publickey/publickey.go b/app/services/publickey/publickey.go index 2213c21fe..a2ce687ca 100644 --- a/app/services/publickey/publickey.go +++ b/app/services/publickey/publickey.go @@ -28,7 +28,11 @@ import ( ) type Service interface { - ValidateKey(ctx context.Context, publicKey ssh.PublicKey, usage enum.PublicKeyUsage) (*types.PrincipalInfo, error) + ValidateKey(ctx context.Context, + username string, + publicKey ssh.PublicKey, + usage enum.PublicKeyUsage, + ) (*types.PrincipalInfo, error) } func NewService( @@ -50,6 +54,7 @@ type LocalService struct { // It updates the verified timestamp of the matched key to mark it as used. func (s LocalService) ValidateKey( ctx context.Context, + _ string, publicKey ssh.PublicKey, usage enum.PublicKeyUsage, ) (*types.PrincipalInfo, error) { diff --git a/ssh/log.go b/ssh/log.go new file mode 100644 index 000000000..a700b8f0a --- /dev/null +++ b/ssh/log.go @@ -0,0 +1,33 @@ +// Copyright 2023 Harness, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ssh + +import ( + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" +) + +const loggerKey contextKey = "logger" + +func getRequestID(reqID string) string { + if len(reqID) > 20 { + reqID = reqID[:20] + } + return reqID +} + +func getLoggerWithRequestID(sessionID string) zerolog.Logger { + return log.Logger.With().Str("request_id", getRequestID(sessionID)).Logger() +} diff --git a/ssh/middleware.go b/ssh/middleware.go new file mode 100644 index 000000000..33390dfe4 --- /dev/null +++ b/ssh/middleware.go @@ -0,0 +1,138 @@ +// Copyright 2023 Harness, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ssh + +import ( + "runtime/debug" + "time" + + "github.com/harness/gitness/app/api/request" + + "github.com/gliderlabs/ssh" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" +) + +type Middleware func(ssh.Handler) ssh.Handler + +// ChainMiddleware combines multiple middleware into a single ssh.Handler. +func ChainMiddleware(handler ssh.Handler, middlewares ...Middleware) ssh.Handler { + for i := len(middlewares) - 1; i >= 0; i-- { // Reverse order to maintain correct chaining + handler = middlewares[i](handler) + } + return handler +} + +// PanicRecoverMiddleware wraps the SSH handler to recover from panics and log them. +func PanicRecoverMiddleware(next ssh.Handler) ssh.Handler { + return func(s ssh.Session) { + defer func() { + if r := recover(); r != nil { + // Log the panic and stack trace + // Get the context and logger + ctx := s.Context() + logger := getLogger(ctx) + logger.Error().Msgf("encountered panic while processing ssh operation: %v\n%s", r, debug.Stack()) + _, _ = s.Write([]byte("Internal server error. Please try again later.\n")) + } + }() + + // Call the next handler + next(s) + } +} + +func HLogAccessLogHandler(next ssh.Handler) ssh.Handler { + return func(s ssh.Session) { + start := time.Now() + user := s.User() + remoteAddr := s.RemoteAddr() + command := s.Command() + + // Get the context and logger + ctx := s.Context() + logger := getLogger(ctx) + // Log session start + logger.Info(). + Str("ssh.user", user). + Str("ssh.remote", remoteAddr.String()). + Strs("ssh.command", command). + Msg("SSH session started") + + // Call the next handler + next(s) + + // Log session completion + duration := time.Since(start) + logger.Info(). + Dur("ssh.elapsed_ms", duration). + Str("ssh.user", user). + Msg("SSH session completed") + } +} + +func HLogRequestIDHandler(next ssh.Handler) ssh.Handler { + return func(s ssh.Session) { + sshCtx := s.Context() // This is ssh.Context + reqID := getRequestID(sshCtx.SessionID()) + request.WithRequestIDSSH(sshCtx, reqID) + + log := getLoggerWithRequestID(reqID) + sshCtx.SetValue(loggerKey, log) + + // continue serving request + next(s) + } +} + +type PublicKeyMiddleware func(next ssh.PublicKeyHandler) ssh.PublicKeyHandler + +func ChainPublicKeyMiddleware(handler ssh.PublicKeyHandler, middlewares ...PublicKeyMiddleware) ssh.PublicKeyHandler { + for i := len(middlewares) - 1; i >= 0; i-- { // Reverse order for correct chaining + handler = middlewares[i](handler) + } + return handler +} + +func LogPublicKeyMiddleware(next ssh.PublicKeyHandler) ssh.PublicKeyHandler { + return func(ctx ssh.Context, key ssh.PublicKey) bool { + reqID := getRequestID(ctx.SessionID()) + request.WithRequestIDSSH(ctx, reqID) + log := getLoggerWithRequestID(reqID) + start := time.Now() + + log.Info(). + Str("ssh.user", ctx.User()). + Str("ssh.remote", ctx.RemoteAddr().String()). + Msg("Public key authentication attempt") + + v := next(ctx, key) + // Log session completion + duration := time.Since(start) + log.Info(). + Dur("ssh.elapsed_ms", duration). + Str("ssh.user", ctx.User()). + Msg("Public key authentication attempt completed") + return v + } +} + +func getLogger(ctx ssh.Context) zerolog.Logger { + logger, ok := ctx.Value(loggerKey).(zerolog.Logger) + if !ok { + logger = log.Logger + } + return logger +} diff --git a/ssh/server.go b/ssh/server.go index 178d36567..5bef5ef2b 100644 --- a/ssh/server.go +++ b/ssh/server.go @@ -31,6 +31,7 @@ import ( "time" "github.com/harness/gitness/app/api/controller/repo" + "github.com/harness/gitness/app/api/request" "github.com/harness/gitness/app/auth" "github.com/harness/gitness/app/services/publickey" "github.com/harness/gitness/errors" @@ -76,8 +77,7 @@ var ( "hmac-sha2-256", "hmac-sha2-512", } - defaultServerKeyPath = "ssh/gitness.rsa" - KeepAliveMsg = "keepalive@openssh.com" + KeepAliveMsg = "keepalive@openssh.com" ) type Server struct { @@ -97,6 +97,8 @@ type Server struct { Verifier publickey.Service RepoCtrl *repo.Controller + + ServerKeyPath string } func (s *Server) sanitize() error { @@ -132,9 +134,17 @@ func (s *Server) ListenAndServe() error { return fmt.Errorf("failed to sanitize server defaults: %w", err) } s.internal = &ssh.Server{ - Addr: net.JoinHostPort(s.Host, strconv.Itoa(s.Port)), - Handler: s.sessionHandler, - PublicKeyHandler: s.publicKeyHandler, + Addr: net.JoinHostPort(s.Host, strconv.Itoa(s.Port)), + Handler: ChainMiddleware( + s.sessionHandler, + PanicRecoverMiddleware, + HLogRequestIDHandler, + HLogAccessLogHandler, + ), + PublicKeyHandler: ChainPublicKeyMiddleware( + s.publicKeyHandler, + LogPublicKeyMiddleware, + ), PtyCallback: func(ssh.Context, ssh.Pty) bool { return false }, @@ -147,7 +157,6 @@ func (s *Server) ListenAndServe() error { return config }, } - err = s.setupHostKeys() if err != nil { return fmt.Errorf("failed to setup host keys: %w", err) @@ -173,11 +182,11 @@ func (s *Server) setupHostKeys() error { if len(keys) == 0 { log.Debug().Msg("no host key provided - setup default key if it doesn't exist yet") - err := createKeyIfNotExists(defaultServerKeyPath) + err := createKeyIfNotExists(s.ServerKeyPath) if err != nil { - return fmt.Errorf("failed to setup default key %q: %w", defaultServerKeyPath, err) + return fmt.Errorf("failed to setup default key %q: %w", s.ServerKeyPath, err) } - keys = append(keys, defaultServerKeyPath) + keys = append(keys, s.ServerKeyPath) } // set keys to internal ssh server @@ -247,12 +256,14 @@ func (s *Server) sessionHandler(session ssh.Session) { ctx, cancel := context.WithCancel(session.Context()) defer cancel() + log := log.Logger.With().Logger() + ctx = request.WithRequestID(ctx, getRequestID(session.Context().SessionID())) + ctx = log.WithContext(ctx) // set keep alive connection if s.KeepAliveInterval > 0 { go sendKeepAliveMsg(ctx, session, s.KeepAliveInterval) } - err = s.RepoCtrl.GitServicePack( ctx, &auth.Session{ @@ -268,11 +279,12 @@ func (s *Server) sessionHandler(session ssh.Session) { }, repoRef, api.ServicePackOptions{ - Service: service, - Stdout: session, - Stdin: session, - Stderr: session.Stderr(), - Protocol: gitProtocol, + Service: service, + Stdout: session, + Stdin: session, + Stderr: session.Stderr(), + Protocol: gitProtocol, + StatelessRPC: false, }, ) if err != nil { @@ -304,6 +316,9 @@ func sendKeepAliveMsg(ctx context.Context, session ssh.Session, interval time.Du } func (s *Server) publicKeyHandler(ctx ssh.Context, key ssh.PublicKey) bool { + log := getLoggerWithRequestID(ctx.SessionID()) + request.WithRequestIDSSH(ctx, getRequestID(ctx.SessionID())) + if slices.Contains(publickey.DisallowedTypes, key.Type()) { log.Warn().Msgf("public key type not supported: %s", key.Type()) return false @@ -316,7 +331,7 @@ func (s *Server) publicKeyHandler(ctx ssh.Context, key ssh.PublicKey) bool { return false } - principal, err := s.Verifier.ValidateKey(ctx, key, enum.PublicKeyUsageAuth) + principal, err := s.Verifier.ValidateKey(ctx, ctx.User(), key, enum.PublicKeyUsageAuth) if errors.IsNotFound(err) { log.Debug().Err(err).Msg("public key is unknown") return false @@ -325,6 +340,7 @@ func (s *Server) publicKeyHandler(ctx ssh.Context, key ssh.PublicKey) bool { log.Warn().Err(err).Msg("failed to validate public key") return false } + log.Debug().Msg("public key verified") // check if we have a certificate if cert, ok := key.(*gossh.Certificate); ok { diff --git a/ssh/wire.go b/ssh/wire.go index 8a368ffb5..667e253e6 100644 --- a/ssh/wire.go +++ b/ssh/wire.go @@ -28,7 +28,7 @@ var WireSet = wire.NewSet( func ProvideServer( config *types.Config, - vierifier publickey.Service, + verifier publickey.Service, repoctrl *repo.Controller, ) *Server { return &Server{ @@ -42,7 +42,8 @@ func ProvideServer( TrustedUserCAKeys: config.SSH.TrustedUserCAKeys, TrustedUserCAKeysParsed: config.SSH.TrustedUserCAKeysParsed, KeepAliveInterval: config.SSH.KeepAliveInterval, - Verifier: vierifier, + Verifier: verifier, RepoCtrl: repoctrl, + ServerKeyPath: config.SSH.ServerKeyPath, } } diff --git a/types/config.go b/types/config.go index 779e3088a..587193557 100644 --- a/types/config.go +++ b/types/config.go @@ -155,6 +155,7 @@ type Config struct { TrustedUserCAKeysFile string `envconfig:"GITNESS_SSH_TRUSTED_USER_CA_KEYS_FILENAME"` TrustedUserCAKeysParsed []gossh.PublicKey KeepAliveInterval time.Duration `envconfig:"GITNESS_SSH_KEEP_ALIVE_INTERVAL" default:"5s"` + ServerKeyPath string `envconfig:"GITNESS_SSH_SERVER_KEY_PATH" default:"ssh/gitness.rsa"` } // CI defines configuration related to build executions.