/* * Teleport * Copyright (C) 2025 Gravitational, Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ package git import ( "context" "io" "log/slog" "net" "strconv" "time" "github.com/google/uuid" "github.com/gravitational/trace" "github.com/jonboulle/clockwork" "github.com/prometheus/client_golang/prometheus" "golang.org/x/crypto/ssh" "github.com/gravitational/teleport" tracessh "github.com/gravitational/teleport/api/observability/tracing/ssh" apissh "github.com/gravitational/teleport/api/ssh" "github.com/gravitational/teleport/api/types" apievents "github.com/gravitational/teleport/api/types/events" "github.com/gravitational/teleport/lib/auth/authclient" "github.com/gravitational/teleport/lib/bpf" "github.com/gravitational/teleport/lib/client" "github.com/gravitational/teleport/lib/events" "github.com/gravitational/teleport/lib/observability/metrics" "github.com/gravitational/teleport/lib/services" rsession "github.com/gravitational/teleport/lib/session" "github.com/gravitational/teleport/lib/srv" "github.com/gravitational/teleport/lib/sshca" "github.com/gravitational/teleport/lib/sshutils" "github.com/gravitational/teleport/lib/utils" logutils "github.com/gravitational/teleport/lib/utils/log" "github.com/gravitational/teleport/session/pam/pamcfg" "github.com/gravitational/teleport/session/reexec" ) var ( execSessionCounter = prometheus.NewCounter(prometheus.CounterOpts{ Namespace: teleport.MetricNamespace, Subsystem: teleport.ComponentGit, Name: "exec_sessions_total", }) execFailureCounter = prometheus.NewCounter(prometheus.CounterOpts{ Namespace: teleport.MetricNamespace, Subsystem: teleport.ComponentGit, Name: "exec_failures_total", }) userKeyAuthFailureCounter = prometheus.NewCounter(prometheus.CounterOpts{ Namespace: teleport.MetricNamespace, Subsystem: teleport.ComponentGit, Name: "userkeyauth_failures_total", }) rbacFailureCounter = prometheus.NewCounter(prometheus.CounterOpts{ Namespace: teleport.MetricNamespace, Subsystem: teleport.ComponentGit, Name: "rbac_failures_total", }) forwardServerPrometheusCollectors = []prometheus.Collector{ execSessionCounter, userKeyAuthFailureCounter, rbacFailureCounter, } ) // ForwardServerConfig is the configuration for the ForwardServer. type ForwardServerConfig struct { // ParentContext is a parent context, used to signal global // closure ParentContext context.Context // TargetServer is the target server that represents the git-hosting // service. TargetServer types.Server // TargetConn is the TCP connection to the remote host. TargetConn net.Conn // AuthClient is a client connected to the Auth server of this local cluster. AuthClient authclient.ClientI // AccessPoint is a caching client that provides access to this local cluster. AccessPoint srv.AccessPoint // Emitter is audit events emitter Emitter events.StreamEmitter // LockWatcher is a lock watcher. LockWatcher *services.LockWatcher // KeyManager manages keys for git proxies. KeyManager *KeyManager // HostCertificate is the SSH host certificate this in-memory server presents // to the client. HostCertificate ssh.Signer // SrcAddr is the source address SrcAddr net.Addr // DstAddr is the destination address DstAddr net.Addr // HostUUID is the UUID of the underlying proxy that the forwarding server // is running in. HostUUID string // Ciphers is a list of ciphers that the server supports. If omitted, // the defaults will be used. Ciphers []string // KEXAlgorithms is a list of key exchange (KEX) algorithms that the // server supports. If omitted, the defaults will be used. KEXAlgorithms []string // MACAlgorithms is a list of message authentication codes (MAC) that // the server supports. If omitted the defaults will be used. MACAlgorithms []string // FIPS mode means Teleport started in a FedRAMP/FIPS compliant // configuration. FIPS bool // Clock is an optoinal clock to override default real time clock Clock clockwork.Clock } // CheckAndSetDefaults checks and sets default values for any missing fields. func (c *ForwardServerConfig) CheckAndSetDefaults() error { if c.TargetServer == nil { return trace.BadParameter("missing parameter TargetServer") } if c.TargetConn == nil { return trace.BadParameter("missing parameter TargetConn") } if c.AuthClient == nil { return trace.BadParameter("missing parameter AuthClient") } if c.AccessPoint == nil { return trace.BadParameter("missing parameter AccessPoint") } if c.Emitter == nil { return trace.BadParameter("missing parameter Emitter") } if c.KeyManager == nil { return trace.BadParameter("missing parameter KeyManager") } if c.HostCertificate == nil { return trace.BadParameter("missing parameter HostCertificate") } if c.ParentContext == nil { return trace.BadParameter("missing parameter ParentContext") } if c.LockWatcher == nil { return trace.BadParameter("missing parameter LockWatcher") } if c.SrcAddr == nil { return trace.BadParameter("source address required to identify client") } if c.DstAddr == nil { return trace.BadParameter("destination address required to identify client") } if c.Clock == nil { c.Clock = clockwork.NewRealClock() } return nil } // ForwardServer is an in-memory SSH server that forwards git commands to remote // git-hosting services like "github.com". type ForwardServer struct { events.StreamEmitter cfg *ForwardServerConfig logger *slog.Logger auth *srv.AuthHandlers reply *sshutils.Reply id string // serverConn is the server side of the pipe to the client connection. serverConn net.Conn // clientConn is the client side of the pipe to the client connection. clientConn net.Conn // remoteClient is the client connected to the git-hosting service. remoteClient *tracessh.Client // verifyRemoteHost is a callback to verify remote host like "github.com". // Can be overridden for tests. Defaults to cfg.KeyManager.HostKeyCallback. verifyRemoteHost ssh.HostKeyCallback // makeRemoteSigner generates the client certificate for connecting to the // remote server. Can be overridden for tests. Defaults to makeRemoteSigner. makeRemoteSigner func(context.Context, *ForwardServerConfig, srv.IdentityContext) (ssh.Signer, error) } // Dial returns the client connection of the pipe func (s *ForwardServer) Dial() (net.Conn, error) { return s.clientConn, nil } // NewForwardServer creates a new in-memory SSH server that forwards git // commands to remote git-hosting services like "github.com". func NewForwardServer(cfg *ForwardServerConfig) (*ForwardServer, error) { if err := cfg.CheckAndSetDefaults(); err != nil { return nil, trace.Wrap(err) } if err := metrics.RegisterPrometheusCollectors(forwardServerPrometheusCollectors...); err != nil { return nil, trace.Wrap(err) } serverConn, clientConn, err := utils.DualPipeNetConn(cfg.SrcAddr, cfg.DstAddr) if err != nil { return nil, trace.Wrap(err) } verifyRemoteHost, err := cfg.KeyManager.HostKeyCallback(cfg.TargetServer) if err != nil { return nil, trace.Wrap(err) } logger := slog.With(teleport.ComponentKey, teleport.ComponentForwardingGit, "src_addr", cfg.SrcAddr.String(), "dst_addr", cfg.DstAddr.String(), ) s := &ForwardServer{ StreamEmitter: cfg.Emitter, cfg: cfg, serverConn: serverConn, clientConn: clientConn, logger: logger, reply: sshutils.NewReply(logger), id: uuid.NewString(), verifyRemoteHost: verifyRemoteHost, makeRemoteSigner: makeRemoteSigner, } // TODO(greedy52) extract common parts from srv.NewAuthHandlers like // CreateIdentityContext and UserKeyAuth to a common package. s.auth, err = srv.NewAuthHandlers(&srv.AuthHandlerConfig{ Server: s, Component: teleport.ComponentForwardingGit, Emitter: s.cfg.Emitter, AccessPoint: cfg.AccessPoint, TargetServer: cfg.TargetServer, FIPS: cfg.FIPS, Clock: cfg.Clock, OnRBACFailure: s.onRBACFailure, ValidatedMFAChallengeVerifier: s.cfg.AuthClient.MFAServiceClientV2(), }) if err != nil { return nil, trace.Wrap(err) } return s, nil } // Serve starts an SSH server that forwards git commands. func (s *ForwardServer) Serve() { defer s.close() s.logger.DebugContext(s.cfg.ParentContext, "Starting forwarding git") defer s.logger.DebugContext(s.cfg.ParentContext, "Finished forwarding git") server, err := sshutils.NewServer( teleport.ComponentForwardingGit, utils.NetAddr{}, /* empty addr, this is one time use so no use for listener*/ sshutils.NewChanHandlerFunc(s.onChannel), sshutils.StaticHostSigners(s.cfg.HostCertificate), sshutils.AuthMethods{ PublicKey: s.userKeyAuth, VerifiedPublicKey: s.auth.VerifiedPublicKeyCallback, }, sshutils.SetFIPS(s.cfg.FIPS), sshutils.SetCiphers(s.cfg.Ciphers), sshutils.SetKEXAlgorithms(s.cfg.KEXAlgorithms), sshutils.SetMACAlgorithms(s.cfg.MACAlgorithms), sshutils.SetClock(s.cfg.Clock), sshutils.SetNewConnHandler(sshutils.NewConnHandlerFunc(s.onConnection)), ) if err != nil { s.logger.ErrorContext(s.cfg.ParentContext, "Failed to create git forward server", "error", err) return } server.HandleConnection(s.serverConn) } func (s *ForwardServer) close() { if err := s.serverConn.Close(); err != nil && !utils.IsOKNetworkError(err) { s.logger.WarnContext(s.cfg.ParentContext, "Failed to close server conn", "error", err) } if err := s.clientConn.Close(); err != nil && !utils.IsOKNetworkError(err) { s.logger.WarnContext(s.cfg.ParentContext, "Failed to close client conn", "error", err) } if err := s.cfg.TargetConn.Close(); err != nil && !utils.IsOKNetworkError(err) { s.logger.WarnContext(s.cfg.ParentContext, "Failed to close target conn", "error", err) } } func (s *ForwardServer) userKeyAuth(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) { cert, ok := key.(*ssh.Certificate) if !ok { return nil, trace.BadParameter("unsupported key type") } ident, err := sshca.DecodeIdentity(cert) if err != nil { return nil, trace.Wrap(err) } if ident.GitHubUserID == "" { return nil, trace.BadParameter("missing GitHub user ID") } // Verify incoming user is "git" and override it with any valid principle // to bypass principle validation. if conn.User() != gitUser { return nil, trace.BadParameter("only git is expected as user for git connections") } if len(ident.Principals) > 0 { conn = sshutils.NewSSHConnMetadataWithUser(conn, ident.Principals[0]) } // Use auth.PublicKeyCallback to verify user cert is signed by UserCA and to evaluate // RBAC permissions. permissions, err := s.auth.PublicKeyCallback(conn, key) if err != nil { userKeyAuthFailureCounter.Inc() return nil, trace.Wrap(err) } if _, ok := permissions.Extensions[utils.ExtIntGitForwardingPermit]; !ok { return nil, trace.Errorf("missing git forwarding permit (this is a bug)") } return permissions, nil } // onRBACFailure is a callback invoked by the auth handler when auth fails specifically due to an RBAC // failure. Used to update events/metrics. func (s *ForwardServer) onRBACFailure(conn ssh.ConnMetadata, ident *sshca.Identity, err error) { rbacFailureCounter.Inc() s.emitEvent(&apievents.AuthAttempt{ Metadata: apievents.Metadata{ Type: events.AuthAttemptEvent, Code: events.AuthAttemptFailureCode, }, UserMetadata: apievents.UserMetadata{ Login: gitUser, User: ident.Username, TrustedDevice: ident.GetDeviceMetadata(), BeamID: ident.BeamID, }, ConnectionMetadata: apievents.ConnectionMetadata{ LocalAddr: conn.LocalAddr().String(), RemoteAddr: conn.RemoteAddr().String(), }, Status: apievents.Status{ Success: false, Error: err.Error(), }, }) } func (s *ForwardServer) onConnection(ctx context.Context, ccx *sshutils.ConnectionContext) (context.Context, error) { s.logger.Log(ctx, logutils.TraceLevel, "Handling new connection") identityCtx, err := s.auth.CreateIdentityContext(ccx.ServerConn) if err != nil { return nil, trace.Wrap(err) } if err := s.initRemoteConn(ctx, ccx, identityCtx); err != nil { s.logger.DebugContext(ctx, "onConnection failed", "error", err) return ctx, trace.Wrap(err) } // TODO(greedy52) decouple from srv.NewServerContext. We only need // connection monitoring. serverCtx, err := srv.NewServerContext(ctx, ccx, s, identityCtx, nil) if err != nil { return nil, trace.Wrap(err) } s.logger.Log(ctx, logutils.TraceLevel, "New connection accepted") ccx.AddCloser(serverCtx) return context.WithValue(ctx, serverContextKey{}, serverCtx), nil } func (s *ForwardServer) onChannel(ctx context.Context, ccx *sshutils.ConnectionContext, nch ssh.NewChannel) { s.logger.DebugContext(ctx, "Handling channel request", "channel", nch.ChannelType()) serverCtx, ok := ctx.Value(serverContextKey{}).(*srv.ServerContext) if !ok { // This should not happen. Double check just in case. s.reply.RejectChannel(ctx, nch, ssh.ResourceShortage, "server context not found") return } // Only expecting a session to execute a command. if nch.ChannelType() != teleport.ChanSession { s.reply.RejectUnknownChannel(ctx, nch) return } // sessionParams will not be passed by old clients (< v19) or OpenSSH clients. sessionParams, err := tracessh.ParseSessionParams(nch.ExtraData()) if err != nil { s.reply.RejectWithAcceptError(ctx, nch, err) return } if s.remoteClient == nil { s.reply.RejectWithNewRemoteSessionError(ctx, nch, trace.NotFound("missing remote client")) return } remoteSession, err := s.remoteClient.NewSessionWithParams(ctx, sessionParams) if err != nil { s.reply.RejectWithNewRemoteSessionError(ctx, nch, err) return } defer remoteSession.Close() ch, in, err := nch.Accept() if err != nil { s.reply.RejectWithAcceptError(ctx, nch, err) return } defer ch.Close() sctx := newSessionContext(serverCtx, ch, remoteSession) for { select { case req := <-in: if req == nil { s.logger.DebugContext(ctx, "Client disconnected", "remote_addr", ccx.ServerConn.RemoteAddr()) return } ok, err := s.dispatch(ctx, sctx, req) if err != nil { s.reply.ReplyError(ctx, req, err) return } s.reply.ReplyRequest(ctx, req, ok, nil) case execErr := <-sctx.waitExec: code := sshutils.ExitCodeFromExecError(execErr) s.logger.DebugContext(ctx, "Exec request complete", "code", code) s.reply.SendExitStatus(ctx, ch, code) return case <-ctx.Done(): return } } } type sessionContext struct { *srv.ServerContext channel ssh.Channel remoteSession *tracessh.Session waitExec chan error sessionID rsession.ID } func newSessionContext(serverCtx *srv.ServerContext, ch ssh.Channel, remoteSession *tracessh.Session) *sessionContext { return &sessionContext{ ServerContext: serverCtx, channel: ch, remoteSession: remoteSession, waitExec: make(chan error, 1), sessionID: rsession.NewID(), } } func (c *sessionContext) GetSessionMetadata() apievents.SessionMetadata { // Overwrite with our own session ID. metadata := c.ServerContext.GetSessionMetadata() metadata.SessionID = c.sessionID.String() return metadata } // dispatch executes an incoming request. If successful, it returns the ok value // for the reply. Otherwise, it returns the error it encountered. func (s *ForwardServer) dispatch(ctx context.Context, sctx *sessionContext, req *ssh.Request) (bool, error) { s.logger.DebugContext(ctx, "Dispatching client request", "request_type", req.Type) switch req.Type { case tracessh.EnvsRequest: s.logger.DebugContext(ctx, "Ignored request", "request_type", req.Type) return true, nil case sshutils.ExecRequest: return true, trace.Wrap(s.handleExec(ctx, sctx, req)) case sshutils.EnvRequest: return true, trace.Wrap(s.handleEnv(ctx, sctx, req)) default: s.logger.WarnContext(ctx, "Received unsupported SSH request", "request_type", req.Type) return false, nil } } // handleExec proxies the Git command between client and the target server. func (s *ForwardServer) handleExec(ctx context.Context, sctx *sessionContext, req *ssh.Request) (err error) { var r sshutils.ExecReq defer func() { if err != nil { execFailureCounter.Inc() s.emitEvent(s.makeGitCommandEvent(sctx, r.Command, err)) } else { execSessionCounter.Inc() } }() if err := ssh.Unmarshal(req.Payload, &r); err != nil { return trace.Wrap(err, "failed to unmarshal exec request") } command, err := ParseSSHCommand(r.Command) if err != nil { return trace.Wrap(err) } if err := checkSSHCommand(s.cfg.TargetServer, command); err != nil { return trace.Wrap(err) } recorder := NewCommandRecorder(ctx, *command) sctx.remoteSession.Stdout = sctx.channel sctx.remoteSession.Stderr = sctx.channel.Stderr() remoteStdin, err := sctx.remoteSession.StdinPipe() if err != nil { return trace.Wrap(err, "failed to open remote session") } go func() { defer remoteStdin.Close() if _, err := io.Copy(io.MultiWriter(remoteStdin, recorder), sctx.channel); err != nil { s.logger.WarnContext(ctx, "Failed to copy git command stdin", "error", err) } }() if err := sctx.remoteSession.Start(ctx, r.Command); err != nil { return trace.Wrap(err, "failed to start git command") } go func() { execErr := sctx.remoteSession.Wait() sctx.waitExec <- execErr s.emitEvent(s.makeGitCommandEventWithExecResult(sctx, recorder, execErr)) }() return nil } func (s *ForwardServer) emitEvent(event apievents.AuditEvent) { if err := s.cfg.Emitter.EmitAuditEvent(s.cfg.ParentContext, event); err != nil { s.logger.WarnContext(s.cfg.ParentContext, "Failed to emit event", "error", err, "event_type", event.GetType(), "event_code", event.GetCode(), ) } } func (s *ForwardServer) makeGitCommandEvent(sctx *sessionContext, command string, err error) *apievents.GitCommand { event := &apievents.GitCommand{ Metadata: apievents.Metadata{ Type: events.GitCommandEvent, Code: events.GitCommandCode, }, UserMetadata: sctx.Identity.GetUserMetadata(), SessionMetadata: sctx.GetSessionMetadata(), CommandMetadata: apievents.CommandMetadata{ Command: command, }, ConnectionMetadata: apievents.ConnectionMetadata{ RemoteAddr: sctx.ServerConn.RemoteAddr().String(), LocalAddr: sctx.ServerConn.LocalAddr().String(), }, ServerMetadata: s.EventMetadata(), } if err != nil { event.Metadata.Code = events.GitCommandFailureCode event.Error = err.Error() } return event } func (s *ForwardServer) makeGitCommandEventWithExecResult(sctx *sessionContext, recorder CommandRecorder, execErr error) *apievents.GitCommand { event := s.makeGitCommandEvent(sctx, recorder.GetCommand().SSHCommand, execErr) event.ExitCode = strconv.Itoa(sshutils.ExitCodeFromExecError(execErr)) event.Path = string(recorder.GetCommand().Repository) event.Service = recorder.GetCommand().Service actions, err := recorder.GetActions() if err != nil { s.logger.WarnContext(s.cfg.ParentContext, "Failed to get actions from Git command recorder. No actions will be recorded in the event.", "error", err) } else { event.Actions = actions } return event } // handleEnv sets env on the target server. func (s *ForwardServer) handleEnv(ctx context.Context, sctx *sessionContext, req *ssh.Request) error { var e sshutils.EnvReqParams if err := ssh.Unmarshal(req.Payload, &e); err != nil { return trace.Wrap(err) } s.logger.DebugContext(ctx, "Setting env on remote Git server", "name", e.Name, "value", e.Value) err := sctx.remoteSession.Setenv(ctx, e.Name, e.Value) if err != nil { s.logger.WarnContext(ctx, "Failed to set env on remote session", "error", err, "request", e) } return nil } func (s *ForwardServer) initRemoteConn(ctx context.Context, ccx *sshutils.ConnectionContext, identityCtx srv.IdentityContext) error { netConfig, err := s.cfg.AccessPoint.GetClusterNetworkingConfig(s.cfg.ParentContext) if err != nil { return trace.Wrap(err) } signer, err := s.makeRemoteSigner(ctx, s.cfg, identityCtx) if err != nil { return trace.Wrap(err) } clientConfig := apissh.ClientConfig{ User: gitUser, PublicKeyAuth: apissh.PublicKeyAuthConfig{ Signers: func() ([]ssh.Signer, error) { return []ssh.Signer{signer}, nil }, }, HostKeyCallback: s.verifyRemoteHost, Timeout: netConfig.GetSSHDialTimeout(), } clientConfig.SSHConfig.Ciphers = s.cfg.Ciphers clientConfig.SSHConfig.KeyExchanges = s.cfg.KEXAlgorithms clientConfig.SSHConfig.MACs = s.cfg.MACAlgorithms s.remoteClient, err = apissh.NewClient( s.cfg.ParentContext, s.cfg.TargetConn, s.cfg.DstAddr.String(), clientConfig, ) if err != nil { return trace.Wrap(err) } ccx.AddCloser(s.remoteClient) return nil } func makeRemoteSigner(ctx context.Context, cfg *ForwardServerConfig, identityCtx srv.IdentityContext) (ssh.Signer, error) { switch cfg.TargetServer.GetSubKind() { case types.SubKindGitHub: return MakeGitHubSigner(ctx, GitHubSignerConfig{ Server: cfg.TargetServer, TeleportUser: identityCtx.TeleportUser, IdentityExpires: identityCtx.CertValidBefore, GitHubUserID: identityCtx.UnmappedIdentity.GitHubUserID, AuthPreferenceGetter: cfg.AccessPoint, GitHubUserCertGenerator: cfg.AuthClient.IntegrationsClient(), Clock: cfg.Clock, }) default: return nil, trace.BadParameter("unsupported subkind %q", cfg.TargetServer.GetSubKind()) } } // Below functions implement srv.Server so git.ForwardServer can be used for // srv.NewServerContext and srv.NewAuthHandlers. // TODO(greedy52) decouple from srv.Server. func (s *ForwardServer) Context() context.Context { return s.cfg.ParentContext } func (s *ForwardServer) EventMetadata() apievents.ServerMetadata { return apievents.ServerMetadata{ ServerVersion: teleport.Version, ServerNamespace: s.cfg.TargetServer.GetNamespace(), ServerAddr: s.cfg.DstAddr.String(), ServerHostname: s.cfg.TargetServer.GetHostname(), ForwardedBy: s.cfg.HostUUID, ServerSubKind: s.cfg.TargetServer.GetSubKind(), } } func (s *ForwardServer) GetInfo() types.Server { return s.cfg.TargetServer } func (s *ForwardServer) ID() string { return s.id } func (s *ForwardServer) HostUUID() string { return s.cfg.HostUUID } func (s *ForwardServer) GetNamespace() string { return s.cfg.TargetServer.GetNamespace() } func (s *ForwardServer) AdvertiseAddr() string { return s.clientConn.RemoteAddr().String() } func (s *ForwardServer) Component() string { return teleport.ComponentForwardingGit } func (s *ForwardServer) PermitUserEnvironment() bool { return false } func (s *ForwardServer) GetAccessPoint() srv.AccessPoint { return s.cfg.AccessPoint } func (s *ForwardServer) GetDataDir() string { return "" } func (s *ForwardServer) GetPAM() *pamcfg.PAMConfig { return &pamcfg.PAMConfig{Enabled: false} } func (s *ForwardServer) GetClock() clockwork.Clock { return s.cfg.Clock } func (s *ForwardServer) UseTunnel() bool { return false } func (s *ForwardServer) GetBPF() bpf.BPF { return nil } func (s *ForwardServer) GetUserAccountingPaths() (utmp, wtmp, btmp, wtmpdb string) { return } func (s *ForwardServer) GetLockWatcher() *services.LockWatcher { return s.cfg.LockWatcher } func (s *ForwardServer) GetCreateHostUser() bool { return false } func (s *ForwardServer) GetHostUsers() srv.HostUsers { return nil } func (s *ForwardServer) GetHostSudoers() srv.HostSudoers { return nil } func (s *ForwardServer) GetSELinuxEnabled() bool { return false } // ChildLogConfig returns a noop log configuration since the git forwarding server // does not spawn child processes. func (s *ForwardServer) ChildLogConfig() srv.ChildLogConfig { return srv.ChildLogConfig{ ExecLogConfig: reexec.ExecLogConfig{}, Writer: io.Discard, } } // GetPresenceMaxDuration returns the max duration that a moderated session // can continue between presence verifications. func (s *ForwardServer) GetPresenceMaxDuration() time.Duration { return client.DefaultPresenceMaxDuration } type serverContextKey struct{} const ( gitUser = "git" )