From bd4dcab802bfce7e2ff87ba3eec332dd9944d221 Mon Sep 17 00:00:00 2001 From: Noah Stride Date: Wed, 15 May 2024 18:30:54 +0100 Subject: [PATCH] Machine ID: Switch to using new `tbot ssh-proxy-command` by default. (#41482) * Add warning to previous command * Hash out basics of new tbot proxying command use * Avoid passing full botConfig into `tbot ssh-proxy-command` * wip * simplify impl * Set up test suite for ssh config generation * Add support for checking if ALPNConnUpgrade is required * Cache the alpn upgrade result * Americanize Spellings * Update lib/tbot/config/template_ssh_client.go Co-authored-by: rosstimothy <39066650+rosstimothy@users.noreply.github.com> * Add CHANGELOG.md entry for breaking change * Update tool/tbot/proxy.go Co-authored-by: Edoardo Spadolini * Update lib/tbot/tbot.go Co-authored-by: Edoardo Spadolini * Switch to `--no` variants rather than `={bool}` * Use SingleFlight on alpn proxy upgrade cache * Improve singleflight * Add rudimentary shell quoting * Uniformly pad the templating * Rename shellQuote -> proxyCommandQuote * Fix imports --------- Co-authored-by: rosstimothy <39066650+rosstimothy@users.noreply.github.com> Co-authored-by: Edoardo Spadolini --- CHANGELOG.md | 7 ++ lib/config/openssh/openssh.go | 31 +++++++- lib/config/openssh/openssh_test.go | 16 ++++ .../test_shellQuote.golden | 15 ++++ lib/tbot/config/bot.go | 6 ++ lib/tbot/config/bot_test.go | 19 +++-- lib/tbot/config/output_identity.go | 1 + lib/tbot/config/template_ssh_client.go | 78 ++++++++++++++++--- lib/tbot/config/template_ssh_client_test.go | 42 ++++++++-- .../latest_OpenSSH/ssh_config.golden | 4 +- .../known_hosts.golden | 4 + .../ssh_config.golden | 26 +++++++ .../known_hosts.golden | 4 + .../ssh_config.golden | 26 +++++++ .../known_hosts.golden | 4 + .../ssh_config.golden | 26 +++++++ .../legacy_OpenSSH/ssh_config.golden | 4 +- lib/tbot/service_outputs.go | 31 ++++++-- lib/tbot/ssh_proxy.go | 50 +++++++----- lib/tbot/tbot.go | 73 +++++++++++++++-- tool/tbot/main.go | 4 +- tool/tbot/proxy.go | 11 ++- tool/tbot/proxy_ssh.go | 8 +- 23 files changed, 417 insertions(+), 73 deletions(-) create mode 100644 lib/config/openssh/testdata/TestSSHConfig_GetSSHConfig/test_shellQuote.golden create mode 100644 lib/tbot/config/testdata/TestTemplateSSHClient_Render/latest_OpenSSH_no_tls_routing/known_hosts.golden create mode 100644 lib/tbot/config/testdata/TestTemplateSSHClient_Render/latest_OpenSSH_no_tls_routing/ssh_config.golden create mode 100644 lib/tbot/config/testdata/TestTemplateSSHClient_Render/latest_OpenSSH_with_alpn_upgrade/known_hosts.golden create mode 100644 lib/tbot/config/testdata/TestTemplateSSHClient_Render/latest_OpenSSH_with_alpn_upgrade/ssh_config.golden create mode 100644 lib/tbot/config/testdata/TestTemplateSSHClient_Render/latest_OpenSSH_with_legacy_proxycommand/known_hosts.golden create mode 100644 lib/tbot/config/testdata/TestTemplateSSHClient_Render/latest_OpenSSH_with_legacy_proxycommand/ssh_config.golden diff --git a/CHANGELOG.md b/CHANGELOG.md index 1843983014b..d700a2e53b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,13 @@ policy [example](docs/pages/includes/dynamodb-iam-policy.mdx). Support for disabling second factor authentication has been removed +#### Machine ID and OpenSSH client config changes + +Users with custom `ssh_config` should modify their ProxyCommand to use the new, +more performant, `tbot ssh-proxy-command`. See +https://goteleport.com/docs/machine-id/reference/v16-upgrade-guide/ for more +details. + ## 15.0.0 (xx/xx/24) ### New features diff --git a/lib/config/openssh/openssh.go b/lib/config/openssh/openssh.go index d4b80a0c555..5cc5b99651a 100644 --- a/lib/config/openssh/openssh.go +++ b/lib/config/openssh/openssh.go @@ -35,7 +35,18 @@ import ( "github.com/gravitational/teleport/lib/utils" ) -var sshConfigTemplate = template.Must(template.New("ssh-config").Parse( +var sshConfigTemplate = template.Must(template.New("ssh-config").Funcs(template.FuncMap{ + // proxyCommandQuote prepares a string for insertion into the ssh_config + "proxyCommandQuote": func(s string) string { + s = `'` + strings.ReplaceAll(s, `'`, `'"'"'`) + `'` + // escape any percent signs which could trigger the percent expansion + // for ProxyCommand. + s = strings.ReplaceAll(s, `%`, `%%`) + // escape any newlines which could impact the parsing of ssh_config + s = strings.ReplaceAll(s, "\n", `'"\n"'`) + return s + }, +}).Parse( `# Begin generated Teleport configuration for {{ .ProxyHost }} by {{ .AppName }} {{$dot := . }} {{- range $clusterName := .ClusterNames }} @@ -52,11 +63,16 @@ Host *.{{ $clusterName }} {{ $dot.ProxyHost }} # Flags for all {{ $clusterName }} hosts except the proxy Host *.{{ $clusterName }} !{{ $dot.ProxyHost }} Port {{ $dot.Port }} - {{- if eq $dot.AppName "tsh" }} +{{- if eq $dot.AppName "tsh" }} ProxyCommand "{{ $dot.ExecutablePath }}" proxy ssh --cluster={{ $clusterName }} --proxy={{ $dot.ProxyHost }}:{{ $dot.ProxyPort }} %r@%h:%p -{{- end }}{{- if eq $dot.AppName "tbot" }} +{{- end }} +{{- if eq $dot.AppName "tbot" }} +{{- if $dot.PureTBotProxyCommand }} + ProxyCommand {{ proxyCommandQuote $dot.ExecutablePath }} ssh-proxy-command --destination-dir={{ proxyCommandQuote $dot.DestinationDir }} --proxy-server={{ proxyCommandQuote (print $dot.ProxyHost ":" $dot.ProxyPort) }} --cluster={{ proxyCommandQuote $clusterName }} {{ if $dot.TLSRouting }}--tls-routing{{ else }}--no-tls-routing{{ end }} {{ if $dot.ConnectionUpgrade }}--connection-upgrade{{ else }}--no-connection-upgrade{{ end }} {{ if $dot.Resume }}--resume{{ else }}--no-resume{{ end }} --user=%r --host=%h --port=%p +{{- else }} ProxyCommand "{{ $dot.ExecutablePath }}" proxy --destination-dir={{ $dot.DestinationDir }} --proxy-server={{ $dot.ProxyHost }}:{{ $dot.ProxyPort }} ssh --cluster={{ $clusterName }} %r@%h:%p {{- end }} +{{- end }} {{- end }} {{- if ne $dot.Username "" }} User "{{ $dot.Username }}" @@ -79,6 +95,15 @@ type SSHConfigParameters struct { DestinationDir string // Port is the node port to use, defaulting to 3022, if not specified by flag Port int + + // PureTBotProxyCommand enables the new `ssh-proxy-command` operating mode + // when generating the ssh_config for tbot. + PureTBotProxyCommand bool + ConnectionUpgrade bool + TLSRouting bool + Insecure bool + FIPS bool + Resume bool } type sshTmplParams struct { diff --git a/lib/config/openssh/openssh_test.go b/lib/config/openssh/openssh_test.go index 7e1bdb4879f..872922e1eb5 100644 --- a/lib/config/openssh/openssh_test.go +++ b/lib/config/openssh/openssh_test.go @@ -137,6 +137,22 @@ func TestSSHConfig_GetSSHConfig(t *testing.T) { Port: 3232, }, }, + { + name: "test shellQuote", + sshVersion: "9.0.0", + config: &SSHConfigParameters{ + AppName: TbotApp, + PureTBotProxyCommand: true, + ClusterNames: []string{"example.com"}, + KnownHostsPath: "/home/alice/.tsh/known_hosts", + IdentityFilePath: "/home/alice/.tsh/keys/example.com/bob", + CertificateFilePath: "/home/alice/.tsh/keys/example.com/bob-ssh/example.com-cert.pub", + ProxyHost: "proxy.example.com", + ProxyPort: "443", + ExecutablePath: "/home/edoardo/$( sudo rm -rf / )/tbot", + DestinationDir: "/home/edo\nardo/$( sudo rm -rf / )/tbot-ou'tput", + }, + }, } for _, tt := range tests { diff --git a/lib/config/openssh/testdata/TestSSHConfig_GetSSHConfig/test_shellQuote.golden b/lib/config/openssh/testdata/TestSSHConfig_GetSSHConfig/test_shellQuote.golden new file mode 100644 index 00000000000..74e9a0bdd7a --- /dev/null +++ b/lib/config/openssh/testdata/TestSSHConfig_GetSSHConfig/test_shellQuote.golden @@ -0,0 +1,15 @@ +# Begin generated Teleport configuration for proxy.example.com by tbot + +# Common flags for all example.com hosts +Host *.example.com proxy.example.com + UserKnownHostsFile "/home/alice/.tsh/known_hosts" + IdentityFile "/home/alice/.tsh/keys/example.com/bob" + CertificateFile "/home/alice/.tsh/keys/example.com/bob-ssh/example.com-cert.pub" + HostKeyAlgorithms rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-256-cert-v01@openssh.com,ssh-rsa-cert-v01@openssh.com + +# Flags for all example.com hosts except the proxy +Host *.example.com !proxy.example.com + Port 3022 + ProxyCommand '/home/edoardo/$( sudo rm -rf / )/tbot' ssh-proxy-command --destination-dir='/home/edo'"\n"'ardo/$( sudo rm -rf / )/tbot-ou'"'"'tput' --proxy-server='proxy.example.com:443' --cluster='example.com' --no-tls-routing --no-connection-upgrade --no-resume --user=%r --host=%h --port=%p + +# End generated Teleport configuration diff --git a/lib/tbot/config/bot.go b/lib/tbot/config/bot.go index 4371d8d74a0..f65d83221a4 100644 --- a/lib/tbot/config/bot.go +++ b/lib/tbot/config/bot.go @@ -61,4 +61,10 @@ type provider interface { // SignX509SVIDs uses the impersonatedClient to call SignX509SVIDs. SignX509SVIDs(ctx context.Context, in *machineidv1pb.SignX509SVIDsRequest, opts ...grpc.CallOption) (*machineidv1pb.SignX509SVIDsResponse, error) + + // IsALPNConnUpgradeRequired returns a (possibly cached) test of whether ALPN + // routing is required. + IsALPNConnUpgradeRequired( + ctx context.Context, addr string, insecure bool, + ) (bool, error) } diff --git a/lib/tbot/config/bot_test.go b/lib/tbot/config/bot_test.go index ec9eef72ebf..5afa7dd843e 100644 --- a/lib/tbot/config/bot_test.go +++ b/lib/tbot/config/bot_test.go @@ -63,10 +63,12 @@ func fakeGetExecutablePath() (string, error) { // mockProvider is a minimal Bot impl that can be used in tests type mockProvider struct { - cfg *BotConfig - proxyAddr string - remoteClusterName string - clusterName string + cfg *BotConfig + proxyAddr string + remoteClusterName string + clusterName string + isTLSRouting bool + isALPNUpgradeRequired bool } func newMockProvider(cfg *BotConfig) *mockProvider { @@ -75,9 +77,16 @@ func newMockProvider(cfg *BotConfig) *mockProvider { proxyAddr: mockProxyAddr, clusterName: mockClusterName, remoteClusterName: mockRemoteClusterName, + isTLSRouting: true, } } +func (p *mockProvider) IsALPNConnUpgradeRequired( + ctx context.Context, addr string, insecure bool, +) (bool, error) { + return p.isALPNUpgradeRequired, nil +} + func (p *mockProvider) GetRemoteClusters(ctx context.Context) ([]types.RemoteCluster, error) { rc, err := types.NewRemoteCluster(p.remoteClusterName) if err != nil { @@ -167,7 +176,7 @@ func (p *mockProvider) ProxyPing(ctx context.Context) (*webclient.PingResponse, return &webclient.PingResponse{ ClusterName: p.clusterName, Proxy: webclient.ProxySettings{ - TLSRoutingEnabled: true, + TLSRoutingEnabled: p.isTLSRouting, SSH: webclient.SSHProxySettings{ PublicAddr: p.proxyAddr, }, diff --git a/lib/tbot/config/output_identity.go b/lib/tbot/config/output_identity.go index ee914057c63..5c897e7207f 100644 --- a/lib/tbot/config/output_identity.go +++ b/lib/tbot/config/output_identity.go @@ -89,6 +89,7 @@ func (o *IdentityOutput) templates() []template { templates = append(templates, &templateSSHClient{ getSSHVersion: openssh.GetSystemSSHVersion, executablePathGetter: os.Executable, + getEnv: os.Getenv, destPath: o.destPath, }) } diff --git a/lib/tbot/config/template_ssh_client.go b/lib/tbot/config/template_ssh_client.go index 60836fd775b..f0f195240c2 100644 --- a/lib/tbot/config/template_ssh_client.go +++ b/lib/tbot/config/template_ssh_client.go @@ -36,10 +36,19 @@ import ( "github.com/gravitational/teleport/lib/utils" ) +// sshConfigProxyModeEnv is the environment variable that controls whether or +// not to use the new proxy command. +// It supports: +// - "legacy" (default in v15): use the legacy proxy command +// - "new" (default in v16): use the new proxy command +// In v17, it will be removed. +const sshConfigProxyModeEnv = "TBOT_SSH_CONFIG_PROXY_COMMAND_MODE" + // templateSSHClient contains parameters for the ssh_config config // template type templateSSHClient struct { getSSHVersion func() (*semver.Version, error) + getEnv func(key string) string executablePathGetter executablePathGetter // destPath controls whether or not to write the SSH config file. // This is lets this be skipped on non-directory destinations where this @@ -159,18 +168,63 @@ func (c *templateSSHClient) render( certificateFilePath := filepath.Join(absDestPath, identity.SSHCertKey) sshConf := openssh.NewSSHConfig(c.getSSHVersion, nil) - if err := sshConf.GetSSHConfig(&sshConfigBuilder, &openssh.SSHConfigParameters{ - AppName: openssh.TbotApp, - ClusterNames: clusterNames, - KnownHostsPath: knownHostsPath, - IdentityFilePath: identityFilePath, - CertificateFilePath: certificateFilePath, - ProxyHost: proxyHost, - ProxyPort: proxyPort, - ExecutablePath: executablePath, - DestinationDir: absDestPath, - }); err != nil { - return trace.Wrap(err) + botConfig := bot.Config() + + if c.getEnv(sshConfigProxyModeEnv) == "legacy" { + // Deprecated: this block will be removed in v17. It exists so users can + // revert to the old behavior if necessary. + // TODO(strideynet) DELETE IN 17.0.0 + if err := sshConf.GetSSHConfig(&sshConfigBuilder, &openssh.SSHConfigParameters{ + AppName: openssh.TbotApp, + ClusterNames: clusterNames, + KnownHostsPath: knownHostsPath, + IdentityFilePath: identityFilePath, + CertificateFilePath: certificateFilePath, + ProxyHost: proxyHost, + ProxyPort: proxyPort, + ExecutablePath: executablePath, + DestinationDir: absDestPath, + }); err != nil { + return trace.Wrap(err) + } + } else { + // Test if ALPN upgrade is required, this will only be necessary if we + // are using TLS routing. + connUpgradeRequired := false + if ping.Proxy.TLSRoutingEnabled { + connUpgradeRequired, err = bot.IsALPNConnUpgradeRequired( + ctx, ping.Proxy.SSH.PublicAddr, botConfig.Insecure, + ) + if err != nil { + return trace.Wrap(err, "determining if ALPN upgrade is required") + } + } + + // Generate SSH config + if err := sshConf.GetSSHConfig(&sshConfigBuilder, &openssh.SSHConfigParameters{ + AppName: openssh.TbotApp, + ClusterNames: clusterNames, + KnownHostsPath: knownHostsPath, + IdentityFilePath: identityFilePath, + CertificateFilePath: certificateFilePath, + ProxyHost: proxyHost, + ProxyPort: proxyPort, + ExecutablePath: executablePath, + DestinationDir: absDestPath, + + PureTBotProxyCommand: true, + Insecure: botConfig.Insecure, + FIPS: botConfig.FIPS, + TLSRouting: ping.Proxy.TLSRoutingEnabled, + ConnectionUpgrade: connUpgradeRequired, + + // Session resumption is enabled by default, this can be + // configurable at a later date if we discover reasons for this to + // be disabled. + Resume: true, + }); err != nil { + return trace.Wrap(err) + } } if err := destination.Write(ctx, sshConfigName, []byte(sshConfigBuilder.String())); err != nil { diff --git a/lib/tbot/config/template_ssh_client_test.go b/lib/tbot/config/template_ssh_client_test.go index c6716724c32..32208de928c 100644 --- a/lib/tbot/config/template_ssh_client_test.go +++ b/lib/tbot/config/template_ssh_client_test.go @@ -35,16 +35,40 @@ import ( func TestTemplateSSHClient_Render(t *testing.T) { tests := []struct { - Name string - Version string + Name string + Version string + Env map[string]string + TLSRouting bool + ALPNUpgrade bool }{ { - Name: "legacy OpenSSH", - Version: "6.5.0", + Name: "legacy OpenSSH", + Version: "6.5.0", + TLSRouting: true, }, { - Name: "latest OpenSSH", + Name: "latest OpenSSH", + Version: "9.0.0", + TLSRouting: true, + }, + { + Name: "latest OpenSSH no tls routing", + Version: "9.0.0", + TLSRouting: false, + }, + { + Name: "latest OpenSSH with alpn upgrade", + Version: "9.0.0", + ALPNUpgrade: true, + TLSRouting: true, + }, + { + Name: "latest OpenSSH with legacy proxycommand", Version: "9.0.0", + Env: map[string]string{ + sshConfigProxyModeEnv: "legacy", + }, + TLSRouting: true, }, } @@ -64,12 +88,20 @@ func TestTemplateSSHClient_Render(t *testing.T) { } mockBot := newMockProvider(cfg) + mockBot.isALPNUpgradeRequired = tc.ALPNUpgrade + mockBot.isTLSRouting = tc.TLSRouting tmpl := templateSSHClient{ getSSHVersion: func() (*semver.Version, error) { return semver.New(tc.Version), nil }, executablePathGetter: fakeGetExecutablePath, destPath: dest.Path, + getEnv: func(key string) string { + if tc.Env == nil { + return "" + } + return tc.Env[key] + }, } err = tmpl.render(context.Background(), mockBot, ident, dest) diff --git a/lib/tbot/config/testdata/TestTemplateSSHClient_Render/latest_OpenSSH/ssh_config.golden b/lib/tbot/config/testdata/TestTemplateSSHClient_Render/latest_OpenSSH/ssh_config.golden index d10583bc2b0..0168ff3c9e6 100644 --- a/lib/tbot/config/testdata/TestTemplateSSHClient_Render/latest_OpenSSH/ssh_config.golden +++ b/lib/tbot/config/testdata/TestTemplateSSHClient_Render/latest_OpenSSH/ssh_config.golden @@ -10,7 +10,7 @@ Host *.tele.blackmesa.gov tele.blackmesa.gov # Flags for all tele.blackmesa.gov hosts except the proxy Host *.tele.blackmesa.gov !tele.blackmesa.gov Port 3022 - ProxyCommand "/path/to/tbot" proxy --destination-dir=/test/dir --proxy-server=tele.blackmesa.gov:443 ssh --cluster=tele.blackmesa.gov %r@%h:%p + ProxyCommand '/path/to/tbot' ssh-proxy-command --destination-dir='/test/dir' --proxy-server='tele.blackmesa.gov:443' --cluster='tele.blackmesa.gov' --tls-routing --no-connection-upgrade --resume --user=%r --host=%h --port=%p # Common flags for all tele.aperture.labs hosts Host *.tele.aperture.labs tele.blackmesa.gov UserKnownHostsFile "/test/dir/known_hosts" @@ -21,6 +21,6 @@ Host *.tele.aperture.labs tele.blackmesa.gov # Flags for all tele.aperture.labs hosts except the proxy Host *.tele.aperture.labs !tele.blackmesa.gov Port 3022 - ProxyCommand "/path/to/tbot" proxy --destination-dir=/test/dir --proxy-server=tele.blackmesa.gov:443 ssh --cluster=tele.aperture.labs %r@%h:%p + ProxyCommand '/path/to/tbot' ssh-proxy-command --destination-dir='/test/dir' --proxy-server='tele.blackmesa.gov:443' --cluster='tele.aperture.labs' --tls-routing --no-connection-upgrade --resume --user=%r --host=%h --port=%p # End generated Teleport configuration diff --git a/lib/tbot/config/testdata/TestTemplateSSHClient_Render/latest_OpenSSH_no_tls_routing/known_hosts.golden b/lib/tbot/config/testdata/TestTemplateSSHClient_Render/latest_OpenSSH_no_tls_routing/known_hosts.golden new file mode 100644 index 00000000000..8b414dc2bc1 --- /dev/null +++ b/lib/tbot/config/testdata/TestTemplateSSHClient_Render/latest_OpenSSH_no_tls_routing/known_hosts.golden @@ -0,0 +1,4 @@ +@cert-authority tele.blackmesa.gov,tele.blackmesa.gov,*.tele.blackmesa.gov ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC8kYdyZA1ZSNjZ4pqybDXvWplHQHkU6fPL+cAYHUkAT5CiQV4GOjwaSTcvZNK5U2fQ0jm6jknCnsZi1t9JujCjXUT3bYHCnSwWhXN55QzIu530Q/MeXz5W8TxYRrWULgPhqqtq8B9N554+s40higG21fmhhdDtpmQzw3vJLspY05mnL1+fW+RIKkM4rb150sdZXKINxfNQvERteE8WX0vL2yG4RuqJzYtGCDEGeHd+HLne7xfmqPxun7bUYaxAlplhm1z2J41hqaj8pBwDSEV9SBOZXvh6FjS9nvJCT7Z1bbZwWrAO/7E2ac0eV+5iEc0J+TyufO3F9uod+J+AICtB type=host +@cert-authority tele.blackmesa.gov,tele.blackmesa.gov,*.tele.blackmesa.gov ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC8kYdyZA1ZSNjZ4pqybDXvWplHQHkU6fPL+cAYHUkAT5CiQV4GOjwaSTcvZNK5U2fQ0jm6jknCnsZi1t9JujCjXUT3bYHCnSwWhXN55QzIu530Q/MeXz5W8TxYRrWULgPhqqtq8B9N554+s40higG21fmhhdDtpmQzw3vJLspY05mnL1+fW+RIKkM4rb150sdZXKINxfNQvERteE8WX0vL2yG4RuqJzYtGCDEGeHd+HLne7xfmqPxun7bUYaxAlplhm1z2J41hqaj8pBwDSEV9SBOZXvh6FjS9nvJCT7Z1bbZwWrAO/7E2ac0eV+5iEc0J+TyufO3F9uod+J+AICtB type=host +@cert-authority tele.blackmesa.gov,tele.aperture.labs,*.tele.aperture.labs ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC8kYdyZA1ZSNjZ4pqybDXvWplHQHkU6fPL+cAYHUkAT5CiQV4GOjwaSTcvZNK5U2fQ0jm6jknCnsZi1t9JujCjXUT3bYHCnSwWhXN55QzIu530Q/MeXz5W8TxYRrWULgPhqqtq8B9N554+s40higG21fmhhdDtpmQzw3vJLspY05mnL1+fW+RIKkM4rb150sdZXKINxfNQvERteE8WX0vL2yG4RuqJzYtGCDEGeHd+HLne7xfmqPxun7bUYaxAlplhm1z2J41hqaj8pBwDSEV9SBOZXvh6FjS9nvJCT7Z1bbZwWrAO/7E2ac0eV+5iEc0J+TyufO3F9uod+J+AICtB type=host +@cert-authority tele.blackmesa.gov,tele.aperture.labs,*.tele.aperture.labs ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC8kYdyZA1ZSNjZ4pqybDXvWplHQHkU6fPL+cAYHUkAT5CiQV4GOjwaSTcvZNK5U2fQ0jm6jknCnsZi1t9JujCjXUT3bYHCnSwWhXN55QzIu530Q/MeXz5W8TxYRrWULgPhqqtq8B9N554+s40higG21fmhhdDtpmQzw3vJLspY05mnL1+fW+RIKkM4rb150sdZXKINxfNQvERteE8WX0vL2yG4RuqJzYtGCDEGeHd+HLne7xfmqPxun7bUYaxAlplhm1z2J41hqaj8pBwDSEV9SBOZXvh6FjS9nvJCT7Z1bbZwWrAO/7E2ac0eV+5iEc0J+TyufO3F9uod+J+AICtB type=host diff --git a/lib/tbot/config/testdata/TestTemplateSSHClient_Render/latest_OpenSSH_no_tls_routing/ssh_config.golden b/lib/tbot/config/testdata/TestTemplateSSHClient_Render/latest_OpenSSH_no_tls_routing/ssh_config.golden new file mode 100644 index 00000000000..0f10cea42eb --- /dev/null +++ b/lib/tbot/config/testdata/TestTemplateSSHClient_Render/latest_OpenSSH_no_tls_routing/ssh_config.golden @@ -0,0 +1,26 @@ +# Begin generated Teleport configuration for tele.blackmesa.gov by tbot + +# Common flags for all tele.blackmesa.gov hosts +Host *.tele.blackmesa.gov tele.blackmesa.gov + UserKnownHostsFile "/test/dir/known_hosts" + IdentityFile "/test/dir/key" + CertificateFile "/test/dir/key-cert.pub" + HostKeyAlgorithms rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-256-cert-v01@openssh.com,ssh-rsa-cert-v01@openssh.com + +# Flags for all tele.blackmesa.gov hosts except the proxy +Host *.tele.blackmesa.gov !tele.blackmesa.gov + Port 3022 + ProxyCommand '/path/to/tbot' ssh-proxy-command --destination-dir='/test/dir' --proxy-server='tele.blackmesa.gov:443' --cluster='tele.blackmesa.gov' --no-tls-routing --no-connection-upgrade --resume --user=%r --host=%h --port=%p +# Common flags for all tele.aperture.labs hosts +Host *.tele.aperture.labs tele.blackmesa.gov + UserKnownHostsFile "/test/dir/known_hosts" + IdentityFile "/test/dir/key" + CertificateFile "/test/dir/key-cert.pub" + HostKeyAlgorithms rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-256-cert-v01@openssh.com,ssh-rsa-cert-v01@openssh.com + +# Flags for all tele.aperture.labs hosts except the proxy +Host *.tele.aperture.labs !tele.blackmesa.gov + Port 3022 + ProxyCommand '/path/to/tbot' ssh-proxy-command --destination-dir='/test/dir' --proxy-server='tele.blackmesa.gov:443' --cluster='tele.aperture.labs' --no-tls-routing --no-connection-upgrade --resume --user=%r --host=%h --port=%p + +# End generated Teleport configuration diff --git a/lib/tbot/config/testdata/TestTemplateSSHClient_Render/latest_OpenSSH_with_alpn_upgrade/known_hosts.golden b/lib/tbot/config/testdata/TestTemplateSSHClient_Render/latest_OpenSSH_with_alpn_upgrade/known_hosts.golden new file mode 100644 index 00000000000..8b414dc2bc1 --- /dev/null +++ b/lib/tbot/config/testdata/TestTemplateSSHClient_Render/latest_OpenSSH_with_alpn_upgrade/known_hosts.golden @@ -0,0 +1,4 @@ +@cert-authority tele.blackmesa.gov,tele.blackmesa.gov,*.tele.blackmesa.gov ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC8kYdyZA1ZSNjZ4pqybDXvWplHQHkU6fPL+cAYHUkAT5CiQV4GOjwaSTcvZNK5U2fQ0jm6jknCnsZi1t9JujCjXUT3bYHCnSwWhXN55QzIu530Q/MeXz5W8TxYRrWULgPhqqtq8B9N554+s40higG21fmhhdDtpmQzw3vJLspY05mnL1+fW+RIKkM4rb150sdZXKINxfNQvERteE8WX0vL2yG4RuqJzYtGCDEGeHd+HLne7xfmqPxun7bUYaxAlplhm1z2J41hqaj8pBwDSEV9SBOZXvh6FjS9nvJCT7Z1bbZwWrAO/7E2ac0eV+5iEc0J+TyufO3F9uod+J+AICtB type=host +@cert-authority tele.blackmesa.gov,tele.blackmesa.gov,*.tele.blackmesa.gov ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC8kYdyZA1ZSNjZ4pqybDXvWplHQHkU6fPL+cAYHUkAT5CiQV4GOjwaSTcvZNK5U2fQ0jm6jknCnsZi1t9JujCjXUT3bYHCnSwWhXN55QzIu530Q/MeXz5W8TxYRrWULgPhqqtq8B9N554+s40higG21fmhhdDtpmQzw3vJLspY05mnL1+fW+RIKkM4rb150sdZXKINxfNQvERteE8WX0vL2yG4RuqJzYtGCDEGeHd+HLne7xfmqPxun7bUYaxAlplhm1z2J41hqaj8pBwDSEV9SBOZXvh6FjS9nvJCT7Z1bbZwWrAO/7E2ac0eV+5iEc0J+TyufO3F9uod+J+AICtB type=host +@cert-authority tele.blackmesa.gov,tele.aperture.labs,*.tele.aperture.labs ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC8kYdyZA1ZSNjZ4pqybDXvWplHQHkU6fPL+cAYHUkAT5CiQV4GOjwaSTcvZNK5U2fQ0jm6jknCnsZi1t9JujCjXUT3bYHCnSwWhXN55QzIu530Q/MeXz5W8TxYRrWULgPhqqtq8B9N554+s40higG21fmhhdDtpmQzw3vJLspY05mnL1+fW+RIKkM4rb150sdZXKINxfNQvERteE8WX0vL2yG4RuqJzYtGCDEGeHd+HLne7xfmqPxun7bUYaxAlplhm1z2J41hqaj8pBwDSEV9SBOZXvh6FjS9nvJCT7Z1bbZwWrAO/7E2ac0eV+5iEc0J+TyufO3F9uod+J+AICtB type=host +@cert-authority tele.blackmesa.gov,tele.aperture.labs,*.tele.aperture.labs ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC8kYdyZA1ZSNjZ4pqybDXvWplHQHkU6fPL+cAYHUkAT5CiQV4GOjwaSTcvZNK5U2fQ0jm6jknCnsZi1t9JujCjXUT3bYHCnSwWhXN55QzIu530Q/MeXz5W8TxYRrWULgPhqqtq8B9N554+s40higG21fmhhdDtpmQzw3vJLspY05mnL1+fW+RIKkM4rb150sdZXKINxfNQvERteE8WX0vL2yG4RuqJzYtGCDEGeHd+HLne7xfmqPxun7bUYaxAlplhm1z2J41hqaj8pBwDSEV9SBOZXvh6FjS9nvJCT7Z1bbZwWrAO/7E2ac0eV+5iEc0J+TyufO3F9uod+J+AICtB type=host diff --git a/lib/tbot/config/testdata/TestTemplateSSHClient_Render/latest_OpenSSH_with_alpn_upgrade/ssh_config.golden b/lib/tbot/config/testdata/TestTemplateSSHClient_Render/latest_OpenSSH_with_alpn_upgrade/ssh_config.golden new file mode 100644 index 00000000000..e5667228991 --- /dev/null +++ b/lib/tbot/config/testdata/TestTemplateSSHClient_Render/latest_OpenSSH_with_alpn_upgrade/ssh_config.golden @@ -0,0 +1,26 @@ +# Begin generated Teleport configuration for tele.blackmesa.gov by tbot + +# Common flags for all tele.blackmesa.gov hosts +Host *.tele.blackmesa.gov tele.blackmesa.gov + UserKnownHostsFile "/test/dir/known_hosts" + IdentityFile "/test/dir/key" + CertificateFile "/test/dir/key-cert.pub" + HostKeyAlgorithms rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-256-cert-v01@openssh.com,ssh-rsa-cert-v01@openssh.com + +# Flags for all tele.blackmesa.gov hosts except the proxy +Host *.tele.blackmesa.gov !tele.blackmesa.gov + Port 3022 + ProxyCommand '/path/to/tbot' ssh-proxy-command --destination-dir='/test/dir' --proxy-server='tele.blackmesa.gov:443' --cluster='tele.blackmesa.gov' --tls-routing --connection-upgrade --resume --user=%r --host=%h --port=%p +# Common flags for all tele.aperture.labs hosts +Host *.tele.aperture.labs tele.blackmesa.gov + UserKnownHostsFile "/test/dir/known_hosts" + IdentityFile "/test/dir/key" + CertificateFile "/test/dir/key-cert.pub" + HostKeyAlgorithms rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-256-cert-v01@openssh.com,ssh-rsa-cert-v01@openssh.com + +# Flags for all tele.aperture.labs hosts except the proxy +Host *.tele.aperture.labs !tele.blackmesa.gov + Port 3022 + ProxyCommand '/path/to/tbot' ssh-proxy-command --destination-dir='/test/dir' --proxy-server='tele.blackmesa.gov:443' --cluster='tele.aperture.labs' --tls-routing --connection-upgrade --resume --user=%r --host=%h --port=%p + +# End generated Teleport configuration diff --git a/lib/tbot/config/testdata/TestTemplateSSHClient_Render/latest_OpenSSH_with_legacy_proxycommand/known_hosts.golden b/lib/tbot/config/testdata/TestTemplateSSHClient_Render/latest_OpenSSH_with_legacy_proxycommand/known_hosts.golden new file mode 100644 index 00000000000..8b414dc2bc1 --- /dev/null +++ b/lib/tbot/config/testdata/TestTemplateSSHClient_Render/latest_OpenSSH_with_legacy_proxycommand/known_hosts.golden @@ -0,0 +1,4 @@ +@cert-authority tele.blackmesa.gov,tele.blackmesa.gov,*.tele.blackmesa.gov ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC8kYdyZA1ZSNjZ4pqybDXvWplHQHkU6fPL+cAYHUkAT5CiQV4GOjwaSTcvZNK5U2fQ0jm6jknCnsZi1t9JujCjXUT3bYHCnSwWhXN55QzIu530Q/MeXz5W8TxYRrWULgPhqqtq8B9N554+s40higG21fmhhdDtpmQzw3vJLspY05mnL1+fW+RIKkM4rb150sdZXKINxfNQvERteE8WX0vL2yG4RuqJzYtGCDEGeHd+HLne7xfmqPxun7bUYaxAlplhm1z2J41hqaj8pBwDSEV9SBOZXvh6FjS9nvJCT7Z1bbZwWrAO/7E2ac0eV+5iEc0J+TyufO3F9uod+J+AICtB type=host +@cert-authority tele.blackmesa.gov,tele.blackmesa.gov,*.tele.blackmesa.gov ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC8kYdyZA1ZSNjZ4pqybDXvWplHQHkU6fPL+cAYHUkAT5CiQV4GOjwaSTcvZNK5U2fQ0jm6jknCnsZi1t9JujCjXUT3bYHCnSwWhXN55QzIu530Q/MeXz5W8TxYRrWULgPhqqtq8B9N554+s40higG21fmhhdDtpmQzw3vJLspY05mnL1+fW+RIKkM4rb150sdZXKINxfNQvERteE8WX0vL2yG4RuqJzYtGCDEGeHd+HLne7xfmqPxun7bUYaxAlplhm1z2J41hqaj8pBwDSEV9SBOZXvh6FjS9nvJCT7Z1bbZwWrAO/7E2ac0eV+5iEc0J+TyufO3F9uod+J+AICtB type=host +@cert-authority tele.blackmesa.gov,tele.aperture.labs,*.tele.aperture.labs ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC8kYdyZA1ZSNjZ4pqybDXvWplHQHkU6fPL+cAYHUkAT5CiQV4GOjwaSTcvZNK5U2fQ0jm6jknCnsZi1t9JujCjXUT3bYHCnSwWhXN55QzIu530Q/MeXz5W8TxYRrWULgPhqqtq8B9N554+s40higG21fmhhdDtpmQzw3vJLspY05mnL1+fW+RIKkM4rb150sdZXKINxfNQvERteE8WX0vL2yG4RuqJzYtGCDEGeHd+HLne7xfmqPxun7bUYaxAlplhm1z2J41hqaj8pBwDSEV9SBOZXvh6FjS9nvJCT7Z1bbZwWrAO/7E2ac0eV+5iEc0J+TyufO3F9uod+J+AICtB type=host +@cert-authority tele.blackmesa.gov,tele.aperture.labs,*.tele.aperture.labs ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC8kYdyZA1ZSNjZ4pqybDXvWplHQHkU6fPL+cAYHUkAT5CiQV4GOjwaSTcvZNK5U2fQ0jm6jknCnsZi1t9JujCjXUT3bYHCnSwWhXN55QzIu530Q/MeXz5W8TxYRrWULgPhqqtq8B9N554+s40higG21fmhhdDtpmQzw3vJLspY05mnL1+fW+RIKkM4rb150sdZXKINxfNQvERteE8WX0vL2yG4RuqJzYtGCDEGeHd+HLne7xfmqPxun7bUYaxAlplhm1z2J41hqaj8pBwDSEV9SBOZXvh6FjS9nvJCT7Z1bbZwWrAO/7E2ac0eV+5iEc0J+TyufO3F9uod+J+AICtB type=host diff --git a/lib/tbot/config/testdata/TestTemplateSSHClient_Render/latest_OpenSSH_with_legacy_proxycommand/ssh_config.golden b/lib/tbot/config/testdata/TestTemplateSSHClient_Render/latest_OpenSSH_with_legacy_proxycommand/ssh_config.golden new file mode 100644 index 00000000000..d10583bc2b0 --- /dev/null +++ b/lib/tbot/config/testdata/TestTemplateSSHClient_Render/latest_OpenSSH_with_legacy_proxycommand/ssh_config.golden @@ -0,0 +1,26 @@ +# Begin generated Teleport configuration for tele.blackmesa.gov by tbot + +# Common flags for all tele.blackmesa.gov hosts +Host *.tele.blackmesa.gov tele.blackmesa.gov + UserKnownHostsFile "/test/dir/known_hosts" + IdentityFile "/test/dir/key" + CertificateFile "/test/dir/key-cert.pub" + HostKeyAlgorithms rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-256-cert-v01@openssh.com,ssh-rsa-cert-v01@openssh.com + +# Flags for all tele.blackmesa.gov hosts except the proxy +Host *.tele.blackmesa.gov !tele.blackmesa.gov + Port 3022 + ProxyCommand "/path/to/tbot" proxy --destination-dir=/test/dir --proxy-server=tele.blackmesa.gov:443 ssh --cluster=tele.blackmesa.gov %r@%h:%p +# Common flags for all tele.aperture.labs hosts +Host *.tele.aperture.labs tele.blackmesa.gov + UserKnownHostsFile "/test/dir/known_hosts" + IdentityFile "/test/dir/key" + CertificateFile "/test/dir/key-cert.pub" + HostKeyAlgorithms rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-256-cert-v01@openssh.com,ssh-rsa-cert-v01@openssh.com + +# Flags for all tele.aperture.labs hosts except the proxy +Host *.tele.aperture.labs !tele.blackmesa.gov + Port 3022 + ProxyCommand "/path/to/tbot" proxy --destination-dir=/test/dir --proxy-server=tele.blackmesa.gov:443 ssh --cluster=tele.aperture.labs %r@%h:%p + +# End generated Teleport configuration diff --git a/lib/tbot/config/testdata/TestTemplateSSHClient_Render/legacy_OpenSSH/ssh_config.golden b/lib/tbot/config/testdata/TestTemplateSSHClient_Render/legacy_OpenSSH/ssh_config.golden index 0f8bc68f7e7..9dbc98377e8 100644 --- a/lib/tbot/config/testdata/TestTemplateSSHClient_Render/legacy_OpenSSH/ssh_config.golden +++ b/lib/tbot/config/testdata/TestTemplateSSHClient_Render/legacy_OpenSSH/ssh_config.golden @@ -10,7 +10,7 @@ Host *.tele.blackmesa.gov tele.blackmesa.gov # Flags for all tele.blackmesa.gov hosts except the proxy Host *.tele.blackmesa.gov !tele.blackmesa.gov Port 3022 - ProxyCommand "/path/to/tbot" proxy --destination-dir=/test/dir --proxy-server=tele.blackmesa.gov:443 ssh --cluster=tele.blackmesa.gov %r@%h:%p + ProxyCommand '/path/to/tbot' ssh-proxy-command --destination-dir='/test/dir' --proxy-server='tele.blackmesa.gov:443' --cluster='tele.blackmesa.gov' --tls-routing --no-connection-upgrade --resume --user=%r --host=%h --port=%p # Common flags for all tele.aperture.labs hosts Host *.tele.aperture.labs tele.blackmesa.gov UserKnownHostsFile "/test/dir/known_hosts" @@ -21,6 +21,6 @@ Host *.tele.aperture.labs tele.blackmesa.gov # Flags for all tele.aperture.labs hosts except the proxy Host *.tele.aperture.labs !tele.blackmesa.gov Port 3022 - ProxyCommand "/path/to/tbot" proxy --destination-dir=/test/dir --proxy-server=tele.blackmesa.gov:443 ssh --cluster=tele.aperture.labs %r@%h:%p + ProxyCommand '/path/to/tbot' ssh-proxy-command --destination-dir='/test/dir' --proxy-server='tele.blackmesa.gov:443' --cluster='tele.aperture.labs' --tls-routing --no-connection-upgrade --resume --user=%r --host=%h --port=%p # End generated Teleport configuration diff --git a/lib/tbot/service_outputs.go b/lib/tbot/service_outputs.go index 598f4487c9a..6025947c2ec 100644 --- a/lib/tbot/service_outputs.go +++ b/lib/tbot/service_outputs.go @@ -63,6 +63,7 @@ type outputsService struct { reloadBroadcaster *channelBroadcaster proxyPingCache *proxyPingCache authPingCache *authPingCache + alpnUpgradeCache *alpnProxyConnUpgradeRequiredCache botClient *authclient.Client getBotIdentity getBotIdentityFn cfg *config.BotConfig @@ -93,10 +94,11 @@ func (s *outputsService) renewOutputs( // create a cache shared across outputs so they don't hammer the auth // server with similar requests drc := &outputRenewalCache{ - proxyPingCache: s.proxyPingCache, - authPingCache: s.authPingCache, - client: s.botClient, - cfg: s.cfg, + proxyPingCache: s.proxyPingCache, + authPingCache: s.authPingCache, + alpnUpgradeCache: s.alpnUpgradeCache, + client: s.botClient, + cfg: s.cfg, } // Determine the default role list based on the bot role. The role's @@ -658,10 +660,11 @@ func fetchDefaultRoles(ctx context.Context, roleGetter services.RoleGetter, iden // requests for the same information. This is shared between all of the // outputs. type outputRenewalCache struct { - client *authclient.Client - cfg *config.BotConfig - proxyPingCache *proxyPingCache - authPingCache *authPingCache + client *authclient.Client + cfg *config.BotConfig + proxyPingCache *proxyPingCache + authPingCache *authPingCache + alpnUpgradeCache *alpnProxyConnUpgradeRequiredCache mu sync.Mutex // These are protected by getter/setters with mutex locks @@ -716,6 +719,18 @@ func (orc *outputRenewalCache) ProxyPing(ctx context.Context) (*webclient.PingRe return res, nil } +// IsALPNConnUpgradeRequired returns a (possibly cached) test of whether ALPN +// routing is required. +func (orc *outputRenewalCache) IsALPNConnUpgradeRequired( + ctx context.Context, addr string, insecure bool, +) (bool, error) { + res, err := orc.alpnUpgradeCache.isUpgradeRequired(ctx, addr, insecure) + if err != nil { + return false, trace.Wrap(err) + } + return res, nil +} + // Config returns the bots config. func (orc *outputRenewalCache) Config() *config.BotConfig { return orc.cfg diff --git a/lib/tbot/ssh_proxy.go b/lib/tbot/ssh_proxy.go index 512ec269a69..8c2dd0b079f 100644 --- a/lib/tbot/ssh_proxy.go +++ b/lib/tbot/ssh_proxy.go @@ -39,14 +39,15 @@ import ( "github.com/gravitational/teleport/lib/resumption" "github.com/gravitational/teleport/lib/tbot/config" "github.com/gravitational/teleport/lib/tbot/identity" - "github.com/gravitational/teleport/lib/tbot/tshwrap" "github.com/gravitational/teleport/lib/utils" ) // ProxySSHConfig contains configuration parameters required // to initialize the local ssh proxy. type ProxySSHConfig struct { - BotConfig *config.BotConfig + DestinationPath string + Insecure bool + FIPS bool TSHConfigPath string ProxyServer string Cluster string @@ -62,17 +63,27 @@ type ProxySSHConfig struct { // ProxySSH creates a local ssh proxy, dialing a node and transferring data through // stdin and stdout, to be used as an OpenSSH/PuTTY proxy command. func ProxySSH(ctx context.Context, proxyConfig ProxySSHConfig) error { - tshConfig, err := libclient.LoadTSHConfig(proxyConfig.TSHConfigPath) - if err != nil { - return trace.Wrap(err) + tshConfig := &libclient.TSHConfig{} + if proxyConfig.TSHConfigPath != "" { + var err error + tshConfig, err = libclient.LoadTSHConfig(proxyConfig.TSHConfigPath) + if err != nil { + return trace.Wrap(err, "loading proxy templates") + } } proxy := proxyConfig.ProxyServer cluster := proxyConfig.Cluster targetHost := proxyConfig.Host - expanded, matched := tshConfig.ProxyTemplates.Apply(net.JoinHostPort(proxyConfig.Host, proxyConfig.Port)) + expanded, matched := tshConfig.ProxyTemplates.Apply( + net.JoinHostPort(proxyConfig.Host, proxyConfig.Port), + ) if matched { - proxyConfig.Log.DebugContext(ctx, "proxy templated matched", "populated_template", expanded) + proxyConfig.Log.DebugContext( + ctx, + "proxy templated matched", + "populated_template", expanded, + ) if expanded.Cluster != "" { cluster = expanded.Cluster } @@ -89,7 +100,13 @@ func ProxySSH(ctx context.Context, proxyConfig ProxySSHConfig) error { return trace.Wrap(err) } - facade, keyring, err := parseIdentity(proxyConfig.BotConfig, proxyHost, cluster) + facade, keyring, err := parseIdentity( + proxyConfig.DestinationPath, + proxyHost, + cluster, + proxyConfig.Insecure, + proxyConfig.FIPS, + ) if err != nil { return trace.Wrap(err) } @@ -125,7 +142,7 @@ func ProxySSH(ctx context.Context, proxyConfig ProxySSHConfig) error { UnaryInterceptors: []grpc.UnaryClientInterceptor{interceptors.GRPCClientUnaryErrorInterceptor}, StreamInterceptors: []grpc.StreamClientInterceptor{interceptors.GRPCClientStreamErrorInterceptor}, SSHConfig: sshConfig, - InsecureSkipVerify: proxyConfig.BotConfig.Insecure, + InsecureSkipVerify: proxyConfig.Insecure, ALPNConnUpgradeRequired: proxyConfig.ConnectionUpgradeRequired, }) if err != nil { @@ -217,17 +234,8 @@ func resolveTargetHost(ctx context.Context, cfg client.Config, search, query str return nodes[0], nil } -func parseIdentity(botConfig *config.BotConfig, proxy, cluster string) (*identity.Facade, agent.ExtendedAgent, error) { - destination, err := tshwrap.GetDestinationDirectory(botConfig) - if err != nil { - return nil, nil, trace.Wrap(err) - } - - identityPath := filepath.Join(destination.Path, config.IdentityFilePath) - if err != nil { - return nil, nil, trace.Wrap(err) - } - +func parseIdentity(destPath, proxy, cluster string, insecure, fips bool) (*identity.Facade, agent.ExtendedAgent, error) { + identityPath := filepath.Join(destPath, config.IdentityFilePath) key, err := identityfile.KeyFromIdentityFile(identityPath, proxy, cluster) if err != nil { return nil, nil, trace.Wrap(err) @@ -258,7 +266,7 @@ func parseIdentity(botConfig *config.BotConfig, proxy, cluster string) (*identit return nil, nil, trace.Wrap(err) } - return identity.NewFacade(botConfig.FIPS, botConfig.Insecure, i), keyring, nil + return identity.NewFacade(fips, insecure, i), keyring, nil } func cleanTargetHost(targetHost, proxyHost, siteName string) string { diff --git a/lib/tbot/tbot.go b/lib/tbot/tbot.go index 0abd55da695..96cab8dfadd 100644 --- a/lib/tbot/tbot.go +++ b/lib/tbot/tbot.go @@ -21,6 +21,7 @@ package tbot import ( "context" "errors" + "fmt" "log/slog" "sync" "time" @@ -29,9 +30,11 @@ import ( "github.com/sirupsen/logrus" "go.opentelemetry.io/otel" "golang.org/x/sync/errgroup" + "golang.org/x/sync/singleflight" "google.golang.org/grpc" "github.com/gravitational/teleport" + "github.com/gravitational/teleport/api/client" "github.com/gravitational/teleport/api/client/proto" "github.com/gravitational/teleport/api/client/webclient" "github.com/gravitational/teleport/api/metadata" @@ -208,6 +211,10 @@ func (b *Bot) Run(ctx context.Context) (err error) { botCfg: b.cfg, log: b.log, } + alpnUpgradeCache := &alpnProxyConnUpgradeRequiredCache{ + botCfg: b.cfg, + log: b.log, + } // Setup all other services if b.cfg.DiagAddr != "" { @@ -220,12 +227,13 @@ func (b *Bot) Run(ctx context.Context) (err error) { }) } services = append(services, &outputsService{ - authPingCache: authPingCache, - proxyPingCache: proxyPingCache, - getBotIdentity: b.botIdentitySvc.GetIdentity, - botClient: b.botIdentitySvc.GetClient(), - cfg: b.cfg, - resolver: resolver, + authPingCache: authPingCache, + proxyPingCache: proxyPingCache, + alpnUpgradeCache: alpnUpgradeCache, + getBotIdentity: b.botIdentitySvc.GetIdentity, + botClient: b.botIdentitySvc.GetClient(), + cfg: b.cfg, + resolver: resolver, log: b.log.With( teleport.ComponentKey, teleport.Component(componentTBot, "outputs"), ), @@ -579,3 +587,56 @@ func (p *proxyPingCache) ping(ctx context.Context) (*webclient.PingResponse, err return p.cachedValue, nil } + +type alpnProxyConnUpgradeRequiredCache struct { + botCfg *config.BotConfig + log *slog.Logger + + mu sync.Mutex + cache map[string]bool + group singleflight.Group +} + +func (a *alpnProxyConnUpgradeRequiredCache) isUpgradeRequired(ctx context.Context, addr string, insecure bool) (bool, error) { + key := fmt.Sprintf("%s-%t", addr, insecure) + + a.mu.Lock() + if a.cache == nil { + a.cache = make(map[string]bool) + } + v, ok := a.cache[key] + if ok { + a.mu.Unlock() + return v, nil + } + a.mu.Unlock() + + val, err, _ := a.group.Do(key, func() (interface{}, error) { + // Recheck the cache in case we've just missed a previous group + // completing + a.mu.Lock() + v, ok := a.cache[key] + if ok { + a.mu.Unlock() + return v, nil + } + a.mu.Unlock() + + // Ok, now we know for sure that the work hasn't already been done or + // isn't in flight, we can complete it. + a.log.DebugContext(ctx, "Testing ALPN upgrade necessary", "addr", addr, "insecure", insecure) + v = client.IsALPNConnUpgradeRequired(ctx, addr, insecure) + a.log.DebugContext(ctx, "Tested ALPN upgrade necessary", "addr", addr, "insecure", insecure, "result", v) + if err := ctx.Err(); err != nil { + // Check for case where false is returned because client canceled ctx. + // We don't want to cache this result. + return v, trace.Wrap(err) + } + + a.mu.Lock() + a.cache[key] = v + a.mu.Unlock() + return v, nil + }) + return val.(bool), err +} diff --git a/tool/tbot/main.go b/tool/tbot/main.go index e760fc8eb04..c7dbdf07201 100644 --- a/tool/tbot/main.go +++ b/tool/tbot/main.go @@ -310,9 +310,9 @@ func Run(args []string, stdout io.Writer) error { case dbCmd.FullCommand(): err = onDBCommand(botConfig, &cf) case proxyCmd.FullCommand(): - err = onProxyCommand(botConfig, &cf) + err = onProxyCommand(ctx, botConfig, &cf) case sshProxyCmd.FullCommand(): - err = onSSHProxyCommand(botConfig, &cf) + err = onSSHProxyCommand(ctx, &cf) case kubeCredentialsCmd.FullCommand(): err = onKubeCredentialsCommand(ctx, botConfig) case spiffeInspectCmd.FullCommand(): diff --git a/tool/tbot/proxy.go b/tool/tbot/proxy.go index 24d35bd73a6..e566f733b51 100644 --- a/tool/tbot/proxy.go +++ b/tool/tbot/proxy.go @@ -19,6 +19,7 @@ package main import ( + "context" "path/filepath" "slices" @@ -29,7 +30,9 @@ import ( "github.com/gravitational/teleport/lib/tbot/tshwrap" ) -func onProxyCommand(botConfig *config.BotConfig, cf *config.CLIConf) error { +func onProxyCommand( + ctx context.Context, botConfig *config.BotConfig, cf *config.CLIConf, +) error { wrapper, err := tshwrap.New() if err != nil { return trace.Wrap(err) @@ -46,9 +49,6 @@ func onProxyCommand(botConfig *config.BotConfig, cf *config.CLIConf) error { } identityPath := filepath.Join(destination.Path, config.IdentityFilePath) - if err != nil { - return trace.Wrap(err) - } // TODO(timothyb89): We could consider supporting a --cluster passthrough // here as in `tbot db ...`. @@ -75,6 +75,9 @@ func onProxyCommand(botConfig *config.BotConfig, cf *config.CLIConf) error { destination.Path, "kubeconfig-proxied.yaml", ) } + if slices.Contains(cf.RemainingArgs, "ssh") { + log.WarnContext(ctx, "`tbot proxy ssh` is deprecated and will stop working in v17. See https://goteleport.com/docs/machine-id/reference/v16-upgrade-guide/") + } return trace.Wrap(wrapper.Exec(env, args...), "executing `tsh proxy`") } diff --git a/tool/tbot/proxy_ssh.go b/tool/tbot/proxy_ssh.go index b6fce6e2bd9..985464fc728 100644 --- a/tool/tbot/proxy_ssh.go +++ b/tool/tbot/proxy_ssh.go @@ -30,8 +30,8 @@ import ( // `tsh proxy ssh` which results in much less memory and cpu consumption. This will // eventually supersede `tbot proxy ssh` as it becomes more feature rich and supports // all the edge cases. -func onSSHProxyCommand(botConfig *config.BotConfig, cf *config.CLIConf) error { - ctx, cancel := context.WithCancel(context.Background()) +func onSSHProxyCommand(ctx context.Context, cf *config.CLIConf) error { + ctx, cancel := context.WithCancel(ctx) defer cancel() if cf.Port == "" { @@ -39,7 +39,9 @@ func onSSHProxyCommand(botConfig *config.BotConfig, cf *config.CLIConf) error { } proxySSHConfig := tbot.ProxySSHConfig{ - BotConfig: botConfig, + DestinationPath: cf.DestinationDir, + Insecure: cf.Insecure, + FIPS: cf.FIPS, ProxyServer: cf.ProxyServer, Cluster: cf.Cluster, User: cf.User,