diff --git a/ci/integration/tags_test.go b/ci/integration/tags_test.go
new file mode 100644
index 00000000..2fb7dfe0
--- /dev/null
+++ b/ci/integration/tags_test.go
@@ -0,0 +1,36 @@
+package integration
+
+import (
+	"context"
+	"testing"
+
+	"cdr.dev/coder-cli/coder-sdk"
+	"cdr.dev/coder-cli/pkg/tcli"
+	"cdr.dev/slog/sloggers/slogtest/assert"
+)
+
+func TestTags(t *testing.T) {
+	t.Parallel()
+	t.Skip("wait for dedicated test cluster so we can create an org")
+	run(t, "tags-cli-tests", func(t *testing.T, ctx context.Context, c *tcli.ContainerRunner) {
+		headlessLogin(ctx, t, c)
+		client := cleanupClient(ctx, t)
+
+		ensureImageImported(ctx, t, client, "ubuntu")
+
+		c.Run(ctx, "coder tags ls").Assert(t,
+			tcli.Error(),
+		)
+		c.Run(ctx, "coder tags ls --image ubuntu --org default").Assert(t,
+			tcli.Success(),
+		)
+		var tags []coder.ImageTag
+		c.Run(ctx, "coder tags ls --image ubuntu --org default --output json").Assert(t,
+			tcli.Success(),
+			tcli.StdoutJSONUnmarshal(&tags),
+		)
+		assert.True(t, "> 0 tags", len(tags) > 0)
+
+		// TODO(@cmoog) add create and rm integration tests
+	})
+}
diff --git a/coder-sdk/tags.go b/coder-sdk/tags.go
new file mode 100644
index 00000000..8c7282a9
--- /dev/null
+++ b/coder-sdk/tags.go
@@ -0,0 +1,68 @@
+package coder
+
+import (
+	"context"
+	"net/http"
+	"time"
+)
+
+// ImageTag is a Docker image tag.
+type ImageTag struct {
+	ImageID           string         `json:"image_id"             table:"-"`
+	Tag               string         `json:"tag"                  table:"Tag"`
+	LatestHash        string         `json:"latest_hash"          table:"-"`
+	HashLastUpdatedAt time.Time      `json:"hash_last_updated_at" table:"-"`
+	OSRelease         *OSRelease     `json:"os_release"           table:"OS"`
+	Environments      []*Environment `json:"environments"         table:"-"`
+	UpdatedAt         time.Time      `json:"updated_at"           table:"UpdatedAt"`
+	CreatedAt         time.Time      `json:"created_at"           table:"-"`
+}
+
+// OSRelease is the marshalled /etc/os-release file.
+type OSRelease struct {
+	ID         string `json:"id"`
+	PrettyName string `json:"pretty_name"`
+	HomeURL    string `json:"home_url"`
+}
+
+func (o OSRelease) String() string {
+	return o.PrettyName
+}
+
+// CreateImageTagReq defines the request parameters for creating a new image tag.
+type CreateImageTagReq struct {
+	Tag     string `json:"tag"`
+	Default bool   `json:"default"`
+}
+
+// CreateImageTag creates a new image tag resource.
+func (c Client) CreateImageTag(ctx context.Context, imageID string, req CreateImageTagReq) (*ImageTag, error) {
+	var tag ImageTag
+	if err := c.requestBody(ctx, http.MethodPost, "/api/images/"+imageID+"/tags", req, tag); err != nil {
+		return nil, err
+	}
+	return &tag, nil
+}
+
+// DeleteImageTag deletes an image tag resource.
+func (c Client) DeleteImageTag(ctx context.Context, imageID, tag string) error {
+	return c.requestBody(ctx, http.MethodDelete, "/api/images/"+imageID+"/tags/"+tag, nil, nil)
+}
+
+// ImageTags fetch all image tags.
+func (c Client) ImageTags(ctx context.Context, imageID string) ([]ImageTag, error) {
+	var tags []ImageTag
+	if err := c.requestBody(ctx, http.MethodGet, "/api/images/"+imageID+"/tags", nil, &tags); err != nil {
+		return nil, err
+	}
+	return tags, nil
+}
+
+// ImageTagByID fetch an image tag by ID.
+func (c Client) ImageTagByID(ctx context.Context, imageID, tagID string) (*ImageTag, error) {
+	var tag ImageTag
+	if err := c.requestBody(ctx, http.MethodGet, "/api/images/"+imageID+"/tags/"+tagID, nil, &tag); err != nil {
+		return nil, err
+	}
+	return &tag, nil
+}
diff --git a/internal/cmd/cmd.go b/internal/cmd/cmd.go
index 937c0558..e7b8aa63 100644
--- a/internal/cmd/cmd.go
+++ b/internal/cmd/cmd.go
@@ -26,6 +26,7 @@ func Make() *cobra.Command {
 		logoutCmd(),
 		shCmd(),
 		usersCmd(),
+		tagsCmd(),
 		configSSHCmd(),
 		secretsCmd(),
 		envsCmd(),
diff --git a/internal/cmd/tags.go b/internal/cmd/tags.go
new file mode 100644
index 00000000..ad77932d
--- /dev/null
+++ b/internal/cmd/tags.go
@@ -0,0 +1,172 @@
+package cmd
+
+import (
+	"encoding/json"
+	"os"
+
+	"cdr.dev/coder-cli/coder-sdk"
+	"cdr.dev/coder-cli/pkg/clog"
+	"cdr.dev/coder-cli/pkg/tablewriter"
+	"github.com/spf13/cobra"
+	"golang.org/x/xerrors"
+)
+
+func tagsCmd() *cobra.Command {
+	cmd := &cobra.Command{
+		Use:    "tags",
+		Hidden: true,
+		Short:  "operate on Coder image tags",
+	}
+
+	cmd.AddCommand(
+		tagsLsCmd(),
+		tagsCreateCmd(),
+		tagsRmCmd(),
+	)
+	return cmd
+}
+
+func tagsCreateCmd() *cobra.Command {
+	var (
+		orgName    string
+		imageName  string
+		defaultTag bool
+	)
+	cmd := &cobra.Command{
+		Use:     "create [tag]",
+		Short:   "add an image tag",
+		Long:    "allow users to create environments with this image tag",
+		Example: `coder tags create latest --image ubuntu --org default`,
+		Args:    cobra.ExactArgs(1),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			ctx := cmd.Context()
+			client, err := newClient(ctx)
+			if err != nil {
+				return err
+			}
+			img, err := findImg(ctx, client, findImgConf{
+				orgName: orgName,
+				imgName: imageName,
+				email:   coder.Me,
+			})
+			if err != nil {
+				return xerrors.Errorf("find image: %w", err)
+			}
+
+			_, err = client.CreateImageTag(ctx, img.ID, coder.CreateImageTagReq{
+				Tag:     args[0],
+				Default: defaultTag,
+			})
+			if err != nil {
+				return xerrors.Errorf("create image tag: %w", err)
+			}
+			clog.LogSuccess("created new tag")
+
+			return nil
+		},
+	}
+
+	cmd.Flags().StringVarP(&imageName, "image", "i", "", "image name")
+	cmd.Flags().StringVarP(&orgName, "org", "o", "", "organization name")
+	cmd.Flags().BoolVar(&defaultTag, "default", false, "make this tag the default for its image")
+	_ = cmd.MarkFlagRequired("org")
+	_ = cmd.MarkFlagRequired("image")
+	return cmd
+}
+
+func tagsLsCmd() *cobra.Command {
+	var (
+		orgName   string
+		imageName string
+		outputFmt string
+	)
+	cmd := &cobra.Command{
+		Use:     "ls",
+		Example: `coder tags ls --image ubuntu --org default --output json`,
+		RunE: func(cmd *cobra.Command, args []string) error {
+			ctx := cmd.Context()
+			client, err := newClient(ctx)
+			if err != nil {
+				return err
+			}
+
+			img, err := findImg(ctx, client, findImgConf{
+				email:   coder.Me,
+				orgName: orgName,
+				imgName: imageName,
+			})
+			if err != nil {
+				return err
+			}
+
+			tags, err := client.ImageTags(ctx, img.ID)
+			if err != nil {
+				return err
+			}
+
+			switch outputFmt {
+			case humanOutput:
+				err = tablewriter.WriteTable(len(tags), func(i int) interface{} { return tags[i] })
+				if err != nil {
+					return err
+				}
+			case jsonOutput:
+				err := json.NewEncoder(os.Stdout).Encode(tags)
+				if err != nil {
+					return err
+				}
+			default:
+				return clog.Error("unknown --output value")
+			}
+
+			return nil
+		},
+	}
+	cmd.Flags().StringVar(&orgName, "org", "", "organization by name")
+	cmd.Flags().StringVarP(&imageName, "image", "i", "", "image by name")
+	cmd.Flags().StringVar(&outputFmt, "output", humanOutput, "output format (human|json)")
+	_ = cmd.MarkFlagRequired("image")
+	_ = cmd.MarkFlagRequired("org")
+	return cmd
+}
+
+func tagsRmCmd() *cobra.Command {
+	var (
+		imageName string
+		orgName   string
+	)
+	cmd := &cobra.Command{
+		Use:     "rm [tag]",
+		Short:   "remove an image tag",
+		Example: `coder tags rm latest --image ubuntu --org default`,
+		Args:    cobra.ExactArgs(1),
+		RunE: func(cmd *cobra.Command, args []string) error {
+			ctx := cmd.Context()
+			client, err := newClient(ctx)
+			if err != nil {
+				return err
+			}
+
+			img, err := findImg(ctx, client, findImgConf{
+				email:   coder.Me,
+				imgName: imageName,
+				orgName: orgName,
+			})
+			if err != nil {
+				return err
+			}
+
+			if err = client.DeleteImageTag(ctx, img.ID, args[0]); err != nil {
+				return err
+			}
+			clog.LogSuccess("removed tag")
+
+			return nil
+		},
+	}
+	cmd.Flags().StringVarP(&orgName, "org", "o", "", "organization by name")
+	cmd.Flags().StringVarP(&imageName, "image", "i", "", "image by name")
+	_ = cmd.MarkFlagRequired("image")
+	_ = cmd.MarkFlagRequired("org")
+	return cmd
+}
