Skip to content

Commit

Permalink
Fix TLS Routing jumphost flow (#11282)
Browse files Browse the repository at this point in the history
  • Loading branch information
smallinsky committed Mar 28, 2022
1 parent c733c69 commit a8b31b8
Show file tree
Hide file tree
Showing 3 changed files with 321 additions and 11 deletions.
60 changes: 49 additions & 11 deletions lib/client/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -2167,14 +2167,12 @@ func (tc *TeleportClient) connectToProxy(ctx context.Context) (*ProxyClient, err
HostKeyCallback: hostKeyCallback,
Auth: authMethods,
}
log.Infof("Connecting proxy=%v login=%q", sshProxyAddr, sshConfig.User)

sshClient, err := makeProxySSHClient(tc, sshConfig)
sshClient, err := makeProxySSHClient(ctx, tc, sshConfig)
if err != nil {
return nil, trace.Wrap(err)
}

log.Infof("Successful auth with proxy %v.", sshProxyAddr)
return &ProxyClient{
teleportClient: tc,
Client: sshClient,
Expand All @@ -2188,26 +2186,66 @@ func (tc *TeleportClient) connectToProxy(ctx context.Context) (*ProxyClient, err
}, nil
}

func makeProxySSHClient(tc *TeleportClient, sshConfig *ssh.ClientConfig) (*ssh.Client, error) {
if tc.Config.TLSRoutingEnabled {
return makeProxySSHClientWithTLSWrapper(tc, sshConfig)
// makeProxySSHClient creates an SSH client by following steps:
// 1) If the current proxy supports TLS Routing and JumpHost address was not provided use TLSWrapper.
// 2) Check JumpHost raw SSH port or Teleport proxy address.
// In case of proxy web address check if the proxy supports TLS Routing and connect to the proxy with TLSWrapper
// 3) Dial sshProxyAddr with raw SSH Dialer where sshProxyAddress is proxy ssh address or JumpHost address if
// JumpHost address was provided.
func makeProxySSHClient(ctx context.Context, tc *TeleportClient, sshConfig *ssh.ClientConfig) (*ssh.Client, error) {
// Use TLS Routing dialer only if proxy support TLS Routing and JumpHost was not set.
if tc.Config.TLSRoutingEnabled && len(tc.JumpHosts) == 0 {
log.Infof("Connecting to proxy=%v login=%q using TLS Routing", tc.Config.WebProxyAddr, sshConfig.User)
c, err := makeProxySSHClientWithTLSWrapper(ctx, tc, sshConfig, tc.Config.WebProxyAddr)
if err != nil {
return nil, trace.Wrap(err)
}
log.Infof("Successful auth with proxy %v.", tc.Config.WebProxyAddr)
return c, nil
}
return makeProxySSHClientDirect(tc, sshConfig)

sshProxyAddr := tc.Config.SSHProxyAddr

// Handle situation where a Jump Host was set to proxy web address and Teleport supports TLS Routing.
if len(tc.JumpHosts) > 0 {
sshProxyAddr = tc.JumpHosts[0].Addr.Addr
// Check if JumpHost address is a proxy web address.
resp, err := webclient.Find(&webclient.Config{Context: ctx, ProxyAddr: sshProxyAddr, Insecure: tc.InsecureSkipVerify})
// If JumpHost address is a proxy web port and proxy supports TLSRouting dial proxy with TLSWrapper.
if err == nil && resp.Proxy.TLSRoutingEnabled {
log.Infof("Connecting to proxy=%v login=%q using TLS Routing JumpHost", sshProxyAddr, sshConfig.User)
c, err := makeProxySSHClientWithTLSWrapper(ctx, tc, sshConfig, sshProxyAddr)
if err != nil {
return nil, trace.Wrap(err)
}
log.Infof("Successful auth with proxy %v.", sshProxyAddr)
return c, nil
}
}

log.Infof("Connecting to proxy=%v login=%q", sshProxyAddr, sshConfig.User)
client, err := makeProxySSHClientDirect(tc, sshConfig, sshProxyAddr)
if err != nil {
return nil, trace.Wrap(err, "failed to authenticate with proxy %v", sshProxyAddr)
}
log.Infof("Successful auth with proxy %v.", sshProxyAddr)
return client, nil
}

func makeProxySSHClientDirect(tc *TeleportClient, sshConfig *ssh.ClientConfig) (*ssh.Client, error) {
func makeProxySSHClientDirect(tc *TeleportClient, sshConfig *ssh.ClientConfig, proxyAddr string) (*ssh.Client, error) {
dialer := proxy.DialerFromEnvironment(tc.Config.SSHProxyAddr)
return dialer.Dial("tcp", tc.Config.SSHProxyAddr, sshConfig)
return dialer.Dial("tcp", proxyAddr, sshConfig)
}

func makeProxySSHClientWithTLSWrapper(tc *TeleportClient, sshConfig *ssh.ClientConfig) (*ssh.Client, error) {
func makeProxySSHClientWithTLSWrapper(ctx context.Context, tc *TeleportClient, sshConfig *ssh.ClientConfig, proxyAddr string) (*ssh.Client, error) {
tlsConfig, err := tc.loadTLSConfig()
if err != nil {
return nil, trace.Wrap(err)
}

tlsConfig.NextProtos = []string{string(alpncommon.ProtocolProxySSH)}
dialer := proxy.DialerFromEnvironment(tc.Config.WebProxyAddr, proxy.WithALPNDialer(tlsConfig))
return dialer.Dial("tcp", tc.Config.WebProxyAddr, sshConfig)
return dialer.Dial("tcp", proxyAddr, sshConfig)
}

func (tc *TeleportClient) rootClusterName() (string, error) {
Expand Down
180 changes: 180 additions & 0 deletions tool/tsh/proxy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"os"
"os/exec"
"os/user"
"path"
"path/filepath"
"strconv"
"testing"
Expand All @@ -45,6 +46,185 @@ import (
"github.com/gravitational/teleport/lib/utils"
)

// TestTSHSSH verifies "tsh proxy ssh" command.
func TestTSHSSH(t *testing.T) {
lib.SetInsecureDevMode(true)
defer lib.SetInsecureDevMode(false)

os.RemoveAll(profile.FullProfilePath(""))
t.Cleanup(func() {
os.RemoveAll(profile.FullProfilePath(""))
})

s := newTestSuite(t,
withRootConfigFunc(func(cfg *service.Config) {
cfg.Version = defaults.TeleportConfigVersionV2
cfg.Auth.NetworkingConfig.SetProxyListenerMode(types.ProxyListenerMode_Multiplex)
}),
withLeafCluster(),
withLeafConfigFunc(func(cfg *service.Config) {
cfg.Version = defaults.TeleportConfigVersionV2
cfg.Proxy.SSHAddr.Addr = localListenerAddr()
}),
)

tests := []struct {
name string
fn func(t *testing.T, s *suite)
}{
{"ssh root cluster access", testRootClusterSSHAccess},
{"ssh leaf cluster access", testLeafClusterSSHAccess},
{"ssh jump host access", testJumpHostSSHAccess},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
tc.fn(t, s)
})
}
}

func testRootClusterSSHAccess(t *testing.T, s *suite) {
err := Run([]string{
"login",
"--insecure",
"--debug",
"--auth", s.connector.GetName(),
"--proxy", s.root.Config.Proxy.WebAddr.String(),
}, func(cf *CLIConf) error {
cf.mockSSOLogin = mockSSOLogin(t, s.root.GetAuthServer(), s.user)
return nil
})
require.NoError(t, err)
err = Run([]string{
"ssh",
s.root.Config.Hostname,
"echo", "hello",
})
require.NoError(t, err)

identityFile := path.Join(t.TempDir(), "identity.pem")
err = Run([]string{
"login",
"--insecure",
"--debug",
"--auth", s.connector.GetName(),
"--proxy", s.root.Config.Proxy.WebAddr.String(),
"--out", identityFile,
}, func(cf *CLIConf) error {
cf.mockSSOLogin = mockSSOLogin(t, s.root.GetAuthServer(), s.user)
return nil
})
require.NoError(t, err)

err = Run([]string{
"--proxy", s.root.Config.Proxy.WebAddr.String(),
"--insecure",
"-i", identityFile,
"ssh",
"localhost",
"echo", "hello",
})
require.NoError(t, err)
}

func testLeafClusterSSHAccess(t *testing.T, s *suite) {
err := Run([]string{
"login",
"--insecure",
"--debug",
"--auth", s.connector.GetName(),
"--proxy", s.root.Config.Proxy.WebAddr.String(),
s.leaf.Config.Auth.ClusterName.GetClusterName(),
}, func(cf *CLIConf) error {
cf.mockSSOLogin = mockSSOLogin(t, s.root.GetAuthServer(), s.user)
return nil
})
require.NoError(t, err)

err = Run([]string{
"ssh",
s.leaf.Config.Hostname,
"echo", "hello",
})
require.NoError(t, err)

identityFile := path.Join(t.TempDir(), "identity.pem")
err = Run([]string{
"login",
"--insecure",
"--debug",
"--auth", s.connector.GetName(),
"--proxy", s.root.Config.Proxy.WebAddr.String(),
"--out", identityFile,
}, func(cf *CLIConf) error {
cf.mockSSOLogin = mockSSOLogin(t, s.root.GetAuthServer(), s.user)
return nil
})
require.NoError(t, err)

err = Run([]string{
"--proxy", s.root.Config.Proxy.WebAddr.String(),
"--insecure",
"-i", identityFile,
"ssh",
"--cluster", s.leaf.Config.Auth.ClusterName.GetClusterName(),
s.leaf.Config.Hostname,
"echo", "hello",
})
require.NoError(t, err)
}

func testJumpHostSSHAccess(t *testing.T, s *suite) {
err := Run([]string{
"login",
"--insecure",
"--auth", s.connector.GetName(),
"--proxy", s.root.Config.Proxy.WebAddr.String(),
s.root.Config.Auth.ClusterName.GetClusterName(),
}, func(cf *CLIConf) error {
cf.mockSSOLogin = mockSSOLogin(t, s.root.GetAuthServer(), s.user)
return nil
})
require.NoError(t, err)

err = Run([]string{
"login",
"--insecure",
s.leaf.Config.Auth.ClusterName.GetClusterName(),
}, func(cf *CLIConf) error {
cf.mockSSOLogin = mockSSOLogin(t, s.root.GetAuthServer(), s.user)
return nil
})
require.NoError(t, err)

// Connect to leaf node though jump host set to leaf proxy SSH port.
err = Run([]string{
"ssh",
"--insecure",
"-J", s.leaf.Config.Proxy.SSHAddr.Addr,
s.leaf.Config.Hostname,
"echo", "hello",
}, func(cf *CLIConf) error {
cf.mockSSOLogin = mockSSOLogin(t, s.root.GetAuthServer(), s.user)
return nil
})
require.NoError(t, err)

// Connect to leaf node though jump host set to proxy web port where TLS Routing is enabled.
err = Run([]string{
"ssh",
"--insecure",
"-J", s.leaf.Config.Proxy.WebAddr.Addr,
s.leaf.Config.Hostname,
"echo", "hello",
}, func(cf *CLIConf) error {
cf.mockSSOLogin = mockSSOLogin(t, s.root.GetAuthServer(), s.user)
return nil
})
require.NoError(t, err)
}

// TestProxySSHDial verifies "tsh proxy ssh" command.
func TestProxySSHDial(t *testing.T) {
createAgent(t)
Expand Down
Loading

0 comments on commit a8b31b8

Please sign in to comment.