From efd71425717cf65e60be525ad02d701592b4ef89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Smoli=C5=84ski?= Date: Mon, 28 Mar 2022 12:31:05 +0200 Subject: [PATCH] Fix TLS Routing jumphost flow (#11282) --- lib/client/api.go | 66 ++++++++++++++++++++++++++++++------- tool/tsh/proxy_test.go | 66 ++++++++++++++++++++++++++++++++++--- tool/tsh/tsh_helper_test.go | 17 ++++++++-- 3 files changed, 130 insertions(+), 19 deletions(-) diff --git a/lib/client/api.go b/lib/client/api.go index a6d21096cd81a..5e620379d5dc3 100644 --- a/lib/client/api.go +++ b/lib/client/api.go @@ -2263,14 +2263,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, @@ -2284,26 +2282,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 + } + + 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) } - return makeProxySSHClientDirect(tc, sshConfig) + 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) { @@ -3176,8 +3214,12 @@ func (tc *TeleportClient) loadTLSConfig() (*tls.Config, error) { if err != nil { return nil, trace.Wrap(err) } + clusters := []string{rootCluster} + if tc.SiteName != "" && rootCluster != tc.SiteName { + clusters = append(clusters, tc.SiteName) + } - tlsConfig, err := tlsKey.TeleportClientTLSConfig(nil, []string{rootCluster}) + tlsConfig, err := tlsKey.TeleportClientTLSConfig(nil, clusters) if err != nil { return nil, trace.Wrap(err, "failed to generate client TLS config") } diff --git a/tool/tsh/proxy_test.go b/tool/tsh/proxy_test.go index ad6a097b57460..6b12b2aa739ac 100644 --- a/tool/tsh/proxy_test.go +++ b/tool/tsh/proxy_test.go @@ -56,10 +56,17 @@ func TestTSHSSH(t *testing.T) { os.RemoveAll(profile.FullProfilePath("")) }) - s := newTestSuite(t, withLeafCluster(), withRootConfigFunc(func(cfg *service.Config) { - cfg.Version = defaults.TeleportConfigVersionV2 - cfg.Auth.NetworkingConfig.SetProxyListenerMode(types.ProxyListenerMode_Multiplex) - })) + 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 @@ -67,6 +74,7 @@ func TestTSHSSH(t *testing.T) { }{ {"ssh root cluster access", testRootClusterSSHAccess}, {"ssh leaf cluster access", testLeafClusterSSHAccess}, + {"ssh jump host access", testJumpHostSSHAccess}, } for _, tc := range tests { @@ -167,6 +175,56 @@ func testLeafClusterSSHAccess(t *testing.T, s *suite) { 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) diff --git a/tool/tsh/tsh_helper_test.go b/tool/tsh/tsh_helper_test.go index 823f0f4f48141..3faae0c85a2c0 100644 --- a/tool/tsh/tsh_helper_test.go +++ b/tool/tsh/tsh_helper_test.go @@ -106,7 +106,7 @@ func (s *suite) setupRootCluster(t *testing.T, options testSuiteOptions) { t.Cleanup(func() { require.NoError(t, s.root.Close()) }) } -func (s *suite) setupLeafCluster(t *testing.T) { +func (s *suite) setupLeafCluster(t *testing.T, options testSuiteOptions) { fileConfig := &config.FileConfig{ Version: "v2", Global: config.Global{ @@ -164,6 +164,9 @@ func (s *suite) setupLeafCluster(t *testing.T) { }) require.NoError(t, err) cfg.Auth.Resources = []types.Resource{sshLoginRole} + if options.leafConfigFunc != nil { + options.leafConfigFunc(cfg) + } s.leaf = runTeleport(t, cfg) _, err = s.leaf.GetAuthServer().UpsertTrustedCluster(s.leaf.ExitContext(), tc) @@ -172,6 +175,7 @@ func (s *suite) setupLeafCluster(t *testing.T) { type testSuiteOptions struct { rootConfigFunc func(cfg *service.Config) + leafConfigFunc func(cfg *service.Config) leafCluster bool } @@ -183,6 +187,12 @@ func withRootConfigFunc(fn func(cfg *service.Config)) testSuiteOptionFunc { } } +func withLeafConfigFunc(fn func(cfg *service.Config)) testSuiteOptionFunc { + return func(o *testSuiteOptions) { + o.leafConfigFunc = fn + } +} + func withLeafCluster() testSuiteOptionFunc { return func(o *testSuiteOptions) { o.leafCluster = true @@ -197,8 +207,9 @@ func newTestSuite(t *testing.T, opts ...testSuiteOptionFunc) *suite { s := &suite{} s.setupRootCluster(t, options) - if options.leafCluster { - s.setupLeafCluster(t) + + if options.leafCluster || options.leafConfigFunc != nil { + s.setupLeafCluster(t, options) require.Eventually(t, func() bool { rt, err := s.root.GetAuthServer().GetTunnelConnections(s.leaf.Config.Auth.ClusterName.GetClusterName()) require.NoError(t, err)