diff --git a/tool/tctl/common/resources/linux_desktop.go b/tool/tctl/common/resources/linux_desktop.go
new file mode 100644
index 00000000000..f1e40f97951
--- /dev/null
+++ b/tool/tctl/common/resources/linux_desktop.go
@@ -0,0 +1,147 @@
+/*
+ * Teleport
+ * Copyright (C) 2026 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package resources
+
+import (
+ "context"
+ "fmt"
+ "io"
+
+ "github.com/gravitational/trace"
+
+ linuxdesktopv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/linuxdesktop/v1"
+ "github.com/gravitational/teleport/api/types"
+ "github.com/gravitational/teleport/api/utils/clientutils"
+ "github.com/gravitational/teleport/lib/asciitable"
+ "github.com/gravitational/teleport/lib/auth/authclient"
+ "github.com/gravitational/teleport/lib/itertools/stream"
+ "github.com/gravitational/teleport/lib/services"
+ "github.com/gravitational/teleport/tool/common"
+)
+
+type linuxDesktopCollection struct {
+ desktops []*linuxdesktopv1.LinuxDesktop
+}
+
+func linuxDesktopHandler() Handler {
+ return Handler{
+ getHandler: getLinuxDesktops,
+ createHandler: createLinuxDesktop,
+ updateHandler: updateLinuxDesktop,
+ deleteHandler: deleteLinuxDesktop,
+ singleton: false,
+ mfaRequired: false,
+ description: "A Linux remote desktop protected by Teleport",
+ }
+}
+
+func (c *linuxDesktopCollection) Resources() (r []types.Resource) {
+ for _, resource := range c.desktops {
+ r = append(r, types.ProtoResource153ToLegacy(resource))
+ }
+ return r
+}
+
+func (c *linuxDesktopCollection) WriteText(w io.Writer, verbose bool) error {
+ var rows [][]string
+ for _, d := range c.desktops {
+ labels := common.FormatLabels(d.Metadata.Labels, verbose)
+ rows = append(rows, []string{d.Metadata.Name, d.Spec.Addr, d.Spec.Hostname, labels})
+ }
+ headers := []string{"Name", "Address", "Hostname", "Labels"}
+ var t asciitable.Table
+ if verbose {
+ t = asciitable.MakeTable(headers, rows...)
+ } else {
+ t = asciitable.MakeTableWithTruncatedColumn(headers, rows, "Labels")
+ }
+ _, err := t.AsBuffer().WriteTo(w)
+ return trace.Wrap(err)
+}
+
+func getLinuxDesktops(ctx context.Context, client *authclient.Client, ref services.Ref, opts GetOpts) (Collection, error) {
+ linuxDesktopClient := client.LinuxDesktopClient()
+
+ if ref.Name != "" {
+ desktop, err := linuxDesktopClient.GetLinuxDesktop(ctx, ref.Name)
+ if err != nil {
+ if trace.IsNotFound(err) {
+ return nil, trace.NotFound("Linux desktop %q not found", ref.Name)
+ }
+ return nil, trace.Wrap(err)
+ }
+ return &linuxDesktopCollection{desktops: []*linuxdesktopv1.LinuxDesktop{desktop}}, nil
+ }
+
+ desktops, err := stream.Collect(clientutils.Resources(ctx, linuxDesktopClient.ListLinuxDesktops))
+ if err != nil {
+ return nil, trace.Wrap(err)
+ }
+
+ return &linuxDesktopCollection{desktops: desktops}, nil
+}
+
+func deleteLinuxDesktop(ctx context.Context, client *authclient.Client, ref services.Ref) error {
+ linuxDesktopClient := client.LinuxDesktopClient()
+ if err := linuxDesktopClient.DeleteLinuxDesktop(ctx, ref.Name); err != nil {
+ return trace.Wrap(err)
+ }
+ fmt.Printf("linux desktop %q has been deleted\n", ref.Name)
+ return nil
+}
+
+func createLinuxDesktop(ctx context.Context, client *authclient.Client, raw services.UnknownResource, opts CreateOpts) error {
+ desktop, err := services.UnmarshalLinuxDesktop(raw.Raw, services.DisallowUnknown())
+ if err != nil {
+ return trace.Wrap(err)
+ }
+
+ linuxDesktopClient := client.LinuxDesktopClient()
+ if _, err := linuxDesktopClient.CreateLinuxDesktop(ctx, desktop); err != nil {
+ if trace.IsAlreadyExists(err) {
+ if !opts.Force {
+ return trace.AlreadyExists("linux desktop %q already exists", desktop.Metadata.Name)
+ }
+ if _, err := linuxDesktopClient.UpsertLinuxDesktop(ctx, desktop); err != nil {
+ return trace.Wrap(err)
+ }
+ fmt.Printf("linux desktop %q has been updated\n", desktop.Metadata.Name)
+ return nil
+ }
+ return trace.Wrap(err)
+ }
+
+ fmt.Printf("linux desktop %q has been created\n", desktop.Metadata.Name)
+ return nil
+}
+
+func updateLinuxDesktop(ctx context.Context, client *authclient.Client, raw services.UnknownResource, opts CreateOpts) error {
+ desktop, err := services.UnmarshalLinuxDesktop(raw.Raw, services.DisallowUnknown())
+ if err != nil {
+ return trace.Wrap(err)
+ }
+
+ linuxDesktopClient := client.LinuxDesktopClient()
+ if _, err := linuxDesktopClient.UpdateLinuxDesktop(ctx, desktop); err != nil {
+ return trace.Wrap(err)
+ }
+
+ fmt.Printf("linux desktop %q has been updated\n", desktop.Metadata.Name)
+ return nil
+}
diff --git a/tool/tctl/common/resources/linux_desktop_test.go b/tool/tctl/common/resources/linux_desktop_test.go
new file mode 100644
index 00000000000..f97c0a6a101
--- /dev/null
+++ b/tool/tctl/common/resources/linux_desktop_test.go
@@ -0,0 +1,99 @@
+/*
+ * Teleport
+ * Copyright (C) 2026 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package resources
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/require"
+
+ headerv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1"
+ linuxdesktopv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/linuxdesktop/v1"
+ "github.com/gravitational/teleport/api/types"
+ "github.com/gravitational/teleport/lib/asciitable"
+)
+
+func makeLinuxDesktop(name, addr, hostname string, labels map[string]string) *linuxdesktopv1.LinuxDesktop {
+ return &linuxdesktopv1.LinuxDesktop{
+ Kind: types.KindLinuxDesktop,
+ Version: types.V1,
+ Metadata: &headerv1.Metadata{
+ Name: name,
+ Labels: labels,
+ },
+ Spec: &linuxdesktopv1.LinuxDesktopSpec{
+ Addr: addr,
+ Hostname: hostname,
+ },
+ }
+}
+
+func TestLinuxDesktopCollection_WriteText(t *testing.T) {
+ desktops := []*linuxdesktopv1.LinuxDesktop{
+ makeLinuxDesktop("desktop-1", "192.168.1.100:22", "prod-host-1", map[string]string{
+ "env": "prod",
+ }),
+ makeLinuxDesktop("desktop-2", "192.168.1.101:22", "dev-host-1", map[string]string{
+ "env": "dev",
+ }),
+ makeLinuxDesktop("desktop-3", "192.168.1.102:22", "test-host-1", map[string]string{
+ "env": "test",
+ }),
+ }
+
+ table := asciitable.MakeTable(
+ []string{"Name", "Address", "Hostname", "Labels"},
+ []string{"desktop-1", "192.168.1.100:22", "prod-host-1", "env=prod"},
+ []string{"desktop-2", "192.168.1.101:22", "dev-host-1", "env=dev"},
+ []string{"desktop-3", "192.168.1.102:22", "test-host-1", "env=test"},
+ )
+
+ formatted := table.AsBuffer().String()
+
+ collectionFormatTest(t, &linuxDesktopCollection{desktops: desktops}, formatted, formatted)
+}
+
+func TestLinuxDesktopCollection_Resources(t *testing.T) {
+ t.Parallel()
+
+ desktop := makeLinuxDesktop("test-desktop", "127.0.0.1:22", "test-host", map[string]string{
+ "env": "test",
+ })
+
+ collection := &linuxDesktopCollection{desktops: []*linuxdesktopv1.LinuxDesktop{desktop}}
+ resources := collection.Resources()
+
+ require.Len(t, resources, 1)
+ require.Equal(t, types.KindLinuxDesktop, resources[0].GetKind())
+ require.Equal(t, "test-desktop", resources[0].GetName())
+}
+
+func TestLinuxDesktopHandler(t *testing.T) {
+ t.Parallel()
+
+ handler := linuxDesktopHandler()
+
+ require.NotNil(t, handler.getHandler)
+ require.NotNil(t, handler.createHandler)
+ require.NotNil(t, handler.updateHandler)
+ require.NotNil(t, handler.deleteHandler)
+ require.False(t, handler.singleton)
+ require.False(t, handler.mfaRequired)
+ require.Equal(t, "A Linux remote desktop protected by Teleport", handler.description)
+}
diff --git a/tool/tctl/common/resources/resource.go b/tool/tctl/common/resources/resource.go
index 97da1627c36..557886c2db8 100644
--- a/tool/tctl/common/resources/resource.go
+++ b/tool/tctl/common/resources/resource.go
@@ -70,6 +70,7 @@ func Handlers() map[string]Handler {
types.KindInstaller: installerHandler(),
types.KindKubeServer: kubeServerHandler(),
types.KindKubernetesCluster: kubeClusterHandler(),
+ types.KindLinuxDesktop: linuxDesktopHandler(),
types.KindLock: lockHandler(),
types.KindNode: serverHandler(),
types.KindOIDCConnector: oidcConnectorHandler(),