diff --git a/constants.go b/constants.go index 2ff3b135bc36e..403c0da2721c6 100644 --- a/constants.go +++ b/constants.go @@ -799,3 +799,9 @@ const ( // the upgraded connection should be handled by the ALPN handler. WebAPIConnUpgradeTypeALPN = "alpn" ) + +const ( + // KubeLegacyProxySuffix is the suffix used for legacy proxy services when + // generating their names Server names. + KubeLegacyProxySuffix = "-proxy_service" +) diff --git a/lib/auth/auth_with_roles.go b/lib/auth/auth_with_roles.go index 0e6c1c0cf3405..94bfca977780d 100644 --- a/lib/auth/auth_with_roles.go +++ b/lib/auth/auth_with_roles.go @@ -1167,12 +1167,22 @@ func (a *ServerWithRoles) KeepAliveServer(ctx context.Context, handle types.Keep return trace.AccessDenied("access denied") } } else { // DELETE IN 13.0. Legacy kubeservice server is heartbeating back. - if serverName != handle.Name { + name := handle.Name + // legacy kube proxy server is heartbeating kubernetes clusters + // with the server name suffixed with "-proxy_service". + // To compare the server name with the name in the heartbeat + // we need to remove the suffix. + if a.hasBuiltinRole(types.RoleProxy) { + name = strings.TrimSuffix(name, teleport.KubeLegacyProxySuffix) + } + if name != serverName { return trace.AccessDenied("access denied") } } - if !a.hasBuiltinRole(types.RoleKube) { + // Legacy kube proxy can heartbeat kube servers from the proxy itself so + // we need to check if the host has the Kube or Proxy role. + if !a.hasBuiltinRole(types.RoleKube, types.RoleProxy) { return trace.AccessDenied("access denied") } if err := a.action(apidefaults.Namespace, types.KindKubeServer, types.VerbUpdate); err != nil { diff --git a/lib/auth/auth_with_roles_test.go b/lib/auth/auth_with_roles_test.go index 4a6c9484ea477..3e8d9fb43558f 100644 --- a/lib/auth/auth_with_roles_test.go +++ b/lib/auth/auth_with_roles_test.go @@ -3272,7 +3272,6 @@ func TestListResources_SearchAsRoles(t *testing.T) { } }) } - } func TestGetAndList_WindowsDesktops(t *testing.T) { @@ -4697,3 +4696,93 @@ func createSnowflakeSessionTestUsers(t *testing.T, authServer *Server) (string, require.NoError(t, err) return "alice", "bob", "admin" } + +func TestKubeKeepAliveServer(t *testing.T) { + t.Parallel() + srv := newTestTLSServer(t) + domainName, err := srv.Auth().GetDomainName() + require.NoError(t, err) + + tests := map[string]struct { + builtInRole types.SystemRole + assertErr require.ErrorAssertionFunc + }{ + "as kube service": { + builtInRole: types.RoleKube, + assertErr: require.NoError, + }, + "as legacy proxy service": { + builtInRole: types.RoleProxy, + assertErr: require.NoError, + }, + "as database service": { + builtInRole: types.RoleDatabase, + assertErr: require.Error, + }, + } + for name, test := range tests { + test := test + t.Run(name, func(t *testing.T) { + t.Parallel() + hostID := uuid.New().String() + // Create a kubernetes cluster. + kube, err := types.NewKubernetesClusterV3( + types.Metadata{ + Name: "kube", + Namespace: defaults.Namespace, + }, + types.KubernetesClusterSpecV3{}, + ) + require.NoError(t, err) + // Create a kubernetes server. + // If the built-in role is proxy, the server name should be + // kube-proxy_service + serverName := "kube" + if test.builtInRole == types.RoleProxy { + serverName += teleport.KubeLegacyProxySuffix + } + kubeServer, err := types.NewKubernetesServerV3( + types.Metadata{ + Name: serverName, + Namespace: defaults.Namespace, + }, + types.KubernetesServerSpecV3{ + Cluster: kube, + HostID: hostID, + }, + ) + require.NoError(t, err) + // Upsert the kubernetes server into the backend. + _, err = srv.Auth().UpsertKubernetesServer(context.Background(), kubeServer) + require.NoError(t, err) + + // Create a built-in role. + authContext, err := contextForBuiltinRole( + BuiltinRole{ + Role: test.builtInRole, + Username: fmt.Sprintf("%s.%s", hostID, domainName), + }, + types.DefaultSessionRecordingConfig(), + ) + require.NoError(t, err) + + // Create a server with the built-in role. + srv := ServerWithRoles{ + authServer: srv.Auth(), + context: *authContext, + } + // Keep alive the server. + err = srv.KeepAliveServer(context.Background(), + types.KeepAlive{ + Type: types.KeepAlive_KUBERNETES, + Expires: time.Now().Add(5 * time.Minute), + Name: serverName, + Namespace: defaults.Namespace, + HostID: hostID, + }, + ) + test.assertErr(t, err) + }, + ) + } +} diff --git a/lib/kube/proxy/server.go b/lib/kube/proxy/server.go index 99413f2d611a3..f02e60dcd1c1f 100644 --- a/lib/kube/proxy/server.go +++ b/lib/kube/proxy/server.go @@ -369,7 +369,7 @@ func (t *TLSServer) getServerInfo(name string) (types.Resource, error) { // Note: we *don't* want to add suffix for kubernetes_service! // This breaks reverse tunnel routing, which uses server.Name. if t.KubeServiceType != KubeService { - name += "-proxy_service" + name += teleport.KubeLegacyProxySuffix } srv, err := types.NewKubernetesServerV3(