diff --git a/lib/client/api.go b/lib/client/api.go index 2b6e1b2812a..ecd4f8e9188 100644 --- a/lib/client/api.go +++ b/lib/client/api.go @@ -1345,6 +1345,25 @@ func (c *Config) DatabaseProxyHostPort(db tlsca.RouteToDatabase) (string, int) { return c.WebProxyHostPort() } +// GetKubeTLSServerName returns k8s server name used in KUBECONFIG to leverage TLS Routing. +func GetKubeTLSServerName(k8host string) string { + isIPFormat := net.ParseIP(k8host) != nil + + if k8host == "" || isIPFormat { + // If proxy is configured without public_addr set the ServerName to the 'kube.teleport.cluster.local' value. + // The k8s server name needs to be a valid hostname but when public_addr is missing from proxy settings + // the web_listen_addr is used thus webHost will contain local proxy IP address like: 0.0.0.0 or 127.0.0.1 + // TODO(smallinsky) UPGRADE IN 10.0. Switch to KubeTeleportProxyALPNPrefix instead. + return addSubdomainPrefix(constants.APIDomain, constants.KubeSNIPrefix) + } + // TODO(smallinsky) UPGRADE IN 10.0. Switch to KubeTeleportProxyALPNPrefix instead. + return addSubdomainPrefix(k8host, constants.KubeSNIPrefix) +} + +func addSubdomainPrefix(domain, prefix string) string { + return fmt.Sprintf("%s%s", prefix, domain) +} + // ProxyHost returns the hostname of the proxy server (without any port numbers) func ProxyHost(proxyHost string) string { host, _, err := net.SplitHostPort(proxyHost) diff --git a/lib/client/api_test.go b/lib/client/api_test.go index 6113196003e..5177530f9ee 100644 --- a/lib/client/api_test.go +++ b/lib/client/api_test.go @@ -425,6 +425,42 @@ func TestWebProxyHostPort(t *testing.T) { } } +func TestGetKubeTLSServerName(t *testing.T) { + tests := []struct { + name string + kubeProxyAddr string + want string + }{ + { + name: "ipv4 format, API domain should be used", + kubeProxyAddr: "127.0.0.1", + want: "kube.teleport.cluster.local", + }, + { + name: "empty host, API domain should be used", + kubeProxyAddr: "", + want: "kube.teleport.cluster.local", + }, + { + name: "ipv4 unspecified, API domain should be used ", + kubeProxyAddr: "0.0.0.0", + want: "kube.teleport.cluster.local", + }, + { + name: "valid hostname", + kubeProxyAddr: "example.com", + want: "kube.example.com", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := GetKubeTLSServerName(tt.kubeProxyAddr) + require.Equal(t, tt.want, got) + }) + } +} + // TestApplyProxySettings validates that settings received from the proxy's // ping endpoint are correctly applied to Teleport client. func TestApplyProxySettings(t *testing.T) { diff --git a/lib/client/identityfile/identity.go b/lib/client/identityfile/identity.go index 0756da2b2a4..764c785b892 100644 --- a/lib/client/identityfile/identity.go +++ b/lib/client/identityfile/identity.go @@ -144,6 +144,8 @@ type WriteConfig struct { // KubeProxyAddr is the public address of the proxy with its kubernetes // port. KubeProxyAddr is only used when Format is FormatKubernetes. KubeProxyAddr string + // KubeTLSServerName is the SNI host value passed to the server. + KubeTLSServerName string // OverwriteDestination forces all existing destination files to be // overwritten. When false, user will be prompted for confirmation of // overwrite first. @@ -331,6 +333,7 @@ func Write(cfg WriteConfig) (filesWritten []string, err error) { TeleportClusterName: cfg.Key.ClusterName, ClusterAddr: cfg.KubeProxyAddr, Credentials: cfg.Key, + TLSServerName: cfg.KubeTLSServerName, }); err != nil { return nil, trace.Wrap(err) } diff --git a/lib/client/identityfile/identity_test.go b/lib/client/identityfile/identity_test.go index 59203e86bcd..047af5094dd 100644 --- a/lib/client/identityfile/identity_test.go +++ b/lib/client/identityfile/identity_test.go @@ -160,9 +160,10 @@ func TestWrite(t *testing.T) { cfg.OutputPath = filepath.Join(outputDir, "kubeconfig") cfg.Format = FormatKubernetes cfg.KubeProxyAddr = "far.away.cluster" + cfg.KubeTLSServerName = "kube.far.away.cluster" _, err = Write(cfg) require.NoError(t, err) - assertKubeconfigContents(t, cfg.OutputPath, key.ClusterName, "far.away.cluster") + assertKubeconfigContents(t, cfg.OutputPath, key.ClusterName, "far.away.cluster", cfg.KubeTLSServerName) } func TestKubeconfigOverwrite(t *testing.T) { @@ -183,17 +184,18 @@ func TestKubeconfigOverwrite(t *testing.T) { cfg.KubeProxyAddr = "far.away.cluster" _, err = Write(cfg) require.NoError(t, err) - assertKubeconfigContents(t, cfg.OutputPath, key.ClusterName, "far.away.cluster") + assertKubeconfigContents(t, cfg.OutputPath, key.ClusterName, "far.away.cluster", "") // Write a kubeconfig for a different cluster to the same file path. It // should be overwritten. cfg.KubeProxyAddr = "other.cluster" + cfg.KubeTLSServerName = "kube.other.cluster" _, err = Write(cfg) require.NoError(t, err) - assertKubeconfigContents(t, cfg.OutputPath, key.ClusterName, "other.cluster") + assertKubeconfigContents(t, cfg.OutputPath, key.ClusterName, "other.cluster", cfg.KubeTLSServerName) } -func assertKubeconfigContents(t *testing.T, path, clusterName, serverAddr string) { +func assertKubeconfigContents(t *testing.T, path, clusterName, serverAddr, kubeTLSName string) { t.Helper() kc, err := kubeconfig.Load(path) @@ -203,4 +205,5 @@ func assertKubeconfigContents(t *testing.T, path, clusterName, serverAddr string require.Len(t, kc.Contexts, 1) require.Len(t, kc.Clusters, 1) require.Equal(t, kc.Clusters[clusterName].Server, serverAddr) + require.Equal(t, kc.Clusters[clusterName].TLSServerName, kubeTLSName) } diff --git a/tool/tctl/common/auth_command.go b/tool/tctl/common/auth_command.go index c6a6509fab4..68006fd1c62 100644 --- a/tool/tctl/common/auth_command.go +++ b/tool/tctl/common/auth_command.go @@ -680,12 +680,25 @@ func (a *AuthCommand) generateUserKeys(ctx context.Context, clusterAPI auth.Clie } key.TrustedCA = auth.AuthoritiesToTrustedCerts(hostCAs) + networkConfig, err := clusterAPI.GetClusterNetworkingConfig(ctx) + if err != nil { + return trace.Wrap(err) + } + + kubeTLSServerName := "" + // Is TLS routing enabled? + if networkConfig.GetProxyListenerMode() == types.ProxyListenerMode_Multiplex { + // If we're in multiplexed mode get SNI name for kube from single multiplexed proxy addr + kubeTLSServerName = client.GetKubeTLSServerName(a.config.Proxy.WebAddr.Host()) + } + // write the cert+private key to the output: filesWritten, err := identityfile.Write(identityfile.WriteConfig{ OutputPath: a.output, Key: key, Format: a.outputFormat, KubeProxyAddr: a.proxyAddr, + KubeTLSServerName: kubeTLSServerName, OverwriteDestination: a.signOverwrite, }) if err != nil { diff --git a/tool/tctl/common/auth_command_test.go b/tool/tctl/common/auth_command_test.go index 2351f93e9e7..6c15497d4ce 100644 --- a/tool/tctl/common/auth_command_test.go +++ b/tool/tctl/common/auth_command_test.go @@ -274,11 +274,20 @@ type mockClient struct { appServices []types.AppServer dbServices []types.DatabaseServer appSession types.WebSession + networkConfig types.ClusterNetworkingConfig } func (c *mockClient) GetClusterName(...services.MarshalOption) (types.ClusterName, error) { return c.clusterName, nil } + +func (c *mockClient) GetClusterNetworkingConfig(ctx context.Context, opts ...services.MarshalOption) (types.ClusterNetworkingConfig, error) { + if c.networkConfig == nil { + return &types.ClusterNetworkingConfigV2{}, nil + } + return c.networkConfig, nil +} + func (c *mockClient) GenerateUserCerts(ctx context.Context, userCertsReq proto.UserCertsRequest) (*proto.Certs, error) { c.userCertsReq = &userCertsReq return c.userCerts, nil diff --git a/tool/tsh/kube.go b/tool/tsh/kube.go index b5b515116b0..8623f9f91ba 100644 --- a/tool/tsh/kube.go +++ b/tool/tsh/kube.go @@ -20,7 +20,6 @@ import ( "context" "fmt" "io" - "net" "net/url" "os" "sort" @@ -30,7 +29,6 @@ import ( "github.com/gravitational/teleport" "github.com/gravitational/teleport/api/client/proto" - "github.com/gravitational/teleport/api/constants" "github.com/gravitational/teleport/api/profile" "github.com/gravitational/teleport/api/types" apiutils "github.com/gravitational/teleport/api/utils" @@ -1012,32 +1010,13 @@ func fetchKubeStatus(ctx context.Context, tc *client.TeleportClient) (*kubernete } if tc.TLSRoutingEnabled { - kubeStatus.tlsServerName = getKubeTLSServerName(tc) + k8host, _ := tc.KubeProxyHostPort() + kubeStatus.tlsServerName = client.GetKubeTLSServerName(k8host) } return kubeStatus, nil } -// getKubeTLSServerName returns k8s server name used in KUBECONFIG to leverage TLS Routing. -func getKubeTLSServerName(tc *client.TeleportClient) string { - k8host, _ := tc.KubeProxyHostPort() - - isIPFormat := net.ParseIP(k8host) != nil - if k8host == "" || isIPFormat { - // If proxy is configured without public_addr set the ServerName to the 'kube.teleport.cluster.local' value. - // The k8s server name needs to be a valid hostname but when public_addr is missing from proxy settings - // the web_listen_addr is used thus webHost will contain local proxy IP address like: 0.0.0.0 or 127.0.0.1 - // TODO(smallinsky) UPGRADE IN 10.0. Switch to KubeTeleportProxyALPNPrefix instead. - return addSubdomainPrefix(constants.APIDomain, constants.KubeSNIPrefix) - } - // TODO(smallinsky) UPGRADE IN 10.0. Switch to KubeTeleportProxyALPNPrefix instead. - return addSubdomainPrefix(k8host, constants.KubeSNIPrefix) -} - -func addSubdomainPrefix(domain, prefix string) string { - return fmt.Sprintf("%s%s", prefix, domain) -} - // buildKubeConfigUpdate returns a kubeconfig.Values suitable for updating the user's kubeconfig // based on the CLI parameters and the given kubernetesStatus. func buildKubeConfigUpdate(cf *CLIConf, kubeStatus *kubernetesStatus) (*kubeconfig.Values, error) { diff --git a/tool/tsh/kube_test.go b/tool/tsh/kube_test.go deleted file mode 100644 index fa4d033fe99..00000000000 --- a/tool/tsh/kube_test.go +++ /dev/null @@ -1,77 +0,0 @@ -/* -Copyright 2021 Gravitational, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package main - -import ( - "testing" - - "github.com/stretchr/testify/require" - - "github.com/gravitational/teleport/lib/client" -) - -func TestGetKubeTLSServerName(t *testing.T) { - tests := []struct { - name string - kubeProxyAddr string - want string - }{ - { - name: "ipv4 format, API domain should be used", - kubeProxyAddr: "127.0.0.1", - want: "kube.teleport.cluster.local", - }, - { - name: "ipv4 with port, API domain should be used", - kubeProxyAddr: "127.0.0.1:3080", - want: "kube.teleport.cluster.local", - }, - { - name: "ipv4 missing host, API domain should be used", - kubeProxyAddr: ":3080", - want: "kube.teleport.cluster.local", - }, - { - name: "ipv4 unspecified, API domain should be used ", - kubeProxyAddr: "0.0.0.0:3080", - want: "kube.teleport.cluster.local", - }, - { - name: "valid hostname with port", - kubeProxyAddr: "example.com:3080", - want: "kube.example.com", - }, - { - name: "valid hostname without port", - kubeProxyAddr: "example.com", - want: "kube.example.com", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tc := &client.TeleportClient{ - Config: client.Config{ - WebProxyAddr: tt.kubeProxyAddr, - }, - } - got := getKubeTLSServerName(tc) - require.Equal(t, tt.want, got) - - }) - } -}