Skip to content

Commit

Permalink
Add support for ProxyJump.
Browse files Browse the repository at this point in the history
This commit implements #2543

In SSH terms ProxyJump is a shortcut for SSH client
connecting the proxy/jumphost and requesting .port forwarding to the
target node.

This commit adds support for direct-tcpip request support
in teleport proxy service that is an alias to the existing proxy
subsystem and reuses most of the code.

This commit also adds support to "route to cluster" metadata
encoded in SSH certificate making it possible to have client
SSH certificates to include the metadata that will cause the proxy
to route the client requests to a specific cluster.

`tsh ssh -J proxy:port ` is supported in a limited way:

Only one jump host is supported (-J supports chaining
that teleport does not utilise) and tsh will return with error
in case of two jumphosts: -J a,b will not work.

In case if `tsh ssh -J user@proxy` is used, it overrides
the SSH proxy coming from the tsh profile and port-forwarding
is used instead of the existing teleport proxy subsystem
  • Loading branch information
klizhentas committed Jul 24, 2019
1 parent 2fe94e4 commit 85c159c
Show file tree
Hide file tree
Showing 15 changed files with 464 additions and 43 deletions.
3 changes: 3 additions & 0 deletions constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,9 @@ const (
CertExtensionPermitPortForwarding = "permit-port-forwarding"
// CertExtensionTeleportRoles is used to propagate teleport roles
CertExtensionTeleportRoles = "teleport-roles"
// CertExtensionTeleportRouteToCluster is used to encode
// the target cluster to route to in the certificate
CertExtensionTeleportRouteToCluster = "teleport-route-to-cluster"
)

const (
Expand Down
1 change: 1 addition & 0 deletions lib/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -501,6 +501,7 @@ func (s *AuthServer) generateUserCert(req certRequest) (*certs, error) {
CertificateFormat: certificateFormat,
PermitPortForwarding: req.roles.CanPortForward(),
PermitAgentForwarding: req.roles.CanForwardAgents(),
RouteToCluster: req.routeToCluster,
})
if err != nil {
return nil, trace.Wrap(err)
Expand Down
16 changes: 10 additions & 6 deletions lib/auth/native/native.go
Original file line number Diff line number Diff line change
Expand Up @@ -268,19 +268,23 @@ func (k *Keygen) GenerateUserCert(c services.UserCertParams) ([]byte, error) {
if !c.PermitPortForwarding {
delete(cert.Permissions.Extensions, teleport.CertExtensionPermitPortForwarding)
}
if len(c.Roles) != 0 {
// only add roles to the certificate extensions if the standard format was
// requested. we allow the option to omit this to support older versions of
// OpenSSH due to a bug in <= OpenSSH 7.1
// https://bugzilla.mindrot.org/show_bug.cgi?id=2387
if c.CertificateFormat == teleport.CertificateFormatStandard {
// Only add roles to the certificate extensions if the standard format was
// requested. we allow the option to omit this to support older versions of
// OpenSSH due to a bug in <= OpenSSH 7.1
// https://bugzilla.mindrot.org/show_bug.cgi?id=2387
if c.CertificateFormat == teleport.CertificateFormatStandard {
if len(c.Roles) != 0 {
roles, err := services.MarshalCertRoles(c.Roles)
if err != nil {
return nil, trace.Wrap(err)
}
cert.Permissions.Extensions[teleport.CertExtensionTeleportRoles] = roles
}
if c.RouteToCluster != "" {
cert.Permissions.Extensions[teleport.CertExtensionTeleportRouteToCluster] = c.RouteToCluster
}
}

signer, err := ssh.ParsePrivateKey(c.PrivateCASigningKey)
if err != nil {
return nil, trace.Wrap(err)
Expand Down
15 changes: 9 additions & 6 deletions lib/auth/testauthority/testauthority.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,18 +110,21 @@ func (n *Keygen) GenerateUserCert(c services.UserCertParams) ([]byte, error) {
if !c.PermitPortForwarding {
delete(cert.Permissions.Extensions, teleport.CertExtensionPermitPortForwarding)
}
if len(c.Roles) != 0 {
// only add roles to the certificate extensions if the standard format was
// requested. we allow the option to omit this to support older versions of
// OpenSSH due to a bug in <= OpenSSH 7.1
// https://bugzilla.mindrot.org/show_bug.cgi?id=2387
if c.CertificateFormat == teleport.CertificateFormatStandard {
// Only add roles to the certificate extensions if the standard format was
// requested. we allow the option to omit this to support older versions of
// OpenSSH due to a bug in <= OpenSSH 7.1
// https://bugzilla.mindrot.org/show_bug.cgi?id=2387
if c.CertificateFormat == teleport.CertificateFormatStandard {
if len(c.Roles) != 0 {
roles, err := services.MarshalCertRoles(c.Roles)
if err != nil {
return nil, trace.Wrap(err)
}
cert.Permissions.Extensions[teleport.CertExtensionTeleportRoles] = roles
}
if c.RouteToCluster != "" {
cert.Permissions.Extensions[teleport.CertExtensionTeleportRouteToCluster] = c.RouteToCluster
}
}
if err := cert.SignCert(rand.Reader, signer); err != nil {
return nil, err
Expand Down
25 changes: 21 additions & 4 deletions lib/client/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,10 @@ type Config struct {
// port setting via -p flag, otherwise '0' is passed which means "use server default"
HostPort int

// JumpHosts if specified are interpreted in a similar way
// as -J flag in ssh - used to dial through
JumpHosts []utils.JumpHost

// WebProxyAddr is the host:port the web proxy can be accessed at.
WebProxyAddr string

Expand Down Expand Up @@ -707,6 +711,9 @@ type ShellCreatedCallback func(s *ssh.Session, c *ssh.Client, terminal io.ReadWr

// NewClient creates a TeleportClient object and fully configures it
func NewClient(c *Config) (tc *TeleportClient, err error) {
if len(c.JumpHosts) > 1 {
return nil, trace.BadParameter("only one jump host is supported, got %v", len(c.JumpHosts))
}
// validate configuration
if c.Username == "" {
c.Username, err = Username()
Expand Down Expand Up @@ -1387,6 +1394,10 @@ func (tc *TeleportClient) getProxySSHPrincipal() string {
if tc.DefaultPrincipal != "" {
proxyPrincipal = tc.DefaultPrincipal
}
if len(tc.JumpHosts) > 1 && tc.JumpHosts[0].Username != "" {
log.Debugf("Setting proxy login to jump host's parameter user %q", tc.JumpHosts[0].Username)
proxyPrincipal = tc.JumpHosts[0].Username
}
// see if we already have a signed key in the cache, we'll use that instead
if !tc.Config.SkipLocalAuth && tc.LocalAgent() != nil {
signers, err := tc.LocalAgent().Signers()
Expand Down Expand Up @@ -1448,12 +1459,18 @@ func (tc *TeleportClient) connectToProxy(ctx context.Context) (*ProxyClient, err
HostKeyCallback: tc.HostKeyCallback,
}

sshProxyAddr := tc.Config.SSHProxyAddr
if len(tc.JumpHosts) > 0 {
log.Debugf("Overriding SSH proxy to JumpHosts's address %q", tc.JumpHosts[0].Addr)
sshProxyAddr = tc.JumpHosts[0].Addr.Addr
}

// helper to create a ProxyClient struct
makeProxyClient := func(sshClient *ssh.Client, m ssh.AuthMethod) *ProxyClient {
return &ProxyClient{
teleportClient: tc,
Client: sshClient,
proxyAddress: tc.Config.SSHProxyAddr,
proxyAddress: sshProxyAddr,
proxyPrincipal: proxyPrincipal,
hostKeyCallback: sshConfig.HostKeyCallback,
authMethod: m,
Expand All @@ -1462,14 +1479,14 @@ func (tc *TeleportClient) connectToProxy(ctx context.Context) (*ProxyClient, err
clientAddr: tc.ClientAddr,
}
}
successMsg := fmt.Sprintf("Successful auth with proxy %v", tc.Config.SSHProxyAddr)
successMsg := fmt.Sprintf("Successful auth with proxy %v", sshProxyAddr)
// try to authenticate using every non interactive auth method we have:
for i, m := range tc.authMethods() {
log.Infof("Connecting proxy=%v login='%v' method=%d", tc.Config.SSHProxyAddr, sshConfig.User, i)
log.Infof("Connecting proxy=%v login='%v' method=%d", sshProxyAddr, sshConfig.User, i)
var sshClient *ssh.Client

sshConfig.Auth = []ssh.AuthMethod{m}
sshClient, err = ssh.Dial("tcp", tc.Config.SSHProxyAddr, sshConfig)
sshClient, err = ssh.Dial("tcp", sshProxyAddr, sshConfig)
if err != nil {
return nil, trace.Wrap(err)
}
Expand Down
67 changes: 67 additions & 0 deletions lib/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,9 @@ func (n *NodeAddr) ProxyFormat() string {
// It returns connected and authenticated NodeClient
func (proxy *ProxyClient) ConnectToNode(ctx context.Context, nodeAddress NodeAddr, user string, quiet bool) (*NodeClient, error) {
log.Infof("Client=%v connecting to node=%v", proxy.clientAddr, nodeAddress)
if len(proxy.teleportClient.JumpHosts) > 0 {
return proxy.PortForwardToNode(ctx, nodeAddress, user, quiet)
}

// parse destination first:
localAddr, err := utils.ParseAddr("tcp://" + proxy.proxyAddress)
Expand Down Expand Up @@ -548,6 +551,70 @@ func (proxy *ProxyClient) ConnectToNode(ctx context.Context, nodeAddress NodeAdd
return nc, nil
}

// PortForwardToNode connects to the ssh server via Proxy
// It returns connected and authenticated NodeClient
func (proxy *ProxyClient) PortForwardToNode(ctx context.Context, nodeAddress NodeAddr, user string, quiet bool) (*NodeClient, error) {
log.Infof("Client=%v jumping to node=%s", proxy.clientAddr, nodeAddress)

// 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)
}

// the client only tries to forward an agent when the proxy is in recording
// mode. we always try and forward an agent here because each new session
// creates a new context which holds the agent. if ForwardToAgent returns an error
// "already have handler for" we ignore it.
if recordingProxy {
err = agent.ForwardToAgent(proxy.Client, proxy.teleportClient.localAgent.Agent)
if err != nil && !strings.Contains(err.Error(), "agent: already have handler for") {
return nil, trace.Wrap(err)
}
}

proxyConn, err := proxy.Client.Dial("tcp", nodeAddress.Addr)
if err != nil {
return nil, trace.ConnectionProblem(err, "failed connecting to node %v. %s", nodeAddress, err)
}

sshConfig := &ssh.ClientConfig{
User: user,
Auth: []ssh.AuthMethod{proxy.authMethod},
HostKeyCallback: proxy.hostKeyCallback,
}
conn, chans, reqs, err := newClientConn(ctx, proxyConn, nodeAddress.Addr, sshConfig)
if err != nil {
if utils.IsHandshakeFailedError(err) {
proxyConn.Close()
return nil, trace.AccessDenied(`access denied to %v connecting to %v`, user, nodeAddress)
}
return nil, trace.Wrap(err)
}

// We pass an empty channel which we close right away to ssh.NewClient
// because the client need to handle requests itself.
emptyCh := make(chan *ssh.Request)
close(emptyCh)

client := ssh.NewClient(conn, chans, emptyCh)

nc := &NodeClient{
Client: client,
Proxy: proxy,
Namespace: defaults.Namespace,
TC: proxy.teleportClient,
}

// Start a goroutine that will run for the duration of the client to process
// global requests from the client. Teleport clients will use this to update
// terminal sizes when the remote PTY size has changed.
go nc.handleGlobalRequests(ctx, reqs)

return nc, nil
}

func (c *NodeClient) handleGlobalRequests(ctx context.Context, requestCh <-chan *ssh.Request) {
for {
select {
Expand Down
4 changes: 4 additions & 0 deletions lib/services/authority.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,10 @@ type UserCertParams struct {
Roles []string
// CertificateFormat is the format of the SSH certificate.
CertificateFormat string
// RouteToCluster specifies the target cluster
// if present in the certificate, will be used
// to route the requests to
RouteToCluster string
}

// CertRoles defines certificate roles
Expand Down
4 changes: 2 additions & 2 deletions lib/srv/authhandlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ type AuthHandlers struct {
AccessPoint auth.AccessPoint
}

// BuildIdentityContext returns an IdentityContext populated with information
// CreateIdentityContext returns an IdentityContext populated with information
// about the logged in user on the connection.
func (h *AuthHandlers) CreateIdentityContext(sconn *ssh.ServerConn) (IdentityContext, error) {
identity := IdentityContext{
Expand All @@ -72,10 +72,10 @@ func (h *AuthHandlers) CreateIdentityContext(sconn *ssh.ServerConn) (IdentityCon
if err != nil {
return IdentityContext{}, trace.Wrap(err)
}
identity.RouteToCluster = certificate.Extensions[teleport.CertExtensionTeleportRouteToCluster]
if certificate.ValidBefore != 0 {
identity.CertValidBefore = time.Unix(int64(certificate.ValidBefore), 0)
}

certAuthority, err := h.authorityForCert(services.UserCA, certificate.SignatureKey)
if err != nil {
return IdentityContext{}, trace.Wrap(err)
Expand Down
14 changes: 14 additions & 0 deletions lib/srv/ctx.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,9 @@ type IdentityContext struct {
// CertValidBefore is set to the expiry time of a certificate, or
// empty, if cert does not expire
CertValidBefore time.Time

// RouteToCluster is derived from the certificate
RouteToCluster string
}

// GetCertificate parses the SSH certificate bytes and returns a *ssh.Certificate.
Expand Down Expand Up @@ -522,6 +525,17 @@ func (c *ServerContext) Close() error {
return nil
}

// CancelContext is a context associated with server context,
// closed whenever this server context is closed
func (c *ServerContext) CancelContext() context.Context {
return c.cancelContext
}

// Cancel is a function that triggers closure
func (c *ServerContext) Cancel() context.CancelFunc {
return c.cancel
}

// SendExecResult sends the result of execution of the "exec" command over the
// ExecResultCh.
func (c *ServerContext) SendExecResult(r ExecResult) {
Expand Down
2 changes: 1 addition & 1 deletion lib/srv/keepalive.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ func StartKeepAliveLoop(p KeepAliveParams) {
for _, conn := range p.Conns {
ok := sendKeepAliveWithTimeout(conn, defaults.ReadHeadersTimeout, p.CloseContext)
if ok {
sentCount += 1
sentCount++
}
}
if sentCount == len(p.Conns) {
Expand Down
Loading

0 comments on commit 85c159c

Please sign in to comment.