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 <edoardo.spadolini@goteleport.com>

* Update lib/tbot/tbot.go

Co-authored-by: Edoardo Spadolini <edoardo.spadolini@goteleport.com>

* 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 <edoardo.spadolini@goteleport.com>
This commit is contained in:
Noah Stride
2024-05-15 17:30:54 +00:00
committed by GitHub
co-authored by rosstimothy Edoardo Spadolini
parent 120efb9bd7
commit bd4dcab802
23 changed files with 417 additions and 73 deletions
+7
View File
@@ -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
+28 -3
View File
@@ -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 {
+16
View File
@@ -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 {
@@ -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
+6
View File
@@ -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)
}
+14 -5
View File
@@ -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,
},
+1
View File
@@ -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,
})
}
+66 -12
View File
@@ -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 {
+37 -5
View File
@@ -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)
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
+23 -8
View File
@@ -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
+29 -21
View File
@@ -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 {
+67 -6
View File
@@ -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
}
+2 -2
View File
@@ -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():
+7 -4
View File
@@ -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`")
}
+5 -3
View File
@@ -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,