mirror of
https://github.com/gravitational/teleport.git
synced 2026-10-25 10:40:02 +08:00
* feat(certs): thread beam_id through identity certificates Carries the beam identifier from a delegation session's BeamIDLabel into issued SSH/TLS certificates and back out via the cert extensions/ASN.1 OID, exposes it on tlsca/sshca Identity and the decision PDP identities, propagates it through scoped cert reissue (auth_with_roles), and surfaces it on event UserMetadata for attribution. * handle missing event cases * prevent overriding beams_id cert extension with roles cert extensions * handle code review comment
789 lines
24 KiB
Go
789 lines
24 KiB
Go
/*
|
|
* 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 <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
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"
|
|
)
|