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 25, 2019
1 parent aa23351 commit 9e22f5e
Show file tree
Hide file tree
Showing 17 changed files with 541 additions and 69 deletions.
3 changes: 3 additions & 0 deletions constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,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
29 changes: 25 additions & 4 deletions integration/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -405,14 +405,25 @@ func SetupUser(process *service.TeleportProcess, username string, roles []servic
return nil
}

// UserCredsRequest is a request to generate user creds
type UserCredsRequest struct {
// Process is a teleport process
Process *service.TeleportProcess
// Username is a user to generate certs for
Username string
// RouteToCluster is an optional cluster to route creds to
RouteToCluster string
}

// GenerateUserCreds generates key to be used by client
func GenerateUserCreds(process *service.TeleportProcess, username string) (*UserCreds, error) {
func GenerateUserCreds(req UserCredsRequest) (*UserCreds, error) {
priv, pub, err := testauthority.New().GenerateKeyPair("")
if err != nil {
return nil, trace.Wrap(err)
}
a := process.GetAuthServer()
sshCert, x509Cert, err := a.GenerateUserTestCerts(pub, username, time.Hour, teleport.CertificateFormatStandard)
a := req.Process.GetAuthServer()
sshCert, x509Cert, err := a.GenerateUserTestCerts(
pub, req.Username, time.Hour, teleport.CertificateFormatStandard, req.RouteToCluster)
if err != nil {
return nil, trace.Wrap(err)
}
Expand Down Expand Up @@ -592,7 +603,7 @@ func (i *TeleInstance) CreateEx(trustedSecrets []*InstanceSecrets, tconf *servic
}
// sign user's keys:
ttl := 24 * time.Hour
user.Key.Cert, user.Key.TLSCert, err = auth.GenerateUserTestCerts(user.Key.Pub, teleUser.GetName(), ttl, teleport.CertificateFormatStandard)
user.Key.Cert, user.Key.TLSCert, err = auth.GenerateUserTestCerts(user.Key.Pub, teleUser.GetName(), ttl, teleport.CertificateFormatStandard, "")
if err != nil {
return err
}
Expand Down Expand Up @@ -915,6 +926,8 @@ type ClientConfig struct {
// ForwardAgent controls if the client requests it's agent be forwarded to
// the server.
ForwardAgent bool
// JumpHost turns on jump host mode
JumpHost bool
}

// NewClientWithCreds creates client with credentials
Expand Down Expand Up @@ -968,6 +981,14 @@ func (i *TeleInstance) NewUnauthenticatedClient(cfg ClientConfig) (tc *client.Te
SSHProxyAddr: sshProxyAddr,
}

// JumpHost turns on jump host mode
if cfg.JumpHost {
cconf.JumpHosts = []utils.JumpHost{{
Username: cfg.Login,
Addr: *utils.MustParseAddr(sshProxyAddr),
}}
}

return client.NewClient(cconf)
}

Expand Down
59 changes: 44 additions & 15 deletions integration/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1477,13 +1477,32 @@ func tryCreateTrustedCluster(c *check.C, authServer *auth.AuthServer, trustedClu
c.Fatalf("Timeout creating trusted cluster")
}

// trustedClusterTest is a test setup for trusted clusters tests
type trustedClusterTest struct {
// multiplex sets up multiplexing of the reversetunnel SSH
// socket and the proxy's web socket
multiplex bool
// useJumpHost turns on jump host mode for the access
// to the proxy instead of the proxy command
useJumpHost bool
}

// TestTrustedClusters tests remote clusters scenarios
// using trusted clusters feature
func (s *IntSuite) TestTrustedClusters(c *check.C) {
tr := utils.NewTracer(utils.ThisFunction()).Start()
defer tr.Stop()

s.trustedClusters(c, false)
s.trustedClusters(c, trustedClusterTest{multiplex: false})
}

// TestJumpTrustedClusters tests remote clusters scenarios
// using trusted clusters feature using jumphost connection
func (s *IntSuite) TestJumpTrustedClusters(c *check.C) {
tr := utils.NewTracer(utils.ThisFunction()).Start()
defer tr.Stop()

s.trustedClusters(c, trustedClusterTest{multiplex: false, useJumpHost: true})
}

// TestMultiplexingTrustedClusters tests remote clusters scenarios
Expand All @@ -1492,15 +1511,15 @@ func (s *IntSuite) TestMultiplexingTrustedClusters(c *check.C) {
tr := utils.NewTracer(utils.ThisFunction()).Start()
defer tr.Stop()

s.trustedClusters(c, true)
s.trustedClusters(c, trustedClusterTest{multiplex: true})
}

func (s *IntSuite) trustedClusters(c *check.C, multiplex bool) {
func (s *IntSuite) trustedClusters(c *check.C, test trustedClusterTest) {
username := s.me.Username

clusterMain := "cluster-main"
clusterAux := "cluster-aux"
main := NewInstance(InstanceConfig{ClusterName: clusterMain, HostID: HostID, NodeName: Host, Ports: s.getPorts(5), Priv: s.priv, Pub: s.pub, MultiplexProxy: multiplex})
main := NewInstance(InstanceConfig{ClusterName: clusterMain, HostID: HostID, NodeName: Host, Ports: s.getPorts(5), Priv: s.priv, Pub: s.pub, MultiplexProxy: test.multiplex})
aux := NewInstance(InstanceConfig{ClusterName: clusterAux, HostID: HostID, NodeName: Host, Ports: s.getPorts(5), Priv: s.priv, Pub: s.pub})

// main cluster has a local user and belongs to role "main-devs"
Expand Down Expand Up @@ -1580,13 +1599,20 @@ func (s *IntSuite) trustedClusters(c *check.C, multiplex bool) {

// Try and connect to a node in the Aux cluster from the Main cluster using
// direct dialing.
tc, err := main.NewClient(ClientConfig{
Login: username,
Cluster: clusterAux,
Host: Loopback,
Port: sshPort,
creds, err := GenerateUserCreds(UserCredsRequest{
Process: main.Process,
Username: username,
RouteToCluster: clusterAux,
})
c.Assert(err, check.IsNil)
tc, err := main.NewClientWithCreds(ClientConfig{
Login: username,
Cluster: clusterAux,
Host: Loopback,
Port: sshPort,
JumpHost: test.useJumpHost,
}, *creds)
c.Assert(err, check.IsNil)
output := &bytes.Buffer{}
tc.Stdout = output
c.Assert(err, check.IsNil)
Expand Down Expand Up @@ -2714,7 +2740,7 @@ func (s *IntSuite) TestRotateSuccess(c *check.C) {
c.Assert(err, check.IsNil)

// capture credentials before reload started to simulate old client
initialCreds, err := GenerateUserCreds(svc, s.me.Username)
initialCreds, err := GenerateUserCreds(UserCredsRequest{Process: svc, Username: s.me.Username})
c.Assert(err, check.IsNil)

l.Infof("Service started. Setting rotation state to %v", services.RotationPhaseUpdateClients)
Expand Down Expand Up @@ -2775,7 +2801,7 @@ func (s *IntSuite) TestRotateSuccess(c *check.C) {
c.Assert(err, check.IsNil)

// new credentials will work from this phase to others
newCreds, err := GenerateUserCreds(svc, s.me.Username)
newCreds, err := GenerateUserCreds(UserCredsRequest{Process: svc, Username: s.me.Username})
c.Assert(err, check.IsNil)

clt, err = t.NewClientWithCreds(cfg, *newCreds)
Expand Down Expand Up @@ -2861,7 +2887,7 @@ func (s *IntSuite) TestRotateRollback(c *check.C) {
c.Assert(err, check.IsNil)

// capture credentials before reload started to simulate old client
initialCreds, err := GenerateUserCreds(svc, s.me.Username)
initialCreds, err := GenerateUserCreds(UserCredsRequest{Process: svc, Username: s.me.Username})
c.Assert(err, check.IsNil)

l.Infof("Service started. Setting rotation state to %v", services.RotationPhaseInit)
Expand Down Expand Up @@ -3030,7 +3056,10 @@ func (s *IntSuite) TestRotateTrustedClusters(c *check.C) {
waitForTunnelConnections(c, svc.GetAuthServer(), aux.Secrets.SiteName, 1)

// capture credentials before has reload started to simulate old client
initialCreds, err := GenerateUserCreds(svc, s.me.Username)
initialCreds, err := GenerateUserCreds(UserCredsRequest{
Process: svc,
Username: s.me.Username,
})
c.Assert(err, check.IsNil)

// credentials should work
Expand Down Expand Up @@ -3115,7 +3144,7 @@ func (s *IntSuite) TestRotateTrustedClusters(c *check.C) {
c.Assert(err, check.IsNil)

// new credentials will work from this phase to others
newCreds, err := GenerateUserCreds(svc, s.me.Username)
newCreds, err := GenerateUserCreds(UserCredsRequest{Process: svc, Username: s.me.Username})
c.Assert(err, check.IsNil)

clt, err = main.NewClientWithCreds(cfg, *newCreds)
Expand Down Expand Up @@ -3491,7 +3520,7 @@ func (s *IntSuite) TestList(c *check.C) {
// Create user, role, and generate credentials.
err = SetupUser(t.Process, tt.inLogin, []services.Role{role})
c.Assert(err, check.IsNil)
initialCreds, err := GenerateUserCreds(t.Process, tt.inLogin)
initialCreds, err := GenerateUserCreds(UserCredsRequest{Process: t.Process, Username: tt.inLogin})
c.Assert(err, check.IsNil)

// Create a Teleport client.
Expand Down
14 changes: 8 additions & 6 deletions lib/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -407,7 +407,7 @@ type certRequest struct {
}

// GenerateUserTestCerts is used to generate user certificate, used internally for tests
func (a *AuthServer) GenerateUserTestCerts(key []byte, username string, ttl time.Duration, compatibility string) ([]byte, []byte, error) {
func (a *AuthServer) GenerateUserTestCerts(key []byte, username string, ttl time.Duration, compatibility, routeToCluster string) ([]byte, []byte, error) {
user, err := a.Identity.GetUser(username)
if err != nil {
return nil, nil, trace.Wrap(err)
Expand All @@ -417,11 +417,12 @@ func (a *AuthServer) GenerateUserTestCerts(key []byte, username string, ttl time
return nil, nil, trace.Wrap(err)
}
certs, err := a.generateUserCert(certRequest{
user: user,
roles: checker,
ttl: ttl,
compatibility: compatibility,
publicKey: key,
user: user,
roles: checker,
ttl: ttl,
compatibility: compatibility,
publicKey: key,
routeToCluster: routeToCluster,
})
if err != nil {
return nil, nil, trace.Wrap(err)
Expand Down Expand Up @@ -501,6 +502,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.String())
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
Loading

0 comments on commit 9e22f5e

Please sign in to comment.