Skip to content

Commit

Permalink
Forwarding to proxy is controlled by a global out-of-band
Browse files Browse the repository at this point in the history
request. Always forward Teleport agent to node in Web UI.
Support the -A flag in tsh to optionally forward agent to
node in CLI.
  • Loading branch information
russjones committed Nov 16, 2017
1 parent a419187 commit 9ad600d
Show file tree
Hide file tree
Showing 9 changed files with 256 additions and 7 deletions.
4 changes: 4 additions & 0 deletions constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,10 @@ const (
// pining each other with it:
KeepAliveReqType = "keepalive@openssh.com"

// RecordingProxyReqType is the name of a global request which returns if
// the proxy is recording sessions or not.
RecordingProxyReqType = "recording-proxy@teleport.com"

// OTP means One-time Password Algorithm for Two-Factor Authentication.
OTP = "otp"

Expand Down
11 changes: 11 additions & 0 deletions lib/client/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,12 @@ type Config struct {
// against Teleport client and obtaining credentials from elsewhere.
SkipLocalAuth bool

// Agent is used when SkipLocalAuth is true
Agent agent.Agent

// ForwardAgent is used by the client to request agent forwarding from the server.
ForwardAgent bool

// AuthMethods are used to login into the cluster. If specified, the client will
// use them in addition to certs stored in its local agent (from disk)
AuthMethods []ssh.AuthMethod
Expand Down Expand Up @@ -368,6 +374,11 @@ func NewClient(c *Config) (tc *TeleportClient, err error) {
if len(c.AuthMethods) == 0 {
return nil, trace.BadParameter("SkipLocalAuth is true but no AuthMethods provided")
}
// if the client was passed an agent in the configuration and skip local auth, use
// the passed in agent.
if c.Agent != nil {
tc.localAgent = &LocalKeyAgent{Agent: c.Agent}
}
} else {
// initialize the local agent (auth agent which uses local SSH keys signed by the CA):
tc.localAgent, err = NewLocalAgent(c.KeysDir, c.Username)
Expand Down
85 changes: 80 additions & 5 deletions lib/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,23 @@ import (
"io"
"io/ioutil"
"net"
"strconv"
"strings"
"time"

"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/agent"

"github.com/gravitational/teleport"
"github.com/gravitational/teleport/lib/auth"
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/sshutils"
"github.com/gravitational/teleport/lib/sshutils/scp"
"github.com/gravitational/teleport/lib/utils"

"github.com/gravitational/trace"

log "github.com/sirupsen/logrus"
"golang.org/x/crypto/ssh"
)

// ProxyClient implements ssh client to a teleport proxy
Expand All @@ -46,6 +50,7 @@ type ProxyClient struct {
hostLogin string
proxyAddress string
proxyPrincipal string
agentForwarded bool
hostKeyCallback utils.HostKeyCallback
authMethod ssh.AuthMethod
siteName string
Expand Down Expand Up @@ -209,6 +214,55 @@ func nodeName(node string) string {
return n
}

type proxyResponse struct {
isRecord bool
err error
}

// isRecordingProxy returns true if the proxy is in recording mode. Note, this
// function can only be called after authentication has occured and should be
// called before the first session is created.
func (proxy *ProxyClient) isRecordingProxy() (bool, error) {
responseCh := make(chan proxyResponse)

// we have to run this in a goroutine because older version of Teleport handled
// global out-of-band requests incorrectly: Teleport would ignore requests it
// does not know about and never reply to them. So if we wait a second and
// don't hear anything back, most likley we are trying to connect to an older
// version of Teleport and we should not try and forward our agent.
go func() {
ok, responseBytes, err := proxy.Client.SendRequest(teleport.RecordingProxyReqType, true, nil)
if err != nil {
responseCh <- proxyResponse{isRecord: false, err: trace.Wrap(err)}
return
}
if !ok {
responseCh <- proxyResponse{isRecord: false, err: trace.AccessDenied("unable to determine proxy type")}
return
}

recordingProxy, err := strconv.ParseBool(string(responseBytes))
if err != nil {
responseCh <- proxyResponse{isRecord: false, err: trace.Wrap(err)}
return
}

responseCh <- proxyResponse{isRecord: recordingProxy, err: nil}
}()

select {
case resp := <-responseCh:
if resp.err != nil {
return false, trace.Wrap(resp.err)
}
return resp.isRecord, nil
case <-time.After(1 * time.Second):
// probably the older version of the proxy or at least someone that is
// responding incorrectly, don't forward agent to it
return false, nil
}
}

// ConnectToNode connects to the ssh server via Proxy.
// It returns connected and authenticated NodeClient
func (proxy *ProxyClient) ConnectToNode(ctx context.Context, nodeAddress string, user string, quiet bool) (*NodeClient, error) {
Expand All @@ -224,6 +278,13 @@ func (proxy *ProxyClient) ConnectToNode(ctx context.Context, nodeAddress string,
return nil, trace.Wrap(err)
}

// after auth but before we create the first session, find out if the proxy
// is in recording mode or not
recordingProxy, err := proxy.isRecordingProxy()
if err != nil {
return nil, trace.Wrap(err)
}

proxySession, err := proxy.Client.NewSession()
if err != nil {
return nil, trace.Wrap(err)
Expand All @@ -249,6 +310,22 @@ func (proxy *ProxyClient) ConnectToNode(ctx context.Context, nodeAddress string,
}
}

// the client only tries to forward an agent when the proxy is in recording
// mode and even then only does this once (otherwise the client will pollute
// the logs with "agent: already have handler" errors).
if recordingProxy && !proxy.agentForwarded {
err = agent.ForwardToAgent(proxy.Client, proxy.teleportClient.localAgent.Agent)
if err != nil {
return nil, trace.Wrap(err)
}
err = agent.RequestAgentForwarding(proxySession)
if err != nil {
return nil, trace.Wrap(err)
}

proxy.agentForwarded = true
}

err = proxySession.RequestSubsystem("proxy:" + nodeAddress)
if err != nil {
// read the stderr output from the failed SSH session and append
Expand Down Expand Up @@ -284,9 +361,7 @@ func (proxy *ProxyClient) ConnectToNode(ctx context.Context, nodeAddress string,
}

client := ssh.NewClient(conn, chans, reqs)
if err != nil {
return nil, trace.Wrap(err)
}

return &NodeClient{Client: client, Proxy: proxy, Namespace: defaults.Namespace}, nil
}

Expand Down
19 changes: 18 additions & 1 deletion lib/client/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,16 @@ import (
"syscall"
"time"

"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/agent"

"github.com/gravitational/teleport"
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/session"
"github.com/gravitational/teleport/lib/sshutils"
"github.com/gravitational/teleport/lib/utils"
"github.com/gravitational/trace"
"github.com/moby/moby/pkg/term"
"golang.org/x/crypto/ssh"

log "github.com/sirupsen/logrus"
)
Expand Down Expand Up @@ -162,6 +164,21 @@ func (ns *NodeSession) createServerSession() (*ssh.Session, error) {
log.Warn(err)
}
}

// if agent forwarding was requested (and we have a agent to forward),
// forward the agent to endpoint.
tc := ns.nodeClient.Proxy.teleportClient
if tc.ForwardAgent && tc.localAgent.Agent != nil {
err = agent.ForwardToAgent(ns.nodeClient.Client, tc.localAgent.Agent)
if err != nil {
return nil, trace.Wrap(err)
}
err = agent.RequestAgentForwarding(sess)
if err != nil {
return nil, trace.Wrap(err)
}
}

return sess, nil
}

Expand Down
48 changes: 47 additions & 1 deletion lib/srv/regular/sshserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -536,12 +536,23 @@ func (s *Server) EmitAuditEvent(eventType string, fields events.EventFields) {
}
}

// HandleRequest is a callback for handling global out-of-band requests.
// HandleRequest processes global out-of-band requests. Global out-of-band
// requests are processed in order (this way the originator knows which
// request we are responding to). If Teleport does not support the request
// type or an error occurs while processing that request Teleport will reply
// req.Reply(false, nil).
//
// For more details: https://tools.ietf.org/html/rfc4254.html#page-4
func (s *Server) HandleRequest(r *ssh.Request) {
switch r.Type {
case teleport.KeepAliveReqType:
s.handleKeepAlive(r)
case teleport.RecordingProxyReqType:
s.handleRecordingProxy(r)
default:
if r.WantReply {
r.Reply(false, nil)
}
log.Debugf("[SSH] Discarding %q global request: %+v", r.Type, r)
}
}
Expand Down Expand Up @@ -715,6 +726,10 @@ func (s *Server) dispatch(ch ssh.Channel, req *ssh.Request, ctx *srv.ServerConte
case "env":
// we currently ignore setting any environment variables via SSH for security purposes
return s.handleEnv(ch, req, ctx)
case sshutils.AgentReq:
// to maintain interoperability with OpenSSH, agent forwarding requests
// should never fail, so accept the request, do nothing, and return success
return nil
default:
return trace.BadParameter(
"proxy doesn't support request type '%v'", req.Type)
Expand Down Expand Up @@ -943,6 +958,37 @@ func (s *Server) handleKeepAlive(req *ssh.Request) {
log.Debugf("[KEEP ALIVE] Replied to %q", req.Type)
}

// handleRecordingProxy responds to global out-of-band with a bool which
// indicates if it is in recording mode or not.
func (s *Server) handleRecordingProxy(req *ssh.Request) {
var recordingProxy bool

log.Debugf("Global request (%v, %v) received", req.Type, req.WantReply)

if req.WantReply {
// get the cluster config, if we can't get it, reply false
clusterConfig, err := s.authService.GetClusterConfig()
if err != nil {
err := req.Reply(false, nil)
if err != nil {
log.Warnf("Unable to respond to global request (%v, %v): %v", req.Type, req.WantReply, err)
}
return
}

// reply true that we were able to process the message and reply with a
// bool if we are in recording mode or not
recordingProxy = clusterConfig.GetSessionRecording() == services.RecordAtProxy
err = req.Reply(true, []byte(strconv.FormatBool(recordingProxy)))
if err != nil {
log.Warnf("Unable to respond to global request (%v, %v): %v: %v", req.Type, req.WantReply, recordingProxy, err)
return
}
}

log.Debugf("Replied to global request (%v, %v): %v", req.Type, req.WantReply, recordingProxy)
}

func replyError(ch ssh.Channel, req *ssh.Request, err error) {
message := []byte(utils.UserMessageFromError(err))
ch.Stderr().Write(message)
Expand Down
46 changes: 46 additions & 0 deletions lib/srv/regular/sshserver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"net"
"os"
"os/user"
"strconv"
"strings"
"testing"
"time"
Expand Down Expand Up @@ -960,6 +961,51 @@ func (s *SrvSuite) TestServerAliveInterval(c *C) {
c.Assert(ok, Equals, true)
}

// TestGlobalRequestRecordingProxy simulates sending a global out-of-band
// recording-proxy@teleport.com request.
func (s *SrvSuite) TestGlobalRequestRecordingProxy(c *C) {
// send request, since no cluster config is set, we should reply false to
// this request
ok, _, err := s.clt.SendRequest(teleport.RecordingProxyReqType, true, nil)
c.Assert(err, IsNil)
c.Assert(ok, Equals, false)

// set cluster config to record at the node
clusterConfig, err := services.NewClusterConfig(services.ClusterConfigSpecV3{
SessionRecording: services.RecordAtNode,
})
c.Assert(err, IsNil)
err = s.a.SetClusterConfig(clusterConfig)
c.Assert(err, IsNil)

// send the request again, we have cluster config and when we parse the
// response, it should be false because recording is occuring at the node.
ok, responseBytes, err := s.clt.SendRequest(teleport.RecordingProxyReqType, true, nil)
c.Assert(err, IsNil)
c.Assert(ok, Equals, true)
response, err := strconv.ParseBool(string(responseBytes))
c.Assert(err, IsNil)
c.Assert(response, Equals, false)

// set cluster config to record at the proxy
clusterConfig, err = services.NewClusterConfig(services.ClusterConfigSpecV3{
SessionRecording: services.RecordAtProxy,
})
c.Assert(err, IsNil)
err = s.a.SetClusterConfig(clusterConfig)
c.Assert(err, IsNil)

// send request again, now that we have cluster config and it's set to record
// at the proxy, we should return true and when we parse the payload it should
// also be true
ok, responseBytes, err = s.clt.SendRequest(teleport.RecordingProxyReqType, true, nil)
c.Assert(err, IsNil)
c.Assert(ok, Equals, true)
response, err = strconv.ParseBool(string(responseBytes))
c.Assert(err, IsNil)
c.Assert(response, Equals, true)
}

// upack holds all ssh signing artefacts needed for signing and checking user keys
type upack struct {
// key is a raw private user key
Expand Down
Loading

0 comments on commit 9ad600d

Please sign in to comment.