diff --git a/docs/pages/includes/config-reference/ssh-service.yaml b/docs/pages/includes/config-reference/ssh-service.yaml index 671893a9ecf4d..d7965552b563c 100644 --- a/docs/pages/includes/config-reference/ssh-service.yaml +++ b/docs/pages/includes/config-reference/ssh-service.yaml @@ -2,90 +2,101 @@ ssh_service: # Turns 'ssh' role on. Default is true enabled: true - # IP and the port for SSH service to bind to. - listen_addr: 0.0.0.0:3022 + # IP and the port for SSH service to bind to. + listen_addr: 0.0.0.0:3022 - # The optional public address the SSH service. This is useful if - # administrators want to allow users to connect to nodes directly, - # bypassing a Teleport proxy. - public_addr: node.example.com:3022 + # The optional public address the SSH service. This is useful if + # administrators want to allow users to connect to nodes directly, + # bypassing a Teleport proxy. + public_addr: node.example.com:3022 - labels: - role: leader - type: postgres + labels: + role: leader + type: postgres - # List of the commands to periodically execute. Their output will be used - # as node labels. - commands: + # List of the commands to periodically execute. Their output will be used + # as node labels. + commands: # this command will add a label 'arch=x86_64' to a node - name: arch command: ['/bin/uname', '-p'] period: 1h0m0s - # Enables reading ~/.tsh/environment on the server before creating a session. - # Disabled by default. Can be enabled here or via the `--permit-user-env` flag. - permit_user_env: false - - # Disables automatic creation of host users on this SSH node. - # Set to false by default. - disable_create_host_user: true - - # Enhanced Session Recording - enhanced_recording: - # Enable or disable enhanced auditing for this node. Default value: - # false. - enabled: false - - # command_buffer_size is optional with a default value of 8 pages. - command_buffer_size: 8 - - # disk_buffer_size is optional with default value of 128 pages. - disk_buffer_size: 128 - - # network_buffer_size is optional with default value of 8 pages. - network_buffer_size: 8 - - # Controls where cgroupv2 hierarchy is mounted. Default value: - # /cgroup2. - cgroup_path: /cgroup2 - - # Optional: Controls the path inside cgroupv2 hierarchy where Teleport - # cgroups will be placed. Default value: /teleport - root_path: /teleport - - # Configures the PAM integration. - pam: - # "no" by default - enabled: yes - # use /etc/pam.d/sshd configuration (the default) - service_name: "sshd" - # use the "auth" modules in the PAM config - # "false" by default - use_pam_auth: true - - # Enables/disables TCP forwarding. Default is 'true' - port_forwarding: true - - # When x11.enabled is set to yes, users with the "permit_x11_forwarding" - # role option will be able to request X11 forwarding sessions with - # "tsh ssh -X". - # - # X11 forwarding will only work if the server has the "xauth" binary - # installed and the Teleport Node can open Unix sockets. - # e.g. "$TEMP/.X11-unix/X[display_number]." - x11: - # no by default - enabled: yes - # display_offset can be used to specify the start of the range of X11 - # displays the server will use when granting X11 forwarding sessions - # 10 by default - display_offset: 10 - # max_display can be set to specify the end of the range of X11 displays - # to use when granting X11 forwarding sessions - # display_offset + 1000 by default - max_display: 1010 - - # Enables/disables remote file operations via SCP/SFTP for this Node. Default - # value: true - ssh_file_copy: true - + # Enables reading ~/.tsh/environment on the server before creating a session. + # Disabled by default. Can be enabled here or via the `--permit-user-env` flag. + permit_user_env: false + + # Disables automatic creation of host users on this SSH node. + # Set to false by default. + disable_create_host_user: true + + # Enables listening on the configured listen_addr when connected + # to the cluster via a reverse tunnel. If no listen_addr is + # configured, the default address is used. + # + # This allows the service to be connectable by users with direct network access. + # All connections still require a valid user certificate to be presented and will + # not permit any additional access. This is intended to provide an optional connection + # path to reduce latency if the Proxy is not co-located with the user and service. + # + # Set to false by default. + force_listen: false + + # Enhanced Session Recording + enhanced_recording: + # Enable or disable enhanced auditing for this node. Default value: + # false. + enabled: false + + # command_buffer_size is optional with a default value of 8 pages. + command_buffer_size: 8 + + # disk_buffer_size is optional with default value of 128 pages. + disk_buffer_size: 128 + + # network_buffer_size is optional with default value of 8 pages. + network_buffer_size: 8 + + # Controls where cgroupv2 hierarchy is mounted. Default value: + # /cgroup2. + cgroup_path: /cgroup2 + + # Optional: Controls the path inside cgroupv2 hierarchy where Teleport + # cgroups will be placed. Default value: /teleport + root_path: /teleport + + # Configures the PAM integration. + pam: + # "no" by default + enabled: yes + # use /etc/pam.d/sshd configuration (the default) + service_name: 'sshd' + # use the "auth" modules in the PAM config + # "false" by default + use_pam_auth: true + + # Enables/disables TCP forwarding. Default is 'true' + port_forwarding: true + + # When x11.enabled is set to yes, users with the "permit_x11_forwarding" + # role option will be able to request X11 forwarding sessions with + # "tsh ssh -X". + # + # X11 forwarding will only work if the server has the "xauth" binary + # installed and the Teleport Node can open Unix sockets. + # e.g. "$TEMP/.X11-unix/X[display_number]." + x11: + # no by default + enabled: yes + # display_offset can be used to specify the start of the range of X11 + # displays the server will use when granting X11 forwarding sessions + # 10 by default + display_offset: 10 + # max_display can be set to specify the end of the range of X11 displays + # to use when granting X11 forwarding sessions + # display_offset + 1000 by default + max_display: 1010 + + # Enables/disables remote file operations via SCP/SFTP for this Node. Default + # value: true + ssh_file_copy: true diff --git a/integration/integration_test.go b/integration/integration_test.go index 52b4f68ea58c9..d1e112edb7187 100644 --- a/integration/integration_test.go +++ b/integration/integration_test.go @@ -161,6 +161,7 @@ func TestIntegrations(t *testing.T) { t.Run("EscapeSequenceTriggers", suite.bind(testEscapeSequenceTriggers)) t.Run("ExecEvents", suite.bind(testExecEvents)) t.Run("ExternalClient", suite.bind(testExternalClient)) + t.Run("ForceListenerInTunnelMode", suite.bind(testForceListenerInTunnelMode)) t.Run("HA", suite.bind(testHA)) t.Run("Interactive (Regular)", suite.bind(testInteractiveRegular)) t.Run("Interactive (Reverse Tunnel)", suite.bind(testInteractiveReverseTunnel)) @@ -9201,3 +9202,140 @@ func testNegotiatedALPNProtocols(t *testing.T, suite *integrationTestSuite) { }) } } + +func testForceListenerInTunnelMode(t *testing.T, suite *integrationTestSuite) { + // InsecureDevMode needed for IoT node handshake + lib.SetInsecureDevMode(true) + defer lib.SetInsecureDevMode(false) + + // Create a Teleport instance with Auth/Proxy. + mainConfig := func() *servicecfg.Config { + tconf := suite.defaultServiceConfig() + tconf.Auth.Enabled = true + + tconf.Proxy.Enabled = true + tconf.Proxy.DisableWebService = false + tconf.Proxy.DisableWebInterface = true + + tconf.SSH.Enabled = false + + return tconf + } + main := suite.NewTeleportWithConfig(t, nil, nil, mainConfig()) + + // Create a Teleport ssh instance. + nodeConfig := func(tunnel, forceListen bool) *servicecfg.Config { + tconf := suite.defaultServiceConfig() + tconf.Hostname = Host + tconf.SetToken("token") + + if tunnel { + tconf.SetAuthServerAddress(utils.NetAddr{ + AddrNetwork: "tcp", + Addr: main.Web, + }) + } else { + tconf.SetAuthServerAddress(utils.NetAddr{ + AddrNetwork: "tcp", + Addr: main.Auth, + }) + } + + tconf.Auth.Enabled = false + + tconf.Proxy.Enabled = false + + tconf.SSH.Enabled = true + + if forceListen { + tconf.SSH.Addr = utils.NetAddr{ + Addr: helpers.NewListenerOn(t, Host, service.ListenerNodeSSH, &tconf.FileDescriptors), + } + tconf.SSH.ForceListen = true + } + + return tconf + } + + forceListenNode, err := main.StartReverseTunnelNode(nodeConfig(true, true)) + require.NoError(t, err) + + tunnelOnlyNode, err := main.StartReverseTunnelNode(nodeConfig(true, false)) + require.NoError(t, err) + + directNode, err := main.StartNode(nodeConfig(false, true)) + require.NoError(t, err) + + forceListenDirectNode, err := main.StartNode(nodeConfig(false, true)) + require.NoError(t, err) + + require.NoError(t, main.WaitForNodeCount(context.Background(), helpers.Site, 4)) + + creds, err := helpers.GenerateUserCreds(helpers.UserCredsRequest{ + Process: main.Process, + Username: suite.Me.Username, + }) + require.NoError(t, err) + + signer, err := creds.KeyRing.SSHSigner() + require.NoError(t, err) + + t.Run("tunnel node", func(t *testing.T) { + t.Run("forced listen node", func(t *testing.T) { + clt, err := ssh.Dial("tcp", forceListenNode.Config.SSH.Addr.Addr, &ssh.ClientConfig{ + User: suite.Me.Username, + Auth: []ssh.AuthMethod{ssh.PublicKeys(signer)}, + HostKeyCallback: ssh.InsecureIgnoreHostKey(), + Timeout: 15 * time.Second, + }) + require.NoError(t, err) + + ok, resp, err := clt.SendRequest(teleport.VersionRequest, true, nil) + require.NoError(t, err) + require.True(t, ok) + require.Equal(t, teleport.Version, string(resp)) + }) + + t.Run("tunnel only node", func(t *testing.T) { + _, err := ssh.Dial("tcp", tunnelOnlyNode.Config.SSH.Addr.Addr, &ssh.ClientConfig{ + User: suite.Me.Username, + Auth: []ssh.AuthMethod{ssh.PublicKeys(signer)}, + HostKeyCallback: ssh.InsecureIgnoreHostKey(), + Timeout: 15 * time.Second, + }) + require.Error(t, err) + }) + }) + + t.Run("direct node", func(t *testing.T) { + t.Run("forced listen node", func(t *testing.T) { + clt, err := ssh.Dial("tcp", forceListenDirectNode.Config.SSH.Addr.Addr, &ssh.ClientConfig{ + User: suite.Me.Username, + Auth: []ssh.AuthMethod{ssh.PublicKeys(signer)}, + HostKeyCallback: ssh.InsecureIgnoreHostKey(), + Timeout: 15 * time.Second, + }) + require.NoError(t, err) + + ok, resp, err := clt.SendRequest(teleport.VersionRequest, true, nil) + require.NoError(t, err) + require.True(t, ok) + require.Equal(t, teleport.Version, string(resp)) + }) + + t.Run("direct node", func(t *testing.T) { + clt, err := ssh.Dial("tcp", directNode.Config.SSH.Addr.Addr, &ssh.ClientConfig{ + User: suite.Me.Username, + Auth: []ssh.AuthMethod{ssh.PublicKeys(signer)}, + HostKeyCallback: ssh.InsecureIgnoreHostKey(), + Timeout: 15 * time.Second, + }) + require.NoError(t, err) + + ok, resp, err := clt.SendRequest(teleport.VersionRequest, true, nil) + require.NoError(t, err) + require.True(t, ok) + require.Equal(t, teleport.Version, string(resp)) + }) + }) +} diff --git a/lib/config/configuration.go b/lib/config/configuration.go index e6ede9f0d332f..baec2ecaf5c94 100644 --- a/lib/config/configuration.go +++ b/lib/config/configuration.go @@ -1598,6 +1598,8 @@ func applySSHConfig(fc *FileConfig, cfg *servicecfg.Config) (err error) { cfg.SSH.AllowFileCopying = fc.SSH.SSHFileCopy() + cfg.SSH.ForceListen = fc.SSH.ForceListen + return nil } diff --git a/lib/config/fileconf.go b/lib/config/fileconf.go index 727674c29435a..3ceba36ea11ac 100644 --- a/lib/config/fileconf.go +++ b/lib/config/fileconf.go @@ -1452,6 +1452,16 @@ type SSH struct { // DisableCreateHostUser disables automatic user provisioning on this // SSH node. DisableCreateHostUser bool `yaml:"disable_create_host_user,omitempty"` + + // ForceListen enables listening on the configured ListenAddress + // when connected to the cluster via a reverse tunnel. If no ListenAddress is + // configured, the default address is used. + // + // This allows the service to be connectable by users with direct network access. + // All connections still require a valid user certificate to be presented and will + // not permit any additional access. This is intended to provide an optional connection + // path to reduce latency if the Proxy is not co-located with the user and service. + ForceListen bool `yaml:"force_listen,omitempty"` } // AllowTCPForwarding checks whether the config file allows TCP forwarding or not. diff --git a/lib/service/service.go b/lib/service/service.go index 092a10d4da686..755b7940dd4e4 100644 --- a/lib/service/service.go +++ b/lib/service/service.go @@ -3031,13 +3031,15 @@ func (process *TeleportProcess) initSSH() error { logger.WarnContext(process.ExitContext(), warn) } + useLocalListener := cfg.SSH.ForceListen || !conn.UseTunnel() + // Provide helpful log message if listen_addr or public_addr are not being // used (tunnel is used to connect to cluster). // // If a tunnel is not being used, set the default here (could not be done in // file configuration because at that time it's not known if server is // joining cluster directly or through a tunnel). - if conn.UseTunnel() { + if !useLocalListener { if !cfg.SSH.Addr.IsEmpty() { logger.InfoContext(process.ExitContext(), "Connected to cluster over tunnel connection, ignoring listen_addr setting.") } @@ -3045,7 +3047,7 @@ func (process *TeleportProcess) initSSH() error { logger.InfoContext(process.ExitContext(), "Connected to cluster over tunnel connection, ignoring public_addr setting.") } } - if !conn.UseTunnel() && cfg.SSH.Addr.IsEmpty() { + if useLocalListener && cfg.SSH.Addr.IsEmpty() { cfg.SSH.Addr = *defaults.SSHServerListenAddr() } @@ -3164,7 +3166,7 @@ func (process *TeleportProcess) initSSH() error { } var agentPool *reversetunnel.AgentPool - if !conn.UseTunnel() { + if useLocalListener { listener, err := process.importOrCreateListener(ListenerNodeSSH, cfg.SSH.Addr.Addr) if err != nil { return trace.Wrap(err) @@ -3212,7 +3214,9 @@ func (process *TeleportProcess) initSSH() error { if err := s.Start(); err != nil { return trace.Wrap(err) } + } + if conn.UseTunnel() { var serverHandler reversetunnel.ServerHandler = s if resumableServer != nil { serverHandler = resumableServer diff --git a/lib/service/servicecfg/ssh.go b/lib/service/servicecfg/ssh.go index a29b93482752f..963b961707849 100644 --- a/lib/service/servicecfg/ssh.go +++ b/lib/service/servicecfg/ssh.go @@ -62,4 +62,13 @@ type SSHConfig struct { // DisableCreateHostUser disables automatic user provisioning on this // SSH node. DisableCreateHostUser bool + + // ForceListen enables the service to listen on the configured [Addr] + // when it is connected to the cluster via a reverse tunnel. + // This allows the service to be connectable by users with direct network access. + // All connections still require a valid user certificate to be presented and will + // not permit any extra access. This is intended to provide an optional connection + // path to hosts that may provide reduced latency if the Proxy is not co-located with + // the user and service. + ForceListen bool }